HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/v1.tar
class-yith-wcwl-rest-v1-controller.php000064400000005566151546377400014017 0ustar00<?php
/**
 * Rest controller abstract class
 */

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

if ( ! class_exists( 'YITH_WCWL_Rest_V1_Controller' ) ) {
	abstract class YITH_WCWL_Rest_V1_Controller extends WP_REST_Controller {
		/**
		 * Get item permission
		 *
		 * TODO: implement this method
		 *
		 * @param WP_REST_Request $request
		 * @return true|WP_Error
		 */
		public function get_item_permissions_check( $request ) {
			return true;
		}

		/**
		 * Get item permission
		 *
		 * TODO: implement this method
		 *
		 * @param $request
		 * @return true|WP_Error
		 */
		public function get_items_permissions_check( $request ) {
			return true;
		}

		/**
		 * @param YITH_WCWL_Wishlist[] $wishlists The wishlist objects.
		 *
		 * @return array
		 */
		protected function prepare_wishlists_for_rest( $wishlists ) {
			$wishlists_for_rest = array();
			foreach ( $wishlists as $wishlist ) {
				$wishlists_for_rest[ $wishlist->get_id() ] = $this->prepare_wishlist_for_rest( $wishlist );
			}

			return $wishlists_for_rest;
		}

		/**
		 * Prepare wishlist object for Rest response
		 *
		 * @param int|YITH_WCWL_Wishlist $wishlist The wishlist object or its id.
		 *
		 * @return array|false
		 */
		protected function prepare_wishlist_for_rest( $wishlist ) {
			$wishlist = $wishlist instanceof YITH_WCWL_Wishlist ? $wishlist : YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist );

			if ( ! $wishlist ) {
				return false;
			}

			return array_merge(
				$wishlist->get_data(),
				array(
					'name'       => $wishlist->get_formatted_name(),
					'token'      => $wishlist->get_token(),
					'session_id' => $wishlist->get_session_id(),
				)
			);
		}

		/**
		 * @param YITH_WCWL_Wishlist[] $wishlists The wishlist objects.
		 *
		 * @return array
		 */
		protected function prepare_products_for_rest( $products ) {
			$products_for_rest = array();
			foreach ( $products as $product ) {
				$products_for_rest[ $product instanceof WC_Product ? $product->get_id() : absint( $product ) ] = $this->prepare_product_for_rest( $product );
			}

			return $products_for_rest;
		}

		/**
		 * Prepare wishlist object for Rest response
		 *
		 * @param int|WC_Product $product The product object or its id.
		 *
		 * @return array|false
		 */
		protected function prepare_product_for_rest( $product, $additional_data = array() ) {
			$product = $product instanceof WC_Product ? $product : wc_get_product( $product );

			if ( ! $product ) {
				return false;
			}

			$product_id    = $product->get_id();
			$product_lists = yith_wcwl_wishlists()->get_wishlists_for_product( $product_id );

			return array_merge(
				array(
					'name'      => $product->get_name(),
					'isAdded'   => ! ! $product_lists,
					'count'     => yith_wcwl_wishlists()->count_add_to_wishlist( $product_id ),
					'wishlists' => $product_lists,
				),
				is_array( $additional_data ) ? $additional_data : array()
			);
		}
	}
}
class-yith-wcwl-rest-v1-items-controller.php000064400000022036151546377400015125 0ustar00<?php
/**
 * REST API Wishlist Items controller class.
 *
 * @package YITH\Wishlist\RestApi
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'YITH_WCWL_Rest_V1_Items_Controller' ) ) {
	/**
	 * REST API Wishlist Items controller class.
	 *
	 * @package YITH\Wishlist\RestApi
	 */
	class YITH_WCWL_Rest_V1_Items_Controller extends YITH_WCWL_Rest_V1_Controller {

		/**
		 * Endpoint namespace.
		 *
		 * @var string
		 */
		protected $namespace = 'yith/wishlist/v1';

		/**
		 * Route base.
		 *
		 * @var string
		 */
		protected $rest_base = 'items';

		/**
		 * Register the routes.
		 */
		public function register_routes() {
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => WP_REST_Server::CREATABLE,
						'callback'            => array( $this, 'add_item' ),
						'permission_callback' => array( $this, 'create_item_permissions_check' ),
						'args'                => array(
							'product_id'          => array(
								'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
								'required'    => true,
							),
							'wishlist_id'         => array(
								'description' => _x( 'The Wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => array( 'integer', 'string' ),
							),
							'quantity'            => array(
								'description' => _x( 'The quantity to add in wishlist', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
							'user_id'             => array(
								'description' => _x( 'The User ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
							'dateadded'           => array(
								'description' => _x( 'The timestamp to use as added date.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
							'wishlist_name'       => array(
								'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'string',
							),
							'wishlist_visibility' => array(
								'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
						),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);

			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => WP_REST_Server::DELETABLE,
						'callback'            => array( $this, 'remove_item' ),
						'permission_callback' => array( $this, 'delete_item_permissions_check' ),
						'args'                => array(
							'product_id'  => array(
								'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
								'required'    => true,
							),
							'wishlist_id' => array(
								'description' => _x( 'The Wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => array( 'integer', 'string' ),
							),
							'user_id'             => array(
								'description' => _x( 'The User ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
						),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);

			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/move',
				array(
					array(
						'methods'             => WP_REST_Server::CREATABLE,
						'callback'            => array( $this, 'move_item' ),
						'permission_callback' => '__return_true',
						'args'                => array(
							'product_id'  => array(
								'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
								'required'    => true,
							),
							'destination_wishlist' => array(
								'description' => _x( 'The destination wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type' => array( 'integer', 'string' ),
								'required' => true,
							),
							'origin_wishlist' => array(
								'description' => _x( 'The origin wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type' => 'integer',
							),
							'wishlist_name'       => array(
								'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'string',
							),
							'wishlist_visibility' => array(
								'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
						),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);
		}

		/**
		 * Add item to a wishlist
		 *
		 * @param WP_REST_Request $request The rest request.
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function add_item( $request ) {
			$args     = $request->get_params();
			$response = array( 'success' => true );

			$creating_default = false;
			if ( ! empty( $args[ 'wishlist_id' ] ) && 'default' === $args[ 'wishlist_id' ] ) {
				$creating_default = true;
				unset( $args[ 'wishlist_id' ] );
			}

			try {
				$data       = YITH_WCWL_Wishlists::get_instance()->add_item( $args );
				$product_id = $args[ 'product_id' ];
				$response   = array(
					'product_data' => array_merge(
						$this->prepare_product_for_rest( $product_id ),
						array(
							'added_to' => $data[ 'wishlist_id' ],
						)
					),
				);

				if ( $creating_default ) {
					$response[ 'wishlist_data' ] = $this->prepare_wishlist_for_rest( $data[ 'wishlist_id' ] );
				}
			} catch ( \Exception $e ) {
				$response = array( 'success' => false, 'message' => $e->getMessage() );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Remove an item from a wishlist
		 *
		 * @param WP_REST_Request $request The rest request.
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function remove_item( $request ) {
			$args     = $request->get_params();
			$response = array( 'success' => true );

			try {
				$data       = YITH_WCWL_Wishlists::get_instance()->remove_item( $args );
				$product_id = $args[ 'product_id' ];
				$response   = array(
					'product_data' => array_merge(
						array( 'removed_from' => $data[ 'wishlist_id' ] ),
						$this->prepare_product_for_rest( $product_id )
					),
				);
			} catch ( \Exception $e ) {
				$response = array( 'success' => false, 'message' => $e->getMessage() );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Remove an item from a wishlist
		 *
		 * @param WP_REST_Request $request The rest request.
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function move_item( $request ) {
			$args = $request->get_params();

			try {
				$result   = YITH_WCWL_Wishlists::get_instance()->move( $args );
				$response = array(
					'moved'        => $result[ 'moved' ],
					'product_data' => $this->prepare_product_for_rest(
						$args[ 'product_id' ],
						array(
							'moved_from' => $args[ 'origin_wishlist' ],
							'moved_to'   => 'new' === $args[ 'destination_wishlist' ] ? $result[ 'destination_wishlist' ]->get_id() : $args[ 'destination_wishlist' ],
						)
					),
				);

				if ( $result[ 'destination_wishlist' ] instanceof YITH_WCWL_Wishlist ) {
					$response[ 'destination_wishlist' ] = $this->prepare_wishlist_for_rest( $result[ 'destination_wishlist' ] );
				}
			} catch ( \Exception $e ) {
				$response = array( 'success' => false, 'message' => $e->getMessage() );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Create item permission checks
		 *
		 * @param WP_REST_Request $request
		 * @return true|WP_Error
		 */
		public function create_item_permissions_check( $request ) {
			$wishlist = $this->get_wishlist_from_request( $request );

			return ! $wishlist || $wishlist->current_user_can( 'add_to_wishlist' );
		}

		/**
		 * Delete item permission checks
		 *
		 * @param WP_REST_Request $request
		 * @return true|WP_Error
		 */
		public function delete_item_permissions_check( $request ) {
			$wishlist = $this->get_wishlist_from_request( $request );

			return ! $wishlist || $wishlist->current_user_can( 'remove_from_wishlist' );
		}

		/**
		 * Get wishlist object from request
		 *
		 * @param WP_REST_Request $request The request.
		 *
		 * @return YITH_WCWL_Wishlist|false
		 */
		protected function get_wishlist_from_request( $request ) {
			$wishlist_id = $request->get_param( 'wishlist_id' );
			$wishlist    = false;

			if ( $wishlist_id ) {
				try {
					$wishlist = new YITH_WCWL_Wishlist( $wishlist_id );
				} catch ( Exception $exception ) {
				}
			}

			return $wishlist;
		}
	}
}
class-yith-wcwl-rest-v1-lists-controller.php000064400000006137151546377400015146 0ustar00<?php
/**
 * REST API Wishlist Lists controller class.
 *
 * @package YITH\Wishlist\RestApi
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'YITH_WCWL_Rest_V1_Lists_Controller' ) ) {
	/**
	 * REST API Wishlist Lists controller class.
	 *
	 * @package YITH\Wishlist\RestApi
	 */
	class YITH_WCWL_Rest_V1_Lists_Controller extends YITH_WCWL_Rest_V1_Controller {

		/**
		 * Endpoint namespace.
		 *
		 * @var string
		 */
		protected $namespace = 'yith/wishlist/v1';

		/**
		 * Route base.
		 *
		 * @var string
		 */
		protected $rest_base = 'lists';

		/**
		 * Register the routes.
		 */
		public function register_routes() {
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => WP_REST_Server::READABLE,
						'callback'            => array( $this, 'get_lists' ),
						'permission_callback' => '__return_true',
						'args'                => $this->get_endpoint_args_for_item_schema(),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => WP_REST_Server::CREATABLE,
						'callback'            => array( $this, 'create_list' ),
						'permission_callback' => '__return_true',
						'args'                => array(
							'wishlist_name'       => array(
								'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'string',
								'required'    => true,
							),
							'wishlist_visibility' => array(
								'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
								'required'    => true,
							),
							'user_id'             => array(
								'description' => _x( 'The unique identifier for the user.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'integer',
							),
							'session_id'          => array(
								'description' => _x( 'The unique identifier for the session.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
								'type'        => 'string',
							),
						),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);
		}


		/**
		 * Get lists
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function get_lists() {
			$response = array(
				'lists' => $this->prepare_wishlists_for_rest( YITH_WCWL_Wishlists::get_instance()->get_current_user_wishlists() ),
			);

			return rest_ensure_response( $response );
		}

		public function create_list( $request ) {
			$args = $request->get_params();

			try {
				$wishlist = YITH_WCWL_Wishlists::get_instance()->create( $args );
				$response = array(
					'wishlist_data' => $this->prepare_wishlist_for_rest( $wishlist ),
				);
			} catch ( YITH_WCWL_Exception $e ) {
				$response = array(
					'success' => false,
					'message' => $e->getMessage(),
				);
			}

			return rest_ensure_response( $response );
		}
	}
}
class-yith-wcwl-rest-v1-products-controller.php000064400000005647151546377400015660 0ustar00<?php
/**
 * REST API Wishlist Products controller class.
 *
 * @package YITH\Wishlist\RestApi
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'YITH_WCWL_REST_V1_Products_Controller' ) ) {
	/**
	 * REST API Wishlist Products controller class.
	 *
	 * @package YITH\Wishlist\RestApi
	 */
	class YITH_WCWL_Rest_V1_Products_Controller extends YITH_WCWL_Rest_V1_Controller {

		/**
		 * Endpoint namespace.
		 *
		 * @var string
		 */
		protected $namespace = 'yith/wishlist/v1';

		/**
		 * Route base.
		 *
		 * @var string
		 */
		protected $rest_base = 'products';

		/**
		 * Register the routes.
		 */
		public function register_routes() {
			register_rest_route( $this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_products_data' ),
					'permission_callback' => '__return_true', // TODO: implement checks for permissions.
					'args'                => array(
						'product_ids' => array(
							'description' => _x( 'The list of product ids for which data is being requested.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
							'type'        => 'array',
							'required'    => true,
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			) );

			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/(?P<product_id>[\d]+)',
				array(
					'args'   => array(
						'product_id' => array(
							'description' => _x( 'The unique identifier for the product.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
					array(
						'methods'             => WP_REST_Server::READABLE,
						'callback'            => array( $this, 'get_product_data' ),
						'permission_callback' => '__return_true',
						'args'                => $this->get_endpoint_args_for_item_schema(),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);
		}

		/**
		 * Get wishlist data related to multiple products
		 *
		 * @param WP_REST_Request $request The rest request.
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function get_products_data( $request ) {
			$args = $request->get_params();

			$products_data = array();

			if ( ! empty( $args[ 'product_ids' ] ) ) {
				$products_data = $this->prepare_products_for_rest( $args[ 'product_ids' ] );
			}

			return rest_ensure_response( $products_data );
		}

		/**
		 * Get wishlist data related to a single products
		 *
		 * @param WP_REST_Request $request The rest request.
		 *
		 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
		 */
		public function get_product_data( $request ) {
			$args = $request->get_params();

			$product_id = $args[ 'product_id' ];

			$response = $this->prepare_product_for_rest($product_id);

			return rest_ensure_response( $response );
		}
	}
}
class-wc-api-authentication.php000064400000027507151550524670012602 0ustar00<?php
/**
 * WooCommerce API Authentication Class
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1.0
 * @version  2.4.0
 */

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

class WC_API_Authentication {

	/**
	 * Setup class
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// To disable authentication, hook into this filter at a later priority and return a valid WP_User
		add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
	}

	/**
	 * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
	 *
	 * @since 2.1
	 * @param WP_User $user
	 * @return null|WP_Error|WP_User
	 */
	public function authenticate( $user ) {

		// Allow access to the index by default
		if ( '/' === WC()->api->server->path ) {
			return new WP_User( 0 );
		}

		try {

			if ( is_ssl() ) {
				$keys = $this->perform_ssl_authentication();
			} else {
				$keys = $this->perform_oauth_authentication();
			}

			// Check API key-specific permission
			$this->check_api_key_permissions( $keys['permissions'] );

			$user = $this->get_user_by_id( $keys['user_id'] );

			$this->update_api_key_last_access( $keys['key_id'] );

		} catch ( Exception $e ) {
			$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		return $user;
	}

	/**
	 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
	 * attacks, so the request can be authenticated by simply looking up the user
	 * associated with the given consumer key and confirming the consumer secret
	 * provided is valid
	 *
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_ssl_authentication() {

		$params = WC()->api->server->params['GET'];

		// Get consumer key
		if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_key = $_SERVER['PHP_AUTH_USER'];

		} elseif ( ! empty( $params['consumer_key'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_key = $params['consumer_key'];

		} else {

			throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
		}

		// Get consumer secret
		if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_secret = $_SERVER['PHP_AUTH_PW'];

		} elseif ( ! empty( $params['consumer_secret'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_secret = $params['consumer_secret'];

		} else {

			throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
		}

		$keys = $this->get_keys_by_consumer_key( $consumer_key );

		if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
			throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
	 *
	 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
	 *
	 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
	 *
	 * 1) There is no token associated with request/responses, only consumer keys/secrets are used
	 *
	 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
	 *    This is because there is no cross-OS function within PHP to get the raw Authorization header
	 *
	 * @link http://tools.ietf.org/html/rfc5849 for the full spec
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_oauth_authentication() {

		$params = WC()->api->server->params['GET'];

		$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );

		// Check for required OAuth parameters
		foreach ( $param_names as $param_name ) {

			if ( empty( $params[ $param_name ] ) ) {
				/* translators: %s: parameter name */
				throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
			}
		}

		// Fetch WP user by consumer key
		$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );

		// Perform OAuth validation
		$this->check_oauth_signature( $keys, $params );
		$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );

		// Authentication successful, return user
		return $keys;
	}

	/**
	 * Return the keys for the given consumer key
	 *
	 * @since 2.4.0
	 * @param string $consumer_key
	 * @return array
	 * @throws Exception
	 */
	private function get_keys_by_consumer_key( $consumer_key ) {
		global $wpdb;

		$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );

		$keys = $wpdb->get_row( $wpdb->prepare( "
			SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
			FROM {$wpdb->prefix}woocommerce_api_keys
			WHERE consumer_key = '%s'
		", $consumer_key ), ARRAY_A );

		if ( empty( $keys ) ) {
			throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Get user by ID
	 *
	 * @since  2.4.0
	 * @param  int $user_id
	 * @return WP_User
	 *
	 * @throws Exception
	 */
	private function get_user_by_id( $user_id ) {
		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
		}

		return $user;
	}

	/**
	 * Check if the consumer secret provided for the given user is valid
	 *
	 * @since 2.1
	 * @param string $keys_consumer_secret
	 * @param string $consumer_secret
	 * @return bool
	 */
	private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
		return hash_equals( $keys_consumer_secret, $consumer_secret );
	}

	/**
	 * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
	 * has a valid key/secret
	 *
	 * @param array $keys
	 * @param array $params the request parameters
	 * @throws Exception
	 */
	private function check_oauth_signature( $keys, $params ) {

		$http_method = strtoupper( WC()->api->server->method );

		$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );

		// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
		$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
		unset( $params['oauth_signature'] );

		// Remove filters and convert them from array to strings to void normalize issues
		if ( isset( $params['filter'] ) ) {
			$filters = $params['filter'];
			unset( $params['filter'] );
			foreach ( $filters as $filter => $filter_value ) {
				$params[ 'filter[' . $filter . ']' ] = $filter_value;
			}
		}

		// Normalize parameter key/values
		$params = $this->normalize_parameters( $params );

		// Sort parameters
		if ( ! uksort( $params, 'strcmp' ) ) {
			throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
		}

		// Form query string
		$query_params = array();
		foreach ( $params as $param_key => $param_value ) {

			$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
		}
		$query_string = implode( '%26', $query_params ); // join with ampersand

		$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;

		if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
			throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
		}

		$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );

		$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );

		if ( ! hash_equals( $signature, $consumer_signature ) ) {
			throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
		}
	}

	/**
	 * Normalize each parameter by assuming each parameter may have already been
	 * encoded, so attempt to decode, and then re-encode according to RFC 3986
	 *
	 * Note both the key and value is normalized so a filter param like:
	 *
	 * 'filter[period]' => 'week'
	 *
	 * is encoded to:
	 *
	 * 'filter%5Bperiod%5D' => 'week'
	 *
	 * This conforms to the OAuth 1.0a spec which indicates the entire query string
	 * should be URL encoded
	 *
	 * @since 2.1
	 * @see rawurlencode()
	 * @param array $parameters un-normalized parameters
	 * @return array normalized parameters
	 */
	private function normalize_parameters( $parameters ) {

		$normalized_parameters = array();

		foreach ( $parameters as $key => $value ) {

			// Percent symbols (%) must be double-encoded
			$key   = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
			$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );

			$normalized_parameters[ $key ] = $value;
		}

		return $normalized_parameters;
	}

	/**
	 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
	 * an attacker could attempt to re-send an intercepted request at a later time.
	 *
	 * - A timestamp is valid if it is within 15 minutes of now
	 * - A nonce is valid if it has not been used within the last 15 minutes
	 *
	 * @param array $keys
	 * @param int $timestamp the unix timestamp for when the request was made
	 * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
	 * @throws Exception
	 */
	private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
		global $wpdb;

		$valid_window = 15 * 60; // 15 minute window

		if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
			throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) );
		}

		$used_nonces = maybe_unserialize( $keys['nonces'] );

		if ( empty( $used_nonces ) ) {
			$used_nonces = array();
		}

		if ( in_array( $nonce, $used_nonces ) ) {
			throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
		}

		$used_nonces[ $timestamp ] = $nonce;

		// Remove expired nonces
		foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
			if ( $nonce_timestamp < ( time() - $valid_window ) ) {
				unset( $used_nonces[ $nonce_timestamp ] );
			}
		}

		$used_nonces = maybe_serialize( $used_nonces );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'nonces' => $used_nonces ),
			array( 'key_id' => $keys['key_id'] ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
	 *
	 * @param string $key_permissions
	 * @throws Exception if the permission check fails
	 */
	public function check_api_key_permissions( $key_permissions ) {
		switch ( WC()->api->server->method ) {

			case 'HEAD':
			case 'GET':
				if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
				}
				break;

			case 'POST':
			case 'PUT':
			case 'PATCH':
			case 'DELETE':
				if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
				}
				break;
		}
	}

	/**
	 * Updated API Key last access datetime
	 *
	 * @since 2.4.0
	 *
	 * @param int $key_id
	 */
	private function update_api_key_last_access( $key_id ) {
		global $wpdb;

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'last_access' => current_time( 'mysql' ) ),
			array( 'key_id' => $key_id ),
			array( '%s' ),
			array( '%d' )
		);
	}
}
class-wc-api-coupons.php000064400000015767151550524670011256 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_Coupons extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/coupons';

	/**
	 * Register the routes for this class
	 *
	 * GET /coupons
	 * GET /coupons/count
	 * GET /coupons/<id>
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
		);

		# GET /coupons/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
		);

		# GET /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),  WC_API_Server::READABLE ),
		);

		# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
		$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all coupons
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_coupons( $filter );

		$coupons = array();

		foreach ( $query->posts as $coupon_id ) {

			if ( ! $this->is_readable( $coupon_id ) ) {
				continue;
			}

			$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'coupons' => $coupons );
	}

	/**
	 * Get the coupon for the given ID
	 *
	 * @since 2.1
	 *
	 * @param int $id the coupon ID
	 * @param string $fields fields to include in response
	 *
	 * @return array|WP_Error
	 * @throws WC_API_Exception
	 */
	public function get_coupon( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'shop_coupon', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$coupon = new WC_Coupon( $id );

		if ( 0 === $coupon->get_id() ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
		}

		$coupon_data = array(
			'id'                           => $coupon->get_id(),
			'code'                         => $coupon->get_code(),
			'type'                         => $coupon->get_discount_type(),
			'created_at'                   => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'updated_at'                   => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
			'amount'                       => wc_format_decimal( $coupon->get_amount(), 2 ),
			'individual_use'               => $coupon->get_individual_use(),
			'product_ids'                  => array_map( 'absint', (array) $coupon->get_product_ids() ),
			'exclude_product_ids'          => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
			'usage_limit'                  => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
			'usage_limit_per_user'         => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
			'limit_usage_to_x_items'       => (int) $coupon->get_limit_usage_to_x_items(),
			'usage_count'                  => (int) $coupon->get_usage_count(),
			'expiry_date'                  => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times.
			'enable_free_shipping'         => $coupon->get_free_shipping(),
			'product_category_ids'         => array_map( 'absint', (array) $coupon->get_product_categories() ),
			'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
			'exclude_sale_items'           => $coupon->get_exclude_sale_items(),
			'minimum_amount'               => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
			'customer_emails'              => $coupon->get_email_restrictions(),
		);

		return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {

		$query = $this->query_coupons( $filter );

		if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Get the coupon for the given code
	 *
	 * @since 2.1
	 * @param string $code the coupon code
	 * @param string $fields fields to include in response
	 * @return int|WP_Error
	 */
	public function get_coupon_by_code( $code, $fields = null ) {
		global $wpdb;

		$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );

		if ( is_null( $id ) ) {
			return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) );
		}

		return $this->get_coupon( $id, $fields );
	}

	/**
	 * Create a coupon
	 *
	 * @param array $data
	 * @return array
	 */
	public function create_coupon( $data ) {

		return array();
	}

	/**
	 * Edit a coupon
	 *
	 * @param int $id the coupon ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_coupon( $id, $data ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_coupon( $id );
	}

	/**
	 * Delete a coupon
	 *
	 * @param int $id the coupon ID
	 * @param bool $force true to permanently delete coupon, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_coupon( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
	}

	/**
	 * Helper method to get coupon post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_coupons( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_coupon',
			'post_status' => 'publish',
		);

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}
}
class-wc-api-customers.php000064400000033130151550524670011574 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_Customers extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/customers';

	/** @var string $created_at_min for date filtering */
	private $created_at_min = null;

	/** @var string $created_at_max for date filtering */
	private $created_at_max = null;

	/**
	 * Setup class, overridden to provide customer data to order response
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		parent::__construct( $server );

		// add customer data to order responses
		add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );

		// modify WP_User_Query to support created_at date filtering
		add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
	}

	/**
	 * Register the routes for this class
	 *
	 * GET /customers
	 * GET /customers/count
	 * GET /customers/<id>
	 * GET /customers/<id>/orders
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),     WC_API_SERVER::READABLE ),
		);

		# GET /customers/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),  WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all customers
	 *
	 * @since 2.1
	 * @param array $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_customers( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_customers( $filter );

		$customers = array();

		foreach ( $query->get_results() as $user_id ) {

			if ( ! $this->is_readable( $user_id ) ) {
				continue;
			}

			$customers[] = current( $this->get_customer( $user_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'customers' => $customers );
	}

	/**
	 * Get the customer for the given ID
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_customer( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$customer      = new WC_Customer( $id );
		$last_order    = $customer->get_last_order();
		$customer_data = array(
			'id'               => $customer->get_id(),
			'created_at'       => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'email'            => $customer->get_email(),
			'first_name'       => $customer->get_first_name(),
			'last_name'        => $customer->get_last_name(),
			'username'         => $customer->get_username(),
			'last_order_id'    => is_object( $last_order ) ? $last_order->get_id() : null,
			'last_order_date'  => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
			'orders_count'     => $customer->get_order_count(),
			'total_spent'      => wc_format_decimal( $customer->get_total_spent(), 2 ),
			'avatar_url'       => $customer->get_avatar_url(),
			'billing_address'  => array(
				'first_name' => $customer->get_billing_first_name(),
				'last_name'  => $customer->get_billing_last_name(),
				'company'    => $customer->get_billing_company(),
				'address_1'  => $customer->get_billing_address_1(),
				'address_2'  => $customer->get_billing_address_2(),
				'city'       => $customer->get_billing_city(),
				'state'      => $customer->get_billing_state(),
				'postcode'   => $customer->get_billing_postcode(),
				'country'    => $customer->get_billing_country(),
				'email'      => $customer->get_billing_email(),
				'phone'      => $customer->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $customer->get_shipping_first_name(),
				'last_name'  => $customer->get_shipping_last_name(),
				'company'    => $customer->get_shipping_company(),
				'address_1'  => $customer->get_shipping_address_1(),
				'address_2'  => $customer->get_shipping_address_2(),
				'city'       => $customer->get_shipping_city(),
				'state'      => $customer->get_shipping_state(),
				'postcode'   => $customer->get_shipping_postcode(),
				'country'    => $customer->get_shipping_country(),
			),
		);

		return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {

		$query = $this->query_customers( $filter );

		if ( ! current_user_can( 'list_users' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => count( $query->get_results() ) );
	}


	/**
	 * Create a customer
	 *
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {

		if ( ! current_user_can( 'create_users' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array();
	}

	/**
	 * Edit a customer
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {

		$id = $this->validate_request( $id, 'customer', 'edit' );

		if ( ! is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_customer( $id );
	}

	/**
	 * Delete a customer
	 *
	 * @param int $id the customer ID
	 * @return array|WP_Error
	 */
	public function delete_customer( $id ) {

		$id = $this->validate_request( $id, 'customer', 'delete' );

		if ( ! is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'customer' );
	}

	/**
	 * Get the orders for a customer
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_orders( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order_ids = wc_get_orders( array(
			'customer' => $id,
			'limit'    => -1,
			'orderby'  => 'date',
			'order'    => 'ASC',
			'return'   => 'ids',
		) );

		if ( empty( $order_ids ) ) {
			return array( 'orders' => array() );
		}

		$orders = array();

		foreach ( $order_ids as $order_id ) {
			$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
		}

		return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
	}

	/**
	 * Helper method to get customer user objects
	 *
	 * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
	 * pagination support
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_User_Query
	 */
	private function query_customers( $args = array() ) {

		// default users per page
		$users_per_page = get_option( 'posts_per_page' );

		// set base query arguments
		$query_args = array(
			'fields'  => 'ID',
			'role'    => 'customer',
			'orderby' => 'registered',
			'number'  => $users_per_page,
		);

		// search
		if ( ! empty( $args['q'] ) ) {
			$query_args['search'] = $args['q'];
		}

		// limit number of users returned
		if ( ! empty( $args['limit'] ) ) {

			$query_args['number'] = absint( $args['limit'] );

			$users_per_page = absint( $args['limit'] );
		}

		// page
		$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;

		// offset
		if ( ! empty( $args['offset'] ) ) {
			$query_args['offset'] = absint( $args['offset'] );
		} else {
			$query_args['offset'] = $users_per_page * ( $page - 1 );
		}

		// created date
		if ( ! empty( $args['created_at_min'] ) ) {
			$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
		}

		if ( ! empty( $args['created_at_max'] ) ) {
			$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
		}

		$query = new WP_User_Query( $query_args );

		// helper members for pagination headers
		$query->total_pages = ceil( $query->get_total() / $users_per_page );
		$query->page = $page;

		return $query;
	}

	/**
	 * Add customer data to orders
	 *
	 * @since 2.1
	 * @param $order_data
	 * @param $order
	 * @return array
	 */
	public function add_customer_data( $order_data, $order ) {

		if ( 0 == $order->get_user_id() ) {

			// add customer data from order
			$order_data['customer'] = array(
				'id'               => 0,
				'email'            => $order->get_billing_email(),
				'first_name'       => $order->get_billing_first_name(),
				'last_name'        => $order->get_billing_last_name(),
				'billing_address'  => array(
					'first_name' => $order->get_billing_first_name(),
					'last_name'  => $order->get_billing_last_name(),
					'company'    => $order->get_billing_company(),
					'address_1'  => $order->get_billing_address_1(),
					'address_2'  => $order->get_billing_address_2(),
					'city'       => $order->get_billing_city(),
					'state'      => $order->get_billing_state(),
					'postcode'   => $order->get_billing_postcode(),
					'country'    => $order->get_billing_country(),
					'email'      => $order->get_billing_email(),
					'phone'      => $order->get_billing_phone(),
				),
				'shipping_address' => array(
					'first_name' => $order->get_shipping_first_name(),
					'last_name'  => $order->get_shipping_last_name(),
					'company'    => $order->get_shipping_company(),
					'address_1'  => $order->get_shipping_address_1(),
					'address_2'  => $order->get_shipping_address_2(),
					'city'       => $order->get_shipping_city(),
					'state'      => $order->get_shipping_state(),
					'postcode'   => $order->get_shipping_postcode(),
					'country'    => $order->get_shipping_country(),
				),
			);

		} else {

			$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
		}

		return $order_data;
	}

	/**
	 * Modify the WP_User_Query to support filtering on the date the customer was created
	 *
	 * @since 2.1
	 * @param WP_User_Query $query
	 */
	public function modify_user_query( $query ) {

		if ( $this->created_at_min ) {
			$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
		}

		if ( $this->created_at_max ) {
			$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid WP_User
	 * 3) the current user has the proper permissions
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param string|int $id the customer ID
	 * @param string $type the request type, unused because this method overrides the parent class
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid user ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		$id = absint( $id );

		// validate ID
		if ( empty( $id ) ) {
			return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// non-existent IDs return a valid WP_User object with the user ID = 0
		$customer = new WP_User( $id );

		if ( 0 === $customer->ID ) {
			return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// validate permissions
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'list_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'edit_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'delete_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}

	/**
	 * Check if the current user can read users
	 *
	 * @since 2.1
	 * @see WC_API_Resource::is_readable()
	 * @param int|WP_Post $post unused
	 * @return bool true if the current user can read users, false otherwise
	 */
	protected function is_readable( $post ) {

		return current_user_can( 'list_users' );
	}
}
class-wc-api-json-handler.php000064400000003702151550524670012136 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_JSON_Handler implements WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $body the raw request body
	 * @return array|mixed
	 */
	public function parse_body( $body ) {

		return json_decode( $body, true );
	}

	/**
	 * Generate a JSON response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {
		if ( isset( $_GET['_jsonp'] ) ) {

			if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
			}

			$jsonp_callback = $_GET['_jsonp'];

			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
			}

			WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );

			// Prepend '/**/' to mitigate possible JSONP Flash attacks.
			// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
			return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
		}

		return wp_json_encode( $data );
	}
}
class-wc-api-orders.php000064400000027600151550524670011053 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_Orders extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/orders';

	/**
	 * Register the routes for this class
	 *
	 * GET /orders
	 * GET /orders/count
	 * GET|PUT /orders/<id>
	 * GET /orders/<id>/notes
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /orders
		$routes[ $this->base ] = array(
			array( array( $this, 'get_orders' ),     WC_API_Server::READABLE ),
		);

		# GET /orders/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
		);

		# GET|PUT /orders/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /orders/<id>/notes
		$routes[ $this->base . '/(?P<id>\d+)/notes' ] = array(
			array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all orders
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 * @return array
	 */
	public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_orders( $filter );

		$orders = array();

		foreach ( $query->posts as $order_id ) {

			if ( ! $this->is_readable( $order_id ) ) {
				continue;
			}

			$orders[] = current( $this->get_order( $order_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'orders' => $orders );
	}


	/**
	 * Get the order for the given ID
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null ) {

		// ensure order ID is valid & user has permission to read
		$id = $this->validate_request( $id, 'shop_order', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order      = wc_get_order( $id );
		$order_data = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'created_at'                => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'                => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'completed_at'              => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'status'                    => $order->get_status(),
			'currency'                  => $order->get_currency(),
			'total'                     => wc_format_decimal( $order->get_total(), 2 ),
			'subtotal'                  => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), 2 ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), 2 ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), 2 ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), 2 ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), 2 ),
			'cart_discount'             => wc_format_decimal( 0, 2 ),
			'order_discount'            => wc_format_decimal( 0, 2 ),
			'shipping_methods'          => $order->get_shipping_method(),
			'payment_details' => array(
				'method_id'    => $order->get_payment_method(),
				'method_title' => $order->get_payment_method_title(),
				'paid'         => ! is_null( $order->get_date_paid() ),
			),
			'billing_address' => array(
				'first_name' => $order->get_billing_first_name(),
				'last_name'  => $order->get_billing_last_name(),
				'company'    => $order->get_billing_company(),
				'address_1'  => $order->get_billing_address_1(),
				'address_2'  => $order->get_billing_address_2(),
				'city'       => $order->get_billing_city(),
				'state'      => $order->get_billing_state(),
				'postcode'   => $order->get_billing_postcode(),
				'country'    => $order->get_billing_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $order->get_shipping_first_name(),
				'last_name'  => $order->get_shipping_last_name(),
				'company'    => $order->get_shipping_company(),
				'address_1'  => $order->get_shipping_address_1(),
				'address_2'  => $order->get_shipping_address_2(),
				'city'       => $order->get_shipping_city(),
				'state'      => $order->get_shipping_state(),
				'postcode'   => $order->get_shipping_postcode(),
				'country'    => $order->get_shipping_country(),
			),
			'note'                      => $order->get_customer_note(),
			'customer_ip'               => $order->get_customer_ip_address(),
			'customer_user_agent'       => $order->get_customer_user_agent(),
			'customer_id'               => $order->get_user_id(),
			'view_order_url'            => $order->get_view_order_url(),
			'line_items'                => array(),
			'shipping_lines'            => array(),
			'tax_lines'                 => array(),
			'fee_lines'                 => array(),
			'coupon_lines'              => array(),
		);

		// add line items
		foreach ( $order->get_items() as $item_id => $item ) {
			$product                    = $item->get_product();
			$order_data['line_items'][] = array(
				'id'         => $item_id,
				'subtotal'   => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
				'total'      => wc_format_decimal( $order->get_line_total( $item ), 2 ),
				'total_tax'  => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
				'price'      => wc_format_decimal( $order->get_item_total( $item ), 2 ),
				'quantity'   => $item->get_quantity(),
				'tax_class'  => $item->get_tax_class(),
				'name'       => $item->get_name(),
				'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
				'sku'        => is_object( $product ) ? $product->get_sku() : null,
			);
		}

		// add shipping
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$order_data['shipping_lines'][] = array(
				'id'           => $shipping_item_id,
				'method_id'    => $shipping_item->get_method_id(),
				'method_title' => $shipping_item->get_name(),
				'total'        => wc_format_decimal( $shipping_item->get_total(), 2 ),
			);
		}

		// add taxes
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$order_data['tax_lines'][] = array(
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, 2 ),
				'compound' => (bool) $tax->is_compound,
			);
		}

		// add fees
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$order_data['fee_lines'][] = array(
				'id'        => $fee_item_id,
				'title'     => $fee_item->get_name(),
				'tax_class' => $fee_item->get_tax_class(),
				'total'     => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ),
			);
		}

		// add coupons
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$order_data['coupon_lines'][] = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ),
			);
		}

		return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.1
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_orders_count( $status = null, $filter = array() ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$query = $this->query_orders( $filter );

		if ( ! current_user_can( 'read_private_shop_orders' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Edit an order
	 *
	 * API v1 only allows updating the status of an order
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {

		$id = $this->validate_request( $id, 'shop_order', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order = wc_get_order( $id );

		if ( ! empty( $data['status'] ) ) {

			$order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' );
		}

		return $this->get_order( $id );
	}

	/**
	 * Delete an order
	 *
	 * @param int $id the order ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array
	 */
	public function delete_order( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_order', 'delete' );

		return $this->delete( $id, 'order',  ( 'true' === $force ) );
	}

	/**
	 * Get the admin order notes for an order
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_notes( $id, $fields = null ) {

		// ensure ID is valid order ID
		$id = $this->validate_request( $id, 'shop_order', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$args = array(
			'post_id' => $id,
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$order_notes = array();

		foreach ( $notes as $note ) {

			$order_notes[] = array(
				'id'            => $note->comment_ID,
				'created_at'    => $this->server->format_datetime( $note->comment_date_gmt ),
				'note'          => $note->comment_content,
				'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
			);
		}

		return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) );
	}

	/**
	 * Helper method to get order post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_orders( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_order',
			'post_status' => array_keys( wc_get_order_statuses() ),
		);

		// add status argument
		if ( ! empty( $args['status'] ) ) {

			$statuses                  = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
			$statuses                  = explode( ',', $statuses );
			$query_args['post_status'] = $statuses;

			unset( $args['status'] );

		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Helper method to get the order subtotal
	 *
	 * @since 2.1
	 * @param WC_Order $order
	 * @return float
	 */
	private function get_order_subtotal( $order ) {
		$subtotal = 0;

		// subtotal
		foreach ( $order->get_items() as $item ) {
			$subtotal += $item->get_subtotal();
		}

		return $subtotal;
	}
}
class-wc-api-products.php000064400000041105151550524670011414 0ustar00<?php
/**
 * WooCommerce API Products Class
 *
 * Handles requests to the /products endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     3.0
 */

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

class WC_API_Products extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/products';

	/**
	 * Register the routes for this class
	 *
	 * GET /products
	 * GET /products/count
	 * GET /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ),     WC_API_Server::READABLE ),
		);

		# GET /products/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
		);

		# GET /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ),  WC_API_Server::READABLE ),
		);

		# GET /products/<id>/reviews
		$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
			array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all products
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param string $type
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		$filter['page'] = $page;

		$query = $this->query_products( $filter );

		$products = array();

		foreach ( $query->posts as $product_id ) {

			if ( ! $this->is_readable( $product_id ) ) {
				continue;
			}

			$products[] = current( $this->get_product( $product_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'products' => $products );
	}

	/**
	 * Get the product for the given ID
	 *
	 * @since 2.1
	 * @param int $id the product ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_product( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		// add data that applies to every product type
		$product_data = $this->get_product_data( $product );

		// add variations to variable products
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$product_data['variations'] = $this->get_variation_data( $product );
		}

		// add the parent product data to an individual variation
		if ( $product->is_type( 'variation' ) ) {
			$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
		}

		return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		if ( ! current_user_can( 'read_private_products' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		$query = $this->query_products( $filter );

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Edit a product
	 *
	 * @param int $id the product ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {

		$id = $this->validate_request( $id, 'product', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_product( $id );
	}

	/**
	 * Delete a product
	 *
	 * @param int $id the product ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_product( $id, $force = false ) {

		$id = $this->validate_request( $id, 'product', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'product', ( 'true' === $force ) );
	}

	/**
	 * Get the reviews for a product
	 *
	 * @since 2.1
	 * @param int $id the product ID to get reviews for
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_product_reviews( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$args = array(
			'post_id' => $id,
			'approve' => 'approve',
		);

		$comments = get_comments( $args );

		$reviews = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => $comment->comment_ID,
				'created_at'     => $this->server->format_datetime( $comment->comment_date_gmt ),
				'review'         => $comment->comment_content,
				'rating'         => get_comment_meta( $comment->comment_ID, 'rating', true ),
				'reviewer_name'  => $comment->comment_author,
				'reviewer_email' => $comment->comment_author_email,
				'verified'       => wc_review_is_from_verified_owner( $comment->comment_ID ),
			);
		}

		return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
	}

	/**
	 * Helper method to get product post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_products( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'product',
			'post_status' => 'publish',
			'meta_query'  => array(),
		);

		if ( ! empty( $args['type'] ) ) {

			$types = explode( ',', $args['type'] );

			$query_args['tax_query'] = array(
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => $types,
				),
			);

			unset( $args['type'] );
		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Get standard product data that applies to every product type
	 *
	 * @since 2.1
	 * @param WC_Product|int $product
	 * @return array
	 */
	private function get_product_data( $product ) {
		if ( is_numeric( $product ) ) {
			$product = wc_get_product( $product );
		}

		if ( ! is_a( $product, 'WC_Product' ) ) {
			return array();
		}

		return array(
			'title'              => $product->get_name(),
			'id'                 => $product->get_id(),
			'created_at'         => $this->server->format_datetime( $product->get_date_created(), false, true ),
			'updated_at'         => $this->server->format_datetime( $product->get_date_modified(), false, true ),
			'type'               => $product->get_type(),
			'status'             => $product->get_status(),
			'downloadable'       => $product->is_downloadable(),
			'virtual'            => $product->is_virtual(),
			'permalink'          => $product->get_permalink(),
			'sku'                => $product->get_sku(),
			'price'              => wc_format_decimal( $product->get_price(), 2 ),
			'regular_price'      => wc_format_decimal( $product->get_regular_price(), 2 ),
			'sale_price'         => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null,
			'price_html'         => $product->get_price_html(),
			'taxable'            => $product->is_taxable(),
			'tax_status'         => $product->get_tax_status(),
			'tax_class'          => $product->get_tax_class(),
			'managing_stock'     => $product->managing_stock(),
			'stock_quantity'     => $product->get_stock_quantity(),
			'in_stock'           => $product->is_in_stock(),
			'backorders_allowed' => $product->backorders_allowed(),
			'backordered'        => $product->is_on_backorder(),
			'sold_individually'  => $product->is_sold_individually(),
			'purchaseable'       => $product->is_purchasable(),
			'featured'           => $product->is_featured(),
			'visible'            => $product->is_visible(),
			'catalog_visibility' => $product->get_catalog_visibility(),
			'on_sale'            => $product->is_on_sale(),
			'weight'             => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
			'dimensions'         => array(
				'length' => $product->get_length(),
				'width'  => $product->get_width(),
				'height' => $product->get_height(),
				'unit'   => get_option( 'woocommerce_dimension_unit' ),
			),
			'shipping_required'  => $product->needs_shipping(),
			'shipping_taxable'   => $product->is_shipping_taxable(),
			'shipping_class'     => $product->get_shipping_class(),
			'shipping_class_id'  => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
			'description'        => apply_filters( 'the_content', $product->get_description() ),
			'short_description'  => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
			'reviews_allowed'    => $product->get_reviews_allowed(),
			'average_rating'     => wc_format_decimal( $product->get_average_rating(), 2 ),
			'rating_count'       => $product->get_rating_count(),
			'related_ids'        => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
			'upsell_ids'         => array_map( 'absint', $product->get_upsell_ids() ),
			'cross_sell_ids'     => array_map( 'absint', $product->get_cross_sell_ids() ),
			'categories'         => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
			'tags'               => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
			'images'             => $this->get_images( $product ),
			'featured_src'       => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
			'attributes'         => $this->get_attributes( $product ),
			'downloads'          => $this->get_downloads( $product ),
			'download_limit'     => $product->get_download_limit(),
			'download_expiry'    => $product->get_download_expiry(),
			'download_type'      => 'standard',
			'purchase_note'      => apply_filters( 'the_content', $product->get_purchase_note() ),
			'total_sales'        => $product->get_total_sales(),
			'variations'         => array(),
			'parent'             => array(),
		);
	}

	/**
	 * Get an individual variation's data
	 *
	 * @since 2.1
	 * @param WC_Product $product
	 * @return array
	 */
	private function get_variation_data( $product ) {
		$variations = array();

		foreach ( $product->get_children() as $child_id ) {
			$variation = wc_get_product( $child_id );

			if ( ! $variation || ! $variation->exists() ) {
				continue;
			}

			$variations[] = array(
				'id'                => $variation->get_id(),
				'created_at'        => $this->server->format_datetime( $variation->get_date_created(), false, true ),
				'updated_at'        => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
				'downloadable'      => $variation->is_downloadable(),
				'virtual'           => $variation->is_virtual(),
				'permalink'         => $variation->get_permalink(),
				'sku'               => $variation->get_sku(),
				'price'             => wc_format_decimal( $variation->get_price(), 2 ),
				'regular_price'     => wc_format_decimal( $variation->get_regular_price(), 2 ),
				'sale_price'        => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null,
				'taxable'           => $variation->is_taxable(),
				'tax_status'        => $variation->get_tax_status(),
				'tax_class'         => $variation->get_tax_class(),
				'stock_quantity'    => (int) $variation->get_stock_quantity(),
				'in_stock'          => $variation->is_in_stock(),
				'backordered'       => $variation->is_on_backorder(),
				'purchaseable'      => $variation->is_purchasable(),
				'visible'           => $variation->variation_is_visible(),
				'on_sale'           => $variation->is_on_sale(),
				'weight'            => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
				'dimensions'        => array(
					'length' => $variation->get_length(),
					'width'  => $variation->get_width(),
					'height' => $variation->get_height(),
					'unit'   => get_option( 'woocommerce_dimension_unit' ),
				),
				'shipping_class'    => $variation->get_shipping_class(),
				'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
				'image'             => $this->get_images( $variation ),
				'attributes'        => $this->get_attributes( $variation ),
				'downloads'         => $this->get_downloads( $variation ),
				'download_limit'    => (int) $product->get_download_limit(),
				'download_expiry'   => (int) $product->get_download_expiry(),
			);
		}

		return $variations;
	}

	/**
	 * Get the images for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_images( $product ) {
		$images        = $attachment_ids = array();
		$product_image = $product->get_image_id();

		// Add featured image.
		if ( ! empty( $product_image ) ) {
			$attachment_ids[] = $product_image;
		}

		// add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {

			$attachment_post = get_post( $attachment_id );

			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );

			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'         => (int) $attachment_id,
				'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
				'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
				'src'        => current( $attachment ),
				'title'      => get_the_title( $attachment_id ),
				'alt'        => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'   => $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {

			$images[] = array(
				'id'         => 0,
				'created_at' => $this->server->format_datetime( time() ), // default to now
				'updated_at' => $this->server->format_datetime( time() ),
				'src'        => wc_placeholder_img_src(),
				'title'      => __( 'Placeholder', 'woocommerce' ),
				'alt'        => __( 'Placeholder', 'woocommerce' ),
				'position'   => 0,
			);
		}

		return $images;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int $product_id
	 * @param array $attribute
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_attributes( $product ) {

		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {

			// variation attributes
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {

				// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
				$attributes[] = array(
					'name'   => ucwords( str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => ucwords( wc_attribute_taxonomy_slug( $attribute['name'] ) ),
					'position'  => $attribute['position'],
					'visible'   => (bool) $attribute['is_visible'],
					'variation' => (bool) $attribute['is_variation'],
					'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
				);
			}
		}

		return $attributes;
	}

	/**
	 * Get the downloads for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_downloads( $product ) {

		$downloads = array();

		if ( $product->is_downloadable() ) {

			foreach ( $product->get_downloads() as $file_id => $file ) {

				$downloads[] = array(
					'id'   => $file_id, // do not cast as int as this is a hash
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}
}
class-wc-api-reports.php000064400000032372151550524670011255 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_Reports extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/reports';

	/** @var WC_Admin_Report instance */
	private $report;

	/**
	 * Register the routes for this class
	 *
	 * GET /reports
	 * GET /reports/sales
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /reports
		$routes[ $this->base ] = array(
			array( array( $this, 'get_reports' ),     WC_API_Server::READABLE ),
		);

		# GET /reports/sales
		$routes[ $this->base . '/sales' ] = array(
			array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
		);

		# GET /reports/sales/top_sellers
		$routes[ $this->base . '/sales/top_sellers' ] = array(
			array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get a simple listing of available reports
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_reports() {

		return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
	}

	/**
	 * Get the sales report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_sales_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		// total sales, taxes, shipping, and order count
		$totals = $this->report->get_order_report_data( array(
			'data' => array(
				'_order_total' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'sales',
				),
				'_order_tax' => array(
					'type'            => 'meta',
					'function'        => 'SUM',
					'name'            => 'tax',
				),
				'_order_shipping_tax' => array(
					'type'            => 'meta',
					'function'        => 'SUM',
					'name'            => 'shipping_tax',
				),
				'_order_shipping' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'shipping',
				),
				'ID' => array(
					'type'     => 'post_data',
					'function' => 'COUNT',
					'name'     => 'order_count',
				),
			),
			'filter_range' => true,
		) );

		// total items ordered
		$total_items = absint( $this->report->get_order_report_data( array(
			'data' => array(
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'query_type' => 'get_var',
			'filter_range' => true,
		) ) );

		// total discount used
		$total_discount = $this->report->get_order_report_data( array(
			'data' => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'query_type' => 'get_var',
			'filter_range' => true,
		) );

		// new customers
		$users_query = new WP_User_Query(
			array(
				'fields'  => array( 'user_registered' ),
				'role'    => 'customer',
			)
		);

		$customers = $users_query->get_results();

		foreach ( $customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
				unset( $customers[ $key ] );
			}
		}

		$total_customers = count( $customers );

		// get order totals grouped by period
		$orders = $this->report->get_order_report_data( array(
			'data' => array(
				'_order_total' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_sales',
				),
				'_order_shipping' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_shipping',
				),
				'_order_tax' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_tax',
				),
				'_order_shipping_tax' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_shipping_tax',
				),
				'ID' => array(
					'type'     => 'post_data',
					'function' => 'COUNT',
					'name'     => 'total_orders',
					'distinct' => true,
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'group_by'     => $this->report->group_by_query,
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		// get order item totals grouped by period
		$order_items = $this->report->get_order_report_data( array(
			'data' => array(
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_count',
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'line_item',
					'operator' => '=',
				),
			),
			'group_by'     => $this->report->group_by_query,
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		// get discount totals grouped by period
		$discounts = $this->report->get_order_report_data( array(
			'data' => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'group_by'     => $this->report->group_by_query . ', order_item_name',
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$period_totals = array();

		// setup period totals by ensuring each period in the interval has data
		for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {

			switch ( $this->report->chart_groupby ) {
				case 'day' :
					$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
					break;
				case 'month' :
					$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
					break;
			}

			// set the customer signups for each period
			$customer_count = 0;
			foreach ( $customers as $customer ) {

				if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
					$customer_count++;
				}
 			}

			$period_totals[ $time ] = array(
				'sales'     => wc_format_decimal( 0.00, 2 ),
				'orders'    => 0,
				'items'     => 0,
				'tax'       => wc_format_decimal( 0.00, 2 ),
				'shipping'  => wc_format_decimal( 0.00, 2 ),
				'discount'  => wc_format_decimal( 0.00, 2 ),
				'customers' => $customer_count,
			);
		}

		// add total sales, total order count, total tax and total shipping for each period
		foreach ( $orders as $order ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['sales']    = wc_format_decimal( $order->total_sales, 2 );
			$period_totals[ $time ]['orders']   = (int) $order->total_orders;
			$period_totals[ $time ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		// add total order items for each period
		foreach ( $order_items as $order_item ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
		}

		// add total discount for each period
		foreach ( $discounts as $discount ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
		}

		$sales_data = array(
			'total_sales'       => wc_format_decimal( $totals->sales, 2 ),
			'average_sales'     => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ),
			'total_orders'      => (int) $totals->order_count,
			'total_items'       => $total_items,
			'total_tax'         => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ),
			'total_shipping'    => wc_format_decimal( $totals->shipping, 2 ),
			'total_discount'    => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ),
			'totals_grouped_by' => $this->report->chart_groupby,
			'totals'            => $period_totals,
			'total_customers'   => $total_customers,
		);

		return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Get the top sellers report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_top_sellers_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		$top_sellers = $this->report->get_order_report_data( array(
			'data' => array(
				'_product_id' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => '',
					'name'            => 'product_id',
				),
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'order_by'     => 'order_item_qty DESC',
			'group_by'     => 'product_id',
			'limit'        => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$top_sellers_data = array();

		foreach ( $top_sellers as $top_seller ) {

			$product = wc_get_product( $top_seller->product_id );

			$top_sellers_data[] = array(
				'title'      => $product->get_name(),
				'product_id' => $top_seller->product_id,
				'quantity'   => $top_seller->order_item_qty,
			);
		}

		return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Setup the report object and parse any date filtering
	 *
	 * @since 2.1
	 * @param array $filter date filtering
	 */
	private function setup_report( $filter ) {

		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );

		$this->report = new WC_Admin_Report();

		if ( empty( $filter['period'] ) ) {

			// custom date range
			$filter['period'] = 'custom';

			if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {

				// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
				$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
				$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;

			} else {

				// default custom range to today
				$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
			}
		} else {

			// ensure period is valid
			if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
				$filter['period'] = 'week';
			}

			// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
			// allow "week" for period instead of "7day"
			if ( 'week' === $filter['period'] ) {
				$filter['period'] = '7day';
			}
		}

		$this->report->calculate_current_range( $filter['period'] );
	}

	/**
	 * Verify that the current user has permission to view reports
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param null $id unused
	 * @param null $type unused
	 * @param null $context unused
	 * @return true|WP_Error
	 */
	protected function validate_request( $id = null, $type = null, $context = null ) {

		if ( ! current_user_can( 'view_woocommerce_reports' ) ) {

			return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );

		} else {

			return true;
		}
	}
}
class-wc-api-resource.php000064400000027744151550524670011415 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_Resource {

	/** @var WC_API_Server the API server */
	protected $server;

	/** @var string sub-classes override this to set a resource-specific base route */
	protected $base;

	/**
	 * Setup class
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		$this->server = $server;

		// automatically register routes for sub-classes
		add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );

		// remove fields from responses when requests specify certain fields
		// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
		// still has the fields filtered properly
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {

			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid post object and matches the provided post type
	 * 3) the current user has the proper permissions to read/edit/delete the post
	 *
	 * @since 2.1
	 * @param string|int $id the post ID
	 * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		$id = absint( $id );

		// validate ID
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		// only custom post types have per-post type/permission checks
		if ( 'customer' !== $type ) {

			$post = get_post( $id );

			// for checking permissions, product variations are the same as the product post type
			$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;

			// validate post type
			if ( $type !== $post_type ) {
				return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
			}

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! $this->is_readable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'edit':
					if ( ! $this->is_editable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'delete':
					if ( ! $this->is_deletable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;
			}
		}

		return $id;
	}

	/**
	 * Add common request arguments to argument list before WP_Query is run
	 *
	 * @since 2.1
	 * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
	 * @param array $request_args arguments provided in the request
	 * @return array
	 */
	protected function merge_query_args( $base_args, $request_args ) {

		$args = array();

		// date
		if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {

			$args['date_query'] = array();

			// resources created after specified date
			if ( ! empty( $request_args['created_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
			}

			// resources created before specified date
			if ( ! empty( $request_args['created_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
			}

			// resources updated after specified date
			if ( ! empty( $request_args['updated_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
			}

			// resources updated before specified date
			if ( ! empty( $request_args['updated_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
			}
		}

		// search
		if ( ! empty( $request_args['q'] ) ) {
			$args['s'] = $request_args['q'];
		}

		// resources per response
		if ( ! empty( $request_args['limit'] ) ) {
			$args['posts_per_page'] = $request_args['limit'];
		}

		// resource offset
		if ( ! empty( $request_args['offset'] ) ) {
			$args['offset'] = $request_args['offset'];
		}

		// resource page
		$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;

		return array_merge( $base_args, $args );
	}

	/**
	 * Add meta to resources when requested by the client. Meta is added as a top-level
	 * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
	 *
	 * @since 2.1
	 * @param array $data the resource data
	 * @param object $resource the resource object (e.g WC_Order)
	 * @return mixed
	 */
	public function maybe_add_meta( $data, $resource ) {

		if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {

			// don't attempt to add meta more than once
			if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
				return $data;
			}

			// define the top-level property name for the meta
			switch ( get_class( $resource ) ) {

				case 'WC_Order':
					$meta_name = 'order_meta';
					break;

				case 'WC_Coupon':
					$meta_name = 'coupon_meta';
					break;

				case 'WP_User':
					$meta_name = 'customer_meta';
					break;

				default:
					$meta_name = 'product_meta';
					break;
			}

			if ( is_a( $resource, 'WP_User' ) ) {

				// customer meta
				$meta = (array) get_user_meta( $resource->ID );

			} else {

				// coupon/order/product meta
				$meta = (array) get_post_meta( $resource->get_id() );
			}

			foreach ( $meta as $meta_key => $meta_value ) {

				// don't add hidden meta by default
				if ( ! is_protected_meta( $meta_key ) ) {
					$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
				}
			}
		}

		return $data;
	}

	/**
	 * Restrict the fields included in the response if the request specified certain only certain fields should be returned
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
	 * @param array|string the requested list of fields to include in the response
	 * @return array response data
	 */
	public function filter_response_fields( $data, $resource, $fields ) {

		if ( ! is_array( $data ) || empty( $fields ) ) {
			return $data;
		}

		$fields = explode( ',', $fields );
		$sub_fields = array();

		// get sub fields
		foreach ( $fields as $field ) {

			if ( false !== strpos( $field, '.' ) ) {

				list( $name, $value ) = explode( '.', $field );

				$sub_fields[ $name ] = $value;
			}
		}

		// iterate through top-level fields
		foreach ( $data as $data_field => $data_value ) {

			// if a field has sub-fields and the top-level field has sub-fields to filter
			if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {

				// iterate through each sub-field
				foreach ( $data_value as $sub_field => $sub_field_value ) {

					// remove non-matching sub-fields
					if ( ! in_array( $sub_field, $sub_fields ) ) {
						unset( $data[ $data_field ][ $sub_field ] );
					}
				}
			} else {

				// remove non-matching top-level fields
				if ( ! in_array( $data_field, $fields ) ) {
					unset( $data[ $data_field ] );
				}
			}
		}

		return $data;
	}

	/**
	 * Delete a given resource
	 *
	 * @since 2.1
	 * @param int $id the resource ID
	 * @param string $type the resource post type, or `customer`
	 * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
	 * @return array|WP_Error
	 */
	protected function delete( $id, $type, $force = false ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		if ( 'customer' === $type ) {

			$result = wp_delete_user( $id );

			if ( $result ) {
				return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
			} else {
				return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
			}
		} else {

			// delete order/coupon/product
			$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );

			if ( ! $result ) {
				return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
			}

			if ( $force ) {
				return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );

			} else {

				$this->server->send_status( '202' );

				return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
			}
		}
	}


	/**
	 * Checks if the given post is readable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_readable( $post ) {

		return $this->check_permission( $post, 'read' );
	}

	/**
	 * Checks if the given post is editable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_editable( $post ) {

		return $this->check_permission( $post, 'edit' );

	}

	/**
	 * Checks if the given post is deletable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_deletable( $post ) {

		return $this->check_permission( $post, 'delete' );
	}

	/**
	 * Checks the permissions for the current user given a post and context
	 *
	 * @since 2.1
	 * @param WP_Post|int $post
	 * @param string $context the type of permission to check, either `read`, `write`, or `delete`
	 * @return bool true if the current user has the permissions to perform the context on the post
	 */
	private function check_permission( $post, $context ) {

		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = get_post( $post );
		}

		if ( is_null( $post ) ) {
			return false;
		}

		$post_type = get_post_type_object( $post->post_type );

		if ( 'read' === $context ) {
			return current_user_can( $post_type->cap->read_private_posts, $post->ID );
		} elseif ( 'edit' === $context ) {
			return current_user_can( $post_type->cap->edit_post, $post->ID );
		} elseif ( 'delete' === $context ) {
			return current_user_can( $post_type->cap->delete_post, $post->ID );
		} else {
			return false;
		}
	}
}
class-wc-api-server.php000064400000050302151550524670011056 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles REST API requests
 *
 * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
 * Many thanks to Ryan McCue and any other contributors!
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

require_once ABSPATH . 'wp-admin/includes/admin.php';

class WC_API_Server {

	const METHOD_GET    = 1;
	const METHOD_POST   = 2;
	const METHOD_PUT    = 4;
	const METHOD_PATCH  = 8;
	const METHOD_DELETE = 16;

	const READABLE   = 1;  // GET
	const CREATABLE  = 2;  // POST
	const EDITABLE   = 14; // POST | PUT | PATCH
	const DELETABLE  = 16; // DELETE
	const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE

	/**
	 * Does the endpoint accept a raw request body?
	 */
	const ACCEPT_RAW_DATA = 64;

	/** Does the endpoint accept a request body? (either JSON or XML) */
	const ACCEPT_DATA = 128;

	/**
	 * Should we hide this endpoint from the index?
	 */
	const HIDDEN_ENDPOINT = 256;

	/**
	 * Map of HTTP verbs to constants
	 * @var array
	 */
	public static $method_map = array(
		'HEAD'   => self::METHOD_GET,
		'GET'    => self::METHOD_GET,
		'POST'   => self::METHOD_POST,
		'PUT'    => self::METHOD_PUT,
		'PATCH'  => self::METHOD_PATCH,
		'DELETE' => self::METHOD_DELETE,
	);

	/**
	 * Requested path (relative to the API root, wp-json.php)
	 *
	 * @var string
	 */
	public $path = '';

	/**
	 * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
	 *
	 * @var string
	 */
	public $method = 'HEAD';

	/**
	 * Request parameters
	 *
	 * This acts as an abstraction of the superglobals
	 * (GET => $_GET, POST => $_POST)
	 *
	 * @var array
	 */
	public $params = array( 'GET' => array(), 'POST' => array() );

	/**
	 * Request headers
	 *
	 * @var array
	 */
	public $headers = array();

	/**
	 * Request files (matches $_FILES)
	 *
	 * @var array
	 */
	public $files = array();

	/**
	 * Request/Response handler, either JSON by default
	 * or XML if requested by client
	 *
	 * @var WC_API_Handler
	 */
	public $handler;


	/**
	 * Setup class and set request/response handler
	 *
	 * @since 2.1
	 * @param $path
	 */
	public function __construct( $path ) {

		if ( empty( $path ) ) {
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
				$path = $_SERVER['PATH_INFO'];
			} else {
				$path = '/';
			}
		}

		$this->path           = $path;
		$this->method         = $_SERVER['REQUEST_METHOD'];
		$this->params['GET']  = $_GET;
		$this->params['POST'] = $_POST;
		$this->headers        = $this->get_headers( $_SERVER );
		$this->files          = $_FILES;

		// Compatibility for clients that can't use PUT/PATCH/DELETE
		if ( isset( $_GET['_method'] ) ) {
			$this->method = strtoupper( $_GET['_method'] );
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
			$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
		}

		// determine type of request/response and load handler, JSON by default
		if ( $this->is_json_request() ) {
			$handler_class = 'WC_API_JSON_Handler';
		} elseif ( $this->is_xml_request() ) {
			$handler_class = 'WC_API_XML_Handler';
		} else {
			$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
		}

		$this->handler = new $handler_class();
	}

	/**
	 * Check authentication for the request
	 *
	 * @since 2.1
	 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
	 */
	public function check_authentication() {

		// allow plugins to remove default authentication or add their own authentication
		$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );

		// API requests run under the context of the authenticated user
		if ( is_a( $user, 'WP_User' ) ) {
			wp_set_current_user( $user->ID );
		} elseif ( ! is_wp_error( $user ) ) {
			// WP_Errors are handled in serve_request()
			$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
		}

		return $user;
	}

	/**
	 * Convert an error to an array
	 *
	 * This iterates over all error codes and messages to change it into a flat
	 * array. This enables simpler client behaviour, as it is represented as a
	 * list in JSON rather than an object/map
	 *
	 * @since 2.1
	 * @param WP_Error $error
	 * @return array List of associative arrays with code and message keys
	 */
	protected function error_to_array( $error ) {
		$errors = array();
		foreach ( (array) $error->errors as $code => $messages ) {
			foreach ( (array) $messages as $message ) {
				$errors[] = array( 'code' => $code, 'message' => $message );
			}
		}
		return array( 'errors' => $errors );
	}

	/**
	 * Handle serving an API request
	 *
	 * Matches the current server URI to a route and runs the first matching
	 * callback then outputs a JSON representation of the returned value.
	 *
	 * @since 2.1
	 * @uses WC_API_Server::dispatch()
	 */
	public function serve_request() {

		do_action( 'woocommerce_api_server_before_serve', $this );

		$this->header( 'Content-Type', $this->handler->get_content_type(), true );

		// the API is enabled by default
		if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {

			$this->send_status( 404 );

			echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );

			return;
		}

		$result = $this->check_authentication();

		// if authorization check was successful, dispatch the request
		if ( ! is_wp_error( $result ) ) {
			$result = $this->dispatch();
		}

		// handle any dispatch errors
		if ( is_wp_error( $result ) ) {
			$data = $result->get_error_data();
			if ( is_array( $data ) && isset( $data['status'] ) ) {
				$this->send_status( $data['status'] );
			}

			$result = $this->error_to_array( $result );
		}

		// This is a filter rather than an action, since this is designed to be
		// re-entrant if needed
		$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );

		if ( ! $served ) {

			if ( 'HEAD' === $this->method ) {
				return;
			}

			echo $this->handler->generate_response( $result );
		}
	}

	/**
	 * Retrieve the route map
	 *
	 * The route map is an associative array with path regexes as the keys. The
	 * value is an indexed array with the callback function/method as the first
	 * item, and a bitmask of HTTP methods as the second item (see the class
	 * constants).
	 *
	 * Each route can be mapped to more than one callback by using an array of
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
	 * and POST requests to another.
	 *
	 * Note that the path regexes (array keys) must have @ escaped, as this is
	 * used as the delimiter with preg_match()
	 *
	 * @since 2.1
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
	 */
	public function get_routes() {

		// index added by default
		$endpoints = array(

			'/' => array( array( $this, 'get_index' ), self::READABLE ),
		);

		$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );

		// Normalise the endpoints
		foreach ( $endpoints as $route => &$handlers ) {
			if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
				$handlers = array( $handlers );
			}
		}

		return $endpoints;
	}

	/**
	 * Match the request to a callback and call it
	 *
	 * @since 2.1
	 * @return mixed The value returned by the callback, or a WP_Error instance
	 */
	public function dispatch() {

		switch ( $this->method ) {

			case 'HEAD':
			case 'GET':
				$method = self::METHOD_GET;
				break;

			case 'POST':
				$method = self::METHOD_POST;
				break;

			case 'PUT':
				$method = self::METHOD_PUT;
				break;

			case 'PATCH':
				$method = self::METHOD_PATCH;
				break;

			case 'DELETE':
				$method = self::METHOD_DELETE;
				break;

			default:
				return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
		}

		foreach ( $this->get_routes() as $route => $handlers ) {
			foreach ( $handlers as $handler ) {
				$callback = $handler[0];
				$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;

				if ( ! ( $supported & $method ) ) {
					continue;
				}

				$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );

				if ( ! $match ) {
					continue;
				}

				if ( ! is_callable( $callback ) ) {
					return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
				}

				$args = array_merge( $args, $this->params['GET'] );
				if ( $method & self::METHOD_POST ) {
					$args = array_merge( $args, $this->params['POST'] );
				}
				if ( $supported & self::ACCEPT_DATA ) {
					$data = $this->handler->parse_body( $this->get_raw_data() );
					$args = array_merge( $args, array( 'data' => $data ) );
				} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
					$data = $this->get_raw_data();
					$args = array_merge( $args, array( 'data' => $data ) );
				}

				$args['_method']  = $method;
				$args['_route']   = $route;
				$args['_path']    = $this->path;
				$args['_headers'] = $this->headers;
				$args['_files']   = $this->files;

				$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );

				// Allow plugins to halt the request via this filter
				if ( is_wp_error( $args ) ) {
					return $args;
				}

				$params = $this->sort_callback_params( $callback, $args );
				if ( is_wp_error( $params ) ) {
					return $params;
				}

				return call_user_func_array( $callback, $params );
			}
		}

		return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
	}

	/**
	 * Sort parameters by order specified in method declaration
	 *
	 * Takes a callback and a list of available params, then filters and sorts
	 * by the parameters the method actually needs, using the Reflection API
	 *
	 * @since 2.1
	 *
	 * @param callable|array $callback the endpoint callback
	 * @param array $provided the provided request parameters
	 *
	 * @return array|WP_Error
	 */
	protected function sort_callback_params( $callback, $provided ) {
		if ( is_array( $callback ) ) {
			$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
		} else {
			$ref_func = new ReflectionFunction( $callback );
		}

		$wanted = $ref_func->getParameters();
		$ordered_parameters = array();

		foreach ( $wanted as $param ) {
			if ( isset( $provided[ $param->getName() ] ) ) {
				// We have this parameters in the list to choose from
				$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
			} elseif ( $param->isDefaultValueAvailable() ) {
				// We don't have this parameter, but it's optional
				$ordered_parameters[] = $param->getDefaultValue();
			} else {
				// We don't have this parameter and it wasn't optional, abort!
				return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
			}
		}
		return $ordered_parameters;
	}

	/**
	 * Get the site index.
	 *
	 * This endpoint describes the capabilities of the site.
	 *
	 * @since 2.1
	 * @return array Index entity
	 */
	public function get_index() {

		// General site data
		$available = array(
			'store' => array(
				'name'        => get_option( 'blogname' ),
				'description' => get_option( 'blogdescription' ),
				'URL'         => get_option( 'siteurl' ),
				'wc_version'  => WC()->version,
				'routes'      => array(),
				'meta'        => array(
					'timezone'			 => wc_timezone_string(),
					'currency'       	 => get_woocommerce_currency(),
					'currency_format'    => get_woocommerce_currency_symbol(),
					'tax_included'   	 => wc_prices_include_tax(),
					'weight_unit'    	 => get_option( 'woocommerce_weight_unit' ),
					'dimension_unit' 	 => get_option( 'woocommerce_dimension_unit' ),
					'ssl_enabled'    	 => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
					'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
					'links'          	 => array(
						'help' => 'https://woocommerce.github.io/woocommerce/rest-api/',
					),
				),
			),
		);

		// Find the available routes
		foreach ( $this->get_routes() as $route => $callbacks ) {
			$data = array();

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
			$methods = array();
			foreach ( self::$method_map as $name => $bitmask ) {
				foreach ( $callbacks as $callback ) {
					// Skip to the next route if any callback is hidden
					if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
						continue 3;
					}

					if ( $callback[1] & $bitmask ) {
						$data['supports'][] = $name;
					}

					if ( $callback[1] & self::ACCEPT_DATA ) {
						$data['accepts_data'] = true;
					}

					// For non-variable routes, generate links
					if ( strpos( $route, '<' ) === false ) {
						$data['meta'] = array(
							'self' => get_woocommerce_api_url( $route ),
						);
					}
				}
			}
			$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
		}
		return apply_filters( 'woocommerce_api_index', $available );
	}

	/**
	 * Send a HTTP status code
	 *
	 * @since 2.1
	 * @param int $code HTTP status
	 */
	public function send_status( $code ) {
		status_header( $code );
	}

	/**
	 * Send a HTTP header
	 *
	 * @since 2.1
	 * @param string $key Header key
	 * @param string $value Header value
	 * @param boolean $replace Should we replace the existing header?
	 */
	public function header( $key, $value, $replace = true ) {
		header( sprintf( '%s: %s', $key, $value ), $replace );
	}

	/**
	 * Send a Link header
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple
	 *
	 * @link http://tools.ietf.org/html/rfc5988
	 * @link http://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @since 2.1
	 * @param string $rel Link relation. Either a registered type, or an absolute URL
	 * @param string $link Target IRI for the link
	 * @param array $other Other parameters to send, as an associative array
	 */
	public function link_header( $rel, $link, $other = array() ) {

		$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );

		foreach ( $other as $key => $value ) {

			if ( 'title' == $key ) {

				$value = '"' . $value . '"';
			}

			$header .= '; ' . $key . '=' . $value;
		}

		$this->header( 'Link', $header, false );
	}

	/**
	 * Send pagination headers for resources
	 *
	 * @since 2.1
	 * @param WP_Query|WP_User_Query $query
	 */
	public function add_pagination_headers( $query ) {

		// WP_User_Query
		if ( is_a( $query, 'WP_User_Query' ) ) {

			$page        = $query->page;
			$single      = count( $query->get_results() ) == 1;
			$total       = $query->get_total();
			$total_pages = $query->total_pages;

		// WP_Query
		} else {

			$page        = $query->get( 'paged' );
			$single      = $query->is_single();
			$total       = $query->found_posts;
			$total_pages = $query->max_num_pages;
		}

		if ( ! $page ) {
			$page = 1;
		}

		$next_page = absint( $page ) + 1;

		if ( ! $single ) {

			// first/prev
			if ( $page > 1 ) {
				$this->link_header( 'first', $this->get_paginated_url( 1 ) );
				$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
			}

			// next
			if ( $next_page <= $total_pages ) {
				$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
			}

			// last
			if ( $page != $total_pages ) {
				$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
			}
		}

		$this->header( 'X-WC-Total', $total );
		$this->header( 'X-WC-TotalPages', $total_pages );

		do_action( 'woocommerce_api_pagination_headers', $this, $query );
	}

	/**
	 * Returns the request URL with the page query parameter set to the specified page
	 *
	 * @since 2.1
	 * @param int $page
	 * @return string
	 */
	private function get_paginated_url( $page ) {

		// remove existing page query param
		$request = remove_query_arg( 'page' );

		// add provided page query param
		$request = urldecode( add_query_arg( 'page', $page, $request ) );

		// get the home host
		$host = parse_url( get_home_url(), PHP_URL_HOST );

		return set_url_scheme( "http://{$host}{$request}" );
	}

	/**
	 * Retrieve the raw request entity (body)
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_raw_data() {
		return file_get_contents( 'php://input' );
	}

	/**
	 * Parse an RFC3339 datetime into a MySQl datetime
	 *
	 * Invalid dates default to unix epoch
	 *
	 * @since 2.1
	 * @param string $datetime RFC3339 datetime
	 * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
	 */
	public function parse_datetime( $datetime ) {

		// Strip millisecond precision (a full stop followed by one or more digits)
		if ( strpos( $datetime, '.' ) !== false ) {
			$datetime = preg_replace( '/\.\d+/', '', $datetime );
		}

		// default timezone to UTC
		$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );

		try {

			$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );

		} catch ( Exception $e ) {

			$datetime = new DateTime( '@0' );

		}

		return $datetime->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
	 *
	 * @since 2.1
	 * @param int|string $timestamp unix timestamp or MySQL datetime
	 * @param bool $convert_to_utc
	 * @param bool $convert_to_gmt Use GMT timezone.
	 * @return string RFC3339 datetime
	 */
	public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
		if ( $convert_to_gmt ) {
			if ( is_numeric( $timestamp ) ) {
				$timestamp = date( 'Y-m-d H:i:s', $timestamp );
			}

			$timestamp = get_gmt_from_date( $timestamp );
		}

		if ( $convert_to_utc ) {
			$timezone = new DateTimeZone( wc_timezone_string() );
		} else {
			$timezone = new DateTimeZone( 'UTC' );
		}

		try {

			if ( is_numeric( $timestamp ) ) {
				$date = new DateTime( "@{$timestamp}" );
			} else {
				$date = new DateTime( $timestamp, $timezone );
			}

			// convert to UTC by adjusting the time based on the offset of the site's timezone
			if ( $convert_to_utc ) {
				$date->modify( -1 * $date->getOffset() . ' seconds' );
			}
		} catch ( Exception $e ) {

			$date = new DateTime( '@0' );
		}

		return $date->format( 'Y-m-d\TH:i:s\Z' );
	}

	/**
	 * Extract headers from a PHP-style $_SERVER array
	 *
	 * @since 2.1
	 * @param array $server Associative array similar to $_SERVER
	 * @return array Headers extracted from the input
	 */
	public function get_headers( $server ) {
		$headers = array();
		// CONTENT_* headers are not prefixed with HTTP_
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );

		foreach ( $server as $key => $value ) {
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
				$headers[ substr( $key, 5 ) ] = $value;
			} elseif ( isset( $additional[ $key ] ) ) {
				$headers[ $key ] = $value;
			}
		}

		return $headers;
	}

	/**
	 * Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or
	 * the HTTP ACCEPT header
	 *
	 * @since 2.1
	 * @return bool
	 */
	private function is_json_request() {

		// check path
		if ( false !== stripos( $this->path, '.json' ) ) {
			return true;
		}

		// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627
		if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or
	 * the HTTP ACCEPT header
	 *
	 * @since 2.1
	 * @return bool
	 */
	private function is_xml_request() {

		// check path
		if ( false !== stripos( $this->path, '.xml' ) ) {
			return true;
		}

		// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376
		if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) {
			return true;
		}

		return false;
	}
}
class-wc-api-xml-handler.php000064400000016707151550524670011776 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing XML request bodies and generating XML responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

class WC_API_XML_Handler implements WC_API_Handler {

	/** @var XMLWriter instance */
	private $xml;

	/**
	 * Add some response filters
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// tweak sales report response data
		add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 );

		// tweak product response data
		add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 );
	}

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return 'application/xml; charset=' . get_option( 'blog_charset' );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $data the raw request body
	 * @return array
	 */
	public function parse_body( $data ) {

		// TODO: implement simpleXML parsing
	}

	/**
	 * Generate an XML response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {

		$this->xml = new XMLWriter();

		$this->xml->openMemory();

		$this->xml->setIndent( true );

		$this->xml->startDocument( '1.0', 'UTF-8' );

		$root_element = key( $data );

		$data = $data[ $root_element ];

		switch ( $root_element ) {

			case 'orders':
				$data = array( 'order' => $data );
				break;

			case 'order_notes':
				$data = array( 'order_note' => $data );
				break;

			case 'customers':
				$data = array( 'customer' => $data );
				break;

			case 'coupons':
				$data = array( 'coupon' => $data );
				break;

			case 'products':
				$data = array( 'product' => $data );
				break;

			case 'product_reviews':
				$data = array( 'product_review' => $data );
				break;

			default:
				$data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element );
				break;
		}

		// generate xml starting with the root element and recursively generating child elements
		$this->array_to_xml( $root_element, $data );

		$this->xml->endDocument();

		return $this->xml->outputMemory();
	}

	/**
	 * Convert array into XML by recursively generating child elements
	 *
	 * @since 2.1
	 * @param string|array $element_key - name for element, e.g. <OrderID>
	 * @param string|array $element_value - value for element, e.g. 1234
	 * @return string - generated XML
	 */
	private function array_to_xml( $element_key, $element_value = array() ) {

		if ( is_array( $element_value ) ) {

			// handle attributes
			if ( '@attributes' === $element_key ) {
				foreach ( $element_value as $attribute_key => $attribute_value ) {

					$this->xml->startAttribute( $attribute_key );
					$this->xml->text( $attribute_value );
					$this->xml->endAttribute();
				}
				return;
			}

			// handle multi-elements (e.g. multiple <Order> elements)
			if ( is_numeric( key( $element_value ) ) ) {

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {

					$this->xml->startElement( $element_key );

					foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
						$this->array_to_xml( $sibling_element_key, $sibling_element_value );
					}

					$this->xml->endElement();
				}
			} else {

				// start root element
				$this->xml->startElement( $element_key );

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {
					$this->array_to_xml( $child_element_key, $child_element_value );
				}

				// end root element
				$this->xml->endElement();
			}
		} else {

			// handle single elements
			if ( '@value' == $element_key ) {

				$this->xml->text( $element_value );

			} else {

				// wrap element in CDATA tags if it contains illegal characters
				if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {

					$this->xml->startElement( $element_key );
					$this->xml->writeCdata( $element_value );
					$this->xml->endElement();

				} else {

					$this->xml->writeElement( $element_key, $element_value );
				}
			}

			return;
		}
	}

	/**
	 * Adjust the sales report array format to change totals keyed with the sales date to become an
	 * attribute for the totals element instead
	 *
	 * @since 2.1
	 * @param array $data
	 * @return array
	 */
	public function format_sales_report_data( $data ) {

		if ( ! empty( $data['totals'] ) ) {

			foreach ( $data['totals'] as $date => $totals ) {

				unset( $data['totals'][ $date ] );

				$data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals );
			}
		}

		return $data;
	}

	/**
	 * Adjust the product data to handle options for attributes without a named child element and other
	 * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) )
	 *
	 * Note that the parent product data for variations is also adjusted in the same manner as needed
	 *
	 * @since 2.1
	 * @param array $data
	 * @return array
	 */
	public function format_product_data( $data ) {

		// handle attribute values
		if ( ! empty( $data['attributes'] ) ) {

			foreach ( $data['attributes'] as $attribute_key => $attribute ) {

				if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {

					foreach ( $attribute['options'] as $option_key => $option ) {

						unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] );

						$data['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
					}
				}
			}
		}

		// simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data
		// array to define a child element name for each field
		$fields_to_fix = array(
			'related_ids'    => 'related_id',
			'upsell_ids'     => 'upsell_id',
			'cross_sell_ids' => 'cross_sell_id',
			'categories'     => 'category',
			'tags'           => 'tag',
		);

		foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {

			if ( ! empty( $data[ $parent_field_name ] ) ) {

				foreach ( $data[ $parent_field_name ] as $field_key => $field ) {

					unset( $data[ $parent_field_name ][ $field_key ] );

					$data[ $parent_field_name ][ $child_field_name ][] = array( $field );
				}
			}
		}

		// handle adjusting the parent product for variations
		if ( ! empty( $data['parent'] ) ) {

			// attributes
			if ( ! empty( $data['parent']['attributes'] ) ) {

				foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) {

					if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {

						foreach ( $attribute['options'] as $option_key => $option ) {

							unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] );

							$data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
						}
					}
				}
			}

			// fields
			foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {

				if ( ! empty( $data['parent'][ $parent_field_name ] ) ) {

					foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) {

						unset( $data['parent'][ $parent_field_name ][ $field_key ] );

						$data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field );
					}
				}
			}
		}

		return $data;
	}
}
interface-wc-api-handler.php000064400000001541151550524670012021 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

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

interface WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * This should return the proper HTTP content-type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type();

	/**
	 * Parse the raw request body entity into an array
	 *
	 * @since 2.1
	 * @param string $data
	 * @return array
	 */
	public function parse_body( $data );

	/**
	 * Generate a response from an array of data
	 *
	 * @since 2.1
	 * @param array $data
	 * @return string
	 */
	public function generate_response( $data );

}