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/legacy.tar
class-yith-wcwl-admin-legacy.php000064400000001344151541041210012633 0ustar00<?php
/**
 * Legacy admin init class
 *
 * @package YITH\Wishlist\Legacy\Classes
 * @author  YITH <plugins@yithemes.com>
 */

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

if ( ! class_exists( 'YITH_WCWL_Admin_Legacy' ) ) {
	/**
	 * Initiator class. Create and populate admin views.
	 *
	 * @since 4.0.0
	 */
	abstract class YITH_WCWL_Admin_Legacy {
		/**
		 * Register wishlist panel
		 *
		 * @return void
		 * @depreacted since 4.0.0
		 * @see        YITH_WCWL_Admin_Panel::register_panel
		 */
		public function register_panel() {
			wc_deprecated_function( 'YITH_WCWL_Admin::register_panel()', '4.0.0', 'YITH_WCWL_Admin_Panel::register_panel()' );
			YITH_WCWL_Admin_Panel::get_instance()->register_panel();
		}
	}
}

class-yith-wcwl-deprecated-hooks.php000064400000015017151541041210013524 0ustar00<?php
/**
 * Deprecated hooks and filters
 *
 * @package YITH\Wishlist\Classes\Legacy
 * @author  YITH <plugins@yithemes.com>
 * @version 3.0.24
 */

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

if ( ! class_exists( 'YITH_WCWL_Deprecated_Hooks' ) ) {
	/**
	 * Class that manages deprecated hooks and filters
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Deprecated_Hooks {

		/**
		 * List of deprecated hooks
		 *
		 * @var array
		 */
		private $deprecated_hooks = array(
			'yith_wcwl_add_to_wishlist_button_html'                     => 'yith_wcwl_add_to_wishlisth_button_html',
			'yith_wcwl_adding_to_wishlist_product_id'                   => 'yith_wcwl_adding_to_wishlist_prod_id',
			'yith_wcwl_add_to_wishlist_button_move_label_option'        => 'yith_wcwl_move_from_wishlist_label',
			'yith_wcwl_add_to_wishlist_button_modal_label_option'       => 'yith_wcwl_button_popup_label',
			'yith_wcwl_add_to_wishlist_button_icon_option'              => 'yith_wcwl_button_icon',
			'yith_wcwl_add_to_wishlist_button_label_option'             => 'yith_wcwl_button_label',
			'yith_wcwl_add_to_wishlist_button_remove_label_option'      => 'yith_wcwl_remove_from_wishlist_label',
			'yith_wcwl_add_to_wishlist_button_added_icon_option'        => 'yith_wcwl_button_added_icon',
			'yith_wcwl_add_to_wishlist_button_custom_icon_alt_option'   => 'yith_wcwl_custom_icon_alt',
			'yith_wcwl_add_to_wishlist_button_custom_icon_width_option' => 'yith_wcwl_custom_width',
			'yith_wcwl_add_to_wishlist_button_browse_label_option'      => 'yith_wcwl_browse_wishlist_label',
			'yith_wcwl_add_to_wishlist_button_already_in_label_option'  => 'yith_wcwl_product_already_in_wishlist_text_button',
			'yith_wcwl_add_to_wishlist_button_added_label_option'       => 'yith_wcwl_product_added_to_wishlist_message_button',
		);

		/**
		 * List of deprecated hooks
		 *
		 * @var array
		 */
		private $deprecated_version = array(
			'yith_wcwl_add_to_wishlist_button_html'                     => '4.0.0',
			'yith_wcwl_adding_to_wishlist_product_id'                   => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_move_label_option'        => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_modal_label_option'       => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_icon_option'              => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_label_option'             => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_remove_label_option'      => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_added_icon_option'        => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_custom_icon_alt_option'   => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_custom_icon_width_option' => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_browse_label_option'      => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_already_in_label_option'  => '4.0.0',
			'yith_wcwl_add_to_wishlist_button_added_label_option'       => '4.0.0',
		);

		/**
		 * Constructor.
		 */
		public function __construct() {
			$new_hooks = array_keys( $this->deprecated_hooks );
			array_walk( $new_hooks, array( $this, 'hook_in' ) );

			add_filter( 'yith_wcwl_adding_to_wishlist_args', array( $this, 'apply_deprecated_filters_when_adding_to_wishlist' ) );
		}

		/**
		 * Hook into the new hook so we can handle deprecated hooks once fired.
		 *
		 * @param string $hook_name Hook name.
		 */
		public function hook_in( $hook_name ) {
			add_filter( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 );
		}

		/**
		 * If the hook is Deprecated, call the old hooks here.
		 */
		public function maybe_handle_deprecated_hook() {
			$new_hook          = current_filter();
			$old_hook          = isset( $this->deprecated_hooks[ $new_hook ] ) ? $this->deprecated_hooks[ $new_hook ] : false;
			$new_callback_args = func_get_args();
			$return_value      = $new_callback_args[ 0 ];

			if ( $old_hook && has_action( $old_hook ) ) {
				$this->display_notice( $old_hook, $new_hook );
				$return_value = apply_filters_ref_array( $old_hook, $new_callback_args );
			}

			return $return_value;
		}

		/**
		 * Display a deprecated notice for old hooks.
		 *
		 * @param string $old_hook Old hook.
		 * @param string $new_hook New hook.
		 */
		protected function display_notice( $old_hook, $new_hook ) {
			$deprecated_version = isset( $this->deprecated_version[ $old_hook ] ) ? $this->deprecated_version[ $old_hook ] : YITH_WCWL_VERSION;

			wc_deprecated_hook( esc_html( $old_hook ), esc_html( $deprecated_version ), esc_html( $new_hook ) );
		}

		/* === DEPRECATED FILTERS COMPATIBILITY === */

		/**
		 * Adding support for deprecated filters when adding a product in wishlist.
		 *
		 * TODO: mark the used filters as deprecated without any replacement
		 *
		 * @param array $args List of arguments when adding to wishlist
		 *
		 * @return array
		 */
		public function apply_deprecated_filters_when_adding_to_wishlist( $args ) {
			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_prod_id
			 *
			 * Filter the ID of the product added to the wishlist.
			 *
			 * @param int $product_id Product ID
			 *
			 * @return int
			 */
			$args[ 'product_id' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_product_id', $args[ 'product_id' ] );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_wishlist_id
			 *
			 * Filter the wishlist ID where the products are added to.
			 *
			 * @param int $wishlist_id Wishlist ID
			 *
			 * @return int
			 */
			$args[ 'wishlist_id' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_id', $args[ 'wishlist_id' ] );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_quantity
			 *
			 * Filter the quantity of the product added to the wishlist.
			 *
			 * @param int $quantity   Product quantity
			 * @param int $product_id Product ID
			 *
			 * @return int
			 */
			$args[ 'quantity' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_quantity', intval( $args[ 'quantity' ] ), $args[ 'product_id' ] );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_user_id
			 *
			 * Filter the user ID saved in the wishlist.
			 *
			 * @param int $user_id User ID
			 *
			 * @return int
			 */
			$args[ 'user_id' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_user_id', intval( $args[ 'user_id' ] ) );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_dateadded
			 *
			 * Filter the date when the wishlist was created.
			 *
			 * @param int $date_added Date when the wishlist was created (timestamp)
			 *
			 * @return int
			 */
			$args[ 'dateadded' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_dateadded', $args[ 'dateadded' ] );

			return $args;
		}
	}
}

return new YITH_WCWL_Deprecated_Hooks();
class-yith-wcwl-frontend-legacy.php000064400000026463151541041210013373 0ustar00<?php
/**
 * Frontend legacy class
 *
 * @package YITH\Wishlist\Legacy
 * @author  YITH <plugins@yithemes.com>
 */

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

if ( ! class_exists( 'YITH_WCWL_Frontend_Legacy' ) ) {
	/**
	 * Frontend legacy class
	 */
	class YITH_WCWL_Frontend_Legacy {
		use YITH_WCWL_Extensible_Singleton_Trait;
		use YITH_WCWL_Rendering_Method_Access_Trait;

		/* === SCRIPTS AND ASSETS === */

		/**
		 * Register styles required by the plugin
		 *
		 * @return void
		 */
		public function register_styles() {
			$woocommerce_base = WC()->template_path();
			$assets_path      = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/';

			// register dependencies.
			wp_register_style( 'jquery-selectBox', YITH_WCWL_URL . 'assets/css/jquery.selectBox.css', array(), '1.2.0' );
			wp_register_style( 'woocommerce_prettyPhoto_css', $assets_path . 'css/prettyPhoto.css', array(), '3.1.6' );

			/**
			 * APPLY_FILTERS: yith_wcwl_main_style_deps
			 *
			 * Filter the style dependencies to be used in the plugin.
			 *
			 * @param array $deps Array of style dependencies
			 *
			 * @return array
			 */
			$deps = apply_filters( 'yith_wcwl_main_style_deps', array( 'jquery-selectBox', 'woocommerce_prettyPhoto_css' ) );

			// register main style.
			$located = locate_template(
				array(
					$woocommerce_base . 'wishlist.css',
					'wishlist.css',
				)
			);

			if ( ! $located ) {
				wp_register_style( 'yith-wcwl-main', YITH_WCWL_URL . 'assets/css/style.css', $deps, YITH_WCWL_VERSION );
			} else {
				$stylesheet_directory     = get_stylesheet_directory();
				$stylesheet_directory_uri = get_stylesheet_directory_uri();
				$template_directory       = get_template_directory();
				$template_directory_uri   = get_template_directory_uri();

				$style_url = ( strpos( $located, $stylesheet_directory ) !== false ) ? str_replace( $stylesheet_directory, $stylesheet_directory_uri, $located ) : str_replace( $template_directory, $template_directory_uri, $located );

				wp_register_style( 'yith-wcwl-user-main', $style_url, $deps, YITH_WCWL_VERSION );
			}

			// theme specific assets.
			$current_theme = wp_get_theme();

			if ( $current_theme->exists() ) {
				$theme_slug = $current_theme->Template; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

				if ( file_exists( YITH_WCWL_DIR . 'assets/css/themes/' . $theme_slug . '.css' ) ) {
					wp_register_style( 'yith-wcwl-theme', YITH_WCWL_URL . 'assets/css/themes/' . $theme_slug . '.css', array( $located ? 'yith-wcwl-user-main' : 'yith-wcwl-main' ), YITH_WCWL_VERSION );
				}
			}
		}

		/**
		 * Register scripts required by the plugin
		 *
		 * @return void
		 */
		public function register_scripts() {
			$woocommerce_base = WC()->template_path();
			$assets_path      = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/';
			$suffix           = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
			$prefix           = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? 'unminified/' : '';

			// register dependencies.
			wp_register_script( 'prettyPhoto', $assets_path . 'js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js', array( 'jquery' ), '3.1.6', true );
			wp_register_script( 'jquery-selectBox', YITH_WCWL_ASSETS_JS_URL . 'jquery.selectBox.min.js', array( 'jquery' ), '1.2.0', true );

			/**
			 * APPLY_FILTERS: yith_wcwl_main_script_deps
			 *
			 * Filter the script dependencies to be used in the plugin.
			 *
			 * @param array $deps Array of script dependencies
			 *
			 * @return array
			 */
			$deps = apply_filters( 'yith_wcwl_main_script_deps', array( 'jquery', 'jquery-selectBox', 'prettyPhoto' ) );

			// get localized variables.
			$yith_wcwl_l10n = $this->get_localize();

			// register main script.
			$located = locate_template(
				array(
					$woocommerce_base . 'wishlist.js',
					'wishlist.js',
				)
			);

			if ( ! $located ) {
				wp_register_script( 'jquery-yith-wcwl', YITH_WCWL_URL . 'assets/js/' . $prefix . 'jquery.yith-wcwl' . $suffix . '.js', $deps, YITH_WCWL_VERSION, true );
				wp_localize_script( 'jquery-yith-wcwl', 'yith_wcwl_l10n', $yith_wcwl_l10n );
			} else {
				wp_register_script( 'jquery-yith-wcwl-user', str_replace( get_stylesheet_directory(), get_stylesheet_directory_uri(), $located ), $deps, YITH_WCWL_VERSION, true );
				wp_localize_script( 'jquery-yith-wcwl-user', 'yith_wcwl_l10n', $yith_wcwl_l10n );
			}
		}

		/**
		 * Enqueue styles, scripts and other stuffs needed in the <head>.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function enqueue_styles_and_stuffs() {
			// main plugin style.
			if ( ! wp_style_is( 'yith-wcwl-user-main', 'registered' ) ) {
				wp_enqueue_style( 'yith-wcwl-main' );
			} else {
				wp_enqueue_style( 'yith-wcwl-user-main' );
			}

			// theme specific style.
			if ( wp_style_is( 'yith-wcwl-theme', 'registered' ) ) {
				wp_enqueue_style( 'yith-wcwl-theme' );
			}

			// custom style.
			$this->enqueue_custom_style();
		}

		/**
		 * Enqueue plugin scripts.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function enqueue_scripts() {
			if ( ! wp_script_is( 'jquery-yith-wcwl-user', 'registered' ) ) {
				wp_enqueue_script( 'jquery-yith-wcwl' );
			} else {
				wp_enqueue_script( 'jquery-yith-wcwl-user' );
			}
		}

		/**
		 * Return localize array
		 *
		 * @return array Array with variables to be localized inside js
		 * @since 2.2.3
		 */
		public function get_localize() {
			/**
			 * APPLY_FILTERS: yith_wcwl_localize_script
			 *
			 * Filter the array with the parameters sent to the plugin scripts trought the localize.
			 *
			 * @param array $localize Array of parameters
			 *
			 * @return array
			 */
			return apply_filters(
				'yith_wcwl_localize_script',
				array(
					'ajax_url'                               => admin_url( 'admin-ajax.php', 'relative' ),
					'redirect_to_cart'                       => get_option( 'yith_wcwl_redirect_cart' ),
					'yith_wcwl_button_position'              => get_option( 'yith_wcwl_button_position' ),
					'multi_wishlist'                         => false,
					/**
					 * APPLY_FILTERS: yith_wcwl_hide_add_button
					 *
					 * Filter whether to hide the 'Add to wishlist' button.
					 *
					 * @param bool $hide_button Whether to hide the ATW button or not
					 *
					 * @return bool
					 */
					'hide_add_button'                        => apply_filters( 'yith_wcwl_hide_add_button', true ),
					'enable_ajax_loading'                    => 'yes' === get_option( 'yith_wcwl_ajax_enable', 'no' ),
					'ajax_loader_url'                        => YITH_WCWL_URL . 'assets/images/ajax-loader-alt.svg',
					'remove_from_wishlist_after_add_to_cart' => 'yes' === get_option( 'yith_wcwl_remove_after_add_to_cart' ),
					/**
					 * APPLY_FILTERS: yith_wcwl_is_wishlist_responsive
					 *
					 * Filter whether to use the responsive layout for the wishlist.
					 *
					 * @param bool $is_responsive Whether to use responsive layout or not
					 *
					 * @return bool
					 */
					'is_wishlist_responsive'                 => apply_filters( 'yith_wcwl_is_wishlist_responsive', true ),
					/**
					 * APPLY_FILTERS: yith_wcwl_time_to_close_prettyphoto
					 *
					 * Filter the time (in miliseconds) to close the popup after the 'Ask for an estimate' request has been sent.
					 *
					 * @param int $time Time to close the popup
					 *
					 * @return int
					 */
					'time_to_close_prettyphoto'              => apply_filters( 'yith_wcwl_time_to_close_prettyphoto', 3000 ),
					/**
					 * APPLY_FILTERS: yith_wcwl_fragments_index_glue
					 *
					 * Filter the character used for the fragments index.
					 *
					 * @param string $char Character
					 *
					 * @return string
					 */
					'fragments_index_glue'                   => apply_filters( 'yith_wcwl_fragments_index_glue', '.' ),
					/**
					 * APPLY_FILTERS: yith_wcwl_reload_on_found_variation
					 *
					 * Filter whether to reload fragments on new variations found.
					 *
					 * @param bool $reload_variations Whether to reload fragments
					 *
					 * @return bool
					 */
					'reload_on_found_variation'              => apply_filters( 'yith_wcwl_reload_on_found_variation', true ),
					/**
					 * APPLY_FILTERS: yith_wcwl_mobile_media_query
					 *
					 * Filter the breakpoint size for the mobile media queries.
					 *
					 * @param int $breakpoint Breakpoint size
					 *
					 * @return int
					 */
					'mobile_media_query'                     => apply_filters( 'yith_wcwl_mobile_media_query', 768 ),
					'labels'                                 => array(
						'cookie_disabled'       => __( 'We are sorry, but this feature is available only if cookies on your browser are enabled.', 'yith-woocommerce-wishlist' ),
						/**
						 * APPLY_FILTERS: yith_wcwl_added_to_cart_message
						 *
						 * Filter the message when a product has been added succesfully to the cart from the wishlist.
						 *
						 * @param string $message Message
						 *
						 * @return string
						 */
						'added_to_cart_message' => sprintf( '<div class="woocommerce-notices-wrapper"><div class="woocommerce-message" role="alert">%s</div></div>', apply_filters( 'yith_wcwl_added_to_cart_message', __( 'Product added to cart successfully', 'yith-woocommerce-wishlist' ) ) ),
					),
					'actions'                                => array(
						'add_to_wishlist_action'                 => 'add_to_wishlist',
						'remove_from_wishlist_action'            => 'remove_from_wishlist',
						'reload_wishlist_and_adding_elem_action' => 'reload_wishlist_and_adding_elem',
						'load_mobile_action'                     => 'load_mobile',
						'delete_item_action'                     => 'delete_item',
						'save_title_action'                      => 'save_title',
						'save_privacy_action'                    => 'save_privacy',
						'load_fragments'                         => 'load_fragments',
					),
					'nonce'                                  => array(
						'add_to_wishlist_nonce'                 => wp_create_nonce( 'add_to_wishlist' ),
						'remove_from_wishlist_nonce'            => wp_create_nonce( 'remove_from_wishlist' ),
						'reload_wishlist_and_adding_elem_nonce' => wp_create_nonce( 'reload_wishlist_and_adding_elem' ),
						'load_mobile_nonce'                     => wp_create_nonce( 'load_mobile' ),
						'delete_item_nonce'                     => wp_create_nonce( 'delete_item' ),
						'save_title_nonce'                      => wp_create_nonce( 'save_title' ),
						'save_privacy_nonce'                    => wp_create_nonce( 'save_privacy' ),
						'load_fragments_nonce'                  => wp_create_nonce( 'load_fragments' ),
					),
					/**
					 * APPLY_FILTERS: yith_wcwl_redirect_after_ask_an_estimate
					 *
					 * Filter whether to redirect after the 'Ask for an estimate' form has been submitted.
					 *
					 * @param bool $redirect Whether to redirect or not
					 *
					 * @return bool
					 */
					'redirect_after_ask_estimate'            => apply_filters( 'yith_wcwl_redirect_after_ask_an_estimate', false ),
					/**
					 * APPLY_FILTERS: yith_wcwl_redirect_url_after_ask_an_estimate
					 *
					 * Filter the URL to redirect after the 'Ask for an estimate' form has been submitted.
					 *
					 * @param string $redirect_url Redirect URL
					 *
					 * @return string
					 */
					'ask_estimate_redirect_url'              => apply_filters( 'yith_wcwl_redirect_url_after_ask_an_estimate', get_home_url() ),
				)
			);
		}
	}
}
class-yith-wcwl-legacy.php000064400000036072151541041210011553 0ustar00<?php
/**
 * Main class
 *
 * @package YITH\Wishlist\Classes
 * @author  YITH <plugins@yithemes.com>
 * @since   4.0.0
 */

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

if ( ! class_exists( 'YITH_WCWL_Legacy' ) ) {
	/**
	 * WooCommerce Wishlist
	 *
	 * @since 1.0.0
	 */
	abstract class YITH_WCWL_Legacy {
		public function __construct() {
		}

		/* === ITEMS METHODS === */

		/**
		 * Add a product in the wishlist.
		 *
		 * @param array $atts Array of parameters; when not passed, params will be searched in $_REQUEST.
		 * @throws YITH_WCWL_Exception When an error occurs with Add to Wishlist operation.
		 *
		 * @return void
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::add_item
		 */
		public function add( $atts = array() ) {
			wc_deprecated_function( 'YITH_WCWL::add', '4.0.0', 'YITH_WCWL_Wishlists::add_item' );

			$atts = $this->get_details( $atts );

			yith_wcwl_wishlists()->add_item( $atts );
		}

		/**
		 * Remove an entry from the wishlist.
		 *
		 * @param array $atts Array of parameters; when not passed, parameters will be retrieved from $_REQUEST.
		 *
		 * @throws YITH_WCWL_Exception When something was wrong with removal.
		 *
		 * @return void
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::remove_item
		 */
		public function remove( $atts = array() ) {
			wc_deprecated_function( 'YITH_WCWL::remove', '4.0.0', 'YITH_WCWL_Wishlists::remove_item' );

			$atts = $this->get_details($atts);

			yith_wcwl_wishlists()->remove_item( $atts );
		}

		/**
		 * Retrieve first list of current user where a specific product occurs; if no wishlist is found, returns false
		 *
		 * @param int $product_id Product id.
		 * @return \YITH_WCWL_Wishlist|bool First wishlist found where the product occurs (system will privilege default lists)
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::get_wishlist_for_product
		 */
		public function get_wishlist_for_product( $product_id ) {
			wc_deprecated_function( 'YITH_WCWL::get_wishlist_for_product', '4.0.0', 'YITH_WCWL_Wishlists::get_wishlist_for_product' );
			return yith_wcwl_wishlists()->get_wishlist_for_product( $product_id );
		}

		/**
		 * Retrieve elements of the wishlist for a specific user
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 *                    [<br/>
		 *                    'user_id'             // Owner of the wishlist; default to current user logged in (if any), or false for cookie wishlist<br/>
		 *                    'product_id'          // Product to search in the wishlist<br/>
		 *                    'wishlist_id'         // wishlist_id for a specific wishlist, false for default, or all for any wishlist<br/>
		 *                    'wishlist_token'      // wishlist token, or false as default<br/>
		 *                    'wishlist_visibility' // all, visible, public, shared, private<br/>
		 *                    'is_default' =>       // whether searched wishlist should be default one <br/>
		 *                    'id' => false,        // only for table select<br/>
		 *                    'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *                    'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 *                    ].
		 *
		 * @return YITH_WCWL_Wishlist_Item[]|bool
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::get_items
		 */
		public function get_products( $args = array() ) {
			wc_deprecated_function( 'YITH_WCWL::get_products', '4.0.0', 'YITH_WCWL_Wishlists::get_items' );
			return yith_wcwl_wishlists()->get_items( $args );
		}

		/**
		 * Retrieve details of a product in the wishlist.
		 *
		 * @param int      $product_id  Product id.
		 * @param int|bool $wishlist_id Wishlist id, or false when default should be applied.
		 * @return YITH_WCWL_Wishlist_Item|bool
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::get_product_item
		 */
		public function get_product_details( $product_id, $wishlist_id = false ) {
			wc_deprecated_function( 'YITH_WCWL::get_product_details', '4.0.0', 'YITH_WCWL_Wishlists::get_product_item' );
			return yith_wcwl_wishlists()->get_product_item( $product_id, $wishlist_id );
		}

		/**
		 * Retrieve the number of products in the wishlist.
		 *
		 * @param string|bool $wishlist_token Wishlist token if any; false for default wishlist.
		 *
		 * @return int
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::count_items_in_wishlist
		 */
		public function count_products( $wishlist_token = false ) {
			wc_deprecated_function( 'YITH_WCWL::count_products', '4.0.0', 'YITH_WCWL_Wishlists::count_items_in_wishlist' );
			return yith_wcwl_wishlists()->count_items_in_wishlist( $wishlist_token );
		}

		/**
		 * Count all user items in wishlists
		 *
		 * @return int Count of items added all over wishlist from current user
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::count_all_items
		 */
		public function count_all_products() {
			wc_deprecated_function( 'YITH_WCWL::count_all_products', '4.0.0', 'YITH_WCWL_Wishlists::count_all_items' );

			return yith_wcwl_wishlists()->count_all_items();
		}

		/**
		 * Count number of times a product was added to users wishlists
		 *
		 * @param int|bool $product_id Product id; false will force method to use global product.
		 *
		 * @return int Number of times the product was added to wishlist
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::count_add_to_wishlist
		 */
		public function count_add_to_wishlist( $product_id = false ) {
			wc_deprecated_function( 'YITH_WCWL::count_add_to_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::count_add_to_wishlist' );
			return yith_wcwl_wishlists()->count_add_to_wishlist( $product_id );
		}

		/**
		 * Count product occurrences in users wishlists
		 *
		 * @param int|bool $product_id Product id; false will force method to use global product.
		 *
		 * @return int
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::count_product_occurrences
		 */
		public function count_product_occurrences( $product_id = false ) {
			wc_deprecated_function( 'YITH_WCWL::count_product_occurrences', '4.0.0', 'YITH_WCWL_Wishlists::count_product_occurrences' );
			return yith_wcwl_wishlists()->count_product_occurrences( $product_id );
		}

		/**
		 * Check if the product exists in the wishlist.
		 *
		 * @param int      $product_id  Product id to check.
		 * @param int|bool $wishlist_id Wishlist where to search (use false to search in default wishlist).
		 * @return bool
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::is_product_in_wishlist
		 */
		public function is_product_in_wishlist( $product_id, $wishlist_id = false ) {
			wc_deprecated_function( 'YITH_WCWL::is_product_in_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::is_product_in_wishlist' );
			return yith_wcwl_wishlists()->is_product_in_wishlist( $product_id, $wishlist_id );
		}

		/* === WISHLISTS METHODS === */

		/**
		 * Add a new wishlist for the user.
		 *
		 * @param array $atts Array of params for wishlist creation.
		 * @return int|false The ID of the wishlist created or false if something go wrong
		 *
		 * @depreacted since 4.0.0
		 * @see        YITH_WCWL_Wishlists::remove_item
		 */
		public function add_wishlist( $atts = array() ) {
			wc_deprecated_function( 'YITH_WCWL::add_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::create' );

			$atts = $this->get_details($atts);

			return yith_wcwl_wishlists()->create( $atts );
		}

		/**
		 * Get details stored in the class or from request
		 *
		 * @return array
		 */
		public function get_details( $atts = array() ){
			$atts = empty( $atts ) && ! empty( $this->details ) ? $this->details : $atts;
			$atts = ! empty( $atts ) ? $atts : $_REQUEST;  // phpcs:ignore WordPress.Security.NonceVerification.Recommended

			if ( isset( $atts[ 'add_to_wishlist' ] ) ) {
				$atts[ 'product_id' ] = $atts[ 'add_to_wishlist' ];
				unset( $atts[ 'add_to_wishlist' ] );
			}

			if ( isset( $atts[ 'remove_from_wishlist' ] ) ) {
				$atts[ 'product_id' ] = $atts[ 'remove_from_wishlist' ];
				unset( $atts[ 'remove_from_wishlist' ] );
			}

			return $atts;
		}

		/**
		 * Update wishlist with arguments passed as second parameter
		 *
		 * @param int   $wishlist_id Wishlist id.
		 * @param array $args        Array of parameters to use in update process.
		 *
		 * @return void
		 *
		 * @depreacted since 4.0.0
		 * @see        YITH_WCWL_Wishlists::update
		 */
		public function update_wishlist( $wishlist_id, $args = array() ) {
			wc_deprecated_function( 'YITH_WCWL::update_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::update' );
			yith_wcwl_wishlists()->update( $wishlist_id, $args );
		}

		/**
		 * Delete indicated wishlist
		 *
		 * @param int $wishlist_id Wishlist id.
		 *
		 * @return void
		 *
		 * @depreacted since 4.0.0
		 * @see        YITH_WCWL_Wishlists::remove
		 */
		public function remove_wishlist( $wishlist_id ) {
			wc_deprecated_function( 'YITH_WCWL::remove_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::remove' );
			yith_wcwl_wishlists()->remove( $wishlist_id );
		}

		/**
		 * Retrieve all the wishlist matching specified arguments
		 *
		 * @param array $args Array of valid arguments<br/>
		 *                    [<br/>
		 *                    'id'                  // Wishlist id to search, if any<br/>
		 *                    'user_id'             // User owner<br/>
		 *                    'wishlist_slug'       // Slug of the wishlist to search<br/>
		 *                    'wishlist_name'       // Name of the wishlist to search<br/>
		 *                    'wishlist_token'      // Token of the wishlist to search<br/>
		 *                    'wishlist_visibility' // Wishlist visibility: all, visible, public, shared, private<br/>
		 *                    'user_search'         // String to match against first name / last name or email of the wishlist owner<br/>
		 *                    'is_default'          // Whether wishlist should be default or not<br/>
		 *                    'orderby'             // Column used to sort final result (could be any wishlist lists column)<br/>
		 *                    'order'               // Sorting order<br/>
		 *                    'limit'               // Pagination param: maximum number of elements in the set. 0 to retrieve all elements<br/>
		 *                    'offset'              // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 *                    'show_empty'          // Whether to show empty lists os not<br/>
		 *                    ].
		 *
		 * @return YITH_WCWL_Wishlist[]
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlist_Factory::get_wishlists
		 */
		public function get_wishlists( $args = array() ) {
			wc_deprecated_function( 'YITH_WCWL::get_wishlists', '4.0.0', 'YITH_WCWL_Wishlist_Factory::get_wishlists' );
			return YITH_WCWL_Wishlist_Factory::get_wishlists( $args );
		}

		/**
		 * Wrapper for \YITH_WCWL::get_wishlists, will return wishlists for current user
		 *
		 * @return YITH_WCWL_Wishlist[]
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::get_current_user_wishlists
		 */
		public function get_current_user_wishlists() {
			wc_deprecated_function( 'YITH_WCWL::get_current_user_wishlists', '4.0.0', 'YITH_WCWL_Wishlists::get_current_user_wishlists' );
			return yith_wcwl_wishlists()->get_current_user_wishlists();
		}

		/**
		 * Returns details of a wishlist, searching it by wishlist id
		 *
		 * @param int $wishlist_id Wishlist id.
		 * @return YITH_WCWL_Wishlist
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlist_Factory::get_wishlist
		 */
		public function get_wishlist_detail( $wishlist_id ) {
			wc_deprecated_function( 'YITH_WCWL::get_wishlist_detail', '4.0.0', 'YITH_WCWL_Wishlist_Factory::get_wishlist' );
			return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
		}

		/**
		 * Returns details of a wishlist, searching it by wishlist token
		 *
		 * @param string $wishlist_token Wishlist token.
		 * @return YITH_WCWL_Wishlist
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::get_wishlist_for_product
		 */
		public function get_wishlist_detail_by_token( $wishlist_token ) {
			wc_deprecated_function( 'YITH_WCWL::get_wishlist_detail_by_token', '4.0.0', 'YITH_WCWL_Wishlist_Factory::get_wishlist' );
			return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_token );
		}

		/**
		 * Generate default wishlist for current user or session
		 *
		 * @param int|bool $id User or session id; false if you want to use current user/session.
		 *
		 * @return int Default wishlist id
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::generate_default_wishlist
		 */
		public function generate_default_wishlist( $id = false ) {
			wc_deprecated_function( 'YITH_WCWL::generate_default_wishlist', '4.0.0', 'YITH_WCWL_Wishlists::generate_default_wishlist' );
			return yith_wcwl_wishlists()->generate_default_wishlist( $id );
		}

		/**
		 * Generate a token to visit wishlist
		 *
		 * @return string token
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlist_Factory::generate_wishlist_token
		 */
		public function generate_wishlist_token() {
			wc_deprecated_function( 'YITH_WCWL::generate_wishlist_token', '4.0.0', 'YITH_WCWL_Wishlist_Factory::generate_wishlist_token' );
			return YITH_WCWL_Wishlist_Factory::generate_wishlist_token();
		}

		/**
		 * Returns an array of users that created and populated a public wishlist
		 *
		 * @param array $args Array of valid arguments<br/>
		 *                    [<br/>
		 *                    'search' // String to match against first name / last name / user login or user email of wishlist owner<br/>
		 *                    'limit'  // Pagination param: number of items to show in one page. 0 to show all items<br/>
		 *                    'offset' // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 *                    ].
		 *
		 * @return array
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlist_Factory::get_wishlist_users
		 */
		public function get_users_with_wishlist( $args = array() ) {
			wc_deprecated_function( 'YITH_WCWL::get_users_with_wishlist', '4.0.0', 'YITH_WCWL_Wishlist_Factory::get_wishlist_users' );
			return YITH_WCWL_Wishlist_Factory::get_wishlist_users( $args );
		}

		/**
		 * Count users that have public wishlists
		 *
		 * @param string $search Search string.
		 *
		 * @return int
		 *
		 * @depreacted since 4.0
		 * @see        YITH_WCWL_Wishlists::count_users_with_wishlists
		 */
		public function count_users_with_wishlists( $search ) {
			wc_deprecated_function( 'YITH_WCWL::count_users_with_wishlists', '4.0.0', 'YITH_WCWL_Wishlists::count_users_with_wishlists' );
			return yith_wcwl_wishlists()->count_users_with_wishlists( $search );
		}

		/* === GENERAL METHODS === */

		/**
		 * Checks whether multi-wishlist feature is enabled for current user
		 *
		 * @return bool Whether feature is enabled or not
		 */
		public function is_multi_wishlist_enabled() {
			wc_deprecated_function( 'YITH_WCWL::is_multi_wishlist_enabled', '4.0.0', 'YITH_WCWL_Wishlists::is_multi_wishlist_enabled' );
			return yith_wcwl_wishlists()->is_multi_wishlist_enabled();
		}
	}
}
class-yith-wcwl-premium-legacy.php000064400000006464151541041210013231 0ustar00<?php
/**
 * Init premium features of the plugin
 *
 * @package YITH\Wishlist\Classes
 * @author  YITH <plugins@yithemes.com>
 * @version 3.0.0
 */

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

if ( ! class_exists( 'YITH_WCWL_Premium' ) ) {
	/**
	 * WooCommerce Wishlist Premium
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Premium_Legacy extends YITH_WCWL_Extended {
		public function __construct() {
			parent::__construct();

			add_filter( 'yith_wcwl_adding_wishlist_args', array( $this, 'apply_deprecated_filters_when_adding_wishlist' ) );
		}

		/**
		 * Adding support for deprecated filters while creating a new wishlist.
		 *
		 * @param array $args The arguments to filter.
		 * @return array
		 */
		public function apply_deprecated_filters_when_adding_wishlist( $args ) {
			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_wishlist_name
			 *
			 * Filter the name of the wishlist to be created.
			 *
			 * @param string $wishlist_name Wishlist name
			 * @return string
			 *
			 * @deprecated since 4.0.0
			 */
			$args[ 'wishlist_name' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_name', $args[ 'wishlist_name' ] );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_wishlist_visibility
			 *
			 * Filter the visibility of the wishlist to be created.
			 *
			 * @param int $wishlist_visibility Wishlist visibility
			 * @return int
			 *
			 * @deprecated since 4.0.0
			 */
			$args[ 'wishlist_visibility' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_visibility', in_array( (int) $args[ 'wishlist_visibility' ], array( 0, 1, 2 ), true ) ? $args[ 'wishlist_visibility' ] : 0 );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_user_id
			 *
			 * Filter the user ID saved in the wishlist.
			 *
			 * @param int $user_id User ID
			 * @return int
			 *
			 * @deprecated since 4.0.0
			 */
			$args[ 'user_id' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_user_id', intval( $args[ 'user_id' ] ) );

			/**
			 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_session_id
			 *
			 * Filter the session ID saved in the wishlist.
			 *
			 * @param int $user_id User ID
			 * @return int
			 *
			 * @deprecated since 4.0.0
			 */
			$args[ 'session_id' ] = apply_filters( 'yith_wcwl_adding_to_wishlist_session_id', $args[ 'session_id' ] );

			return $args;
		}

		/* === WISHLIST METHODS === */

		/**
		 * Update wishlist with arguments passed as second parameter
		 *
		 * @param int   $wishlist_id Wishlist id.
		 * @param array $args        Array of parameters to use for update query.
		 * @throws YITH_WCWL_Exception When something goes wrong with update.
		 * @return void
		 * @since 2.0.0
		 */
		public function update_wishlist( $wishlist_id, $args = array() ) {
			wc_deprecated_function( 'YITH_WCWL_Premium_Legacy::update_wishlist', '4.0.0', 'YITH_WCWL_Wishlists_Premium::update' );
			yith_wcwl_wishlists()->update( $wishlist_id, $args );
		}

		/**
		 * Delete indicated wishlist
		 *
		 * @param int $wishlist_id Wishlist id.
		 * @throws YITH_WCWL_Exception When something goes wrong with deletion.
		 * @return void
		 * @since 3.0.0
		 */
		public function remove_wishlist( $wishlist_id ) {
			wc_deprecated_function( 'YITH_WCWL_Premium_Legacy::remove_wishlist', '4.0.0', 'YITH_WCWL_Wishlists_Premium::remove' );
			yith_wcwl_wishlists()->remove( $wishlist_id );
		}
	}
}
functions-yith-wcwl-legacy.php000064400000000306151541041210012445 0ustar00<?php
/**
 * Legacy Functions
 *
 * @package YITH\Wishlist\Classes\Legacy
 * @author  YITH <plugins@yithemes.com>
 * @version 3.0.0
 */

defined( 'YITH_WCWL' ) || exit; // Exit if accessed directly
abstract-wc-legacy-order.php000064400000061245151542631340012056 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Abstract Order
 *
 * Legacy and deprecated functions are here to keep the WC_Abstract_Order clean.
 * This class will be removed in future versions.
 *
 * @version	 3.0.0
 * @package	 WooCommerce\Abstracts
 * @category	Abstract Class
 * @author	  WooThemes
 */
abstract class WC_Abstract_Legacy_Order extends WC_Data {

	/**
	 * Add coupon code to the order.
	 * @param string|array $code
	 * @param int $discount tax amount.
	 * @param int $discount_tax amount.
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_coupon( $code = array(), $discount = 0, $discount_tax = 0 ) {
		wc_deprecated_function( 'WC_Order::add_coupon', '3.0', 'a new WC_Order_Item_Coupon object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Coupon();
		$item->set_props( array(
			'code'         => $code,
			'discount'     => $discount,
			'discount_tax' => $discount_tax,
			'order_id'     => $this->get_id(),
		) );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_coupon', array( $this->get_id(), $item->get_id(), $code, $discount, $discount_tax ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a tax row to the order.
	 * @param int $tax_rate_id
	 * @param int $tax_amount amount of tax.
	 * @param int $shipping_tax_amount shipping amount.
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_tax( $tax_rate_id, $tax_amount = 0, $shipping_tax_amount = 0 ) {
		wc_deprecated_function( 'WC_Order::add_tax', '3.0', 'a new WC_Order_Item_Tax object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Tax();
		$item->set_props( array(
			'rate_id'            => $tax_rate_id,
			'tax_total'          => $tax_amount,
			'shipping_tax_total' => $shipping_tax_amount,
		) );
		$item->set_rate( $tax_rate_id );
		$item->set_order_id( $this->get_id() );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_tax', array( $this->get_id(), $item->get_id(), $tax_rate_id, $tax_amount, $shipping_tax_amount ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a shipping row to the order.
	 * @param WC_Shipping_Rate shipping_rate
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_shipping( $shipping_rate ) {
		wc_deprecated_function( 'WC_Order::add_shipping', '3.0', 'a new WC_Order_Item_Shipping object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Shipping();
		$item->set_props( array(
			'method_title' => $shipping_rate->label,
			'method_id'    => $shipping_rate->id,
			'total'        => wc_format_decimal( $shipping_rate->cost ),
			'taxes'        => $shipping_rate->taxes,
			'order_id'     => $this->get_id(),
		) );
		foreach ( $shipping_rate->get_meta_data() as $key => $value ) {
			$item->add_meta_data( $key, $value, true );
		}
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_shipping', array( $this->get_id(), $item->get_id(), $shipping_rate ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a fee to the order.
	 * Order must be saved prior to adding items.
	 *
	 * Fee is an amount of money charged for a particular piece of work
	 * or for a particular right or service, and not supposed to be negative.
	 *
	 * @throws WC_Data_Exception
	 * @param  object $fee Fee data.
	 * @return int         Updated order item ID.
	 */
	public function add_fee( $fee ) {
		wc_deprecated_function( 'WC_Order::add_fee', '3.0', 'a new WC_Order_Item_Fee object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Fee();
		$item->set_props( array(
			'name'      => $fee->name,
			'tax_class' => $fee->taxable ? $fee->tax_class : 0,
			'total'     => $fee->amount,
			'total_tax' => $fee->tax,
			'taxes'     => array(
				'total' => $fee->tax_data,
			),
			'order_id'  => $this->get_id(),
		) );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_fee', array( $this->get_id(), $item->get_id(), $fee ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Update a line item for the order.
	 *
	 * Note this does not update order totals.
	 *
	 * @param object|int $item order item ID or item object.
	 * @param WC_Product $product
	 * @param array $args data to update.
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_product( $item, $product, $args ) {
		wc_deprecated_function( 'WC_Order::update_product', '3.0', 'an interaction with the WC_Order_Item_Product class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'line_item' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility with old args
		if ( isset( $args['totals'] ) ) {
			foreach ( $args['totals'] as $key => $value ) {
				if ( 'tax' === $key ) {
					$args['total_tax'] = $value;
				} elseif ( 'tax_data' === $key ) {
					$args['taxes'] = $value;
				} else {
					$args[ $key ] = $value;
				}
			}
		}

		// Handle qty if set.
		if ( isset( $args['qty'] ) ) {
			if ( $product->backorders_require_notification() && $product->is_on_backorder( $args['qty'] ) ) {
				$item->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $item ), $args['qty'] - max( 0, $product->get_stock_quantity() ), true );
			}
			$args['subtotal'] = $args['subtotal'] ? $args['subtotal'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) );
			$args['total']	= $args['total'] ? $args['total'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) );
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();
		do_action( 'woocommerce_order_edit_product', $this->get_id(), $item->get_id(), $args, $product );

		return $item->get_id();
	}

	/**
	 * Update coupon for order. Note this does not update order totals.
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_coupon( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_coupon', '3.0', 'an interaction with the WC_Order_Item_Coupon class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'coupon' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility for old args
		if ( isset( $args['discount_amount'] ) ) {
			$args['discount'] = $args['discount_amount'];
		}
		if ( isset( $args['discount_amount_tax'] ) ) {
			$args['discount_tax'] = $args['discount_amount_tax'];
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_coupon', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update shipping method for order.
	 *
	 * Note this does not update the order total.
	 *
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_shipping( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_shipping', '3.0', 'an interaction with the WC_Order_Item_Shipping class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'shipping' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility for old args
		if ( isset( $args['cost'] ) ) {
			$args['total'] = $args['cost'];
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();
		$this->calculate_shipping();

		do_action( 'woocommerce_order_update_shipping', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update fee for order.
	 *
	 * Note this does not update order totals.
	 *
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_fee( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_fee', '3.0', 'an interaction with the WC_Order_Item_Fee class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'fee' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_fee', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update tax line on order.
	 * Note this does not update order totals.
	 *
	 * @since 3.0
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_tax( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_tax', '3.0', 'an interaction with the WC_Order_Item_Tax class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'tax' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_tax', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Get a product (either product or variation).
	 * @deprecated 4.4.0
	 * @param object $item
	 * @return WC_Product|bool
	 */
	public function get_product_from_item( $item ) {
		wc_deprecated_function( 'WC_Abstract_Legacy_Order::get_product_from_item', '4.4.0', '$item->get_product()' );
		if ( is_callable( array( $item, 'get_product' ) ) ) {
			$product = $item->get_product();
		} else {
			$product = false;
		}
		return apply_filters( 'woocommerce_get_product_from_item', $product, $item, $this );
	}

	/**
	 * Set the customer address.
	 * @param array $address Address data.
	 * @param string $type Type of address; 'billing' or 'shipping'.
	 */
	public function set_address( $address, $type = 'billing' ) {
		foreach ( $address as $key => $value ) {
			update_post_meta( $this->get_id(), "_{$type}_" . $key, $value );
			if ( is_callable( array( $this, "set_{$type}_{$key}" ) ) ) {
				$this->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Set an order total.
	 * @param float $amount
	 * @param string $total_type
	 * @return bool
	 */
	public function legacy_set_total( $amount, $total_type = 'total' ) {
		if ( ! in_array( $total_type, array( 'shipping', 'tax', 'shipping_tax', 'total', 'cart_discount', 'cart_discount_tax' ) ) ) {
			return false;
		}

		switch ( $total_type ) {
			case 'total' :
				$amount = wc_format_decimal( $amount, wc_get_price_decimals() );
				$this->set_total( $amount );
				update_post_meta( $this->get_id(), '_order_total', $amount );
				break;
			case 'cart_discount' :
				$amount = wc_format_decimal( $amount );
				$this->set_discount_total( $amount );
				update_post_meta( $this->get_id(), '_cart_discount', $amount );
				break;
			case 'cart_discount_tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_discount_tax( $amount );
				update_post_meta( $this->get_id(), '_cart_discount_tax', $amount );
				break;
			case 'shipping' :
				$amount = wc_format_decimal( $amount );
				$this->set_shipping_total( $amount );
				update_post_meta( $this->get_id(), '_order_shipping', $amount );
				break;
			case 'shipping_tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_shipping_tax( $amount );
				update_post_meta( $this->get_id(), '_order_shipping_tax', $amount );
				break;
			case 'tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_cart_tax( $amount );
				update_post_meta( $this->get_id(), '_order_tax', $amount );
				break;
		}

		return true;
	}

	/**
	 * Magic __isset method for backwards compatibility. Handles legacy properties which could be accessed directly in the past.
	 *
	 * @param string $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_props = array( 'completed_date', 'id', 'order_type', 'post', 'status', 'post_status', 'customer_note', 'customer_message', 'user_id', 'customer_user', 'prices_include_tax', 'tax_display_cart', 'display_totals_ex_tax', 'display_cart_ex_tax', 'order_date', 'modified_date', 'cart_discount', 'cart_discount_tax', 'order_shipping', 'order_shipping_tax', 'order_total', 'order_tax', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country', 'customer_ip_address', 'customer_user_agent', 'payment_method_title', 'payment_method', 'order_currency' );
		return $this->get_id() ? ( in_array( $key, $legacy_props ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) ) : false;
	}

	/**
	 * Magic __get method for backwards compatibility.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Order properties should not be accessed directly.', '3.0' );

		if ( 'completed_date' === $key ) {
			return $this->get_date_completed() ? gmdate( 'Y-m-d H:i:s', $this->get_date_completed()->getOffsetTimestamp() ) : '';
		} elseif ( 'paid_date' === $key ) {
			return $this->get_date_paid() ? gmdate( 'Y-m-d H:i:s', $this->get_date_paid()->getOffsetTimestamp() ) : '';
		} elseif ( 'modified_date' === $key ) {
			return $this->get_date_modified() ? gmdate( 'Y-m-d H:i:s', $this->get_date_modified()->getOffsetTimestamp() ) : '';
		} elseif ( 'order_date' === $key ) {
			return $this->get_date_created() ? gmdate( 'Y-m-d H:i:s', $this->get_date_created()->getOffsetTimestamp() ) : '';
		} elseif ( 'id' === $key ) {
			return $this->get_id();
		} elseif ( 'post' === $key ) {
			return get_post( $this->get_id() );
		} elseif ( 'status' === $key ) {
			return $this->get_status();
		} elseif ( 'post_status' === $key ) {
			return get_post_status( $this->get_id() );
		} elseif ( 'customer_message' === $key || 'customer_note' === $key ) {
			return $this->get_customer_note();
		} elseif ( in_array( $key, array( 'user_id', 'customer_user' ) ) ) {
			return $this->get_customer_id();
		} elseif ( 'tax_display_cart' === $key ) {
			return get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'display_totals_ex_tax' === $key ) {
			return 'excl' === get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'display_cart_ex_tax' === $key ) {
			return 'excl' === get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'cart_discount' === $key ) {
			return $this->get_total_discount();
		} elseif ( 'cart_discount_tax' === $key ) {
			return $this->get_discount_tax();
		} elseif ( 'order_tax' === $key ) {
			return $this->get_cart_tax();
		} elseif ( 'order_shipping_tax' === $key ) {
			return $this->get_shipping_tax();
		} elseif ( 'order_shipping' === $key ) {
			return $this->get_shipping_total();
		} elseif ( 'order_total' === $key ) {
			return $this->get_total();
		} elseif ( 'order_type' === $key ) {
			return $this->get_type();
		} elseif ( 'order_currency' === $key ) {
			return $this->get_currency();
		} elseif ( 'order_version' === $key ) {
			return $this->get_version();
	 	} elseif ( is_callable( array( $this, "get_{$key}" ) ) ) {
			return $this->{"get_{$key}"}();
		} else {
			return get_post_meta( $this->get_id(), '_' . $key, true );
		}
	}

	/**
	 * has_meta function for order items. This is different to the WC_Data
	 * version and should be removed in future versions.
	 *
	 * @deprecated 3.0
	 *
	 * @param int $order_item_id
	 *
	 * @return array of meta data.
	 */
	public function has_meta( $order_item_id ) {
		global $wpdb;

		wc_deprecated_function( 'WC_Order::has_meta( $order_item_id )', '3.0', 'WC_Order_item::get_meta_data' );

		return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, order_item_id
			FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d
			ORDER BY meta_id", absint( $order_item_id ) ), ARRAY_A );
	}

	/**
	 * Display meta data belonging to an item.
	 * @param  array $item
	 */
	public function display_item_meta( $item ) {
		wc_deprecated_function( 'WC_Order::display_item_meta', '3.0', 'wc_display_item_meta' );
		$product   = $item->get_product();
		$item_meta = new WC_Order_Item_Meta( $item, $product );
		$item_meta->display();
	}

	/**
	 * Display download links for an order item.
	 * @param  array $item
	 */
	public function display_item_downloads( $item ) {
		wc_deprecated_function( 'WC_Order::display_item_downloads', '3.0', 'wc_display_item_downloads' );
		$product   = $item->get_product();

		if ( $product && $product->exists() && $product->is_downloadable() && $this->is_download_permitted() ) {
			$download_files = $this->get_item_downloads( $item );
			$i			  = 0;
			$links		  = array();

			foreach ( $download_files as $download_id => $file ) {
				$i++;
				/* translators: 1: current item count */
				$prefix  = count( $download_files ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' );
				$links[] = '<small class="download-url">' . esc_html( $prefix ) . ': <a href="' . esc_url( $file['download_url'] ) . '" target="_blank">' . esc_html( $file['name'] ) . '</a></small>' . "\n";
			}

			echo '<br/>' . implode( '<br/>', $links );
		}
	}

	/**
	 * Get the Download URL.
	 *
	 * @param  int $product_id
	 * @param  int $download_id
	 * @return string
	 */
	public function get_download_url( $product_id, $download_id ) {
		wc_deprecated_function( 'WC_Order::get_download_url', '3.0', 'WC_Order_Item_Product::get_item_download_url' );
		return add_query_arg( array(
			'download_file' => $product_id,
			'order'         => $this->get_order_key(),
			'email'         => urlencode( $this->get_billing_email() ),
			'key'           => $download_id,
		), trailingslashit( home_url() ) );
	}

	/**
	 * Get the downloadable files for an item in this order.
	 *
	 * @param  array $item
	 * @return array
	 */
	public function get_item_downloads( $item ) {
		wc_deprecated_function( 'WC_Order::get_item_downloads', '3.0', 'WC_Order_Item_Product::get_item_downloads' );

		if ( ! $item instanceof WC_Order_Item ) {
			if ( ! empty( $item['variation_id'] ) ) {
				$product_id = $item['variation_id'];
			} elseif ( ! empty( $item['product_id'] ) ) {
				$product_id = $item['product_id'];
			} else {
				return array();
			}

			// Create a 'virtual' order item to allow retrieving item downloads when
			// an array of product_id is passed instead of actual order item.
			$item = new WC_Order_Item_Product();
			$item->set_product( wc_get_product( $product_id ) );
			$item->set_order_id( $this->get_id() );
		}

		return $item->get_item_downloads();
	}

	/**
	 * Gets shipping total. Alias of WC_Order::get_shipping_total().
	 * @deprecated 3.0.0 since this is an alias only.
	 * @return float
	 */
	public function get_total_shipping() {
		return $this->get_shipping_total();
	}

	/**
	 * Get order item meta.
	 * @deprecated 3.0.0
	 * @param mixed $order_item_id
	 * @param string $key (default: '')
	 * @param bool $single (default: false)
	 * @return array|string
	 */
	public function get_item_meta( $order_item_id, $key = '', $single = false ) {
		wc_deprecated_function( 'WC_Order::get_item_meta', '3.0', 'wc_get_order_item_meta' );
		return get_metadata( 'order_item', $order_item_id, $key, $single );
	}

	/**
	 * Get all item meta data in array format in the order it was saved. Does not group meta by key like get_item_meta().
	 *
	 * @param mixed $order_item_id
	 * @return array of objects
	 */
	public function get_item_meta_array( $order_item_id ) {
		wc_deprecated_function( 'WC_Order::get_item_meta_array', '3.0', 'WC_Order_Item::get_meta_data() (note the format has changed)' );
		$item            = $this->get_item( $order_item_id );
		$meta_data       = $item->get_meta_data();
		$item_meta_array = array();

		foreach ( $meta_data as $meta ) {
			$item_meta_array[ $meta->id ] = $meta;
		}

		return $item_meta_array;
	}

	/**
	 * Get coupon codes only.
	 *
	 * @deprecated 3.7.0 - Replaced with better named method to reflect the actual data being returned.
	 * @return array
	 */
	public function get_used_coupons() {
		wc_deprecated_function( 'get_used_coupons', '3.7', 'WC_Abstract_Order::get_coupon_codes' );
		return $this->get_coupon_codes();
	}

	/**
	 * Expand item meta into the $item array.
	 * @deprecated 3.0.0 Item meta no longer expanded due to new order item
	 *		classes. This function now does nothing to avoid data breakage.
	 * @param array $item before expansion.
	 * @return array
	 */
	public function expand_item_meta( $item ) {
		wc_deprecated_function( 'WC_Order::expand_item_meta', '3.0' );
		return $item;
	}

	/**
	 * Load the order object. Called from the constructor.
	 * @deprecated 3.0.0 Logic moved to constructor
	 * @param int|object|WC_Order $order Order to init.
	 */
	protected function init( $order ) {
		wc_deprecated_function( 'WC_Order::init', '3.0', 'Logic moved to constructor' );
		if ( is_numeric( $order ) ) {
			$this->set_id( $order );
		} elseif ( $order instanceof WC_Order ) {
			$this->set_id( absint( $order->get_id() ) );
		} elseif ( isset( $order->ID ) ) {
			$this->set_id( absint( $order->ID ) );
		}
		$this->set_object_read( false );
		$this->data_store->read( $this );
	}

	/**
	 * Gets an order from the database.
	 * @deprecated 3.0
	 * @param int $id (default: 0).
	 * @return bool
	 */
	public function get_order( $id = 0 ) {
		wc_deprecated_function( 'WC_Order::get_order', '3.0' );

		if ( ! $id ) {
			return false;
		}

		$result = get_post( $id );

		if ( $result ) {
			$this->populate( $result );
			return true;
		}

		return false;
	}

	/**
	 * Populates an order from the loaded post data.
	 * @deprecated 3.0
	 * @param mixed $result
	 */
	public function populate( $result ) {
		wc_deprecated_function( 'WC_Order::populate', '3.0' );
		$this->set_id( $result->ID );
		$this->set_object_read( false );
		$this->data_store->read( $this );
	}

	/**
	 * Cancel the order and restore the cart (before payment).
	 * @deprecated 3.0.0 Moved to event handler.
	 * @param string $note (default: '') Optional note to add.
	 */
	public function cancel_order( $note = '' ) {
		wc_deprecated_function( 'WC_Order::cancel_order', '3.0', 'WC_Order::update_status' );
		WC()->session->set( 'order_awaiting_payment', false );
		$this->update_status( 'cancelled', $note );
	}

	/**
	 * Record sales.
	 * @deprecated 3.0.0
	 */
	public function record_product_sales() {
		wc_deprecated_function( 'WC_Order::record_product_sales', '3.0', 'wc_update_total_sales_counts' );
		wc_update_total_sales_counts( $this->get_id() );
	}

	/**
	 * Increase applied coupon counts.
	 * @deprecated 3.0.0
	 */
	public function increase_coupon_usage_counts() {
		wc_deprecated_function( 'WC_Order::increase_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' );
		wc_update_coupon_usage_counts( $this->get_id() );
	}

	/**
	 * Decrease applied coupon counts.
	 * @deprecated 3.0.0
	 */
	public function decrease_coupon_usage_counts() {
		wc_deprecated_function( 'WC_Order::decrease_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' );
		wc_update_coupon_usage_counts( $this->get_id() );
	}

	/**
	 * Reduce stock levels for all line items in the order.
	 * @deprecated 3.0.0
	 */
	public function reduce_order_stock() {
		wc_deprecated_function( 'WC_Order::reduce_order_stock', '3.0', 'wc_reduce_stock_levels' );
		wc_reduce_stock_levels( $this->get_id() );
	}

	/**
	 * Send the stock notifications.
	 * @deprecated 3.0.0 No longer needs to be called directly.
	 *
	 * @param $product
	 * @param $new_stock
	 * @param $qty_ordered
	 */
	public function send_stock_notifications( $product, $new_stock, $qty_ordered ) {
		wc_deprecated_function( 'WC_Order::send_stock_notifications', '3.0' );
	}

	/**
	 * Output items for display in html emails.
	 * @deprecated 3.0.0 Moved to template functions.
	 * @param array $args Items args.
	 * @return string
	 */
	public function email_order_items_table( $args = array() ) {
		wc_deprecated_function( 'WC_Order::email_order_items_table', '3.0', 'wc_get_email_order_items' );
		return wc_get_email_order_items( $this, $args );
	}

	/**
	 * Get currency.
	 * @deprecated 3.0.0
	 */
	public function get_order_currency() {
		wc_deprecated_function( 'WC_Order::get_order_currency', '3.0', 'WC_Order::get_currency' );
		return apply_filters( 'woocommerce_get_order_currency', $this->get_currency(), $this );
	}
}
abstract-wc-legacy-payment-token.php000064400000003566151542631340013540 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Payment Tokens.
 * Payment Tokens were introduced in 2.6.0 with create and update as methods.
 * Major CRUD changes occurred in 3.0, so these were deprecated (save and delete still work).
 * This legacy class is for backwards compatibility in case any code called ->read, ->update or ->create
 * directly on the object.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooCommerce
 */
abstract class WC_Legacy_Payment_Token extends WC_Data {

	/**
	 * Sets the type of this payment token (CC, eCheck, or something else).
	 *
	 * @param string Payment Token Type (CC, eCheck)
	 */
	public function set_type( $type ) {
		wc_deprecated_function( 'WC_Payment_Token::set_type', '3.0.0', 'Type cannot be overwritten.' );
	}

	/**
	 * Read a token by ID.
	 * @deprecated 3.0.0 - Init a token class with an ID.
	 *
	 * @param int $token_id
	 */
	public function read( $token_id ) {
		wc_deprecated_function( 'WC_Payment_Token::read', '3.0.0', 'a new token class initialized with an ID.' );
		$this->set_id( $token_id );
		$data_store = WC_Data_Store::load( 'payment-token' );
		$data_store->read( $this );
	}

	/**
	 * Update a token.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function update() {
		wc_deprecated_function( 'WC_Payment_Token::update', '3.0.0', 'WC_Payment_Token::save instead.' );
		$data_store = WC_Data_Store::load( 'payment-token' );
		try {
			$data_store->update( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

	/**
	 * Create a token.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function create() {
		wc_deprecated_function( 'WC_Payment_Token::create', '3.0.0', 'WC_Payment_Token::save instead.' );
		$data_store = WC_Data_Store::load( 'payment-token' );
		try {
			$data_store->create( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

}
abstract-wc-legacy-product.php000064400000052544151542631340012425 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Abstract Product
 *
 * Legacy and deprecated functions are here to keep the WC_Abstract_Product
 * clean.
 * This class will be removed in future versions.
 *
 * @version  3.0.0
 * @package  WooCommerce\Abstracts
 * @category Abstract Class
 * @author   WooThemes
 */
abstract class WC_Abstract_Legacy_Product extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 *
	 * @param  string $key Key name.
	 * @return bool
	 */
	public function __isset( $key ) {
		$valid = array(
			'id',
			'product_attributes',
			'visibility',
			'sale_price_dates_from',
			'sale_price_dates_to',
			'post',
			'download_type',
			'product_image_gallery',
			'variation_shipping_class',
			'shipping_class',
			'total_stock',
			'crosssell_ids',
			'parent',
		);
		if ( $this->is_type( 'variation' ) ) {
			$valid = array_merge( $valid, array(
				'variation_id',
				'variation_data',
				'variation_has_stock',
				'variation_shipping_class_id',
				'variation_has_sku',
				'variation_has_length',
				'variation_has_width',
				'variation_has_height',
				'variation_has_weight',
				'variation_has_tax_class',
				'variation_has_downloadable_files',
			) );
		}
		return in_array( $key, array_merge( $valid, array_keys( $this->data ) ) ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) || metadata_exists( 'post', $this->get_parent_id(), '_' . $key );
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 *
	 * @param  string $key Key name.
	 * @return mixed
	 */
	public function __get( $key ) {

		if ( 'post_type' === $key ) {
			return $this->post_type;
		}

		wc_doing_it_wrong( $key, __( 'Product properties should not be accessed directly.', 'woocommerce' ), '3.0' );

		switch ( $key ) {
			case 'id' :
				$value = $this->is_type( 'variation' ) ? $this->get_parent_id() : $this->get_id();
				break;
			case 'product_type' :
				$value = $this->get_type();
				break;
			case 'product_attributes' :
				$value = isset( $this->data['attributes'] ) ? $this->data['attributes'] : '';
				break;
			case 'visibility' :
				$value = $this->get_catalog_visibility();
				break;
			case 'sale_price_dates_from' :
				return $this->get_date_on_sale_from() ? $this->get_date_on_sale_from()->getTimestamp() : '';
				break;
			case 'sale_price_dates_to' :
				return $this->get_date_on_sale_to() ? $this->get_date_on_sale_to()->getTimestamp() : '';
				break;
			case 'post' :
				$value = get_post( $this->get_id() );
				break;
			case 'download_type' :
				return 'standard';
				break;
			case 'product_image_gallery' :
				$value = $this->get_gallery_image_ids();
				break;
			case 'variation_shipping_class' :
			case 'shipping_class' :
				$value = $this->get_shipping_class();
				break;
			case 'total_stock' :
				$value = $this->get_total_stock();
				break;
			case 'downloadable' :
			case 'virtual' :
			case 'manage_stock' :
			case 'featured' :
			case 'sold_individually' :
				$value = $this->{"get_$key"}() ? 'yes' : 'no';
				break;
			case 'crosssell_ids' :
				$value = $this->get_cross_sell_ids();
				break;
			case 'upsell_ids' :
				$value = $this->get_upsell_ids();
				break;
			case 'parent' :
				$value = wc_get_product( $this->get_parent_id() );
				break;
			case 'variation_id' :
				$value = $this->is_type( 'variation' ) ? $this->get_id() : '';
				break;
			case 'variation_data' :
				$value = $this->is_type( 'variation' ) ? wc_get_product_variation_attributes( $this->get_id() ) : '';
				break;
			case 'variation_has_stock' :
				$value = $this->is_type( 'variation' ) ? $this->managing_stock() : '';
				break;
			case 'variation_shipping_class_id' :
				$value = $this->is_type( 'variation' ) ? $this->get_shipping_class_id() : '';
				break;
			case 'variation_has_sku' :
			case 'variation_has_length' :
			case 'variation_has_width' :
			case 'variation_has_height' :
			case 'variation_has_weight' :
			case 'variation_has_tax_class' :
			case 'variation_has_downloadable_files' :
				$value = true; // These were deprecated in 2.2 and simply returned true in 2.6.x.
				break;
			default :
				if ( in_array( $key, array_keys( $this->data ) ) ) {
					$value = $this->{"get_$key"}();
				} else {
					$value = get_post_meta( $this->id, '_' . $key, true );
				}
				break;
		}
		return $value;
	}

	/**
	 * If set, get the default attributes for a variable product.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_variation_default_attributes() {
		wc_deprecated_function( 'WC_Product_Variable::get_variation_default_attributes', '3.0', 'WC_Product::get_default_attributes' );
		return apply_filters( 'woocommerce_product_default_attributes', $this->get_default_attributes(), $this );
	}

	/**
	 * Returns the gallery attachment ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_gallery_attachment_ids() {
		wc_deprecated_function( 'WC_Product::get_gallery_attachment_ids', '3.0', 'WC_Product::get_gallery_image_ids' );
		return $this->get_gallery_image_ids();
	}

	/**
	 * Set stock level of the product.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int $amount
	 * @param string $mode
	 *
	 * @return int
	 */
	public function set_stock( $amount = null, $mode = 'set' ) {
		wc_deprecated_function( 'WC_Product::set_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, $mode );
	}

	/**
	 * Reduce stock level of the product.
	 *
	 * @deprecated 3.0.0
	 * @param int $amount Amount to reduce by. Default: 1
	 * @return int new stock level
	 */
	public function reduce_stock( $amount = 1 ) {
		wc_deprecated_function( 'WC_Product::reduce_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, 'decrease' );
	}

	/**
	 * Increase stock level of the product.
	 *
	 * @deprecated 3.0.0
	 * @param int $amount Amount to increase by. Default 1.
	 * @return int new stock level
	 */
	public function increase_stock( $amount = 1 ) {
		wc_deprecated_function( 'WC_Product::increase_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, 'increase' );
	}

	/**
	 * Check if the stock status needs changing.
	 *
	 * @deprecated 3.0.0 Sync is done automatically on read/save, so calling this should not be needed any more.
	 */
	public function check_stock_status() {
		wc_deprecated_function( 'WC_Product::check_stock_status', '3.0' );
	}

	/**
	 * Get and return related products.
	 * @deprecated 3.0.0 Use wc_get_related_products instead.
	 *
	 * @param int $limit
	 *
	 * @return array
	 */
	public function get_related( $limit = 5 ) {
		wc_deprecated_function( 'WC_Product::get_related', '3.0', 'wc_get_related_products' );
		return wc_get_related_products( $this->get_id(), $limit );
	}

	/**
	 * Retrieves related product terms.
	 * @deprecated 3.0.0 Use wc_get_product_term_ids instead.
	 *
	 * @param $term
	 *
	 * @return array
	 */
	protected function get_related_terms( $term ) {
		wc_deprecated_function( 'WC_Product::get_related_terms', '3.0', 'wc_get_product_term_ids' );
		return array_merge( array( 0 ), wc_get_product_term_ids( $this->get_id(), $term ) );
	}

	/**
	 * Builds the related posts query.
	 * @deprecated 3.0.0 Use Product Data Store get_related_products_query instead.
	 *
	 * @param $cats_array
	 * @param $tags_array
	 * @param $exclude_ids
	 * @param $limit
	 */
	protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
		wc_deprecated_function( 'WC_Product::build_related_query', '3.0', 'Product Data Store get_related_products_query' );
		$data_store = WC_Data_Store::load( 'product' );
		return $data_store->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit );
	}

	/**
	 * Returns the child product.
	 * @deprecated 3.0.0 Use wc_get_product instead.
	 * @param mixed $child_id
	 * @return WC_Product|WC_Product|WC_Product_variation
	 */
	public function get_child( $child_id ) {
		wc_deprecated_function( 'WC_Product::get_child', '3.0', 'wc_get_product' );
		return wc_get_product( $child_id );
	}

	/**
	 * Functions for getting parts of a price, in html, used by get_price_html.
	 *
	 * @deprecated 3.0.0
	 * @return string
	 */
	public function get_price_html_from_text() {
		wc_deprecated_function( 'WC_Product::get_price_html_from_text', '3.0', 'wc_get_price_html_from_text' );
		return wc_get_price_html_from_text();
	}

	/**
	 * Functions for getting parts of a price, in html, used by get_price_html.
	 *
	 * @deprecated 3.0.0 Use wc_format_sale_price instead.
	 * @param  string $from String or float to wrap with 'from' text
	 * @param  mixed $to String or float to wrap with 'to' text
	 * @return string
	 */
	public function get_price_html_from_to( $from, $to ) {
		wc_deprecated_function( 'WC_Product::get_price_html_from_to', '3.0', 'wc_format_sale_price' );
		return apply_filters( 'woocommerce_get_price_html_from_to', wc_format_sale_price( $from, $to ), $from, $to, $this );
	}

	/**
	 * Lists a table of attributes for the product page.
	 * @deprecated 3.0.0 Use wc_display_product_attributes instead.
	 */
	public function list_attributes() {
		wc_deprecated_function( 'WC_Product::list_attributes', '3.0', 'wc_display_product_attributes' );
		wc_display_product_attributes( $this );
	}

	/**
	 * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_including_tax instead.
	 * @param  int $qty
	 * @param  string $price to calculate, left blank to just use get_price()
	 * @return string
	 */
	public function get_price_including_tax( $qty = 1, $price = '' ) {
		wc_deprecated_function( 'WC_Product::get_price_including_tax', '3.0', 'wc_get_price_including_tax' );
		return wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_to_display instead.
	 * @param  string  $price to calculate, left blank to just use get_price()
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax()
	 * @return string
	 */
	public function get_display_price( $price = '', $qty = 1 ) {
		wc_deprecated_function( 'WC_Product::get_display_price', '3.0', 'wc_get_price_to_display' );
		return wc_get_price_to_display( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting.
	 * Uses store base tax rates. Can work for a specific $qty for more accurate taxes.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_excluding_tax instead.
	 * @param  int $qty
	 * @param  string $price to calculate, left blank to just use get_price()
	 * @return string
	 */
	public function get_price_excluding_tax( $qty = 1, $price = '' ) {
		wc_deprecated_function( 'WC_Product::get_price_excluding_tax', '3.0', 'wc_get_price_excluding_tax' );
		return wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Adjust a products price dynamically.
	 *
	 * @deprecated 3.0.0
	 * @param mixed $price
	 */
	public function adjust_price( $price ) {
		wc_deprecated_function( 'WC_Product::adjust_price', '3.0', 'WC_Product::set_price / WC_Product::get_price' );
		$this->data['price'] = $this->data['price'] + $price;
	}

	/**
	 * Returns the product categories.
	 *
	 * @deprecated 3.0.0
	 * @param string $sep (default: ', ').
	 * @param string $before (default: '').
	 * @param string $after (default: '').
	 * @return string
	 */
	public function get_categories( $sep = ', ', $before = '', $after = '' ) {
		wc_deprecated_function( 'WC_Product::get_categories', '3.0', 'wc_get_product_category_list' );
		return wc_get_product_category_list( $this->get_id(), $sep, $before, $after );
	}

	/**
	 * Returns the product tags.
	 *
	 * @deprecated 3.0.0
	 * @param string $sep (default: ', ').
	 * @param string $before (default: '').
	 * @param string $after (default: '').
	 * @return array
	 */
	public function get_tags( $sep = ', ', $before = '', $after = '' ) {
		wc_deprecated_function( 'WC_Product::get_tags', '3.0', 'wc_get_product_tag_list' );
		return wc_get_product_tag_list( $this->get_id(), $sep, $before, $after );
	}

	/**
	 * Get the product's post data.
	 *
	 * @deprecated 3.0.0
	 * @return WP_Post
	 */
	public function get_post_data() {
		wc_deprecated_function( 'WC_Product::get_post_data', '3.0', 'get_post' );

		// In order to keep backwards compatibility it's required to use the parent data for variations.
		if ( $this->is_type( 'variation' ) ) {
			$post_data = get_post( $this->get_parent_id() );
		} else {
			$post_data = get_post( $this->get_id() );
		}

		return $post_data;
	}

	/**
	 * Get the parent of the post.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_parent() {
		wc_deprecated_function( 'WC_Product::get_parent', '3.0', 'WC_Product::get_parent_id' );
		return apply_filters( 'woocommerce_product_parent', absint( $this->get_post_data()->post_parent ), $this );
	}

	/**
	 * Returns the upsell product ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_upsells() {
		wc_deprecated_function( 'WC_Product::get_upsells', '3.0', 'WC_Product::get_upsell_ids' );
		return apply_filters( 'woocommerce_product_upsell_ids', $this->get_upsell_ids(), $this );
	}

	/**
	 * Returns the cross sell product ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_cross_sells() {
		wc_deprecated_function( 'WC_Product::get_cross_sells', '3.0', 'WC_Product::get_cross_sell_ids' );
		return apply_filters( 'woocommerce_product_crosssell_ids', $this->get_cross_sell_ids(), $this );
	}

	/**
	 * Check if variable product has default attributes set.
	 *
	 * @deprecated 3.0.0
	 * @return bool
	 */
	public function has_default_attributes() {
		wc_deprecated_function( 'WC_Product_Variable::has_default_attributes', '3.0', 'a check against WC_Product::get_default_attributes directly' );
		if ( ! $this->get_default_attributes() ) {
			return true;
		}
		return false;
	}

	/**
	 * Get variation ID.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_variation_id() {
		wc_deprecated_function( 'WC_Product::get_variation_id', '3.0', 'WC_Product::get_id(). It will always be the variation ID if this is a variation.' );
		return $this->get_id();
	}

	/**
	 * Get product variation description.
	 *
	 * @deprecated 3.0.0
	 * @return string
	 */
	public function get_variation_description() {
		wc_deprecated_function( 'WC_Product::get_variation_description', '3.0', 'WC_Product::get_description()' );
		return $this->get_description();
	}

	/**
	 * Check if all variation's attributes are set.
	 *
	 * @deprecated 3.0.0
	 * @return boolean
	 */
	public function has_all_attributes_set() {
		wc_deprecated_function( 'WC_Product::has_all_attributes_set', '3.0', 'an array filter on get_variation_attributes for a quick solution.' );
		$set = true;

		// undefined attributes have null strings as array values
		foreach ( $this->get_variation_attributes() as $att ) {
			if ( ! $att ) {
				$set = false;
				break;
			}
		}
		return $set;
	}

	/**
	 * Returns whether or not the variations parent is visible.
	 *
	 * @deprecated 3.0.0
	 * @return bool
	 */
	public function parent_is_visible() {
		wc_deprecated_function( 'WC_Product::parent_is_visible', '3.0' );
		return $this->is_visible();
	}

	/**
	 * Get total stock - This is the stock of parent and children combined.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_total_stock() {
		wc_deprecated_function( 'WC_Product::get_total_stock', '3.0', 'get_stock_quantity on each child. Beware of performance issues in doing so.' );
		if ( sizeof( $this->get_children() ) > 0 ) {
			$total_stock = max( 0, $this->get_stock_quantity() );

			foreach ( $this->get_children() as $child_id ) {
				if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) {
					$stock = get_post_meta( $child_id, '_stock', true );
					$total_stock += max( 0, wc_stock_amount( $stock ) );
				}
			}
		} else {
			$total_stock = $this->get_stock_quantity();
		}
		return wc_stock_amount( $total_stock );
	}

	/**
	 * Get formatted variation data with WC < 2.4 back compat and proper formatting of text-based attribute names.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param bool $flat
	 *
	 * @return string
	 */
	public function get_formatted_variation_attributes( $flat = false ) {
		wc_deprecated_function( 'WC_Product::get_formatted_variation_attributes', '3.0', 'wc_get_formatted_variation' );
		return wc_get_formatted_variation( $this, $flat );
	}

	/**
	 * Sync variable product prices with the children lowest/highest prices.
	 *
	 * @deprecated 3.0.0 not used in core.
	 *
	 * @param int $product_id
	 */
	public function variable_product_sync( $product_id = 0 ) {
		wc_deprecated_function( 'WC_Product::variable_product_sync', '3.0' );
		if ( empty( $product_id ) ) {
			$product_id = $this->get_id();
		}

		// Sync prices with children
		if ( is_callable( array( __CLASS__, 'sync' ) ) ) {
			self::sync( $product_id );
		}
	}

	/**
	 * Sync the variable product's attributes with the variations.
	 *
	 * @param $product
	 * @param bool $children
	 */
	public static function sync_attributes( $product, $children = false ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$product = wc_get_product( $product );
		}

		/**
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
		 * Attempt to get full version of the text attribute from the parent and UPDATE meta.
		 */
		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
			$parent_attributes = array_filter( (array) get_post_meta( $product->get_id(), '_product_attributes', true ) );

			if ( ! $children ) {
				$children = $product->get_children( 'edit' );
			}

			foreach ( $children as $child_id ) {
				$all_meta = get_post_meta( $child_id );

				foreach ( $all_meta as $name => $value ) {
					if ( 0 !== strpos( $name, 'attribute_' ) ) {
						continue;
					}
					if ( sanitize_title( $value[0] ) === $value[0] ) {
						foreach ( $parent_attributes as $attribute ) {
							if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
								continue;
							}
							$text_attributes = wc_get_text_attributes( $attribute['value'] );
							foreach ( $text_attributes as $text_attribute ) {
								if ( sanitize_title( $text_attribute ) === $value[0] ) {
									update_post_meta( $child_id, $name, $text_attribute );
									break;
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Match a variation to a given set of attributes using a WP_Query.
	 * @deprecated 3.0.0 in favour of Product data store's find_matching_product_variation.
	 *
	 * @param array $match_attributes
	 */
	public function get_matching_variation( $match_attributes = array() ) {
		wc_deprecated_function( 'WC_Product::get_matching_variation', '3.0', 'Product data store find_matching_product_variation' );
		$data_store = WC_Data_Store::load( 'product' );
		return $data_store->find_matching_product_variation( $this, $match_attributes );
	}

	/**
	 * Returns whether or not we are showing dimensions on the product page.
	 * @deprecated 3.0.0 Unused.
	 * @return bool
	 */
	public function enable_dimensions_display() {
		wc_deprecated_function( 'WC_Product::enable_dimensions_display', '3.0' );
		return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() );
	}

	/**
	 * Returns the product rating in html format.
	 *
	 * @deprecated 3.0.0
	 * @param string $rating (default: '')
	 * @return string
	 */
	public function get_rating_html( $rating = null ) {
		wc_deprecated_function( 'WC_Product::get_rating_html', '3.0', 'wc_get_rating_html' );
		return wc_get_rating_html( $rating );
	}

	/**
	 * Sync product rating. Can be called statically.
	 *
	 * @deprecated 3.0.0
	 * @param  int $post_id
	 */
	public static function sync_average_rating( $post_id ) {
		wc_deprecated_function( 'WC_Product::sync_average_rating', '3.0', 'WC_Comments::get_average_rating_for_product or leave to CRUD.' );
		// See notes in https://github.com/woocommerce/woocommerce/pull/22909#discussion_r262393401.
		// Sync count first like in the original method https://github.com/woocommerce/woocommerce/blob/2.6.0/includes/abstracts/abstract-wc-product.php#L1101-L1128.
		self::sync_rating_count( $post_id );
		$average = WC_Comments::get_average_rating_for_product( wc_get_product( $post_id ) );
		update_post_meta( $post_id, '_wc_average_rating', $average );
	}

	/**
	 * Sync product rating count. Can be called statically.
	 *
	 * @deprecated 3.0.0
	 * @param  int $post_id
	 */
	public static function sync_rating_count( $post_id ) {
		wc_deprecated_function( 'WC_Product::sync_rating_count', '3.0', 'WC_Comments::get_rating_counts_for_product or leave to CRUD.' );
		$counts     = WC_Comments::get_rating_counts_for_product( wc_get_product( $post_id ) );
		update_post_meta( $post_id, '_wc_rating_count', $counts );
	}

	/**
	 * Same as get_downloads in CRUD.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_files() {
		wc_deprecated_function( 'WC_Product::get_files', '3.0', 'WC_Product::get_downloads' );
		return $this->get_downloads();
	}

	/**
	 * @deprecated 3.0.0 Sync is taken care of during save - no need to call this directly.
	 */
	public function grouped_product_sync() {
		wc_deprecated_function( 'WC_Product::grouped_product_sync', '3.0' );
	}
}
api/class-wc-rest-legacy-coupons-controller.php000064400000011710151542631340015630 0ustar00<?php
/**
 * REST API Legacy Coupons controller
 *
 * Handles requests to the /coupons endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Coupons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Query args.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array $args Query args
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		if ( ! empty( $request['code'] ) ) {
			$id = wc_get_coupon_id_by_code( $request['code'] );
			$args['post__in'] = array( $id );
		}

		return $args;
	}

	/**
	 * Prepare a single coupon output for response.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$coupon = new WC_Coupon( (int) $post->ID );
		$data   = $coupon->get_data();

		$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
		$format_date    = array( 'date_created', 'date_modified', 'date_expires' );
		$format_null    = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null;
		}

		// Format null values.
		foreach ( $format_null as $key ) {
			$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $post, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare a single coupon for create or update.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $data Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		global $wpdb;

		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$coupon    = new WC_Coupon( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Validate required POST fields.
		if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
			if ( empty( $request['code'] ) ) {
				return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
			}
		}

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'code' :
						$coupon_code = wc_format_coupon_code( $value );
						$id          = $coupon->get_id() ? $coupon->get_id() : 0;
						$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

						if ( $id_from_code ) {
							return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
						}

						$coupon->set_code( $coupon_code );
						break;
					case 'meta_data' :
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					case 'description' :
						$coupon->set_description( wp_filter_post_kses( $value ) );
						break;
					default :
						if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
							$coupon->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the query_vars used in `get_items` for the constructed query.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for insertion.
		 *
		 * @param WC_Coupon       $coupon        The coupon object.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
	}
}
api/class-wc-rest-legacy-orders-controller.php000064400000022351151542631340015443 0ustar00<?php
/**
 * REST API Legacy Orders controller
 *
 * Handles requests to the /orders endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Query args.
	 *
	 * @deprecated 3.0
	 *
	 * @param array $args
	 * @param WP_REST_Request $request
	 * @return array
	 */
	public function query_args( $args, $request ) {
		global $wpdb;

		// Set post_status.
		if ( 'any' !== $request['status'] ) {
			$args['post_status'] = 'wc-' . $request['status'];
		} else {
			$args['post_status'] = 'any';
		}

		if ( ! empty( $request['customer'] ) ) {
			if ( ! empty( $args['meta_query'] ) ) {
				$args['meta_query'] = array();
			}

			$args['meta_query'][] = array(
				'key'   => '_customer_user',
				'value' => $request['customer'],
				'type'  => 'NUMERIC',
			);
		}

		// Search by product.
		if ( ! empty( $request['product'] ) ) {
			$order_ids = $wpdb->get_col( $wpdb->prepare( "
				SELECT order_id
				FROM {$wpdb->prefix}woocommerce_order_items
				WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
				AND order_item_type = 'line_item'
			 ", $request['product'] ) );

			// Force WP_Query return empty if don't found any order.
			$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );

			$args['post__in'] = $order_ids;
		}

		// Search.
		if ( ! empty( $args['s'] ) ) {
			$order_ids = wc_order_search( $args['s'] );

			if ( ! empty( $order_ids ) ) {
				unset( $args['s'] );
				$args['post__in'] = array_merge( $order_ids, array( 0 ) );
			}
		}

		return $args;
	}

	/**
	 * Prepare a single order output for response.
	 *
	 * @deprecated 3.0
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$this->request       = $request;
		$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
		$statuses            = wc_get_order_statuses();
		$order               = wc_get_order( $post );
		$data                = array_merge( array( 'id' => $order->get_id() ), $order->get_data() );
		$format_decimal      = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
		$format_date         = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
		$format_line_items   = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false;
		}

		// Format the order status.
		$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];

		// Format line items.
		foreach ( $format_line_items as $key ) {
			$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
		}

		// Refunds.
		$data['refunds'] = array();
		foreach ( $order->get_refunds() as $refund ) {
			$data['refunds'][] = array(
				'id'     => $refund->get_id(),
				'refund' => $refund->get_reason() ? $refund->get_reason() : '',
				'total'  => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
			);
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $order, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare a single order for create.
	 *
	 * @deprecated 3.0
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_Error|WC_Order $data Object.
	 */
	protected function prepare_item_for_database( $request ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$order     = new WC_Order( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Handle all writable props
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'billing' :
					case 'shipping' :
						$this->update_address( $order, $value, $key );
						break;
					case 'line_items' :
					case 'shipping_lines' :
					case 'fee_lines' :
					case 'coupon_lines' :
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
										$order->remove_item( $item['id'] );
									} else {
										$this->set_item( $order, $key, $item );
									}
								}
							}
						}
						break;
					case 'meta_data' :
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					default :
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
							$order->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the data for the insert.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WC_Order           $order      The Order object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
	}

	/**
	 * Create base WC Order object.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array $data
	 * @return WC_Order
	 */
	protected function create_base_order( $data ) {
		return wc_create_order( $data );
	}

	/**
	 * Create order.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function create_order( $request ) {
		try {
			// Make sure customer exists.
			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
			}

			// Make sure customer is part of blog.
			if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
				add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
			}

			$order = $this->prepare_item_for_database( $request );
			$order->set_created_via( 'rest-api' );
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
			$order->calculate_totals();
			$order->save();

			// Handle set paid.
			if ( true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update order.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function update_order( $request ) {
		try {
			$order = $this->prepare_item_for_database( $request );
			$order->save();

			// Handle set paid.
			if ( $order->needs_payment() && true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			// If items have changed, recalculate order totals.
			if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
				$order->calculate_totals();
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/class-wc-rest-legacy-products-controller.php000064400000055362151542631340016020 0ustar00<?php
/**
 * REST API Legacy Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Products controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Query args.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array           $args    Request args.
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		// Set post_status.
		$args['post_status'] = $request['status'];

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		// Filter product type by slug.
		if ( ! empty( $request['type'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['type'],
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query;
		}

		// Filter featured.
		if ( is_bool( $request['featured'] ) ) {
			$args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => 'featured',
				'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
			);
		}

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'     => '_sku',
				'value'   => $skus,
				'compare' => 'IN',
			) );
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'   => '_tax_class',
				'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
			) );
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );
		}

		// Filter product in stock or out of stock.
		if ( is_bool( $request['in_stock'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'   => '_stock_status',
				'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
			) );
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key           = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$args[ $on_sale_key ] += wc_get_product_ids_on_sale();
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		return $args;
	}

	/**
	 * Prepare a single product output for response.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $post, $request ) {
		$product = wc_get_product( $post );
		$data    = $this->get_product_data( $product );

		// Add variations to variable products.
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$data['variations'] = $product->get_children();
		}

		// Add grouped products data.
		if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
			$data['grouped_products'] = $product->get_children();
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $product, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Get product menu order.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return int
	 */
	protected function get_product_menu_order( $product ) {
		return $product->get_menu_order();
	}

	/**
	 * Save product meta.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product
	 * @param WP_REST_Request $request
	 * @return bool
	 * @throws WC_REST_Exception
	 */
	protected function save_product_meta( $product, $request ) {
		$product = $this->set_product_meta( $product, $request );
		$product->save();

		return true;
	}

	/**
	 * Set product meta.
	 *
	 * @deprecated 3.0.0
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product Product instance.
	 * @param WP_REST_Request $request Request data.
	 * @return WC_Product
	 */
	protected function set_product_meta( $product, $request ) {
		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $request['purchase_note'] ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}
		}

		// Product parent ID for groups.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status.
		if ( isset( $request['in_stock'] ) ) {
			$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( $request['button_text'] );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		return $product;
	}

	/**
	 * Save variations.
	 *
	 * @deprecated 3.0.0
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product          Product instance.
	 * @param WP_REST_Request $request          Request data.
	 * @return bool
	 */
	protected function save_variations_data( $product, $request ) {
		foreach ( $request['variations'] as $menu_order => $data ) {
			$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = $data['image'];
				$image = current( $image );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->set_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$variation->set_downloadable( $data['downloadable'] );
			}

			// Downloads.
			if ( $variation->get_downloadable() ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			if ( isset( $data['manage_stock'] ) ) {
				$variation->set_manage_stock( $data['manage_stock'] );
			}

			if ( isset( $data['in_stock'] ) ) {
				$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
			}

			if ( isset( $data['backorders'] ) ) {
				$variation->set_backorders( $data['backorders'] );
			}

			if ( $variation->get_manage_stock() ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$variation->set_stock_quantity( $stock_quantity );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['date_on_sale_from'] ) ) {
				$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
			}

			if ( isset( $data['date_on_sale_to'] ) ) {
				$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Description.
			if ( isset( $data['description'] ) ) {
				$variation->set_description( wp_kses_post( $data['description'] ) );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$attributes = array();
				$parent_attributes = $product->get_attributes();

				foreach ( $data['attributes'] as $attribute ) {
					$attribute_id   = 0;
					$attribute_name = '';

					// Check ID for global attributes or name for product attributes.
					if ( ! empty( $attribute['id'] ) ) {
						$attribute_id   = absint( $attribute['id'] );
						$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
					} elseif ( ! empty( $attribute['name'] ) ) {
						$attribute_name = sanitize_title( $attribute['name'] );
					}

					if ( ! $attribute_id && ! $attribute_name ) {
						continue;
					}

					if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
						continue;
					}

					$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
					$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

					if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
						// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
						$term = get_term_by( 'name', $attribute_value, $attribute_name );

						if ( $term && ! is_wp_error( $term ) ) {
							$attribute_value = $term->slug;
						} else {
							$attribute_value = sanitize_title( $attribute_value );
						}
					}

					$attributes[ $attribute_key ] = $attribute_value;
				}

				$variation->set_attributes( $attributes );
			}

			$variation->save();

			do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
		}

		return true;
	}

	/**
	 * Add post meta fields.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function add_post_meta_fields( $post, $request ) {
		return $this->update_post_meta_fields( $post, $request );
	}

	/**
	 * Update post meta fields.
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function update_post_meta_fields( $post, $request ) {
		$product = wc_get_product( $post );

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Save product meta fields.
		$product = $this->set_product_meta( $product, $request );

		// Save the product data.
		$product->save();

		// Save variations.
		if ( $product->is_type( 'variable' ) ) {
			if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
				$this->save_variations_data( $product, $request );
			}
		}

		// Clear caches here so in sync with any new variations/children.
		wc_delete_product_transients( $product->get_id() );
		wp_cache_delete( 'product-' . $product->get_id(), 'products' );

		return true;
	}

	/**
	 * Delete post.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int|WP_Post $id Post ID or WP_Post instance.
	 */
	protected function delete_post( $id ) {
		if ( ! empty( $id->ID ) ) {
			$id = $id->ID;
		} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
			return;
		}

		// Delete product attachments.
		$attachments = get_posts( array(
			'post_parent' => $id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product.
		$product = wc_get_product( $id );
		$product->delete( true );
	}

	/**
	 * Get post types.
	 *
	 * @deprecated 3.0.0
	 *
	 * @return array
	 */
	protected function get_post_types() {
		return array( 'product', 'product_variation' );
	}

	/**
	 * Save product images.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int $product_id
	 * @param array $images
	 * @throws WC_REST_Exception
	 */
	protected function save_product_images( $product_id, $images ) {
		$product = wc_get_product( $product_id );

		return set_product_images( $product, $images );
	}
}
api/v1/class-wc-api-authentication.php000064400000027507151542631340013673 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' )
		);
	}
}
api/v1/class-wc-api-coupons.php000064400000015767151542631340012347 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 );
	}
}
api/v1/class-wc-api-customers.php000064400000033130151542631340012665 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' );
	}
}
api/v1/class-wc-api-json-handler.php000064400000003702151542631340013227 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 );
	}
}
api/v1/class-wc-api-orders.php000064400000027600151542631340012144 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;
	}
}
api/v1/class-wc-api-products.php000064400000041105151542631340012505 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;
	}
}
api/v1/class-wc-api-reports.php000064400000032372151542631340012346 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;
		}
	}
}
api/v1/class-wc-api-resource.php000064400000027744151542631340012506 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;
		}
	}
}
api/v1/class-wc-api-server.php000064400000050302151542631340012147 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;
	}
}
api/v1/class-wc-api-xml-handler.php000064400000016707151542631340013067 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;
	}
}
api/v1/interface-wc-api-handler.php000064400000001541151542631340013112 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 );

}
api/v2/class-wc-api-authentication.php000064400000027436151542631340013675 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 ] ) ) {
				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' ), 401 );
		}

		$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' )
		);
	}
}
api/v2/class-wc-api-coupons.php000064400000050074151542631340012336 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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/POST /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_coupon' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),    WC_API_Server::READABLE ),
			array( array( $this, 'edit_coupon' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
		);

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

		# POST|PUT /coupons/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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
	 */
	public function get_coupon( $id, $fields = null ) {
		try {

			$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'                  => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // 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 ),
				'maximum_amount'               => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
				'customer_emails'              => $coupon->get_email_restrictions(),
				'description'                  => $coupon->get_description(),
			);

			return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
			}

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

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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;

		try {
			$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 ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
			}

			return $this->get_coupon( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a coupon
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_coupon( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			// Check user permission
			if ( ! current_user_can( 'publish_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );

			// Check if coupon code is specified
			if ( ! isset( $data['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
			}

			$coupon_code  = wc_format_coupon_code( $data['code'] );
			$id_from_code = wc_get_coupon_id_by_code( $coupon_code );

			if ( $id_from_code ) {
				throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
			}

			$defaults = array(
				'type'                         => 'fixed_cart',
				'amount'                       => 0,
				'individual_use'               => false,
				'product_ids'                  => array(),
				'exclude_product_ids'          => array(),
				'usage_limit'                  => '',
				'usage_limit_per_user'         => '',
				'limit_usage_to_x_items'       => '',
				'usage_count'                  => '',
				'expiry_date'                  => '',
				'enable_free_shipping'         => false,
				'product_category_ids'         => array(),
				'exclude_product_category_ids' => array(),
				'exclude_sale_items'           => false,
				'minimum_amount'               => '',
				'maximum_amount'               => '',
				'customer_emails'              => array(),
				'description'                  => '',
			);

			$coupon_data = wp_parse_args( $data, $defaults );

			// Validate coupon types
			if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
			}

			$new_coupon = array(
				'post_title'   => $coupon_code,
				'post_content' => '',
				'post_status'  => 'publish',
				'post_author'  => get_current_user_id(),
				'post_type'    => 'shop_coupon',
				'post_excerpt' => $coupon_data['description'],
	 		);

			$id = wp_insert_post( $new_coupon, true );

			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
			}

			// Set coupon meta
			update_post_meta( $id, 'discount_type', $coupon_data['type'] );
			update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
			update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
			update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
			update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
			update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
			update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
			update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
			update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
			update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
			update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
			update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
			update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );

			do_action( 'woocommerce_api_create_coupon', $id, $data );
			do_action( 'woocommerce_new_coupon', $id );

			$this->server->send_status( 201 );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

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

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

			$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );

			if ( isset( $data['code'] ) ) {
				global $wpdb;

				$coupon_code  = wc_format_coupon_code( $data['code'] );
				$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

				if ( $id_from_code ) {
					throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
				}

				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['description'] ) ) {
				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['type'] ) ) {
				// Validate coupon types
				if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
				}
				update_post_meta( $id, 'discount_type', $data['type'] );
			}

			if ( isset( $data['amount'] ) ) {
				update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
			}

			if ( isset( $data['individual_use'] ) ) {
				update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_ids'] ) ) {
				update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
			}

			if ( isset( $data['exclude_product_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
			}

			if ( isset( $data['usage_limit'] ) ) {
				update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
			}

			if ( isset( $data['usage_limit_per_user'] ) ) {
				update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
			}

			if ( isset( $data['limit_usage_to_x_items'] ) ) {
				update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
			}

			if ( isset( $data['usage_count'] ) ) {
				update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
			}

			if ( isset( $data['expiry_date'] ) ) {
				update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
				update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
			}

			if ( isset( $data['enable_free_shipping'] ) ) {
				update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_category_ids'] ) ) {
				update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_product_category_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_sale_items'] ) ) {
				update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['minimum_amount'] ) ) {
				update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
			}

			if ( isset( $data['maximum_amount'] ) ) {
				update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
			}

			if ( isset( $data['customer_emails'] ) ) {
				update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
			}

			do_action( 'woocommerce_api_edit_coupon', $id, $data );
			do_action( 'woocommerce_update_coupon', $id );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a coupon
	 *
	 * @since  2.2
	 * @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;
		}

		do_action( 'woocommerce_api_delete_coupon', $id, $this );

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

	/**
	 * expiry_date format
	 *
	 * @since  2.3.0
	 * @param  string $expiry_date
	 * @param bool $as_timestamp (default: false)
	 * @return string|int
	 */
	protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
		if ( '' != $expiry_date ) {
			if ( $as_timestamp ) {
				return strtotime( $expiry_date );
			}

			return date( 'Y-m-d', strtotime( $expiry_date ) );
		}

		return '';
	}

	/**
	 * 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 );
	}

	/**
	 * Bulk update or insert coupons
	 * Accepts an array with coupons in the formats supported by
	 * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['coupons'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
			}

			$data  = $data['coupons'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$coupons = array();

			foreach ( $data as $_coupon ) {
				$coupon_id = 0;

				// Try to get the coupon ID
				if ( isset( $_coupon['id'] ) ) {
					$coupon_id = intval( $_coupon['id'] );
				}

				// Coupon exists / edit coupon
				if ( $coupon_id ) {
					$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $edit ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$coupons[] = $edit['coupon'];
					}
				} else {

					// Coupon don't exists / create coupon
					$new = $this->create_coupon( array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $new ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$coupons[] = $new['coupon'];
					}
				}
			}

			return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v2/class-wc-api-customers.php000064400000061142151542631340012672 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

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.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),   WC_API_SERVER::READABLE ),
			array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),    WC_API_SERVER::READABLE ),
			array( array( $this, 'edit_customer' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /customers/email/<email>
		$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
			array( array( $this, 'get_customer_by_email' ), 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 ),
		);

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

		# POST|PUT /customers/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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 array $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(),
			'role'             => $customer->get_role(),
			'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 customer for the given email
	 *
	 * @since 2.1
	 *
	 * @param string $email the customer email
	 * @param array $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_customer_by_email( $email, $fields = null ) {
		try {
			if ( is_email( $email ) ) {
				$customer = get_user_by( 'email', $email );
				if ( ! is_object( $customer ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
				}
			} else {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
			}

			return $this->get_customer( $customer->ID, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'list_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
			}

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

			return array( 'count' => $query->get_total() );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get customer billing address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_billing_address() {
		$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
			'email',
			'phone',
		) );

		return $billing_address;
	}

	/**
	 * Get customer shipping address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_shipping_address() {
		$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		) );

		return $shipping_address;
	}

	/**
	 * Add/Update customer data.
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param array $data
	 * @param WC_Customer $customer
	 */
	protected function update_customer_data( $id, $data, $customer ) {

		// Customer first name.
		if ( isset( $data['first_name'] ) ) {
			$customer->set_first_name( wc_clean( $data['first_name'] ) );
		}

		// Customer last name.
		if ( isset( $data['last_name'] ) ) {
			$customer->set_last_name( wc_clean( $data['last_name'] ) );
		}

		// Customer billing address.
		if ( isset( $data['billing_address'] ) ) {
			foreach ( $this->get_customer_billing_address() as $field ) {
				if ( isset( $data['billing_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
						$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
					}
				}
			}
		}

		// Customer shipping address.
		if ( isset( $data['shipping_address'] ) ) {
			foreach ( $this->get_customer_shipping_address() as $field ) {
				if ( isset( $data['shipping_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
						$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
					}
				}
			}
		}

		do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
	}

	/**
	 * Create a customer
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Checks with can create new users.
			if ( ! current_user_can( 'create_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );

			// Checks with the email is missing.
			if ( ! isset( $data['email'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
			}

			// Create customer.
			$customer = new WC_Customer;
			$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
			$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
			$customer->set_email( $data['email'] );
			$customer->save();

			if ( ! $customer->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
			}

			// Added customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );
			$customer->save();

			do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );

			$this->server->send_status( 201 );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a customer
	 *
	 * @since 2.2
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Validate the customer ID.
			$id = $this->validate_request( $id, 'customer', 'edit' );

			// Return the validate error.
			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );

			$customer = new WC_Customer( $id );

			// Customer email.
			if ( isset( $data['email'] ) ) {
				$customer->set_email( $data['email'] );
			}

			// Customer password.
			if ( isset( $data['password'] ) ) {
				$customer->set_password( $data['password'] );
			}

			// Update customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );

			$customer->save();

			do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

		// Validate the customer ID.
		$id = $this->validate_request( $id, 'customer', 'delete' );

		// Return the validate error.
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_customer', $id, $this );

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

	/**
	 * Get the available downloads for a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_downloads( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

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

		$downloads  = array();
		$_downloads = wc_get_customer_available_downloads( $id );

		foreach ( $_downloads as $key => $download ) {
			$downloads[] = array(
				'download_url'        => $download['download_url'],
				'download_id'         => $download['download_id'],
				'product_id'          => $download['product_id'],
				'download_name'       => $download['download_name'],
				'order_id'            => $download['order_id'],
				'order_key'           => $download['order_key'],
				'downloads_remaining' => $download['downloads_remaining'],
				'access_expires'      => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
				'file'                => $download['file'],
			);
		}

		return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $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
	 *
	 * The filter for role can only be a single role in a string.
	 *
	 * @since 2.3
	 * @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,
		);

		// Custom Role
		if ( ! empty( $args['role'] ) ) {
			$query_args['role'] = $args['role'];
		}

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

		// Limit number of users returned
		if ( ! empty( $args['limit'] ) ) {
			if ( -1 == $args['limit'] ) {
				unset( $query_args['number'] );
			} else {
				$query_args['number'] = absint( $args['limit'] );
				$users_per_page       = absint( $args['limit'] );
			}
		} else {
			$args['limit'] = $query_args['number'];
		}

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

		// Order (ASC or DESC, ASC by default)
		if ( ! empty( $args['order'] ) ) {
			$query_args['order'] = $args['order'];
		}

		// Order by
		if ( ! empty( $args['orderby'] ) ) {
			$query_args['orderby'] = $args['orderby'];

			// Allow sorting by meta value
			if ( ! empty( $args['orderby_meta_key'] ) ) {
				$query_args['meta_key'] = $args['orderby_meta_key'];
			}
		}

		$query = new WP_User_Query( $query_args );

		// Helper members for pagination headers
		$query->total_pages = ( -1 == $args['limit'] ) ? 1 : 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 integer $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 ) {

		try {
			$id = absint( $id );

			// validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
			}

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

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

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! current_user_can( 'list_users' ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'edit':
					if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'delete':
					if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
					}
					break;
			}

			return $id;
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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' );
	}

	/**
	 * Bulk update or insert customers
	 * Accepts an array with customers in the formats supported by
	 * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['customers'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
			}

			$data  = $data['customers'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$customers = array();

			foreach ( $data as $_customer ) {
				$customer_id = 0;

				// Try to get the customer ID
				if ( isset( $_customer['id'] ) ) {
					$customer_id = intval( $_customer['id'] );
				}

				// Customer exists / edit customer
				if ( $customer_id ) {
					$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );

					if ( is_wp_error( $edit ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$customers[] = $edit['customer'];
					}
				} else {
					// Customer don't exists / create customer
					$new = $this->create_customer( array( 'customer' => $_customer ) );

					if ( is_wp_error( $new ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$customers[] = $new['customer'];
					}
				}
			}

			return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v2/class-wc-api-exception.php000064400000002213151542631340012636 0ustar00<?php
/**
 * WooCommerce API Exception Class
 *
 * Extends Exception to provide additional data
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.2
 */

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

class WC_API_Exception extends Exception {

	/** @var string sanitized error code */
	protected $error_code;

	/**
	 * Setup exception, requires 3 params:
	 *
	 * error code - machine-readable, e.g. `woocommerce_invalid_product_id`
	 * error message - friendly message, e.g. 'Product ID is invalid'
	 * http status code - proper HTTP status code to respond with, e.g. 400
	 *
	 * @since 2.2
	 * @param string $error_code
	 * @param string $error_message user-friendly translated error message
	 * @param int $http_status_code HTTP status code to respond with
	 */
	public function __construct( $error_code, $error_message, $http_status_code ) {
		$this->error_code = $error_code;
		parent::__construct( $error_message, $http_status_code );
	}

	/**
	 * Returns the error code
	 *
	 * @since 2.2
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}
}
api/v2/class-wc-api-json-handler.php000064400000003656151542631340013240 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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 );
	}
}
api/v2/class-wc-api-orders.php000064400000167421151542631340012153 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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';

	/** @var string $post_type the custom post type */
	protected $post_type = 'shop_order';

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

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

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

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

		# GET|PUT|DELETE /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 ),
			array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ),
		);

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

		# GET|PUT|DELETE /orders/<order_id>/notes/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ),
		);

		# GET|POST /orders/<order_id>/refunds
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array(
			array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/refunds/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /orders/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		$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
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null, $filter = array() ) {

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

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

		// Get the decimal precession
		$dp         = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 );
		$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(), $dp ),
			'subtotal'                  => wc_format_decimal( $order->get_subtotal(), $dp ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), $dp ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), $dp ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), $dp ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), $dp ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), $dp ),
			'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();
			$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
			$item_meta  = $item->get_all_formatted_meta_data( $hideprefix );

			foreach ( $item_meta as $key => $values ) {
				$item_meta[ $key ]->label = $values->display_key;
				unset( $item_meta[ $key ]->display_key );
				unset( $item_meta[ $key ]->display_value );
			}

			$order_data['line_items'][] = array(
				'id'           => $item_id,
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item->get_total_tax(), $dp ),
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
				'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,
				'meta'         => array_values( $item_meta ),
			);
		}

		// 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(), $dp ),
			);
		}

		// add taxes
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$order_data['tax_lines'][] = array(
				'id'       => $tax->id,
				'rate_id'  => $tax->rate_id,
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, $dp ),
				'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 ), $dp ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
			);
		}

		// 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(), $dp ),
			);
		}

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

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

		try {
			if ( ! current_user_can( 'read_private_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {

				if ( 'any' === $status ) {

					$order_statuses = array();

					foreach ( wc_get_order_statuses() as $slug => $name ) {
						$filter['status'] = str_replace( 'wc-', '', $slug );
						$query = $this->query_orders( $filter );
						$order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts;
					}

					return array( 'count' => $order_statuses );

				} else {
					$filter['status'] = $status;
				}
			}

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

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

		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a list of valid order statuses
	 *
	 * Note this requires no specific permissions other than being an authenticated
	 * API user. Order statuses (particularly custom statuses) could be considered
	 * private information which is why it's not in the API index.
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_order_statuses() {

		$order_statuses = array();

		foreach ( wc_get_order_statuses() as $slug => $name ) {
			$order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name;
		}

		return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) );
	}

	/**
	 * Create an order
	 *
	 * @since 2.2
	 *
	 * @param array $data raw order data
	 *
	 * @return array|WP_Error
	 */
	public function create_order( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_data', $data, $this );

			// default order args, note that status is checked for validity in wc_create_order()
			$default_order_args = array(
				'status'        => isset( $data['status'] ) ? $data['status'] : '',
				'customer_note' => isset( $data['note'] ) ? $data['note'] : null,
			);

			// if creating order for existing customer
			if ( ! empty( $data['customer_id'] ) ) {

				// make sure customer exists
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				$default_order_args['customer_id'] = $data['customer_id'];
			}

			// create the pending order
			$order = $this->create_base_order( $default_order_args, $data );

			if ( is_wp_error( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 );
			}

			// billing/shipping addresses
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$set_item = "set_{$line_type}";

					foreach ( $data[ $line ] as $item ) {

						$this->$set_item( $order, $item, 'create' );
					}
				}
			}

			// calculate totals and set them
			$order->calculate_totals();

			// payment method (and payment_complete() if `paid` == true)
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// method ID & title are required
				if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );

				// mark as paid if set
				if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// set order currency
			if ( isset( $data['currency'] ) ) {

				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// set order meta
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_new_order', $order->get_id() );

			return $this->get_order( $order->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Creates new WC_Order.
	 *
	 * Requires a separate function for classes that extend WC_API_Orders.
	 *
	 * @since 2.3
	 *
	 * @param $args array
	 * @param $data
	 *
	 * @return WC_Order
	 */
	protected function create_base_order( $args, $data ) {
		return wc_create_order( $args );
	}

	/**
	 * Edit an order
	 *
	 * @since 2.2
	 *
	 * @param int $id the order ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {
		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			$update_totals = false;

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

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

			$data  = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this );
			$order = wc_get_order( $id );

			if ( empty( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$order_args = array( 'order_id' => $order->get_id() );

			// Customer note.
			if ( isset( $data['note'] ) ) {
				$order_args['customer_note'] = $data['note'];
			}

			// Customer ID.
			if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] );
			}

			// Billing/shipping address.
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$update_totals = true;

					foreach ( $data[ $line ] as $item ) {

						// Item ID is always required.
						if ( ! array_key_exists( 'id', $item ) ) {
							$item['id'] = null;
						}

						// Create item.
						if ( is_null( $item['id'] ) ) {
							$this->set_item( $order, $line_type, $item, 'create' );
						} elseif ( $this->item_is_null( $item ) ) {
							// Delete item.
							wc_delete_order_item( $item['id'] );
						} else {
							// Update item.
							$this->set_item( $order, $line_type, $item, 'update' );
						}
					}
				}
			}

			// Payment method (and payment_complete() if `paid` == true and order needs payment).
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// Method ID.
				if ( isset( $data['payment_details']['method_id'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				}

				// Method title.
				if ( isset( $data['payment_details']['method_title'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );
				}

				// Mark as paid if set.
				if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// Set order currency.
			if ( isset( $data['currency'] ) ) {
				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// If items have changed, recalculate order totals.
			if ( $update_totals ) {
				$order->calculate_totals();
			}

			// Update order meta.
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// Update the order post to set customer note/modified date.
			wc_update_order( $order_args );

			// Order status.
			if ( ! empty( $data['status'] ) ) {
				// Refresh the order instance.
				$order = wc_get_order( $order->get_id() );
				$order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true );
			}

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_update_order', $order->get_id() );

			return $this->get_order( $id );

		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

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

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

		wc_delete_shop_order_transients( $id );

		do_action( 'woocommerce_api_delete_order', $id, $this );

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

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

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => $this->post_type,
			'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 set/update the billing & shipping addresses for
	 * an order
	 *
	 * @since 2.1
	 * @param \WC_Order $order
	 * @param array $data
	 */
	protected function set_order_addresses( $order, $data ) {

		$address_fields = array(
			'first_name',
			'last_name',
			'company',
			'email',
			'phone',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		);

		$billing_address = $shipping_address = array();

		// billing address
		if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['billing_address'][ $field ] ) ) {
					$billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] );
				}
			}

			unset( $address_fields['email'] );
			unset( $address_fields['phone'] );
		}

		// shipping address
		if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['shipping_address'][ $field ] ) ) {
					$shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] );
				}
			}
		}

		$this->update_address( $order, $billing_address, 'billing' );
		$this->update_address( $order, $shipping_address, 'shipping' );

		// update user meta
		if ( $order->get_user_id() ) {
			foreach ( $billing_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'billing_' . $key, $value );
			}
			foreach ( $shipping_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value );
			}
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order
	 * @param array $posted
	 * @param string $type Type of address; 'billing' or 'shipping'.
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Helper method to add/update order meta, with two restrictions:
	 *
	 * 1) Only non-protected meta (no leading underscore) can be set
	 * 2) Meta values must be scalar (int, string, bool)
	 *
	 * @since 2.2
	 * @param int $order_id valid order ID
	 * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format
	 */
	protected function set_order_meta( $order_id, $order_meta ) {

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

			if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) {
				update_post_meta( $order_id, $meta_key, $meta_value );
			}
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null
	 *
	 * Items can be deleted by setting the resource ID to null
	 *
	 * @since 2.2
	 * @param array $item item provided in the request body
	 * @return bool true if the item resource ID is null, false otherwise
	 */
	protected function item_is_null( $item ) {

		$keys = array( 'product_id', 'method_id', 'title', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Wrapper method to create/update order items
	 *
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @since 2.2
	 * @param \WC_Order $order order
	 * @param string $item_type
	 * @param array $item item provided in the request body
	 * @param string $action either 'create' or 'update'
	 * @throws WC_API_Exception if item ID is not associated with order
	 */
	protected function set_item( $order, $item_type, $item, $action ) {
		global $wpdb;

		$set_method = "set_{$item_type}";

		// verify provided line item ID is associated with order
		if ( 'update' === $action ) {

			$result = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
				absint( $item['id'] ),
				absint( $order->get_id() )
			) );

			if ( is_null( $result ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		$this->$set_method( $order, $item, $action );
	}

	/**
	 * Create or update a line item
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $item line item data
	 * @param string $action 'create' to add line item or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_line_item( $order, $item, $action ) {
		$creating  = ( 'create' === $action );

		// product is always required
		if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 );
		}

		// when updating, ensure product ID provided matches
		if ( 'update' === $action ) {

			$item_product_id   = wc_get_order_item_meta( $item['id'], '_product_id' );
			$item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' );

			if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 );
			}
		}

		if ( isset( $item['product_id'] ) ) {
			$product_id = $item['product_id'];
		} elseif ( isset( $item['sku'] ) ) {
			$product_id = wc_get_product_id_by_sku( $item['sku'] );
		}

		// variations must each have a key & value
		$variation_id = 0;
		if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) {
			foreach ( $item['variations'] as $key => $value ) {
				if ( ! $key || ! $value ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 );
				}
			}
			$variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] );
		}

		$product = wc_get_product( $variation_id ? $variation_id : $product_id );

		// must be a valid WC_Product
		if ( ! is_object( $product ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 );
		}

		// quantity must be positive float
		if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 );
		}

		// quantity is required when creating
		if ( $creating && ! isset( $item['quantity'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 );
		}

		if ( $creating ) {
			$line_item = new WC_Order_Item_Product();
		} else {
			$line_item = new WC_Order_Item_Product( $item['id'] );
		}

		$line_item->set_product( $product );
		$line_item->set_order_id( $order->get_id() );

		if ( isset( $item['quantity'] ) ) {
			$line_item->set_quantity( $item['quantity'] );
		}
		if ( isset( $item['total'] ) ) {
			$line_item->set_total( floatval( $item['total'] ) );
		} elseif ( $creating ) {
			$total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) );
			$line_item->set_total( $total );
			$line_item->set_subtotal( $total );
		}
		if ( isset( $item['total_tax'] ) ) {
			$line_item->set_total_tax( floatval( $item['total_tax'] ) );
		}
		if ( isset( $item['subtotal'] ) ) {
			$line_item->set_subtotal( floatval( $item['subtotal'] ) );
		}
		if ( isset( $item['subtotal_tax'] ) ) {
			$line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) );
		}
		if ( $variation_id ) {
			$line_item->set_variation_id( $variation_id );
			$line_item->set_variation( $item['variations'] );
		}

		// Save or add to order.
		if ( $creating ) {
			$order->add_item( $line_item );
		} else {
			$item_id = $line_item->save();

			if ( ! $item_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Given a product ID & API provided variations, find the correct variation ID to use for calculation
	 * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass
	 * the cheapest variation ID but provide other information so we have to look up the variation ID.
	 *
	 * @param WC_Product $product
	 * @param array $variations
	 *
	 * @return int returns an ID if a valid variation was found for this product
	 */
	function get_variation_id( $product, $variations = array() ) {
		$variation_id = null;
		$variations_normalized = array();

		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			if ( isset( $variations ) && is_array( $variations ) ) {
				// start by normalizing the passed variations
				foreach ( $variations as $key => $value ) {
					$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php
					$variations_normalized[ $key ] = strtolower( $value );
				}
				// now search through each product child and see if our passed variations match anything
				foreach ( $product->get_children() as $variation ) {
					$meta = array();
					foreach ( get_post_meta( $variation ) as $key => $value ) {
						$value = $value[0];
						$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) );
						$meta[ $key ] = strtolower( $value );
					}
					// if the variation array is a part of the $meta array, we found our match
					if ( $this->array_contains( $variations_normalized, $meta ) ) {
						$variation_id = $variation;
						break;
					}
				}
			}
		}

		return $variation_id;
	}

	/**
	 * Utility function to see if the meta array contains data from variations
	 *
	 * @param array $needles
	 * @param array $haystack
	 *
	 * @return bool
	 */
	protected function array_contains( $needles, $haystack ) {
		foreach ( $needles as $key => $value ) {
			if ( $haystack[ $key ] !== $value ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Create or update an order shipping method
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $shipping item data
	 * @param string $action 'create' to add shipping or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_shipping( $order, $shipping, $action ) {

		// total must be a positive float
		if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// method ID is required
			if ( ! isset( $shipping['method_id'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}

			$rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] );
			$item = new WC_Order_Item_Shipping();
			$item->set_order_id( $order->get_id() );
			$item->set_shipping_rate( $rate );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Shipping( $shipping['id'] );

			if ( isset( $shipping['method_id'] ) ) {
				$item->set_method_id( $shipping['method_id'] );
			}

			if ( isset( $shipping['method_title'] ) ) {
				$item->set_method_title( $shipping['method_title'] );
			}

			if ( isset( $shipping['total'] ) ) {
				$item->set_total( floatval( $shipping['total'] ) );
			}

			$shipping_id = $item->save();

			if ( ! $shipping_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order fee
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $fee item data
	 * @param string $action 'create' to add fee or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_fee( $order, $fee, $action ) {

		if ( 'create' === $action ) {

			// fee title is required
			if ( ! isset( $fee['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Fee();
			$item->set_order_id( $order->get_id() );
			$item->set_name( wc_clean( $fee['title'] ) );
			$item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 );

			// if taxable, tax class and total are required
			if ( ! empty( $fee['taxable'] ) ) {
				if ( ! isset( $fee['tax_class'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 );
				}

				$item->set_tax_status( 'taxable' );
				$item->set_tax_class( $fee['tax_class'] );

				if ( isset( $fee['total_tax'] ) ) {
					$item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 );
				}

				if ( isset( $fee['tax_data'] ) ) {
					$item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) );
					$item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) );
				}
			}

			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Fee( $fee['id'] );

			if ( isset( $fee['title'] ) ) {
				$item->set_name( wc_clean( $fee['title'] ) );
			}

			if ( isset( $fee['tax_class'] ) ) {
				$item->set_tax_class( $fee['tax_class'] );
			}

			if ( isset( $fee['total'] ) ) {
				$item->set_total( floatval( $fee['total'] ) );
			}

			if ( isset( $fee['total_tax'] ) ) {
				$item->set_total_tax( floatval( $fee['total_tax'] ) );
			}

			$fee_id = $item->save();

			if ( ! $fee_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order coupon
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $coupon item data
	 * @param string $action 'create' to add coupon or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_coupon( $order, $coupon, $action ) {

		// coupon amount must be positive float
		if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// coupon code is required
			if ( empty( $coupon['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Coupon();
			$item->set_props( array(
				'code'         => $coupon['code'],
				'discount'     => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0,
				'discount_tax' => 0,
				'order_id'     => $order->get_id(),
			) );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Coupon( $coupon['id'] );

			if ( isset( $coupon['code'] ) ) {
				$item->set_code( $coupon['code'] );
			}

			if ( isset( $coupon['amount'] ) ) {
				$item->set_discount( floatval( $coupon['amount'] ) );
			}

			$coupon_id = $item->save();

			if ( ! $coupon_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 );
			}
		}
	}

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

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

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

		$args = array(
			'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) );
		}

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

	/**
	 * Get an order note for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param string $id order note ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_order_note( $order_id, $id, $fields = null ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order note for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @return WP_Error|array error or created note response data
	 */
	public function create_order_note( $order_id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 );
			}

			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			$order = wc_get_order( $order_id );

			$data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this );

			// note content is required
			if ( ! isset( $data['note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 );
			}

			$is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] );

			// create the note
			$note_id = $order->add_order_note( $data['note'], $is_customer_note );

			if ( ! $note_id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this );

			return $this->get_order_note( $order->get_id(), $note_id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit the order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited note response data
	 */
	public function edit_order_note( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			$order = wc_get_order( $order_id );

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this );

			// Note content
			if ( isset( $data['note'] ) ) {

				wp_update_comment(
					array(
						'comment_ID'      => $note->comment_ID,
						'comment_content' => $data['note'],
					)
				);
			}

			// Customer note
			if ( isset( $data['customer_note'] ) ) {

				update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 );
			}

			do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this );

			return $this->get_order_note( $order->get_id(), $note->comment_ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_note( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

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

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			// Force delete since trashed order notes could not be managed through comments list table
			$result = wc_delete_order_note( $note->comment_ID );

			if ( ! $result ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 );
			}

			do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this );

			return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the order refunds for an order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_refunds( $order_id, $fields = null ) {

		// Ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

		$refund_items = wc_get_orders( array(
			'type'   => 'shop_order_refund',
			'parent' => $order_id,
			'limit'  => -1,
			'return' => 'ids',
		) );
		$order_refunds = array();

		foreach ( $refund_items as $refund_id ) {
			$order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) );
		}

		return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) );
	}

	/**
	 * Get an order refund for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param int $id
	 * @param string|null $fields fields to limit response to
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			$order  = wc_get_order( $order_id );
			$refund = wc_get_order( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			$line_items = array();

			// Add line items
			foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) {
				$product    = $item->get_product();
				$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
				$item_meta  = $item->get_all_formatted_meta_data( $hideprefix );

				foreach ( $item_meta as $key => $values ) {
					$item_meta[ $key ]->label = $values->display_key;
					unset( $item_meta[ $key ]->display_key );
					unset( $item_meta[ $key ]->display_value );
				}

				$line_items[] = array(
					'id'               => $item_id,
					'subtotal'         => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
					'subtotal_tax'     => wc_format_decimal( $item->get_subtotal_tax(), 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,
					'meta'             => array_values( $item_meta ),
					'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ),
				);
			}

			$order_refund = array(
				'id'         => $refund->get_id(),
				'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ),
				'amount'     => wc_format_decimal( $refund->get_amount(), 2 ),
				'reason'     => $refund->get_reason(),
				'line_items' => $line_items,
			);

			return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order refund for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @param bool $api_refund do refund using a payment gateway API
	 * @return WP_Error|array error or created refund response data
	 */
	public function create_order_refund( $order_id, $data, $api_refund = true ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 );
			}

			$order_id = absint( $order_id );

			if ( empty( $order_id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this );

			// Refund amount is required
			if ( ! isset( $data['amount'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 );
			} elseif ( 0 > $data['amount'] ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 );
			}

			$data['order_id']  = $order_id;
			$data['refund_id'] = 0;

			// Create the refund
			$refund = wc_create_refund( $data );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
			}

			// Refund via API
			if ( $api_refund ) {
				if ( WC()->payment_gateways() ) {
					$payment_gateways = WC()->payment_gateways->payment_gateways();
				}

				$order = wc_get_order( $order_id );

				if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) {
					$result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() );

					if ( is_wp_error( $result ) ) {
						return $result;
					} elseif ( ! $result ) {
						throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 );
					}
				}
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit an order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited refund response data
	 */
	public function edit_order_refund( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure order ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this );

			// Update reason
			if ( isset( $data['reason'] ) ) {
				$updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) );

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

			// Update refund amount
			if ( isset( $data['amount'] ) && 0 < $data['amount'] ) {
				update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) );
			}

			do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_refund( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

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

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure refund ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			wc_delete_shop_order_transients( $order_id );

			do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this );

			return $this->delete( $refund->ID, 'refund', true );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Bulk update or insert orders
	 * Accepts an array with orders in the formats supported by
	 * WC_API_Orders->create_order() and WC_API_Orders->edit_order()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['orders'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 );
			}

			$data  = $data['orders'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$orders = array();

			foreach ( $data as $_order ) {
				$order_id = 0;

				// Try to get the order ID
				if ( isset( $_order['id'] ) ) {
					$order_id = intval( $_order['id'] );
				}

				// Order exists / edit order
				if ( $order_id ) {
					$edit = $this->edit_order( $order_id, array( 'order' => $_order ) );

					if ( is_wp_error( $edit ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$orders[] = $edit['order'];
					}
				} else {
					// Order don't exists / create order
					$new = $this->create_order( array( 'order' => $_order ) );

					if ( is_wp_error( $new ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$orders[] = $new['order'];
					}
				}
			}

			return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v2/class-wc-api-products.php000064400000220631151542631340012511 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/POST /products
	 * GET /products/count
	 * GET/PUT/DELETE /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
		);

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

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

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

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

		# GET/POST /products/attributes
		$routes[ $this->base . '/attributes' ] = array(
			array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /attributes/<id>
		$routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ),
		);

		# GET /products/sku/<product sku>
		$routes[ $this->base . '/sku/(?P<sku>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ),
		);

		# POST|PUT /products/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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->get_parent_id() ) {
			$_product = wc_get_product( $product->get_parent_id() );
			$product_data['parent'] = $this->get_product_data( $_product );
		}

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

	/**
	 * Get the total number of products
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 );
			}

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

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

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product
	 *
	 * @since 2.2
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product( $data ) {
		$id = 0;

		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			// Check permissions
			if ( ! current_user_can( 'publish_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );

			// Check if product title is specified
			if ( ! isset( $data['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 );
			}

			// Check product type
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'simple';
			}

			// Set visible visibility when not sent
			if ( ! isset( $data['catalog_visibility'] ) ) {
				$data['catalog_visibility'] = 'visible';
			}

			// Validate the product type
			if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Enable description html tags.
			$post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
			if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {

				$post_content = wp_filter_post_kses( $data['description'] );
			}

			// Enable short description html tags.
			$post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
			if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
				$post_excerpt = wp_filter_post_kses( $data['short_description'] );
			}

			$classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] );
			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}
			$product = new $classname();

			$product->set_name( wc_clean( $data['title'] ) );
			$product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' );
			$product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' );
			$product->set_description( isset( $data['description'] ) ? $post_content : '' );

			// Attempts to create the new product.
			$product->save();
			$id = $product->get_id();

			// Checks for an error in the product creation
			if ( 0 >= $id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
			}

			// Check for featured/gallery images, upload it and set it
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields
			$product = $this->save_product_meta( $product, $data );
			$product->save();

			// Save variations
			if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
				$this->save_variations( $product, $data );
			}

			do_action( 'woocommerce_api_create_product', $id, $data );

			// Clear cache/transients
			wc_delete_product_transients( $id );

			$this->server->send_status( 201 );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {
		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

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

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

			$product = wc_get_product( $id );

			$data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );

			// Product title.
			if ( isset( $data['title'] ) ) {
				$product->set_name( wc_clean( $data['title'] ) );
			}

			// Product name (slug).
			if ( isset( $data['name'] ) ) {
				$product->set_slug( wc_clean( $data['name'] ) );
			}

			// Product status.
			if ( isset( $data['status'] ) ) {
				$product->set_status( wc_clean( $data['status'] ) );
			}

			// Product short description.
			if ( isset( $data['short_description'] ) ) {
				// Enable short description html tags.
				$post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] );
				$product->set_short_description( $post_excerpt );
			}

			// Product description.
			if ( isset( $data['description'] ) ) {
				// Enable description html tags.
				$post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] );
				$product->set_description( $post_content );
			}

			// Validate the product type.
			if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );

			// Save variations.
			if ( $product->is_type( 'variable' ) ) {
				if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
					$this->save_variations( $product, $data );
				} else {
					// Just sync variations.
					$product = WC_Product_Variable::sync( $product, false );
				}
			}

			$product->save();

			do_action( 'woocommerce_api_edit_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product.
	 *
	 * @since 2.2
	 *
	 * @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;
		}

		$product = wc_get_product( $id );

		do_action( 'woocommerce_api_delete_product', $id, $this );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $product->is_type( 'variable' ) ) {
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$product->delete( true );
			$result = ! ( $product->get_id() > 0 );
		} else {
			$product->delete();
			$result = 'trash' === $product->get_status();
		}

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

		// Delete parent product transients.
		if ( $parent_id = wp_get_post_parent_id( $id ) ) {
			wc_delete_product_transients( $parent_id );
		}

		if ( $force ) {
			return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) );
		} else {
			$this->server->send_status( '202' );

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

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

		$comments = get_approved_comments( $id );
		$reviews  = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => intval( $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 ) );
	}

	/**
	 * Get the orders for a product
	 *
	 * @since 2.4.0
	 * @param int $id the product ID to get orders for
	 * @param string fields  fields to retrieve
	 * @param array $filter filters to include in response
	 * @param string $status the order status to retrieve
	 * @param $page  $page   page to retrieve
	 * @return array|WP_Error
	 */
	public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) {
		global $wpdb;

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

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

		$order_ids = $wpdb->get_col( $wpdb->prepare( "
			SELECT order_id
			FROM {$wpdb->prefix}woocommerce_order_items
			WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
			AND order_item_type = 'line_item'
		 ", $id ) );

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

		$filter = array_merge( $filter, array(
			'in' => implode( ',', $order_ids ),
		) );

		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page );

		return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) );
	}

	/**
	 * Get a listing of product categories
	 *
	 * @since 2.2
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_categories( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$product_categories = array();

			$terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
			}

			return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product category for the given ID
	 *
	 * @since 2.2
	 *
	 * @param string $id product category term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_category( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_cat' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			// Get category display type
			$display_type = get_term_meta( $term_id, 'display_type', true );

			// Get category image
			$image = '';
			if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) {
				$image = wp_get_attachment_url( $image_id );
			}

			$product_category = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'display'     => $display_type ? $display_type : 'default',
				'image'       => $image ? esc_url( $image ) : '',
				'count'       => intval( $term->count ),
			);

			return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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'] );
		}

		// Filter products by category
		if ( ! empty( $args['category'] ) ) {
			$query_args['product_cat'] = $args['category'];
		}

		// Filter by specific sku
		if ( ! empty( $args['sku'] ) ) {
			if ( ! is_array( $query_args['meta_query'] ) ) {
				$query_args['meta_query'] = array();
			}

			$query_args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => $args['sku'],
				'compare' => '=',
			);

			$query_args['post_type'] = array( 'product', 'product_variation' );
		}

		$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();
		}

		$prices_precision = wc_get_price_decimals();
		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(), $prices_precision ),
			'regular_price'      => wc_format_decimal( $product->get_regular_price(), $prices_precision ),
			'sale_price'         => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : 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(),
			'product_url'        => $product->is_type( 'external' ) ? $product->get_product_url() : '',
			'button_text'        => $product->is_type( 'external' ) ? $product->get_button_text() : '',
			'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'        => wpautop( do_shortcode( $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() ),
			'parent_id'          => $product->get_parent_id(),
			'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'      => wpautop( do_shortcode( wp_kses_post( $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 ) {
		$prices_precision = wc_get_price_decimals();
		$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(), $prices_precision ),
				'regular_price'     => wc_format_decimal( $variation->get_regular_price(), $prices_precision ),
				'sale_price'        => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null,
				'taxable'           => $variation->is_taxable(),
				'tax_status'        => $variation->get_tax_status(),
				'tax_class'         => $variation->get_tax_class(),
				'managing_stock'    => $variation->managing_stock(),
				'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;
	}

	/**
	 * Save default attributes.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product
	 * @param array $request
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		// Update default attributes options setting.
		if ( isset( $request['default_attribute'] ) ) {
			$request['default_attributes'] = $request['default_attribute'];
		}

		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) {
				if ( ! isset( $default_attr['name'] ) ) {
					continue;
				}

				$taxonomy = sanitize_title( $default_attr['name'] );

				if ( isset( $default_attr['slug'] ) ) {
					$taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
				}

				if ( isset( $attributes[ $taxonomy ] ) ) {
					$_attribute = $attributes[ $taxonomy ];

					if ( $_attribute['is_variation'] ) {
						$value = '';

						if ( isset( $default_attr['option'] ) ) {
							if ( $_attribute['is_taxonomy'] ) {
								// Don't use wc_clean as it destroys sanitized characters.
								$value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
							} else {
								$value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
							}
						}

						if ( $value ) {
							$default_attributes[ $taxonomy ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Save product meta
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $data
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_meta( $product, $data ) {
		global $wpdb;

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$product->set_virtual( $data['virtual'] );
		}

		// Tax status
		if ( isset( $data['tax_status'] ) ) {
			$product->set_tax_status( wc_clean( $data['tax_status'] ) );
		}

		// Tax Class
		if ( isset( $data['tax_class'] ) ) {
			$product->set_tax_class( wc_clean( $data['tax_class'] ) );
		}

		// Catalog Visibility
		if ( isset( $data['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) );
		}

		// Purchase Note
		if ( isset( $data['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $data['purchase_note'] ) );
		}

		// Featured Product
		if ( isset( $data['featured'] ) ) {
			$product->set_featured( $data['featured'] );
		}

		// Shipping data
		$product = $this->save_product_shipping_data( $product, $data );

		// SKU
		if ( isset( $data['sku'] ) ) {
			$sku     = $product->get_sku();
			$new_sku = wc_clean( $data['sku'] );

			if ( '' == $new_sku ) {
				$product->set_sku( '' );
			} elseif ( $new_sku !== $sku ) {
				if ( ! empty( $new_sku ) ) {
					$unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku );
					if ( ! $unique_sku ) {
						throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 );
					} else {
						$product->set_sku( $new_sku );
					}
				} else {
					$product->set_sku( '' );
				}
			}
		}

		// Attributes
		if ( isset( $data['attributes'] ) ) {
			$attributes = array();

			foreach ( $data['attributes'] as $attribute ) {
				$is_taxonomy = 0;
				$taxonomy    = 0;

				if ( ! isset( $attribute['name'] ) ) {
					continue;
				}

				$attribute_slug = sanitize_title( $attribute['name'] );

				if ( isset( $attribute['slug'] ) ) {
					$taxonomy       = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					$attribute_slug = sanitize_title( $attribute['slug'] );
				}

				if ( $taxonomy ) {
					$is_taxonomy = 1;
				}

				if ( $is_taxonomy ) {

					$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] );

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					// Update post terms
					if ( taxonomy_exists( $taxonomy ) ) {
						wp_set_object_terms( $product->get_id(), $values, $taxonomy );
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $taxonomy );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Array based
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];

					// Text based, separate by pipe
					} else {
						$values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) );
					}

					// Custom attribute - Add attribute to array and set the values.
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute['name'] );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}

			uasort( $attributes, 'wc_product_attribute_uasort_comparison' );

			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) {

			// Variable and grouped products have no prices.
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );

		} else {

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price'];
				$product->set_regular_price( $regular_price );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price'];
				$product->set_sale_price( $sale_price );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$date_from = $data['sale_price_dates_from'];
			} else {
				$date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '';
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$date_to = $data['sale_price_dates_to'];
			} else {
				$date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '';
			}

			if ( $date_to && ! $date_from ) {
				$date_from = strtotime( 'NOW', current_time( 'timestamp', true ) );
			}

			$product->set_date_on_sale_to( $date_to );
			$product->set_date_on_sale_from( $date_from );

			if ( $product->is_on_sale( 'edit' ) ) {
				$product->set_price( $product->get_sale_price( 'edit' ) );
			} else {
				$product->set_price( $product->get_regular_price( 'edit' ) );
			}
		}

		// Product parent ID for groups
		if ( isset( $data['parent_id'] ) ) {
			$product->set_parent_id( absint( $data['parent_id'] ) );
		}

		// Sold Individually
		if ( isset( $data['sold_individually'] ) ) {
			$product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' );
		}

		// Stock status
		if ( isset( $data['in_stock'] ) ) {
			$stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();

			if ( '' === $stock_status ) {
				$stock_status = 'instock';
			}
		}

		// Stock Data
		if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock
			if ( isset( $data['managing_stock'] ) ) {
				$managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
				$product->set_manage_stock( $managing_stock );
			} else {
				$managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
			}

			// Backorders
			if ( isset( $data['backorders'] ) ) {
				if ( 'notify' == $data['backorders'] ) {
					$backorders = 'notify';
				} else {
					$backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
				}

				$product->set_backorders( $backorders );
			} else {
				$backorders = $product->get_backorders();
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( 'yes' == $managing_stock ) {
				$product->set_backorders( $backorders );

				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity
				if ( isset( $data['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_backorders( $backorders );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells
		if ( isset( $data['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $data['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}

				$product->set_upsell_ids( $upsells );
			} else {
				$product->set_upsell_ids( array() );
			}
		}

		// Cross sells
		if ( isset( $data['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $data['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}

				$product->set_cross_sell_ids( $crosssells );
			} else {
				$product->set_cross_sell_ids( array() );
			}
		}

		// Product categories
		if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
			$product->set_category_ids( $data['categories'] );
		}

		// Product tags
		if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
			$product->set_tag_ids( $data['tags'] );
		}

		// Downloadable
		if ( isset( $data['downloadable'] ) ) {
			$is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
			$product->set_downloadable( $is_downloadable );
		} else {
			$is_downloadable = $product->get_downloadable() ? 'yes' : 'no';
		}

		// Downloadable options
		if ( 'yes' == $is_downloadable ) {

			// Downloadable files
			if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $data['downloads'] );
			}

			// Download limit
			if ( isset( $data['download_limit'] ) ) {
				$product->set_download_limit( $data['download_limit'] );
			}

			// Download expiry
			if ( isset( $data['download_expiry'] ) ) {
				$product->set_download_expiry( $data['download_expiry'] );
			}
		}

		// Product url
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $data['product_url'] ) ) {
				$product->set_product_url( $data['product_url'] );
			}

			if ( isset( $data['button_text'] ) ) {
				$product->set_button_text( $data['button_text'] );
			}
		}

		// Reviews allowed
		if ( isset( $data['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $data['reviews_allowed'] );
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $data );
		}

		// Do action for product type
		do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data );

		return $product;
	}

	/**
	 * Save variations
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $request
	 *
	 * @return true
	 *
	 * @throws WC_API_Exception
	 */
	protected function save_variations( $product, $request ) {
		global $wpdb;

		$id         = $product->get_id();
		$attributes = $product->get_attributes();

		foreach ( $request['variations'] as $menu_order => $data ) {
			$variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;
			$variation    = new WC_Product_Variation( $variation_id );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = current( $data['image'] );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->save_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$is_downloadable = $data['downloadable'];
				$variation->set_downloadable( $is_downloadable );
			} else {
				$is_downloadable = $variation->get_downloadable();
			}

			// Downloads.
			if ( $is_downloadable ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			$manage_stock = (bool) $variation->get_manage_stock();
			if ( isset( $data['managing_stock'] ) ) {
				$manage_stock = $data['managing_stock'];
			}
			$variation->set_manage_stock( $manage_stock );

			$stock_status = $variation->get_stock_status();
			if ( isset( $data['in_stock'] ) ) {
				$stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock';
			}
			$variation->set_stock_status( $stock_status );

			$backorders = $variation->get_backorders();
			if ( isset( $data['backorders'] ) ) {
				$backorders = $data['backorders'];
			}
			$variation->set_backorders( $backorders );

			if ( $manage_stock ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$variation->set_date_on_sale_from( $data['sale_price_dates_from'] );
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$variation->set_date_on_sale_to( $data['sale_price_dates_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$_attributes = array();

				foreach ( $data['attributes'] as $attribute_key => $attribute ) {
					if ( ! isset( $attribute['name'] ) ) {
						continue;
					}

					$taxonomy   = 0;
					$_attribute = array();

					if ( isset( $attribute['slug'] ) ) {
						$taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					}

					if ( ! $taxonomy ) {
						$taxonomy = sanitize_title( $attribute['name'] );
					}

					if ( isset( $attributes[ $taxonomy ] ) ) {
						$_attribute = $attributes[ $taxonomy ];
					}

					if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
						$_attribute_key = sanitize_title( $_attribute['name'] );

						if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
							// Don't use wc_clean as it destroys sanitized characters
							$_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
						} else {
							$_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
						}

						$_attributes[ $_attribute_key ] = $_attribute_value;
					}
				}

				$variation->set_attributes( $_attributes );
			}

			$variation->save();

			do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
		}

		return true;
	}

	/**
	 * Save product shipping data
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $data
	 * @return WC_Product
	 */
	private function save_product_shipping_data( $product, $data ) {
		if ( isset( $data['weight'] ) ) {
			$product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) );
		}

		// Product dimensions
		if ( isset( $data['dimensions'] ) ) {
			// Height
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
			}

			// Width
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
			}

			// Length
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
			}
		}

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';

			if ( 'yes' == $virtual ) {
				$product->set_weight( '' );
				$product->set_height( '' );
				$product->set_length( '' );
				$product->set_width( '' );
			}
		}

		// Shipping class
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $downloads
	 * @param int $deprecated Deprecated since 3.0.
	 * @return WC_Product
	 */
	private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( isset( $file['url'] ) ) {
				$file['file'] = $file['url'];
			}

			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[]  = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Get attribute taxonomy by slug.
	 *
	 * @since 2.2
	 * @param string $slug
	 * @return string|null
	 */
	private function get_attribute_taxonomy_by_slug( $slug ) {
		$taxonomy = null;
		$attribute_taxonomies = wc_get_attribute_taxonomies();

		foreach ( $attribute_taxonomies as $key => $tax ) {
			if ( $slug == $tax->attribute_name ) {
				$taxonomy = 'pa_' . $tax->attribute_name;

				break;
			}
		}

		return $taxonomy;
	}

	/**
	 * 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'   => (int) $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;
	}

	/**
	 * Save product images
	 *
	 * @since  2.2
	 *
	 * @param WC_Product $product
	 * @param array      $images
	 *
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_images( $product, $images ) {
		if ( is_array( $images ) ) {
			$gallery = array();

			foreach ( $images as $image ) {
				if ( isset( $image['position'] ) && 0 == $image['position'] ) {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$product->set_image_id( $attachment_id );
				} else {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					} else {
						$gallery[] = $attachment_id;
					}
				}
			}

			if ( ! empty( $gallery ) ) {
				$product->set_gallery_image_ids( $gallery );
			}
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Upload image from URL
	 *
	 * @since  2.2
	 *
	 * @param  string $image_url
	 *
	 * @return array
	 *
	 * @throws WC_API_Exception
	 */
	public function upload_product_image( $image_url ) {
		$upload = wc_rest_upload_image_from_url( $image_url );
		if ( is_wp_error( $upload ) ) {
			throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 );
		}

		return $upload;
	}

	/**
	 * Sets product image as attachment and returns the attachment ID.
	 *
	 * @since 2.2
	 * @param array $upload
	 * @param int $id
	 * @return int
	 */
	protected function set_product_image_as_attachment( $upload, $id ) {
		$info    = wp_check_filetype( $upload['file'] );
		$title   = '';
		$content = '';

		if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = wc_clean( $image_meta['title'] );
			}
			if ( trim( $image_meta['caption'] ) ) {
				$content = wc_clean( $image_meta['caption'] );
			}
		}

		$attachment = array(
			'post_mime_type' => $info['type'],
			'guid'           => $upload['url'],
			'post_parent'    => $id,
			'post_title'     => $title,
			'post_content'   => $content,
		);

		$attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
		if ( ! is_wp_error( $attachment_id ) ) {
			wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
		}

		return $attachment_id;
	}

	/**
	 * 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'   => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ),
					'slug'   => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => wc_attribute_label( $attribute['name'] ),
					'slug'      => wc_attribute_taxonomy_slug( $attribute['name'] ),
					'position'  => (int) $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;
	}

	/**
	 * Get a listing of product attributes
	 *
	 * @since 2.4.0
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attributes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$product_attributes   = array();
			$attribute_taxonomies = wc_get_attribute_taxonomies();

			foreach ( $attribute_taxonomies as $attribute ) {
				$product_attributes[] = array(
					'id'           => intval( $attribute->attribute_id ),
					'name'         => $attribute->attribute_label,
					'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
					'type'         => $attribute->attribute_type,
					'order_by'     => $attribute->attribute_orderby,
					'has_archives' => (bool) $attribute->attribute_public,
				);
			}

			return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute for the given ID
	 *
	 * @since 2.4.0
	 *
	 * @param string $id product attribute term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$attribute = $wpdb->get_row( $wpdb->prepare( "
				SELECT *
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$product_attribute = array(
				'id'           => intval( $attribute->attribute_id ),
				'name'         => $attribute->attribute_label,
				'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
				'type'         => $attribute->attribute_type,
				'order_by'     => $attribute->attribute_orderby,
				'has_archives' => (bool) $attribute->attribute_public,
			);

			return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate attribute data.
	 *
	 * @since  2.4.0
	 * @param  string $name
	 * @param  string $slug
	 * @param  string $type
	 * @param  string $order_by
	 * @param  bool   $new_data
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
		if ( empty( $name ) ) {
			throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
		}

		if ( strlen( $slug ) > 28 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 );
		}

		// Validate the attribute type
		if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
		}

		// Validate the attribute order by
		if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
		}

		return true;
	}

	/**
	 * Create a new product attribute
	 *
	 * @since 2.4.0
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$data = $data['product_attribute'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );

			if ( ! isset( $data['name'] ) ) {
				$data['name'] = '';
			}

			// Set the attribute slug
			if ( ! isset( $data['slug'] ) ) {
				$data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
			} else {
				$data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
			}

			// Set attribute type when not sent
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'select';
			}

			// Set order by when not sent
			if ( ! isset( $data['order_by'] ) ) {
				$data['order_by'] = 'menu_order';
			}

			// Validate the attribute data
			$this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );

			$insert = $wpdb->insert(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $data['name'],
					'attribute_name'    => $data['slug'],
					'attribute_type'    => $data['type'],
					'attribute_orderby' => $data['order_by'],
					'attribute_public'  => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
				),
				array( '%s', '%s', '%s', '%s', '%d' )
			);

			// Checks for an error in the product creation
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
			}

			$id = $wpdb->insert_id;

			do_action( 'woocommerce_api_create_product_attribute', $id, $data );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			$this->server->send_status( 201 );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute
	 *
	 * @since 2.4.0
	 *
	 * @param int $id the attribute ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$data      = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this );
			$attribute = $this->get_product_attribute( $id );

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

			$attribute_name     = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name'];
			$attribute_type     = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type'];
			$attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by'];

			if ( isset( $data['slug'] ) ) {
				$attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) );
			} else {
				$attribute_slug = $attribute['product_attribute']['slug'];
			}
			$attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug );

			if ( isset( $data['has_archives'] ) ) {
				$attribute_public = true === $data['has_archives'] ? 1 : 0;
			} else {
				$attribute_public = $attribute['product_attribute']['has_archives'];
			}

			// Validate the attribute data
			$this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false );

			$update = $wpdb->update(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $attribute_name,
					'attribute_name'    => $attribute_slug,
					'attribute_type'    => $attribute_type,
					'attribute_orderby' => $attribute_order_by,
					'attribute_public'  => $attribute_public,
				),
				array( 'attribute_id' => $id ),
				array( '%s', '%s', '%s', '%s', '%d' ),
				array( '%d' )
			);

			// Checks for an error in the product creation
			if ( false === $update ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute', $id, $data );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute
	 *
	 * @since  2.4.0
	 *
	 * @param  int $id the product attribute ID
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			$attribute_name = $wpdb->get_var( $wpdb->prepare( "
				SELECT attribute_name
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_null( $attribute_name ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$deleted = $wpdb->delete(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array( 'attribute_id' => $id ),
				array( '%d' )
			);

			if ( false === $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name( $attribute_name );

			if ( taxonomy_exists( $taxonomy ) ) {
				$terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' );
				foreach ( $terms as $term ) {
					wp_delete_term( $term->term_id, $taxonomy );
				}
			}

			do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy );
			do_action( 'woocommerce_api_delete_product_attribute', $id, $this );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get product by SKU
	 *
	 * @deprecated 2.4.0
	 *
	 * @since  2.3.0
	 *
	 * @param  int    $sku the product SKU
	 * @param  string $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_product_by_sku( $sku, $fields = null ) {
		try {
			$id = wc_get_product_id_by_sku( $sku );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 );
			}

			return $this->get_product( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Clear product
	 *
	 * @param int $product_id
	 */
	protected function clear_product( $product_id ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		// Delete product attachments
		$attachments = get_children( array(
			'post_parent' => $product_id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product
		$product = wc_get_product( $product_id );
		$product->delete();
	}

	/**
	 * Bulk update or insert products
	 * Accepts an array with products in the formats supported by
	 * WC_API_Products->create_product() and WC_API_Products->edit_product()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['products'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 );
			}

			$data  = $data['products'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$products = array();

			foreach ( $data as $_product ) {
				$product_id  = 0;
				$product_sku = '';

				// Try to get the product ID
				if ( isset( $_product['id'] ) ) {
					$product_id = intval( $_product['id'] );
				}

				if ( ! $product_id && isset( $_product['sku'] ) ) {
					$product_sku = wc_clean( $_product['sku'] );
					$product_id  = wc_get_product_id_by_sku( $product_sku );
				}

				if ( $product_id ) {

					// Product exists / edit product
					$edit = $this->edit_product( $product_id, array( 'product' => $_product ) );

					if ( is_wp_error( $edit ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$products[] = $edit['product'];
					}
				} else {

					// Product don't exists / create product
					$new = $this->create_product( array( 'product' => $_product ) );

					if ( is_wp_error( $new ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$products[] = $new['product'];
					}
				}
			}

			return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v2/class-wc-api-reports.php000064400000023071151542631340012343 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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();

		// check for WP_Error
		if ( is_wp_error( $check ) ) {
			return $check;
		}

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

		// 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 );
		$report_data     = $this->report->get_report_data();
		$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;
				default :
					$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 ( $report_data->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 ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		foreach ( $report_data->order_counts 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 ]['orders']   = (int) $order->count;
		}

		// add total order items for each period
		foreach ( $report_data->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 ( $report_data->coupons 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'       => $report_data->total_sales,
			'net_sales'         => $report_data->net_sales,
			'average_sales'     => $report_data->average_sales,
			'total_orders'      => $report_data->total_orders,
			'total_items'       => $report_data->total_items,
			'total_tax'         => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
			'total_shipping'    => $report_data->total_shipping,
			'total_refunds'     => $report_data->total_refunds,
			'total_discount'    => $report_data->total_coupons,
			'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 );

			if ( $product ) {
				$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' );
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );

		$this->report = new WC_Report_Sales_By_Date();

		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 bool|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;
		}
	}
}
api/v2/class-wc-api-resource.php000064400000033103151542631340012471 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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' ) );

		// maybe add meta to top-level resource responses
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
		}

		$response_names = array(
			'order',
			'coupon',
			'customer',
			'product',
			'report',
			'customer_orders',
			'customer_downloads',
			'order_note',
			'order_refund',
			'product_reviews',
			'product_category',
		);

		foreach ( $response_names as $name ) {

			/**
			 * 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
			 */
			add_filter( "woocommerce_api_{$name}_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 || 'shop_webhook' === $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 );

			if ( null === $post ) {
				return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
			}

			// 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'];
		}

		// order (ASC or DESC, ASC by default)
		if ( ! empty( $request_args['order'] ) ) {
			$args['order'] = $request_args['order'];
		}

		// orderby
		if ( ! empty( $request_args['orderby'] ) ) {
			$args['orderby'] = $request_args['orderby'];

			// allow sorting by meta value
			if ( ! empty( $request_args['orderby_meta_key'] ) ) {
				$args['meta_key'] = $request_args['orderby_meta_key'];
			}
		}

		// allow post status change
		if ( ! empty( $request_args['post_status'] ) ) {
			$args['post_status'] = $request_args['post_status'];
			unset( $request_args['post_status'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

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

		$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );

		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/webhook
			$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 ( 'revision' !== $post->post_type && 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;
		}
	}
}
api/v2/class-wc-api-server.php000064400000047771151542631340012170 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
 */

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

		// load response handler
		$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 );

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

			// API requests run under the context of the authenticated 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 ) );
	}

	/**
	 * urldecode deep.
	 *
	 * @since  2.2
	 * @param  string|array $value Data to decode with urldecode.
	 * @return string|array        Decoded data.
	 */
	protected function urldecode_deep( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( $this, 'urldecode_deep' ), $value );
		} else {
			return urldecode( $value );
		}
	}

	/**
	 * 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.2
	 *
	 * @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
				if ( 'data' == $param->getName() ) {
					$ordered_parameters[] = $provided[ $param->getName() ];
					continue;
				}

				$ordered_parameters[] = $this->urldecode_deep( $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.3
	 * @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(),
					'currency_position'  => get_option( 'woocommerce_currency_pos' ),
					'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
					'decimal_separator'  => get_option( 'woocommerce_price_decimal_sep' ),
					'price_num_decimals' => wc_get_price_decimals(),
					'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' ) ),
					'generate_password'  => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
					'links'              => array(
						'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
					),
				),
			),
		);

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

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );

			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|stdClass $query
	 */
	public function add_pagination_headers( $query ) {

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

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

			if ( $query->get( 'number' ) > 0 ) {
				$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
				$total_pages = ceil( $total / $query->get( 'number' ) );
			} else {
				$page = 1;
				$total_pages = 1;
			}
		} elseif ( is_a( $query, 'stdClass' ) ) {
			$page        = $query->page;
			$single      = $query->is_single;
			$total       = $query->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;
	}
}
api/v2/class-wc-api-webhooks.php000064400000036363151542631340012476 0ustar00<?php
/**
 * WooCommerce API Webhooks class
 *
 * Handles requests to the /webhooks endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

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

class WC_API_Webhooks extends WC_API_Resource {

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

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

		# GET|POST /webhooks
		$routes[ $this->base ] = array(
			array( array( $this, 'get_webhooks' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_webhook' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET|PUT|DELETE /webhooks/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
		);

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

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

		return $routes;
	}

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

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

		$filter['page'] = $page;

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

		$webhooks = array();

		foreach ( $query['results'] as $webhook_id ) {
			$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query['headers'] );

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

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

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

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

		$webhook = wc_get_webhook( $id );

		$webhook_data = array(
			'id'           => $webhook->get_id(),
			'name'         => $webhook->get_name(),
			'status'       => $webhook->get_status(),
			'topic'        => $webhook->get_topic(),
			'resource'     => $webhook->get_resource(),
			'event'        => $webhook->get_event(),
			'hooks'        => $webhook->get_hooks(),
			'delivery_url' => $webhook->get_delivery_url(),
			'created_at'   => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'   => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
		);

		return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
	}

	/**
	 * Get the total number of webhooks
	 *
	 * @since 2.2
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_webhooks_count( $status = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
			}

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

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

			return array( 'count' => $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create an webhook
	 *
	 * @since 2.2
	 *
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function create_webhook( $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			// permission check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );

			// validate topic
			if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
			}

			// validate delivery URL
			if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
			}

			$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
				'post_type'     => 'shop_webhook',
				'post_status'   => 'publish',
				'ping_status'   => 'closed',
				'post_author'   => get_current_user_id(),
				'post_password' => 'webhook_' . wp_generate_password(),
				'post_title'    => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ),
			), $data, $this );

			$webhook = new WC_Webhook();

			$webhook->set_name( $webhook_data['post_title'] );
			$webhook->set_user_id( $webhook_data['post_author'] );
			$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
			$webhook->set_topic( $data['topic'] );
			$webhook->set_delivery_url( $data['delivery_url'] );
			$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
			$webhook->set_api_version( 'legacy_v3' );
			$webhook->save();

			$webhook->deliver_ping();

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a webhook
	 *
	 * @since 2.2
	 *
	 * @param int $id webhook ID
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function edit_webhook( $id, $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

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

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

			$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );

			$webhook = wc_get_webhook( $id );

			// update topic
			if ( ! empty( $data['topic'] ) ) {

				if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {

					$webhook->set_topic( $data['topic'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
				}
			}

			// update delivery URL
			if ( ! empty( $data['delivery_url'] ) ) {
				if ( wc_is_valid_url( $data['delivery_url'] ) ) {

					$webhook->set_delivery_url( $data['delivery_url'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
				}
			}

			// update secret
			if ( ! empty( $data['secret'] ) ) {
				$webhook->set_secret( $data['secret'] );
			}

			// update status
			if ( ! empty( $data['status'] ) ) {
				$webhook->set_status( $data['status'] );
			}

			// update name
			if ( ! empty( $data['name'] ) ) {
				$webhook->set_name( $data['name'] );
			}

			$webhook->save();

			do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a webhook
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @return array|WP_Error
	 */
	public function delete_webhook( $id ) {

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

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

		do_action( 'woocommerce_api_delete_webhook', $id, $this );

		$webhook = wc_get_webhook( $id );

		return $webhook->delete( true );
	}

	/**
	 * Helper method to get webhook post objects
	 *
	 * @since 2.2
	 * @param array $args Request arguments for filtering query.
	 * @return array
	 */
	private function query_webhooks( $args ) {
		$args = $this->merge_query_args( array(), $args );

		$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );

		if ( empty( $args['offset'] ) ) {
			$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
		}

		$page = $args['paged'];
		unset( $args['paged'], $args['posts_per_page'] );

		if ( isset( $args['s'] ) ) {
			$args['search'] = $args['s'];
			unset( $args['s'] );
		}

		// Post type to webhook status.
		if ( ! empty( $args['post_status'] ) ) {
			$args['status'] = $args['post_status'];
			unset( $args['post_status'] );
		}

		if ( ! empty( $args['post__in'] ) ) {
			$args['include'] = $args['post__in'];
			unset( $args['post__in'] );
		}

		if ( ! empty( $args['date_query'] ) ) {
			foreach ( $args['date_query'] as $date_query ) {
				if ( 'post_date_gmt' === $date_query['column'] ) {
					$args['after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
					$args['modified_after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				}
			}

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

		$args['paginate'] = true;

		// Get the webhooks.
		$data_store = WC_Data_Store::load( 'webhook' );
		$results    = $data_store->search_webhooks( $args );

		// Get total items.
		$headers              = new stdClass;
		$headers->page        = $page;
		$headers->total       = $results->total;
		$headers->is_single   = $args['limit'] > $headers->total;
		$headers->total_pages = $results->max_num_pages;

		return array(
			'results' => $results->webhooks,
			'headers' => $headers,
		);
	}

	/**
	 * Get deliveries for a webhook
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_webhook_deliveries( $webhook_id, $fields = null ) {

		// Ensure ID is valid webhook ID
		$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

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

		return array( 'webhook_deliveries' => array() );
	}

	/**
	 * Get the delivery log for the given webhook ID and delivery ID
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string $id delivery log ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
		try {
			// Validate webhook ID
			$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
			}

			$webhook = new WC_Webhook( $webhook_id );

			$log = 0;

			if ( ! $log ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
			}

			return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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 3.3.0
	 * @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 ) {
		$id = absint( $id );

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

		$webhook = wc_get_webhook( $id );

		if ( null === $webhook ) {
			return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
		}

		// Validate permissions.
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}
}
api/v2/interface-wc-api-handler.php000064400000001515151542631340013114 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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 );

}
api/v3/class-wc-api-authentication.php000064400000031433151542631340013666 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'];

		// if the $_GET parameters are present, use those first
		if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) {
			$keys = $this->get_keys_by_consumer_key( $params['consumer_key'] );

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

			return $keys;
		}

		// if the above is not present, we will do full basic auth
		if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) {
			$this->exit_with_unauthorized_headers();
		}

		$keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] );

		if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) {
			$this->exit_with_unauthorized_headers();
		}

		return $keys;
	}

	/**
	 * If the consumer_key and consumer_secret $_GET parameters are NOT provided
	 * and the Basic auth headers are either not present or the consumer secret does not match the consumer
	 * key provided, then return the correct Basic headers and an error message.
	 *
	 * @since 2.4
	 */
	private function exit_with_unauthorized_headers() {
		$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
		header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' );
		header( 'HTTP/1.0 401 Unauthorized' );
		throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 );
	}

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

		$server_path = WC()->api->server->path;

		// if the requested URL has a trailingslash, make sure our base URL does as well
		if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) {
			$server_path .= '/';
		}

		$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $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'] );

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

		// Normalize parameter key/values
		$params = $this->normalize_parameters( $params );
		$query_parameters = array();
		foreach ( $params as $param_key => $param_value ) {
			if ( is_array( $param_value ) ) {
				foreach ( $param_value as $param_key_inner => $param_value_inner ) {
					$query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
				}
			} else {
				$query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign
			}
		}
		$query_string = implode( '%26', $query_parameters ); // 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'] ) );

		$secret = $keys['consumer_secret'] . '&';
		$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $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 ) {
		$keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) );
		$values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) );
		$parameters = array_combine( $keys, $values );
		return $parameters;
	}

	/**
	 * Encodes a value according to RFC 3986. Supports multidimensional arrays.
	 *
	 * @since 2.4
	 * @param  string|array $value The value to encode
	 * @return string|array        Encoded values
	 */
	public static function urlencode_rfc3986( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value );
		} else {
			// Percent symbols (%) must be double-encoded
			return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
		}
	}

	/**
	 * 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' ), 401 );
		}

		$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' )
		);
	}
}
api/v3/class-wc-api-coupons.php000064400000050102151542631340012327 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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/POST /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_coupon' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),    WC_API_Server::READABLE ),
			array( array( $this, 'edit_coupon' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
		);

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

		# POST|PUT /coupons/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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
	 */
	public function get_coupon( $id, $fields = null ) {
		try {

			$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'                  => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // 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 ),
				'maximum_amount'               => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
				'customer_emails'              => $coupon->get_email_restrictions(),
				'description'                  => $coupon->get_description(),
			);

			return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
			}

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

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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;

		try {
			$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 ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
			}

			return $this->get_coupon( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a coupon
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_coupon( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			// Check user permission
			if ( ! current_user_can( 'publish_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );

			// Check if coupon code is specified
			if ( ! isset( $data['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
			}

			$coupon_code  = wc_format_coupon_code( $data['code'] );
			$id_from_code = wc_get_coupon_id_by_code( $coupon_code );

			if ( $id_from_code ) {
				throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
			}

			$defaults = array(
				'type'                         => 'fixed_cart',
				'amount'                       => 0,
				'individual_use'               => false,
				'product_ids'                  => array(),
				'exclude_product_ids'          => array(),
				'usage_limit'                  => '',
				'usage_limit_per_user'         => '',
				'limit_usage_to_x_items'       => '',
				'usage_count'                  => '',
				'expiry_date'                  => '',
				'enable_free_shipping'         => false,
				'product_category_ids'         => array(),
				'exclude_product_category_ids' => array(),
				'exclude_sale_items'           => false,
				'minimum_amount'               => '',
				'maximum_amount'               => '',
				'customer_emails'              => array(),
				'description'                  => '',
			);

			$coupon_data = wp_parse_args( $data, $defaults );

			// Validate coupon types
			if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
			}

			$new_coupon = array(
				'post_title'   => $coupon_code,
				'post_content' => '',
				'post_status'  => 'publish',
				'post_author'  => get_current_user_id(),
				'post_type'    => 'shop_coupon',
				'post_excerpt' => $coupon_data['description'],
	 		);

			$id = wp_insert_post( $new_coupon, true );

			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
			}

			// Set coupon meta
			update_post_meta( $id, 'discount_type', $coupon_data['type'] );
			update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
			update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
			update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
			update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
			update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
			update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
			update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
			update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
			update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
			update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
			update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
			update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );

			do_action( 'woocommerce_api_create_coupon', $id, $data );
			do_action( 'woocommerce_new_coupon', $id );

			$this->server->send_status( 201 );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

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

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

			$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );

			if ( isset( $data['code'] ) ) {
				global $wpdb;

				$coupon_code  = wc_format_coupon_code( $data['code'] );
				$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

				if ( $id_from_code ) {
					throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
				}

				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['description'] ) ) {
				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['type'] ) ) {
				// Validate coupon types
				if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
				}
				update_post_meta( $id, 'discount_type', $data['type'] );
			}

			if ( isset( $data['amount'] ) ) {
				update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
			}

			if ( isset( $data['individual_use'] ) ) {
				update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_ids'] ) ) {
				update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
			}

			if ( isset( $data['exclude_product_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
			}

			if ( isset( $data['usage_limit'] ) ) {
				update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
			}

			if ( isset( $data['usage_limit_per_user'] ) ) {
				update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
			}

			if ( isset( $data['limit_usage_to_x_items'] ) ) {
				update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
			}

			if ( isset( $data['usage_count'] ) ) {
				update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
			}

			if ( isset( $data['expiry_date'] ) ) {
				update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
				update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
			}

			if ( isset( $data['enable_free_shipping'] ) ) {
				update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_category_ids'] ) ) {
				update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_product_category_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_sale_items'] ) ) {
				update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['minimum_amount'] ) ) {
				update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
			}

			if ( isset( $data['maximum_amount'] ) ) {
				update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
			}

			if ( isset( $data['customer_emails'] ) ) {
				update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
			}

			do_action( 'woocommerce_api_edit_coupon', $id, $data );
			do_action( 'woocommerce_update_coupon', $id );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

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

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

		do_action( 'woocommerce_api_delete_coupon', $id, $this );

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

	/**
	 * expiry_date format
	 *
	 * @since  2.3.0
	 * @param  string $expiry_date
	 * @param bool $as_timestamp (default: false)
	 * @return string|int
	 */
	protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
		if ( '' != $expiry_date ) {
			if ( $as_timestamp ) {
				return strtotime( $expiry_date );
			}

			return date( 'Y-m-d', strtotime( $expiry_date ) );
		}

		return '';
	}

	/**
	 * 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 );
	}

	/**
	 * Bulk update or insert coupons
	 * Accepts an array with coupons in the formats supported by
	 * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['coupons'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
			}

			$data  = $data['coupons'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$coupons = array();

			foreach ( $data as $_coupon ) {
				$coupon_id = 0;

				// Try to get the coupon ID
				if ( isset( $_coupon['id'] ) ) {
					$coupon_id = intval( $_coupon['id'] );
				}

				if ( $coupon_id ) {

					// Coupon exists / edit coupon
					$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $edit ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$coupons[] = $edit['coupon'];
					}
				} else {

					// Coupon don't exists / create coupon
					$new = $this->create_coupon( array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $new ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$coupons[] = $new['coupon'];
					}
				}
			}

			return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v3/class-wc-api-customers.php000064400000061021151542631340012667 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

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.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),   WC_API_SERVER::READABLE ),
			array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),    WC_API_SERVER::READABLE ),
			array( array( $this, 'edit_customer' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /customers/email/<email>
		$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
			array( array( $this, 'get_customer_by_email' ), 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 ),
		);

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

		# POST|PUT /customers/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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 array $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.
			'last_update'      => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->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(),
			'role'             => $customer->get_role(),
			'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 customer for the given email
	 *
	 * @since 2.1
	 *
	 * @param string $email the customer email
	 * @param array $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_customer_by_email( $email, $fields = null ) {
		try {
			if ( is_email( $email ) ) {
				$customer = get_user_by( 'email', $email );
				if ( ! is_object( $customer ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
				}
			} else {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
			}

			return $this->get_customer( $customer->ID, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'list_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
			}

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

			return array( 'count' => $query->get_total() );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get customer billing address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_billing_address() {
		$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
			'email',
			'phone',
		) );

		return $billing_address;
	}

	/**
	 * Get customer shipping address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_shipping_address() {
		$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		) );

		return $shipping_address;
	}

	/**
	 * Add/Update customer data.
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param array $data
	 * @param WC_Customer $customer
	 */
	protected function update_customer_data( $id, $data, $customer ) {

		// Customer first name.
		if ( isset( $data['first_name'] ) ) {
			$customer->set_first_name( wc_clean( $data['first_name'] ) );
		}

		// Customer last name.
		if ( isset( $data['last_name'] ) ) {
			$customer->set_last_name( wc_clean( $data['last_name'] ) );
		}

		// Customer billing address.
		if ( isset( $data['billing_address'] ) ) {
			foreach ( $this->get_customer_billing_address() as $field ) {
				if ( isset( $data['billing_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
						$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
					}
				}
			}
		}

		// Customer shipping address.
		if ( isset( $data['shipping_address'] ) ) {
			foreach ( $this->get_customer_shipping_address() as $field ) {
				if ( isset( $data['shipping_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
						$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
					}
				}
			}
		}

		do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
	}

	/**
	 * Create a customer
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Checks with can create new users.
			if ( ! current_user_can( 'create_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );

			// Checks with the email is missing.
			if ( ! isset( $data['email'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
			}

			// Create customer.
			$customer = new WC_Customer;
			$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
			$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
			$customer->set_email( $data['email'] );
			$customer->save();

			if ( ! $customer->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
			}

			// Added customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );
			$customer->save();

			do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );

			$this->server->send_status( 201 );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a customer
	 *
	 * @since 2.2
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Validate the customer ID.
			$id = $this->validate_request( $id, 'customer', 'edit' );

			// Return the validate error.
			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );

			$customer = new WC_Customer( $id );

			// Customer email.
			if ( isset( $data['email'] ) ) {
				$customer->set_email( $data['email'] );
			}

			// Customer password.
			if ( isset( $data['password'] ) ) {
				$customer->set_password( $data['password'] );
			}

			// Update customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );

			$customer->save();

			do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

		// Validate the customer ID.
		$id = $this->validate_request( $id, 'customer', 'delete' );

		// Return the validate error.
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_customer', $id, $this );

		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
	 * @param array $filter filters
	 * @return array|WP_Error
	 */
	public function get_customer_orders( $id, $fields = null, $filter = array() ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

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

		$filter['customer_id'] = $id;
		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 );

		return $orders;
	}

	/**
	 * Get the available downloads for a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_downloads( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

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

		$downloads  = array();
		$_downloads = wc_get_customer_available_downloads( $id );

		foreach ( $_downloads as $key => $download ) {
			$downloads[] = array(
				'download_url'        => $download['download_url'],
				'download_id'         => $download['download_id'],
				'product_id'          => $download['product_id'],
				'download_name'       => $download['download_name'],
				'order_id'            => $download['order_id'],
				'order_key'           => $download['order_key'],
				'downloads_remaining' => $download['downloads_remaining'],
				'access_expires'      => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
				'file'                => $download['file'],
			);
		}

		return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $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
	 *
	 * The filter for role can only be a single role in a string.
	 *
	 * @since 2.3
	 * @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,
		);

		// Custom Role
		if ( ! empty( $args['role'] ) ) {
			$query_args['role'] = $args['role'];

			// Show users on all roles
			if ( 'all' === $query_args['role'] ) {
				unset( $query_args['role'] );
			}
		}

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

		// Limit number of users returned
		if ( ! empty( $args['limit'] ) ) {
			if ( -1 == $args['limit'] ) {
				unset( $query_args['number'] );
			} else {
				$query_args['number'] = absint( $args['limit'] );
				$users_per_page       = absint( $args['limit'] );
			}
		} else {
			$args['limit'] = $query_args['number'];
		}

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

		// Order (ASC or DESC, ASC by default)
		if ( ! empty( $args['order'] ) ) {
			$query_args['order'] = $args['order'];
		}

		// Order by
		if ( ! empty( $args['orderby'] ) ) {
			$query_args['orderby'] = $args['orderby'];

			// Allow sorting by meta value
			if ( ! empty( $args['orderby_meta_key'] ) ) {
				$query_args['meta_key'] = $args['orderby_meta_key'];
			}
		}

		$query = new WP_User_Query( $query_args );

		// Helper members for pagination headers
		$query->total_pages = ( -1 == $args['limit'] ) ? 1 : 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 integer $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 ) {

		try {
			$id = absint( $id );

			// validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
			}

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

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

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! current_user_can( 'list_users' ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'edit':
					if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'delete':
					if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
					}
					break;
			}

			return $id;
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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' );
	}

	/**
	 * Bulk update or insert customers
	 * Accepts an array with customers in the formats supported by
	 * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['customers'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
			}

			$data  = $data['customers'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$customers = array();

			foreach ( $data as $_customer ) {
				$customer_id = 0;

				// Try to get the customer ID
				if ( isset( $_customer['id'] ) ) {
					$customer_id = intval( $_customer['id'] );
				}

				if ( $customer_id ) {

					// Customer exists / edit customer
					$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );

					if ( is_wp_error( $edit ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$customers[] = $edit['customer'];
					}
				} else {

					// Customer don't exists / create customer
					$new = $this->create_customer( array( 'customer' => $_customer ) );

					if ( is_wp_error( $new ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$customers[] = $new['customer'];
					}
				}
			}

			return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v3/class-wc-api-exception.php000064400000002213151542631340012637 0ustar00<?php
/**
 * WooCommerce API Exception Class
 *
 * Extends Exception to provide additional data
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.2
 */

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

class WC_API_Exception extends Exception {

	/** @var string sanitized error code */
	protected $error_code;

	/**
	 * Setup exception, requires 3 params:
	 *
	 * error code - machine-readable, e.g. `woocommerce_invalid_product_id`
	 * error message - friendly message, e.g. 'Product ID is invalid'
	 * http status code - proper HTTP status code to respond with, e.g. 400
	 *
	 * @since 2.2
	 * @param string $error_code
	 * @param string $error_message user-friendly translated error message
	 * @param int $http_status_code HTTP status code to respond with
	 */
	public function __construct( $error_code, $error_message, $http_status_code ) {
		$this->error_code = $error_code;
		parent::__construct( $error_message, $http_status_code );
	}

	/**
	 * Returns the error code
	 *
	 * @since 2.2
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}
}
api/v3/class-wc-api-json-handler.php000064400000003656151542631340013241 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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 );
	}
}
api/v3/class-wc-api-orders.php000064400000172344151542631340012154 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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';

	/** @var string $post_type the custom post type */
	protected $post_type = 'shop_order';

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

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

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

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

		# GET|PUT|DELETE /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 ),
			array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ),
		);

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

		# GET|PUT|DELETE /orders/<order_id>/notes/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ),
		);

		# GET|POST /orders/<order_id>/refunds
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array(
			array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/refunds/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /orders/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		$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 Request fields.
	 * @param array $filter Request filters.
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null, $filter = array() ) {

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

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

		// Get the decimal precession.
		$dp     = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 );
		$order  = wc_get_order( $id );
		$expand = array();

		if ( ! empty( $filter['expand'] ) ) {
			$expand = explode( ',', $filter['expand'] );
		}

		$order_data = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'order_key'                 => $order->get_order_key(),
			'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(), $dp ),
			'subtotal'                  => wc_format_decimal( $order->get_subtotal(), $dp ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), $dp ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), $dp ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), $dp ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), $dp ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), $dp ),
			'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();
			$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
			$item_meta  = $item->get_all_formatted_meta_data( $hideprefix );

			foreach ( $item_meta as $key => $values ) {
				$item_meta[ $key ]->label = $values->display_key;
				unset( $item_meta[ $key ]->display_key );
				unset( $item_meta[ $key ]->display_value );
			}

			$line_item = array(
				'id'           => $item_id,
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item->get_total_tax(), $dp ),
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
				'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,
				'meta'         => array_values( $item_meta ),
			);

			if ( in_array( 'products', $expand ) && is_object( $product ) ) {
				$_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() );

				if ( isset( $_product_data['product'] ) ) {
					$line_item['product_data'] = $_product_data['product'];
				}
			}

			$order_data['line_items'][] = $line_item;
		}

		// 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(), $dp ),
			);
		}

		// Add taxes.
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$tax_line = array(
				'id'       => $tax->id,
				'rate_id'  => $tax->rate_id,
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, $dp ),
				'compound' => (bool) $tax->is_compound,
			);

			if ( in_array( 'taxes', $expand ) ) {
				$_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id );

				if ( isset( $_rate_data['tax'] ) ) {
					$tax_line['rate_data'] = $_rate_data['tax'];
				}
			}

			$order_data['tax_lines'][] = $tax_line;
		}

		// 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 ), $dp ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
			);
		}

		// Add coupons.
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$coupon_line = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ),
			);

			if ( in_array( 'coupons', $expand ) ) {
				$_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() );

				if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) {
					$coupon_line['coupon_data'] = $_coupon_data['coupon'];
				}
			}

			$order_data['coupon_lines'][] = $coupon_line;
		}

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

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

		try {
			if ( ! current_user_can( 'read_private_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {

				if ( 'any' === $status ) {

					$order_statuses = array();

					foreach ( wc_get_order_statuses() as $slug => $name ) {
						$filter['status'] = str_replace( 'wc-', '', $slug );
						$query = $this->query_orders( $filter );
						$order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts;
					}

					return array( 'count' => $order_statuses );

				} else {
					$filter['status'] = $status;
				}
			}

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

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

		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a list of valid order statuses
	 *
	 * Note this requires no specific permissions other than being an authenticated
	 * API user. Order statuses (particularly custom statuses) could be considered
	 * private information which is why it's not in the API index.
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_order_statuses() {

		$order_statuses = array();

		foreach ( wc_get_order_statuses() as $slug => $name ) {
			$order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name;
		}

		return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) );
	}

	/**
	 * Create an order
	 *
	 * @since 2.2
	 * @param array $data raw order data
	 * @return array|WP_Error
	 */
	public function create_order( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_data', $data, $this );

			// default order args, note that status is checked for validity in wc_create_order()
			$default_order_args = array(
				'status'        => isset( $data['status'] ) ? $data['status'] : '',
				'customer_note' => isset( $data['note'] ) ? $data['note'] : null,
			);

			// if creating order for existing customer
			if ( ! empty( $data['customer_id'] ) ) {

				// make sure customer exists
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				$default_order_args['customer_id'] = $data['customer_id'];
			}

			// create the pending order
			$order = $this->create_base_order( $default_order_args, $data );

			if ( is_wp_error( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 );
			}

			// billing/shipping addresses
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$set_item = "set_{$line_type}";

					foreach ( $data[ $line ] as $item ) {

						$this->$set_item( $order, $item, 'create' );
					}
				}
			}

			// set is vat exempt
			if ( isset( $data['is_vat_exempt'] ) ) {
				update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' );
			}

			// calculate totals and set them
			$order->calculate_totals();

			// payment method (and payment_complete() if `paid` == true)
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// method ID & title are required
				if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );

				// mark as paid if set
				if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// set order currency
			if ( isset( $data['currency'] ) ) {

				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// set order meta
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_new_order', $order->get_id() );

			return $this->get_order( $order->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Creates new WC_Order.
	 *
	 * Requires a separate function for classes that extend WC_API_Orders.
	 *
	 * @since 2.3
	 *
	 * @param $args array
	 * @param $data
	 *
	 * @return WC_Order
	 */
	protected function create_base_order( $args, $data ) {
		return wc_create_order( $args );
	}

	/**
	 * Edit an order
	 *
	 * @since 2.2
	 * @param int $id the order ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {
		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			$update_totals = false;

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

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

			$data  = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this );
			$order = wc_get_order( $id );

			if ( empty( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$order_args = array( 'order_id' => $order->get_id() );

			// Customer note.
			if ( isset( $data['note'] ) ) {
				$order_args['customer_note'] = $data['note'];
			}

			// Customer ID.
			if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] );
			}

			// Billing/shipping address.
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$update_totals = true;

					foreach ( $data[ $line ] as $item ) {
						// Item ID is always required.
						if ( ! array_key_exists( 'id', $item ) ) {
							$item['id'] = null;
						}

						// Create item.
						if ( is_null( $item['id'] ) ) {
							$this->set_item( $order, $line_type, $item, 'create' );
						} elseif ( $this->item_is_null( $item ) ) {
							// Delete item.
							wc_delete_order_item( $item['id'] );
						} else {
							// Update item.
							$this->set_item( $order, $line_type, $item, 'update' );
						}
					}
				}
			}

			// Payment method (and payment_complete() if `paid` == true and order needs payment).
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// Method ID.
				if ( isset( $data['payment_details']['method_id'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				}

				// Method title.
				if ( isset( $data['payment_details']['method_title'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );
				}

				// Mark as paid if set.
				if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// Set order currency.
			if ( isset( $data['currency'] ) ) {
				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// If items have changed, recalculate order totals.
			if ( $update_totals ) {
				$order->calculate_totals();
			}

			// Update order meta.
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// Update the order post to set customer note/modified date.
			wc_update_order( $order_args );

			// Order status.
			if ( ! empty( $data['status'] ) ) {
				// Refresh the order instance.
				$order = wc_get_order( $order->get_id() );
				$order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true );
			}

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_update_order', $order->get_id() );

			return $this->get_order( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

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

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

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

		wc_delete_shop_order_transients( $id );

		do_action( 'woocommerce_api_delete_order', $id, $this );

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

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

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => $this->post_type,
			'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'] );
		}

		if ( ! empty( $args['customer_id'] ) ) {
			$query_args['meta_query'] = array(
				array(
					'key'     => '_customer_user',
					'value'   => absint( $args['customer_id'] ),
					'compare' => '=',
				),
			);
		}

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

		return new WP_Query( $query_args );
	}

	/**
	 * Helper method to set/update the billing & shipping addresses for
	 * an order
	 *
	 * @since 2.1
	 * @param \WC_Order $order
	 * @param array $data
	 */
	protected function set_order_addresses( $order, $data ) {

		$address_fields = array(
			'first_name',
			'last_name',
			'company',
			'email',
			'phone',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		);

		$billing_address = $shipping_address = array();

		// billing address
		if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['billing_address'][ $field ] ) ) {
					$billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] );
				}
			}

			unset( $address_fields['email'] );
			unset( $address_fields['phone'] );
		}

		// shipping address
		if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['shipping_address'][ $field ] ) ) {
					$shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] );
				}
			}
		}

		$this->update_address( $order, $billing_address, 'billing' );
		$this->update_address( $order, $shipping_address, 'shipping' );

		// update user meta
		if ( $order->get_user_id() ) {
			foreach ( $billing_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'billing_' . $key, $value );
			}
			foreach ( $shipping_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value );
			}
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order
	 * @param array $posted
	 * @param string $type Type of address; 'billing' or 'shipping'.
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Helper method to add/update order meta, with two restrictions:
	 *
	 * 1) Only non-protected meta (no leading underscore) can be set
	 * 2) Meta values must be scalar (int, string, bool)
	 *
	 * @since 2.2
	 * @param int $order_id valid order ID
	 * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format
	 */
	protected function set_order_meta( $order_id, $order_meta ) {

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

			if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) {
				update_post_meta( $order_id, $meta_key, $meta_value );
			}
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null
	 *
	 * Items can be deleted by setting the resource ID to null
	 *
	 * @since 2.2
	 * @param array $item item provided in the request body
	 * @return bool true if the item resource ID is null, false otherwise
	 */
	protected function item_is_null( $item ) {

		$keys = array( 'product_id', 'method_id', 'title', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Wrapper method to create/update order items
	 *
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @since 2.2
	 * @param \WC_Order $order order
	 * @param string $item_type
	 * @param array $item item provided in the request body
	 * @param string $action either 'create' or 'update'
	 * @throws WC_API_Exception if item ID is not associated with order
	 */
	protected function set_item( $order, $item_type, $item, $action ) {
		global $wpdb;

		$set_method = "set_{$item_type}";

		// verify provided line item ID is associated with order
		if ( 'update' === $action ) {

			$result = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
				absint( $item['id'] ),
				absint( $order->get_id() )
			) );

			if ( is_null( $result ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		$this->$set_method( $order, $item, $action );
	}

	/**
	 * Create or update a line item
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $item line item data
	 * @param string $action 'create' to add line item or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_line_item( $order, $item, $action ) {
		$creating  = ( 'create' === $action );

		// product is always required
		if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 );
		}

		// when updating, ensure product ID provided matches
		if ( 'update' === $action ) {

			$item_product_id   = wc_get_order_item_meta( $item['id'], '_product_id' );
			$item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' );

			if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 );
			}
		}

		if ( isset( $item['product_id'] ) ) {
			$product_id = $item['product_id'];
		} elseif ( isset( $item['sku'] ) ) {
			$product_id = wc_get_product_id_by_sku( $item['sku'] );
		}

		// variations must each have a key & value
		$variation_id = 0;
		if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) {
			foreach ( $item['variations'] as $key => $value ) {
				if ( ! $key || ! $value ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 );
				}
			}
			$variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] );
		}

		$product = wc_get_product( $variation_id ? $variation_id : $product_id );

		// must be a valid WC_Product
		if ( ! is_object( $product ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 );
		}

		// quantity must be positive float
		if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 );
		}

		// quantity is required when creating
		if ( $creating && ! isset( $item['quantity'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 );
		}

		// quantity
		if ( $creating ) {
			$line_item = new WC_Order_Item_Product();
		} else {
			$line_item = new WC_Order_Item_Product( $item['id'] );
		}

		$line_item->set_product( $product );
		$line_item->set_order_id( $order->get_id() );

		if ( isset( $item['quantity'] ) ) {
			$line_item->set_quantity( $item['quantity'] );
		}
		if ( isset( $item['total'] ) ) {
			$line_item->set_total( floatval( $item['total'] ) );
		} elseif ( $creating ) {
			$total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) );
			$line_item->set_total( $total );
			$line_item->set_subtotal( $total );
		}
		if ( isset( $item['total_tax'] ) ) {
			$line_item->set_total_tax( floatval( $item['total_tax'] ) );
		}
		if ( isset( $item['subtotal'] ) ) {
			$line_item->set_subtotal( floatval( $item['subtotal'] ) );
		}
		if ( isset( $item['subtotal_tax'] ) ) {
			$line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) );
		}
		if ( $variation_id ) {
			$line_item->set_variation_id( $variation_id );
			$line_item->set_variation( $item['variations'] );
		}

		// Save or add to order.
		if ( $creating ) {
			$order->add_item( $line_item );
		} else {
			$item_id = $line_item->save();

			if ( ! $item_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Given a product ID & API provided variations, find the correct variation ID to use for calculation
	 * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass
	 * the cheapest variation ID but provide other information so we have to look up the variation ID.
	 *
	 * @param  WC_Product $product Product instance
	 * @param array $variations
	 *
	 * @return int Returns an ID if a valid variation was found for this product
	 */
	public function get_variation_id( $product, $variations = array() ) {
		$variation_id = null;
		$variations_normalized = array();

		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			if ( isset( $variations ) && is_array( $variations ) ) {
				// start by normalizing the passed variations
				foreach ( $variations as $key => $value ) {
					$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php
					$variations_normalized[ $key ] = strtolower( $value );
				}
				// now search through each product child and see if our passed variations match anything
				foreach ( $product->get_children() as $variation ) {
					$meta = array();
					foreach ( get_post_meta( $variation ) as $key => $value ) {
						$value = $value[0];
						$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) );
						$meta[ $key ] = strtolower( $value );
					}
					// if the variation array is a part of the $meta array, we found our match
					if ( $this->array_contains( $variations_normalized, $meta ) ) {
						$variation_id = $variation;
						break;
					}
				}
			}
		}

		return $variation_id;
	}

	/**
	 * Utility function to see if the meta array contains data from variations
	 *
	 * @param array $needles
	 * @param array $haystack
	 *
	 * @return bool
	 */
	protected function array_contains( $needles, $haystack ) {
		foreach ( $needles as $key => $value ) {
			if ( $haystack[ $key ] !== $value ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Create or update an order shipping method
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $shipping item data
	 * @param string $action 'create' to add shipping or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_shipping( $order, $shipping, $action ) {

		// total must be a positive float
		if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// method ID is required
			if ( ! isset( $shipping['method_id'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}

			$rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] );
			$item = new WC_Order_Item_Shipping();
			$item->set_order_id( $order->get_id() );
			$item->set_shipping_rate( $rate );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Shipping( $shipping['id'] );

			if ( isset( $shipping['method_id'] ) ) {
				$item->set_method_id( $shipping['method_id'] );
			}

			if ( isset( $shipping['method_title'] ) ) {
				$item->set_method_title( $shipping['method_title'] );
			}

			if ( isset( $shipping['total'] ) ) {
				$item->set_total( floatval( $shipping['total'] ) );
			}

			$shipping_id = $item->save();

			if ( ! $shipping_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order fee
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $fee item data
	 * @param string $action 'create' to add fee or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_fee( $order, $fee, $action ) {

		if ( 'create' === $action ) {

			// fee title is required
			if ( ! isset( $fee['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Fee();
			$item->set_order_id( $order->get_id() );
			$item->set_name( wc_clean( $fee['title'] ) );
			$item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 );

			// if taxable, tax class and total are required
			if ( ! empty( $fee['taxable'] ) ) {
				if ( ! isset( $fee['tax_class'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 );
				}

				$item->set_tax_status( 'taxable' );
				$item->set_tax_class( $fee['tax_class'] );

				if ( isset( $fee['total_tax'] ) ) {
					$item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 );
				}

				if ( isset( $fee['tax_data'] ) ) {
					$item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) );
					$item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) );
				}
			}

			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Fee( $fee['id'] );

			if ( isset( $fee['title'] ) ) {
				$item->set_name( wc_clean( $fee['title'] ) );
			}

			if ( isset( $fee['tax_class'] ) ) {
				$item->set_tax_class( $fee['tax_class'] );
			}

			if ( isset( $fee['total'] ) ) {
				$item->set_total( floatval( $fee['total'] ) );
			}

			if ( isset( $fee['total_tax'] ) ) {
				$item->set_total_tax( floatval( $fee['total_tax'] ) );
			}

			$fee_id = $item->save();

			if ( ! $fee_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order coupon
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $coupon item data
	 * @param string $action 'create' to add coupon or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_coupon( $order, $coupon, $action ) {

		// coupon amount must be positive float
		if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// coupon code is required
			if ( empty( $coupon['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Coupon();
			$item->set_props( array(
				'code'         => $coupon['code'],
				'discount'     => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0,
				'discount_tax' => 0,
				'order_id'     => $order->get_id(),
			) );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Coupon( $coupon['id'] );

			if ( isset( $coupon['code'] ) ) {
				$item->set_code( $coupon['code'] );
			}

			if ( isset( $coupon['amount'] ) ) {
				$item->set_discount( floatval( $coupon['amount'] ) );
			}

			$coupon_id = $item->save();

			if ( ! $coupon_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 );
			}
		}
	}

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

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

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

		$args = array(
			'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) );
		}

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

	/**
	 * Get an order note for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param string $id order note ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_order_note( $order_id, $id, $fields = null ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order note for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @return WP_Error|array error or created note response data
	 */
	public function create_order_note( $order_id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 );
			}

			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			$order = wc_get_order( $order_id );

			$data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this );

			// note content is required
			if ( ! isset( $data['note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 );
			}

			$is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] );

			// create the note
			$note_id = $order->add_order_note( $data['note'], $is_customer_note );

			if ( ! $note_id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this );

			return $this->get_order_note( $order->get_id(), $note_id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit the order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited note response data
	 */
	public function edit_order_note( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			$order = wc_get_order( $order_id );

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this );

			// Note content
			if ( isset( $data['note'] ) ) {

				wp_update_comment(
					array(
						'comment_ID'      => $note->comment_ID,
						'comment_content' => $data['note'],
					)
				);
			}

			// Customer note
			if ( isset( $data['customer_note'] ) ) {

				update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 );
			}

			do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this );

			return $this->get_order_note( $order->get_id(), $note->comment_ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_note( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

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

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			// Force delete since trashed order notes could not be managed through comments list table
			$result = wc_delete_order_note( $note->comment_ID );

			if ( ! $result ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 );
			}

			do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this );

			return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the order refunds for an order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_refunds( $order_id, $fields = null ) {

		// Ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

		$refund_items = wc_get_orders( array(
			'type'   => 'shop_order_refund',
			'parent' => $order_id,
			'limit'  => -1,
			'return' => 'ids',
		) );
		$order_refunds = array();

		foreach ( $refund_items as $refund_id ) {
			$order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) );
		}

		return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) );
	}

	/**
	 * Get an order refund for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param int $id
	 * @param string|null $fields fields to limit response to
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			$order  = wc_get_order( $order_id );
			$refund = wc_get_order( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			$line_items = array();

			// Add line items
			foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) {
				$product    = $item->get_product();
				$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
				$item_meta  = $item->get_all_formatted_meta_data( $hideprefix );

				foreach ( $item_meta as $key => $values ) {
					$item_meta[ $key ]->label = $values->display_key;
					unset( $item_meta[ $key ]->display_key );
					unset( $item_meta[ $key ]->display_value );
				}

				$line_items[] = array(
					'id'               => $item_id,
					'subtotal'         => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
					'subtotal_tax'     => wc_format_decimal( $item->get_subtotal_tax(), 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,
					'meta'             => array_values( $item_meta ),
					'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ),
				);
			}

			$order_refund = array(
				'id'         => $refund->get_id(),
				'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ),
				'amount'     => wc_format_decimal( $refund->get_amount(), 2 ),
				'reason'     => $refund->get_reason(),
				'line_items' => $line_items,
			);

			return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order refund for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @param bool $api_refund do refund using a payment gateway API
	 * @return WP_Error|array error or created refund response data
	 */
	public function create_order_refund( $order_id, $data, $api_refund = true ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 );
			}

			$order_id = absint( $order_id );

			if ( empty( $order_id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this );

			// Refund amount is required
			if ( ! isset( $data['amount'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 );
			} elseif ( 0 > $data['amount'] ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 );
			}

			$data['order_id']  = $order_id;
			$data['refund_id'] = 0;

			// Create the refund
			$refund = wc_create_refund( $data );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
			}

			// Refund via API
			if ( $api_refund ) {
				if ( WC()->payment_gateways() ) {
					$payment_gateways = WC()->payment_gateways->payment_gateways();
				}

				$order = wc_get_order( $order_id );

				if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) {
					$result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() );

					if ( is_wp_error( $result ) ) {
						return $result;
					} elseif ( ! $result ) {
						throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 );
					}
				}
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit an order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited refund response data
	 */
	public function edit_order_refund( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

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

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure order ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this );

			// Update reason
			if ( isset( $data['reason'] ) ) {
				$updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) );

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

			// Update refund amount
			if ( isset( $data['amount'] ) && 0 < $data['amount'] ) {
				update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) );
			}

			do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_refund( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

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

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure refund ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			wc_delete_shop_order_transients( $order_id );

			do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this );

			return $this->delete( $refund->ID, 'refund', true );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Bulk update or insert orders
	 * Accepts an array with orders in the formats supported by
	 * WC_API_Orders->create_order() and WC_API_Orders->edit_order()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['orders'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 );
			}

			$data  = $data['orders'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$orders = array();

			foreach ( $data as $_order ) {
				$order_id = 0;

				// Try to get the order ID
				if ( isset( $_order['id'] ) ) {
					$order_id = intval( $_order['id'] );
				}

				if ( $order_id ) {

					// Order exists / edit order
					$edit = $this->edit_order( $order_id, array( 'order' => $_order ) );

					if ( is_wp_error( $edit ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$orders[] = $edit['order'];
					}
				} else {

					// Order don't exists / create order
					$new = $this->create_order( array( 'order' => $_order ) );

					if ( is_wp_error( $new ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$orders[] = $new['order'];
					}
				}
			}

			return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v3/class-wc-api-products.php000064400000330554151542631340012520 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/POST /products
	 * GET /products/count
	 * GET/PUT/DELETE /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
		);

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

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

		# GET/POST /products/categories
		$routes[ $this->base . '/categories' ] = array(
			array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/categories/<id>
		$routes[ $this->base . '/categories/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/tags
		$routes[ $this->base . '/tags' ] = array(
			array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/tags/<id>
		$routes[ $this->base . '/tags/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/shipping_classes
		$routes[ $this->base . '/shipping_classes' ] = array(
			array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/shipping_classes/<id>
		$routes[ $this->base . '/shipping_classes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/attributes
		$routes[ $this->base . '/attributes' ] = array(
			array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/attributes/<id>
		$routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/attributes/<attribute_id>/terms
		$routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms' ] = array(
			array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/attributes/<attribute_id>/terms/<id>
		$routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ),
		);

		# POST|PUT /products/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		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->get_parent_id() ) {
			$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
		}

		// Add grouped products data
		if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
			$product_data['grouped_products'] = $this->get_grouped_products_data( $product );
		}

		if ( $product->is_type( 'simple' ) ) {
			$parent_id = $product->get_parent_id();
			if ( ! empty( $parent_id ) ) {
				$_product               = wc_get_product( $parent_id );
				$product_data['parent'] = $this->get_product_data( $_product );
			}
		}

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

	/**
	 * Get the total number of products
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 );
			}

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

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

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product.
	 *
	 * @since 2.2
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product( $data ) {
		$id = 0;

		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			// Check permissions.
			if ( ! current_user_can( 'publish_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );

			// Check if product title is specified.
			if ( ! isset( $data['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 );
			}

			// Check product type.
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'simple';
			}

			// Set visible visibility when not sent.
			if ( ! isset( $data['catalog_visibility'] ) ) {
				$data['catalog_visibility'] = 'visible';
			}

			// Validate the product type.
			if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Enable description html tags.
			$post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
			if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {

				$post_content = wp_filter_post_kses( $data['description'] );
			}

			// Enable short description html tags.
			$post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
			if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
				$post_excerpt = wp_filter_post_kses( $data['short_description'] );
			}

			$classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] );
			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}
			$product = new $classname();

			$product->set_name( wc_clean( $data['title'] ) );
			$product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' );
			$product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' );
			$product->set_description( isset( $data['description'] ) ? $post_content : '' );
			$product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 );

			if ( ! empty( $data['name'] ) ) {
				$product->set_slug( sanitize_title( $data['name'] ) );
			}

			// Attempts to create the new product.
			$product->save();
			$id = $product->get_id();

			// Checks for an error in the product creation.
			if ( 0 >= $id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );
			$product->save();

			// Save variations.
			if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
				$this->save_variations( $product, $data );
			}

			do_action( 'woocommerce_api_create_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			$this->server->send_status( 201 );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {
		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

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

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

			$product = wc_get_product( $id );

			$data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );

			// Product title.
			if ( isset( $data['title'] ) ) {
				$product->set_name( wc_clean( $data['title'] ) );
			}

			// Product name (slug).
			if ( isset( $data['name'] ) ) {
				$product->set_slug( wc_clean( $data['name'] ) );
			}

			// Product status.
			if ( isset( $data['status'] ) ) {
				$product->set_status( wc_clean( $data['status'] ) );
			}

			// Product short description.
			if ( isset( $data['short_description'] ) ) {
				// Enable short description html tags.
				$post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] );
				$product->set_short_description( $post_excerpt );
			}

			// Product description.
			if ( isset( $data['description'] ) ) {
				// Enable description html tags.
				$post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] );
				$product->set_description( $post_content );
			}

			// Validate the product type.
			if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Menu order.
			if ( isset( $data['menu_order'] ) ) {
				$product->set_menu_order( intval( $data['menu_order'] ) );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );

			// Save variations.
			if ( $product->is_type( 'variable' ) ) {
				if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
					$this->save_variations( $product, $data );
				} else {
					// Just sync variations.
					$product = WC_Product_Variable::sync( $product, false );
				}
			}

			$product->save();

			do_action( 'woocommerce_api_edit_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product.
	 *
	 * @since 2.2
	 *
	 * @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;
		}

		$product = wc_get_product( $id );

		do_action( 'woocommerce_api_delete_product', $id, $this );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $product->is_type( 'variable' ) ) {
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$product->delete( true );
			$result = ! ( $product->get_id() > 0 );
		} else {
			$product->delete();
			$result = 'trash' === $product->get_status();
		}

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

		// Delete parent product transients.
		if ( $parent_id = wp_get_post_parent_id( $id ) ) {
			wc_delete_product_transients( $parent_id );
		}

		if ( $force ) {
			return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) );
		} else {
			$this->server->send_status( '202' );

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

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

		$comments = get_approved_comments( $id );
		$reviews  = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => intval( $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 ) );
	}

	/**
	 * Get the orders for a product
	 *
	 * @since 2.4.0
	 * @param int $id the product ID to get orders for
	 * @param string fields  fields to retrieve
	 * @param array $filter filters to include in response
	 * @param string $status the order status to retrieve
	 * @param $page  $page   page to retrieve
	 * @return array|WP_Error
	 */
	public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) {
		global $wpdb;

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

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

		$order_ids = $wpdb->get_col( $wpdb->prepare( "
			SELECT order_id
			FROM {$wpdb->prefix}woocommerce_order_items
			WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
			AND order_item_type = 'line_item'
		 ", $id ) );

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

		$filter = array_merge( $filter, array(
			'in' => implode( ',', $order_ids ),
		) );

		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page );

		return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) );
	}

	/**
	 * Get a listing of product categories
	 *
	 * @since 2.2
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_categories( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$product_categories = array();

			$terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
			}

			return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product category for the given ID
	 *
	 * @since 2.2
	 *
	 * @param string $id product category term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_category( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_cat' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			// Get category display type
			$display_type = get_term_meta( $term_id, 'display_type', true );

			// Get category image
			$image = '';
			if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) {
				$image = wp_get_attachment_url( $image_id );
			}

			$product_category = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'display'     => $display_type ? $display_type : 'default',
				'image'       => $image ? esc_url( $image ) : '',
				'count'       => intval( $term->count ),
			);

			return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product category.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product category if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function create_product_category( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_category'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
				'parent'      => 0,
				'display'     => 'default',
				'image'       => '',
			);

			$data = wp_parse_args( $data['product_category'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this );

			// Check parent.
			$data['parent'] = absint( $data['parent'] );
			if ( $data['parent'] ) {
				$parent = get_term_by( 'id', $data['parent'], 'product_cat' );
				if ( ! $parent ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 );
				}
			}

			// If value of image is numeric, assume value as image_id.
			$image    = $data['image'];
			$image_id = 0;
			if ( is_numeric( $image ) ) {
				$image_id = absint( $image );
			} elseif ( ! empty( $image ) ) {
				$upload   = $this->upload_product_category_image( esc_url_raw( $image ) );
				$image_id = $this->set_product_category_image_as_attachment( $upload );
			}

			$insert = wp_insert_term( $data['name'], 'product_cat', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 );
			}

			$id = $insert['term_id'];

			update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );

			// Check if image_id is a valid image attachment before updating the term meta.
			if ( $image_id && wp_attachment_is_image( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );
			}

			do_action( 'woocommerce_api_create_product_category', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_category( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product category.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product category term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product category if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function edit_product_category( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_category'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_category'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 );
			}

			$data     = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this );
			$category = $this->get_product_category( $id );

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

			if ( isset( $data['image'] ) ) {
				$image_id = 0;

				// If value of image is numeric, assume value as image_id.
				$image = $data['image'];
				if ( is_numeric( $image ) ) {
					$image_id = absint( $image );
				} elseif ( ! empty( $image ) ) {
					$upload   = $this->upload_product_category_image( esc_url_raw( $image ) );
					$image_id = $this->set_product_category_image_as_attachment( $upload );
				}

				// In case client supplies invalid image or wants to unset category image.
				if ( ! wp_attachment_is_image( $image_id ) ) {
					$image_id = '';
				}
			}

			$update = wp_update_term( $id, 'product_cat', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_category', __( 'Could not edit the category', 'woocommerce' ), 400 );
			}

			if ( ! empty( $data['display'] ) ) {
				update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );
			}

			if ( isset( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );
			}

			do_action( 'woocommerce_api_edit_product_category', $id, $data );

			return $this->get_product_category( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product category.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product category term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_category( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_cat' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_category', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product tags.
	 *
	 * @since  2.5.0
	 *
	 * @param  string|null $fields Fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_tags( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 );
			}

			$product_tags = array();

			$terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_tags[] = current( $this->get_product_tag( $term_id, $fields ) );
			}

			return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product tag for the given ID.
	 *
	 * @since  2.5.0
	 *
	 * @param  string $id          Product tag term ID
	 * @param  string|null $fields Fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_tag( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_tag' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			$tag = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'description' => $term->description,
				'count'       => intval( $term->count ),
			);

			return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product tag.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product tag if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function create_product_tag( $data ) {
		try {
			if ( ! isset( $data['product_tag'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
			);

			$data = wp_parse_args( $data['product_tag'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this );

			$insert = wp_insert_term( $data['name'], 'product_tag', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 );
			}
			$id = $insert['term_id'];

			do_action( 'woocommerce_api_create_product_tag', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_tag( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product tag.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product tag term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product tag if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function edit_product_tag( $id, $data ) {
		try {
			if ( ! isset( $data['product_tag'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_tag'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this );
			$tag  = $this->get_product_tag( $id );

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

			$update = wp_update_term( $id, 'product_tag', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_tag', $id, $data );

			return $this->get_product_tag( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product tag.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product tag term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_tag( $id ) {
		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_tag' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_tag', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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(),
		);

		// Taxonomy query to filter products by type, category, tag, shipping class, and
		// attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies_arg_map = array(
			'product_type'           => 'type',
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Add attribute taxonomy names into the map.
		foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) {
			$taxonomies_arg_map[ $attribute_name ] = $attribute_name;
		}

		// Set tax_query for each passed arg.
		foreach ( $taxonomies_arg_map as $tax_name => $arg ) {
			if ( ! empty( $args[ $arg ] ) ) {
				$terms = explode( ',', $args[ $arg ] );

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

				unset( $args[ $arg ] );
			}
		}

		if ( ! empty( $tax_query ) ) {
			$query_args['tax_query'] = $tax_query;
		}

		// Filter by specific sku
		if ( ! empty( $args['sku'] ) ) {
			if ( ! is_array( $query_args['meta_query'] ) ) {
				$query_args['meta_query'] = array();
			}

			$query_args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => $args['sku'],
				'compare' => '=',
			);

			$query_args['post_type'] = array( 'product', 'product_variation' );
		}

		$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'              => $product->get_price(),
			'regular_price'      => $product->get_regular_price(),
			'sale_price'         => $product->get_sale_price() ? $product->get_sale_price() : 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(),
			'product_url'        => $product->is_type( 'external' ) ? $product->get_product_url() : '',
			'button_text'        => $product->is_type( 'external' ) ? $product->get_button_text() : '',
			'weight'             => $product->get_weight() ? $product->get_weight() : 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'        => wpautop( do_shortcode( $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() ),
			'parent_id'          => $product->get_parent_id(),
			'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'      => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ),
			'total_sales'        => $product->get_total_sales(),
			'variations'         => array(),
			'parent'             => array(),
			'grouped_products'   => array(),
			'menu_order'         => $this->get_product_menu_order( $product ),
		);
	}

	/**
	 * Get product menu order.
	 *
	 * @since 2.5.3
	 * @param WC_Product $product
	 * @return int
	 */
	private function get_product_menu_order( $product ) {
		$menu_order = $product->get_menu_order();

		return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product );
	}

	/**
	 * 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'              => $variation->get_price(),
				'regular_price'      => $variation->get_regular_price(),
				'sale_price'         => $variation->get_sale_price() ? $variation->get_sale_price() : null,
				'taxable'            => $variation->is_taxable(),
				'tax_status'         => $variation->get_tax_status(),
				'tax_class'          => $variation->get_tax_class(),
				'managing_stock'     => $variation->managing_stock(),
				'stock_quantity'     => $variation->get_stock_quantity(),
				'in_stock'           => $variation->is_in_stock(),
				'backorders_allowed' => $variation->backorders_allowed(),
				'backordered'        => $variation->is_on_backorder(),
				'purchaseable'       => $variation->is_purchasable(),
				'visible'            => $variation->variation_is_visible(),
				'on_sale'            => $variation->is_on_sale(),
				'weight'             => $variation->get_weight() ? $variation->get_weight() : 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 grouped products data
	 *
	 * @since  2.5.0
	 * @param  WC_Product $product
	 *
	 * @return array
	 */
	private function get_grouped_products_data( $product ) {
		$products = array();

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

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

			$products[] = $this->get_product_data( $_product );

		}

		return $products;
	}

	/**
	 * Save default attributes.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product
	 * @param WP_REST_Request $request
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		// Update default attributes options setting.
		if ( isset( $request['default_attribute'] ) ) {
			$request['default_attributes'] = $request['default_attribute'];
		}

		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) {
				if ( ! isset( $default_attr['name'] ) ) {
					continue;
				}

				$taxonomy = sanitize_title( $default_attr['name'] );

				if ( isset( $default_attr['slug'] ) ) {
					$taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
				}

				if ( isset( $attributes[ $taxonomy ] ) ) {
					$_attribute = $attributes[ $taxonomy ];

					if ( $_attribute['is_variation'] ) {
						$value = '';

						if ( isset( $default_attr['option'] ) ) {
							if ( $_attribute['is_taxonomy'] ) {
								// Don't use wc_clean as it destroys sanitized characters
								$value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
							} else {
								$value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
							}
						}

						if ( $value ) {
							$default_attributes[ $taxonomy ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Save product meta.
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $data
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_meta( $product, $data ) {
		global $wpdb;

		// Virtual.
		if ( isset( $data['virtual'] ) ) {
			$product->set_virtual( $data['virtual'] );
		}

		// Tax status.
		if ( isset( $data['tax_status'] ) ) {
			$product->set_tax_status( wc_clean( $data['tax_status'] ) );
		}

		// Tax Class.
		if ( isset( $data['tax_class'] ) ) {
			$product->set_tax_class( wc_clean( $data['tax_class'] ) );
		}

		// Catalog Visibility.
		if ( isset( $data['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) );
		}

		// Purchase Note.
		if ( isset( $data['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $data['purchase_note'] ) );
		}

		// Featured Product.
		if ( isset( $data['featured'] ) ) {
			$product->set_featured( $data['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $data );

		// SKU.
		if ( isset( $data['sku'] ) ) {
			$sku     = $product->get_sku();
			$new_sku = wc_clean( $data['sku'] );

			if ( '' == $new_sku ) {
				$product->set_sku( '' );
			} elseif ( $new_sku !== $sku ) {
				if ( ! empty( $new_sku ) ) {
					$unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku );
					if ( ! $unique_sku ) {
						throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 );
					} else {
						$product->set_sku( $new_sku );
					}
				} else {
					$product->set_sku( '' );
				}
			}
		}

		// Attributes.
		if ( isset( $data['attributes'] ) ) {
			$attributes = array();

			foreach ( $data['attributes'] as $attribute ) {
				$is_taxonomy = 0;
				$taxonomy    = 0;

				if ( ! isset( $attribute['name'] ) ) {
					continue;
				}

				$attribute_slug = sanitize_title( $attribute['name'] );

				if ( isset( $attribute['slug'] ) ) {
					$taxonomy       = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					$attribute_slug = sanitize_title( $attribute['slug'] );
				}

				if ( $taxonomy ) {
					$is_taxonomy = 1;
				}

				if ( $is_taxonomy ) {

					$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] );

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					// Update post terms
					if ( taxonomy_exists( $taxonomy ) ) {
						wp_set_object_terms( $product->get_id(), $values, $taxonomy );
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $taxonomy );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Array based.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];

					// Text based, separate by pipe.
					} else {
						$values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) );
					}

					// Custom attribute - Add attribute to array and set the values.
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute['name'] );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}

			uasort( $attributes, 'wc_product_attribute_uasort_comparison' );

			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) {

			// Variable and grouped products have no prices.
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );

		} else {

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price'];
				$product->set_regular_price( $regular_price );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price'];
				$product->set_sale_price( $sale_price );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$date_from = $data['sale_price_dates_from'];
			} else {
				$date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '';
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$date_to = $data['sale_price_dates_to'];
			} else {
				$date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '';
			}

			if ( $date_to && ! $date_from ) {
				$date_from = strtotime( 'NOW', current_time( 'timestamp', true ) );
			}

			$product->set_date_on_sale_to( $date_to );
			$product->set_date_on_sale_from( $date_from );

			if ( $product->is_on_sale( 'edit' ) ) {
				$product->set_price( $product->get_sale_price( 'edit' ) );
			} else {
				$product->set_price( $product->get_regular_price( 'edit' ) );
			}
		}

		// Product parent ID for groups.
		if ( isset( $data['parent_id'] ) ) {
			$product->set_parent_id( absint( $data['parent_id'] ) );
		}

		// Sold Individually.
		if ( isset( $data['sold_individually'] ) ) {
			$product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' );
		}

		// Stock status.
		if ( isset( $data['in_stock'] ) ) {
			$stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();

			if ( '' === $stock_status ) {
				$stock_status = 'instock';
			}
		}

		// Stock Data.
		if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $data['managing_stock'] ) ) {
				$managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
				$product->set_manage_stock( $managing_stock );
			} else {
				$managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
			}

			// Backorders.
			if ( isset( $data['backorders'] ) ) {
				if ( 'notify' === $data['backorders'] ) {
					$backorders = 'notify';
				} else {
					$backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
				}

				$product->set_backorders( $backorders );
			} else {
				$backorders = $product->get_backorders();
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( 'yes' == $managing_stock ) {
				$product->set_backorders( $backorders );

				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $data['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_backorders( $backorders );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $data['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $data['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}

				$product->set_upsell_ids( $upsells );
			} else {
				$product->set_upsell_ids( array() );
			}
		}

		// Cross sells.
		if ( isset( $data['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $data['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}

				$product->set_cross_sell_ids( $crosssells );
			} else {
				$product->set_cross_sell_ids( array() );
			}
		}

		// Product categories.
		if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
			$product->set_category_ids( $data['categories'] );
		}

		// Product tags.
		if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
			$product->set_tag_ids( $data['tags'] );
		}

		// Downloadable.
		if ( isset( $data['downloadable'] ) ) {
			$is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
			$product->set_downloadable( $is_downloadable );
		} else {
			$is_downloadable = $product->get_downloadable() ? 'yes' : 'no';
		}

		// Downloadable options.
		if ( 'yes' == $is_downloadable ) {

			// Downloadable files.
			if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $data['downloads'] );
			}

			// Download limit.
			if ( isset( $data['download_limit'] ) ) {
				$product->set_download_limit( $data['download_limit'] );
			}

			// Download expiry.
			if ( isset( $data['download_expiry'] ) ) {
				$product->set_download_expiry( $data['download_expiry'] );
			}
		}

		// Product url.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $data['product_url'] ) ) {
				$product->set_product_url( $data['product_url'] );
			}

			if ( isset( $data['button_text'] ) ) {
				$product->set_button_text( $data['button_text'] );
			}
		}

		// Reviews allowed.
		if ( isset( $data['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $data['reviews_allowed'] );
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $data );
		}

		// Do action for product type
		do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data );

		return $product;
	}

	/**
	 * Save variations.
	 *
	 * @since  2.2
	 *
	 * @param  WC_Product $product
	 * @param  array $request
	 *
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function save_variations( $product, $request ) {
		global $wpdb;

		$id         = $product->get_id();
		$variations = $request['variations'];
		$attributes = $product->get_attributes();

		foreach ( $variations as $menu_order => $data ) {
			$variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;
			$variation    = new WC_Product_Variation( $variation_id );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = current( $data['image'] );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->save_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$is_downloadable = $data['downloadable'];
				$variation->set_downloadable( $is_downloadable );
			} else {
				$is_downloadable = $variation->get_downloadable();
			}

			// Downloads.
			if ( $is_downloadable ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			$manage_stock = (bool) $variation->get_manage_stock();
			if ( isset( $data['managing_stock'] ) ) {
				$manage_stock = $data['managing_stock'];
			}
			$variation->set_manage_stock( $manage_stock );

			$stock_status = $variation->get_stock_status();
			if ( isset( $data['in_stock'] ) ) {
				$stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock';
			}
			$variation->set_stock_status( $stock_status );

			$backorders = $variation->get_backorders();
			if ( isset( $data['backorders'] ) ) {
				$backorders = $data['backorders'];
			}
			$variation->set_backorders( $backorders );

			if ( $manage_stock ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$variation->set_stock_quantity( $stock_quantity );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$variation->set_date_on_sale_from( $data['sale_price_dates_from'] );
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$variation->set_date_on_sale_to( $data['sale_price_dates_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Description.
			if ( isset( $data['description'] ) ) {
				$variation->set_description( wp_kses_post( $data['description'] ) );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$_attributes = array();

				foreach ( $data['attributes'] as $attribute_key => $attribute ) {
					if ( ! isset( $attribute['name'] ) ) {
						continue;
					}

					$taxonomy   = 0;
					$_attribute = array();

					if ( isset( $attribute['slug'] ) ) {
						$taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					}

					if ( ! $taxonomy ) {
						$taxonomy = sanitize_title( $attribute['name'] );
					}

					if ( isset( $attributes[ $taxonomy ] ) ) {
						$_attribute = $attributes[ $taxonomy ];
					}

					if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
						$_attribute_key = sanitize_title( $_attribute['name'] );

						if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
							// Don't use wc_clean as it destroys sanitized characters.
							$_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
						} else {
							$_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
						}

						$_attributes[ $_attribute_key ] = $_attribute_value;
					}
				}

				$variation->set_attributes( $_attributes );
			}

			$variation->save();

			do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
		}

		return true;
	}

	/**
	 * Save product shipping data
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $data
	 * @return WC_Product
	 */
	private function save_product_shipping_data( $product, $data ) {
		if ( isset( $data['weight'] ) ) {
			$product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) );
		}

		// Product dimensions
		if ( isset( $data['dimensions'] ) ) {
			// Height
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
			}

			// Width
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
			}

			// Length
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
			}
		}

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';

			if ( 'yes' == $virtual ) {
				$product->set_weight( '' );
				$product->set_height( '' );
				$product->set_length( '' );
				$product->set_width( '' );
			}
		}

		// Shipping class
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $downloads
	 * @param int $deprecated Deprecated since 3.0.
	 * @return WC_Product
	 */
	private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( isset( $file['url'] ) ) {
				$file['file'] = $file['url'];
			}

			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[]  = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Get attribute taxonomy by slug.
	 *
	 * @since 2.2
	 * @param string $slug
	 * @return string|null
	 */
	private function get_attribute_taxonomy_by_slug( $slug ) {
		$taxonomy = null;
		$attribute_taxonomies = wc_get_attribute_taxonomies();

		foreach ( $attribute_taxonomies as $key => $tax ) {
			if ( $slug == $tax->attribute_name ) {
				$taxonomy = 'pa_' . $tax->attribute_name;

				break;
			}
		}

		return $taxonomy;
	}

	/**
	 * 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'   => (int) $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;
	}

	/**
	 * Save product images.
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $images
	 * @throws WC_API_Exception
	 * @return WC_Product
	 */
	protected function save_product_images( $product, $images ) {
		if ( is_array( $images ) ) {
			$gallery = array();

			foreach ( $images as $image ) {
				if ( isset( $image['position'] ) && 0 == $image['position'] ) {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$product->set_image_id( $attachment_id );
				} else {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$gallery[] = $attachment_id;
				}

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) && $attachment_id ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image title if present.
				if ( ! empty( $image['title'] ) && $attachment_id ) {
					wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
				}
			}

			if ( ! empty( $gallery ) ) {
				$product->set_gallery_image_ids( $gallery );
			}
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Upload image from URL
	 *
	 * @since 2.2
	 * @param string $image_url
	 * @return int|WP_Error attachment id
	 */
	public function upload_product_image( $image_url ) {
		return $this->upload_image_from_url( $image_url, 'product_image' );
	}

	/**
	 * Upload product category image from URL.
	 *
	 * @since 2.5.0
	 * @param string $image_url
	 * @return int|WP_Error attachment id
	 */
	public function upload_product_category_image( $image_url ) {
		return $this->upload_image_from_url( $image_url, 'product_category_image' );
	}

	/**
	 * Upload image from URL.
	 *
	 * @throws WC_API_Exception
	 *
	 * @since 2.5.0
	 * @param string $image_url
	 * @param string $upload_for
	 * @return array
	 */
	protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) {
		$upload = wc_rest_upload_image_from_url( $image_url );
		if ( is_wp_error( $upload ) ) {
			throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 );
		}

		do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for );

		return $upload;
	}

	/**
	 * Sets product image as attachment and returns the attachment ID.
	 *
	 * @since 2.2
	 * @param array $upload
	 * @param int $id
	 * @return int
	 */
	protected function set_product_image_as_attachment( $upload, $id ) {
		return $this->set_uploaded_image_as_attachment( $upload, $id );
	}

	/**
	 * Sets uploaded category image as attachment and returns the attachment ID.
	 *
	 * @since  2.5.0
	 * @param  integer $upload Upload information from wp_upload_bits
	 * @return int             Attachment ID
	 */
	protected function set_product_category_image_as_attachment( $upload ) {
		return $this->set_uploaded_image_as_attachment( $upload );
	}

	/**
	 * Set uploaded image as attachment.
	 *
	 * @since  2.5.0
	 * @param  array $upload Upload information from wp_upload_bits
	 * @param  int   $id     Post ID. Default to 0.
	 * @return int           Attachment ID
	 */
	protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) {
		$info    = wp_check_filetype( $upload['file'] );
		$title   = '';
		$content = '';

		if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = wc_clean( $image_meta['title'] );
			}
			if ( trim( $image_meta['caption'] ) ) {
				$content = wc_clean( $image_meta['caption'] );
			}
		}

		$attachment = array(
			'post_mime_type' => $info['type'],
			'guid'           => $upload['url'],
			'post_parent'    => $id,
			'post_title'     => $title,
			'post_content'   => $content,
		);

		$attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
		if ( ! is_wp_error( $attachment_id ) ) {
			wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
		}

		return $attachment_id;
	}

	/**
	 * 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'   => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ),
					'slug'   => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => wc_attribute_label( $attribute['name'], $product ),
					'slug'      => wc_attribute_taxonomy_slug( $attribute['name'] ),
					'position'  => (int) $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;
	}

	/**
	 * Get a listing of product attributes
	 *
	 * @since 2.5.0
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attributes( $fields = null ) {
		try {
			// Permissions check.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$product_attributes   = array();
			$attribute_taxonomies = wc_get_attribute_taxonomies();

			foreach ( $attribute_taxonomies as $attribute ) {
				$product_attributes[] = array(
					'id'           => intval( $attribute->attribute_id ),
					'name'         => $attribute->attribute_label,
					'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
					'type'         => $attribute->attribute_type,
					'order_by'     => $attribute->attribute_orderby,
					'has_archives' => (bool) $attribute->attribute_public,
				);
			}

			return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute for the given ID
	 *
	 * @since 2.5.0
	 *
	 * @param string $id product attribute term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$attribute = $wpdb->get_row( $wpdb->prepare( "
				SELECT *
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$product_attribute = array(
				'id'           => intval( $attribute->attribute_id ),
				'name'         => $attribute->attribute_label,
				'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
				'type'         => $attribute->attribute_type,
				'order_by'     => $attribute->attribute_orderby,
				'has_archives' => (bool) $attribute->attribute_public,
			);

			return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate attribute data.
	 *
	 * @since  2.5.0
	 * @param  string $name
	 * @param  string $slug
	 * @param  string $type
	 * @param  string $order_by
	 * @param  bool   $new_data
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
		if ( empty( $name ) ) {
			throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
		}

		if ( strlen( $slug ) > 28 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 );
		}

		// Validate the attribute type
		if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
		}

		// Validate the attribute order by
		if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
		}

		return true;
	}

	/**
	 * Create a new product attribute.
	 *
	 * @since 2.5.0
	 *
	 * @param array $data Posted data.
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$data = $data['product_attribute'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );

			if ( ! isset( $data['name'] ) ) {
				$data['name'] = '';
			}

			// Set the attribute slug.
			if ( ! isset( $data['slug'] ) ) {
				$data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
			} else {
				$data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
			}

			// Set attribute type when not sent.
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'select';
			}

			// Set order by when not sent.
			if ( ! isset( $data['order_by'] ) ) {
				$data['order_by'] = 'menu_order';
			}

			// Validate the attribute data.
			$this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );

			$insert = $wpdb->insert(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $data['name'],
					'attribute_name'    => $data['slug'],
					'attribute_type'    => $data['type'],
					'attribute_orderby' => $data['order_by'],
					'attribute_public'  => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
				),
				array( '%s', '%s', '%s', '%s', '%d' )
			);

			// Checks for an error in the product creation.
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
			}

			$id = $wpdb->insert_id;

			do_action( 'woocommerce_api_create_product_attribute', $id, $data );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			$this->server->send_status( 201 );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute.
	 *
	 * @since 2.5.0
	 *
	 * @param int $id the attribute ID.
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$data      = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this );
			$attribute = $this->get_product_attribute( $id );

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

			$attribute_name     = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name'];
			$attribute_type     = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type'];
			$attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by'];

			if ( isset( $data['slug'] ) ) {
				$attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) );
			} else {
				$attribute_slug = $attribute['product_attribute']['slug'];
			}
			$attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug );

			if ( isset( $data['has_archives'] ) ) {
				$attribute_public = true === $data['has_archives'] ? 1 : 0;
			} else {
				$attribute_public = $attribute['product_attribute']['has_archives'];
			}

			// Validate the attribute data.
			$this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false );

			$update = $wpdb->update(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $attribute_name,
					'attribute_name'    => $attribute_slug,
					'attribute_type'    => $attribute_type,
					'attribute_orderby' => $attribute_order_by,
					'attribute_public'  => $attribute_public,
				),
				array( 'attribute_id' => $id ),
				array( '%s', '%s', '%s', '%s', '%d' ),
				array( '%d' )
			);

			// Checks for an error in the product creation.
			if ( false === $update ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute', $id, $data );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute.
	 *
	 * @since  2.5.0
	 *
	 * @param  int $id the product attribute ID.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute( $id ) {
		global $wpdb;

		try {
			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			$attribute_name = $wpdb->get_var( $wpdb->prepare( "
				SELECT attribute_name
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_null( $attribute_name ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$deleted = $wpdb->delete(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array( 'attribute_id' => $id ),
				array( '%d' )
			);

			if ( false === $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name( $attribute_name );

			if ( taxonomy_exists( $taxonomy ) ) {
				$terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' );
				foreach ( $terms as $term ) {
					wp_delete_term( $term->term_id, $taxonomy );
				}
			}

			do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy );
			do_action( 'woocommerce_api_delete_product_attribute', $id, $this );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product attribute terms.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param string|null $fields Fields to limit response to.
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute_terms( $attribute_id, $fields = null ) {
		try {
			// Permissions check.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 );
			}

			$attribute_id = absint( $attribute_id );
			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
			$attribute_terms = array();

			foreach ( $terms as $term ) {
				$attribute_terms[] = array(
					'id'    => $term->term_id,
					'slug'  => $term->slug,
					'name'  => $term->name,
					'count' => $term->count,
				);
			}

			return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute term for the given ID.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param string $id Product attribute term ID.
	 * @param string|null $fields Fields to limit response to.
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute_term( $attribute_id, $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );
			$attribute_id = absint( $attribute_id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term = get_term( $id, $taxonomy );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$attribute_term = array(
				'id'    => $term->term_id,
				'name'  => $term->name,
				'slug'  => $term->slug,
				'count' => $term->count,
			);

			return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product attribute term.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param array $data Posted data.
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute_term( $attribute_id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute_term'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 );
			}

			$data = $data['product_attribute_term'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this );

			// Check if attribute term name is specified.
			if ( ! isset( $data['name'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
			}

			$args = array();

			// Set the attribute term slug.
			if ( isset( $data['slug'] ) ) {
				$args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) );
			}

			$term = wp_insert_term( $data['name'], $taxonomy, $args );

			// Checks for an error in the term creation.
			if ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 );
			}

			$id = $term['term_id'];

			do_action( 'woocommerce_api_create_product_attribute_term', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_attribute_term( $attribute_id, $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute term.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param int $id the attribute ID.
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute_term( $attribute_id, $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute_term'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute_term'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this );

			$args = array();

			// Update name.
			if ( isset( $data['name'] ) ) {
				$args['name'] = wc_clean( wp_unslash( $data['name'] ) );
			}

			// Update slug.
			if ( isset( $data['slug'] ) ) {
				$args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) );
			}

			$term = wp_update_term( $id, $taxonomy, $args );

			if ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data );

			return $this->get_product_attribute_term( $attribute_id, $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute term.
	 *
	 * @since  2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param int $id the product attribute ID.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute_term( $attribute_id, $id ) {
		global $wpdb;

		try {
			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$id   = absint( $id );
			$term = wp_delete_term( $id, $taxonomy );

			if ( ! $term ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 );
			} elseif ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 );
			}

			do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Clear product
	 *
	 * @param int $product_id
	 */
	protected function clear_product( $product_id ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		// Delete product attachments
		$attachments = get_children( array(
			'post_parent' => $product_id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product
		$product = wc_get_product( $product_id );
		$product->delete( true );
	}

	/**
	 * Bulk update or insert products
	 * Accepts an array with products in the formats supported by
	 * WC_API_Products->create_product() and WC_API_Products->edit_product()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['products'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 );
			}

			$data  = $data['products'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$products = array();

			foreach ( $data as $_product ) {
				$product_id  = 0;
				$product_sku = '';

				// Try to get the product ID
				if ( isset( $_product['id'] ) ) {
					$product_id = intval( $_product['id'] );
				}

				if ( ! $product_id && isset( $_product['sku'] ) ) {
					$product_sku = wc_clean( $_product['sku'] );
					$product_id  = wc_get_product_id_by_sku( $product_sku );
				}

				if ( $product_id ) {

					// Product exists / edit product
					$edit = $this->edit_product( $product_id, array( 'product' => $_product ) );

					if ( is_wp_error( $edit ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$products[] = $edit['product'];
					}
				} else {

					// Product don't exists / create product
					$new = $this->create_product( array( 'product' => $_product ) );

					if ( is_wp_error( $new ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$products[] = $new['product'];
					}
				}
			}

			return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product shipping classes.
	 *
	 * @since  2.5.0
	 * @param  string|null    $fields Fields to limit response to
	 * @return array|WP_Error         List of product shipping classes if succeed,
	 *                                otherwise WP_Error will be returned
	 */
	public function get_product_shipping_classes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 );
			}

			$product_shipping_classes = array();

			$terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) );
			}

			return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product shipping class for the given ID.
	 *
	 * @since  2.5.0
	 * @param  string         $id     Product shipping class term ID
	 * @param  string|null    $fields Fields to limit response to
	 * @return array|WP_Error         Product shipping class if succeed, otherwise
	 *                                WP_Error will be returned
	 */
	public function get_product_shipping_class( $id, $fields = null ) {
		try {
			$id = absint( $id );
			if ( ! $id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_shipping_class' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			$product_shipping_class = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'count'       => intval( $term->count ),
			);

			return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product shipping class.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product shipping class if succeed, otherwise
	 *                              WP_Error will be returned
	 */
	public function create_product_shipping_class( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_shipping_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
				'parent'      => 0,
			);

			$data = wp_parse_args( $data['product_shipping_class'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this );

			// Check parent.
			$data['parent'] = absint( $data['parent'] );
			if ( $data['parent'] ) {
				$parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' );
				if ( ! $parent ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 );
				}
			}

			$insert = wp_insert_term( $data['name'], 'product_shipping_class', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 );
			}

			$id = $insert['term_id'];

			do_action( 'woocommerce_api_create_product_shipping_class', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_shipping_class( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product shipping class.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product shipping class term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product shipping class if succeed, otherwise
	 *                              WP_Error will be returned
	 */
	public function edit_product_shipping_class( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_shipping_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_shipping_class'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 );
			}

			$data           = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this );
			$shipping_class = $this->get_product_shipping_class( $id );

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

			$update = wp_update_term( $id, 'product_shipping_class', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data );

			return $this->get_product_shipping_class( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product shipping class.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product shipping class term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_shipping_class( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_shipping_class' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v3/class-wc-api-reports.php000064400000023064151542631340012346 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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();

		// check for WP_Error
		if ( is_wp_error( $check ) ) {
			return $check;
		}

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

		// 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 );
		$report_data     = $this->report->get_report_data();
		$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;
				default :
					$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 ( $report_data->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 ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		foreach ( $report_data->order_counts 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 ]['orders']   = (int) $order->count;
		}

		// add total order items for each period
		foreach ( $report_data->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 ( $report_data->coupons 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'       => $report_data->total_sales,
			'net_sales'         => $report_data->net_sales,
			'average_sales'     => $report_data->average_sales,
			'total_orders'      => $report_data->total_orders,
			'total_items'       => $report_data->total_items,
			'total_tax'         => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
			'total_shipping'    => $report_data->total_shipping,
			'total_refunds'     => $report_data->total_refunds,
			'total_discount'    => $report_data->total_coupons,
			'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 );

			if ( $product ) {
				$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' );
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );

		$this->report = new WC_Report_Sales_By_Date();

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

		return new WP_Error(
			'woocommerce_api_user_cannot_read_report',
			__( 'You do not have permission to read this report', 'woocommerce' ),
			array( 'status' => 401 )
		);
	}
}
api/v3/class-wc-api-resource.php000064400000034150151542631340012475 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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' ) );

		// maybe add meta to top-level resource responses
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
		}

		$response_names = array(
			'order',
			'coupon',
			'customer',
			'product',
			'report',
			'customer_orders',
			'customer_downloads',
			'order_note',
			'order_refund',
			'product_reviews',
			'product_category',
			'tax',
			'tax_class',
		);

		foreach ( $response_names as $name ) {

			/**
			 * 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
			 */
			add_filter( "woocommerce_api_{$name}_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 || 'shop_webhook' === $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 ) {
			return $id;
		}

		$post = get_post( $id );

		// Orders request are a special case.
		$is_invalid_orders_request = ( 'shop_order' === $type && ( ! $post || ! is_a ( $post, 'WP_Post' ) || 'shop_order' !== $post->post_type ) && ! wc_rest_check_post_permissions( 'shop_order', 'read' ) );

		if ( ! $is_invalid_orders_request ) {
			if ( null === $post ) {
				return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
			}

			// 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 ( $is_invalid_orders_request || ! $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 ( $is_invalid_orders_request || ! $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 ( $is_invalid_orders_request || ! $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'];
		}

		// order (ASC or DESC, ASC by default)
		if ( ! empty( $request_args['order'] ) ) {
			$args['order'] = $request_args['order'];
		}

		// orderby
		if ( ! empty( $request_args['orderby'] ) ) {
			$args['orderby'] = $request_args['orderby'];

			// allow sorting by meta value
			if ( ! empty( $request_args['orderby_meta_key'] ) ) {
				$args['meta_key'] = $request_args['orderby_meta_key'];
			}
		}

		// allow post status change
		if ( ! empty( $request_args['post_status'] ) ) {
			$args['post_status'] = $request_args['post_status'];
			unset( $request_args['post_status'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

		// exclude by a list of post id
		if ( ! empty( $request_args['not_in'] ) ) {
			$args['post__not_in'] = explode( ',', $request_args['not_in'] );
			unset( $request_args['not_in'] );
		}

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

		$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );

		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/webhook
			$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 ) {
		$permission = false;

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

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

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

		if ( 'read' === $context ) {
			$permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID );
		} elseif ( 'edit' === $context ) {
			$permission = current_user_can( $post_type->cap->edit_post, $post->ID );
		} elseif ( 'delete' === $context ) {
			$permission = current_user_can( $post_type->cap->delete_post, $post->ID );
		}

		return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type );
	}
}
api/v3/class-wc-api-server.php000064400000050071151542631340012154 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
 */

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

		// load response handler
		$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 );

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

			// API requests run under the context of the authenticated 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 ) );
	}

	/**
	 * urldecode deep.
	 *
	 * @since  2.2
	 * @param  string|array $value Data to decode with urldecode.
	 *
	 * @return string|array        Decoded data.
	 */
	protected function urldecode_deep( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( $this, 'urldecode_deep' ), $value );
		} else {
			return urldecode( $value );
		}
	}

	/**
	 * 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.2
	 *
	 * @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
				if ( 'data' == $param->getName() ) {
					$ordered_parameters[] = $provided[ $param->getName() ];
					continue;
				}

				$ordered_parameters[] = $this->urldecode_deep( $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.3
	 * @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,
				'version'     => WC_API::VERSION,
				'routes'      => array(),
				'meta'        => array(
					'timezone'           => wc_timezone_string(),
					'currency'           => get_woocommerce_currency(),
					'currency_format'    => get_woocommerce_currency_symbol(),
					'currency_position'  => get_option( 'woocommerce_currency_pos' ),
					'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
					'decimal_separator'  => get_option( 'woocommerce_price_decimal_sep' ),
					'price_num_decimals' => wc_get_price_decimals(),
					'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' ) || wc_site_is_https() ),
					'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
					'generate_password'  => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
					'links'              => array(
						'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
					),
				),
			),
		);

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

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );

			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|stdClass $query
	 */
	public function add_pagination_headers( $query ) {

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

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

			if ( $query->get( 'number' ) > 0 ) {
				$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
				$total_pages = ceil( $total / $query->get( 'number' ) );
			} else {
				$page = 1;
				$total_pages = 1;
			}
		} elseif ( is_a( $query, 'stdClass' ) ) {
			$page        = $query->page;
			$single      = $query->is_single;
			$total       = $query->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;
	}
}
api/v3/class-wc-api-taxes.php000064400000044235151542631340011777 0ustar00<?php
/**
 * WooCommerce API Taxes Class
 *
 * Handles requests to the /taxes endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.5.0
 */

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

class WC_API_Taxes extends WC_API_Resource {

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

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

		# GET/POST /taxes
		$routes[ $this->base ] = array(
			array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET/PUT/DELETE /taxes/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_tax' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ),
		);

		# GET/POST /taxes/classes
		$routes[ $this->base . '/classes' ] = array(
			array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET /taxes/classes/<slug>
		$routes[ $this->base . '/classes/(?P<slug>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /taxes/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all taxes
	 *
	 * @since 2.5.0
	 *
	 * @param string $fields
	 * @param array  $filter
	 * @param string $class
	 * @param int    $page
	 *
	 * @return array
	 */
	public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) {
		if ( ! empty( $class ) ) {
			$filter['tax_rate_class'] = $class;
		}

		$filter['page'] = $page;

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

		$taxes = array();

		foreach ( $query['results'] as $tax ) {
			$taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) );
		}

		// Set pagination headers
		$this->server->add_pagination_headers( $query['headers'] );

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

	/**
	 * Get the tax for the given ID
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 * @param string $fields fields to include in response
	 *
	 * @return array|WP_Error
	 */
	public function get_tax( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Permissions check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 );
			}

			// Get tax rate details
			$tax = WC_Tax::_get_tax_rate( $id );

			if ( is_wp_error( $tax ) || empty( $tax ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$tax_data = array(
				'id'       => (int) $tax['tax_rate_id'],
				'country'  => $tax['tax_rate_country'],
				'state'    => $tax['tax_rate_state'],
				'postcode' => '',
				'city'     => '',
				'rate'     => $tax['tax_rate'],
				'name'     => $tax['tax_rate_name'],
				'priority' => (int) $tax['tax_rate_priority'],
				'compound' => (bool) $tax['tax_rate_compound'],
				'shipping' => (bool) $tax['tax_rate_shipping'],
				'order'    => (int) $tax['tax_rate_order'],
				'class'    => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard',
			);

			// Get locales from a tax rate
			$locales = $wpdb->get_results( $wpdb->prepare( "
				SELECT location_code, location_type
				FROM {$wpdb->prefix}woocommerce_tax_rate_locations
				WHERE tax_rate_id = %d
			", $id ) );

			if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
				foreach ( $locales as $locale ) {
					$tax_data[ $locale->location_type ] = $locale->location_code;
				}
			}

			return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a tax
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_tax( $data ) {
		try {
			if ( ! isset( $data['tax'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this );

			$tax_data = array(
				'tax_rate_country'  => '',
				'tax_rate_state'    => '',
				'tax_rate'          => '',
				'tax_rate_name'     => '',
				'tax_rate_priority' => 1,
				'tax_rate_compound' => 0,
				'tax_rate_shipping' => 1,
				'tax_rate_order'    => 0,
				'tax_rate_class'    => '',
			);

			foreach ( $tax_data as $key => $value ) {
				$new_key = str_replace( 'tax_rate_', '', $key );
				$new_key = 'tax_rate' === $new_key ? 'rate' : $new_key;

				if ( isset( $data[ $new_key ] ) ) {
					if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) {
						$tax_data[ $key ] = $data[ $new_key ] ? 1 : 0;
					} else {
						$tax_data[ $key ] = $data[ $new_key ];
					}
				}
			}

			// Create tax rate
			$id = WC_Tax::_insert_tax_rate( $tax_data );

			// Add locales
			if ( ! empty( $data['postcode'] ) ) {
				WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
			}

			if ( ! empty( $data['city'] ) ) {
				WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
			}

			do_action( 'woocommerce_api_create_tax', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_tax( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a tax
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_tax( $id, $data ) {
		try {
			if ( ! isset( $data['tax'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 );
			}

			$data = $data['tax'];

			// Get current tax rate data
			$tax = $this->get_tax( $id );

			if ( is_wp_error( $tax ) ) {
				$error_data = $tax->get_error_data();
				throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] );
			}

			$current_data   = $tax['tax'];
			$data           = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this );
			$tax_data       = array();
			$default_fields = array(
				'tax_rate_country',
				'tax_rate_state',
				'tax_rate',
				'tax_rate_name',
				'tax_rate_priority',
				'tax_rate_compound',
				'tax_rate_shipping',
				'tax_rate_order',
				'tax_rate_class',
			);

			foreach ( $data as $key => $value ) {
				$new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key;

				// Check if the key is valid
				if ( ! in_array( $new_key, $default_fields ) ) {
					continue;
				}

				// Test new data against current data
				if ( $value === $current_data[ $key ] ) {
					continue;
				}

				// Fix compound and shipping values
				if ( in_array( $key, array( 'compound', 'shipping' ) ) ) {
					$value = $value ? 1 : 0;
				}

				$tax_data[ $new_key ] = $value;
			}

			// Update tax rate
			WC_Tax::_update_tax_rate( $id, $tax_data );

			// Update locales
			if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) {
				WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
			}

			if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) {
				WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
			}

			do_action( 'woocommerce_api_edit_tax_rate', $id, $data );

			return $this->get_tax( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a tax
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 *
	 * @return array|WP_Error
	 */
	public function delete_tax( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			WC_Tax::_delete_tax_rate( $id );

			if ( 0 === $wpdb->rows_affected ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 );
			}

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of taxes
	 *
	 * @since 2.5.0
	 *
	 * @param string $class
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_taxes_count( $class = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 );
			}

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

			$query = $this->query_tax_rates( $filter, true );

			return array( 'count' => (int) $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Helper method to get tax rates objects
	 *
	 * @since 2.5.0
	 *
	 * @param  array $args
	 * @param  bool  $count_only
	 *
	 * @return array
	 */
	protected function query_tax_rates( $args, $count_only = false ) {
		global $wpdb;

		$results = '';

		// Set args
		$args = $this->merge_query_args( $args, array() );

		$query = "
			SELECT tax_rate_id
			FROM {$wpdb->prefix}woocommerce_tax_rates
			WHERE 1 = 1
		";

		// Filter by tax class
		if ( ! empty( $args['tax_rate_class'] ) ) {
			$tax_rate_class = esc_sql( 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : '' );
			$query .= " AND tax_rate_class = '$tax_rate_class'";
		}

		// Order tax rates
		$order_by = ' ORDER BY tax_rate_order';

		// Pagination
		$per_page   = absint( isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ) );
		$offset     = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0;
		$pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page );

		if ( ! $count_only ) {
			$results = $wpdb->get_results( $query . $order_by . $pagination );
		}

		$wpdb->get_results( $query );
		$headers              = new stdClass;
		$headers->page        = $args['paged'];
		$headers->total       = (int) $wpdb->num_rows;
		$headers->is_single   = $per_page > $headers->total;
		$headers->total_pages = ceil( $headers->total / $per_page );

		return array(
			'results' => $results,
			'headers' => $headers,
		);
	}

	/**
	 * Bulk update or insert taxes
	 * Accepts an array with taxes in the formats supported by
	 * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax()
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {
		try {
			if ( ! isset( $data['taxes'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 );
			}

			$data  = $data['taxes'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$taxes = array();

			foreach ( $data as $_tax ) {
				$tax_id = 0;

				// Try to get the tax rate ID
				if ( isset( $_tax['id'] ) ) {
					$tax_id = intval( $_tax['id'] );
				}

				if ( $tax_id ) {

					// Tax rate exists / edit tax rate
					$edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) );

					if ( is_wp_error( $edit ) ) {
						$taxes[] = array(
							'id'    => $tax_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$taxes[] = $edit['tax'];
					}
				} else {

					// Tax rate don't exists / create tax rate
					$new = $this->create_tax( array( 'tax' => $_tax ) );

					if ( is_wp_error( $new ) ) {
						$taxes[] = array(
							'id'    => $tax_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$taxes[] = $new['tax'];
					}
				}
			}

			return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get all tax classes
	 *
	 * @since 2.5.0
	 *
	 * @param string $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_tax_classes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 );
			}

			$tax_classes = array();

			// Add standard class
			$tax_classes[] = array(
				'slug' => 'standard',
				'name' => __( 'Standard rate', 'woocommerce' ),
			);

			$classes = WC_Tax::get_tax_classes();

			foreach ( $classes as $class ) {
				$tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array(
					'slug' => sanitize_title( $class ),
					'name' => $class,
				), $class, $fields, $this );
			}

			return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a tax class.
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_tax_class( $data ) {
		try {
			if ( ! isset( $data['tax_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 );
			}

			$data = $data['tax_class'];

			if ( empty( $data['name'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
			}

			$name      = sanitize_text_field( $data['name'] );
			$tax_class = WC_Tax::create_tax_class( $name );

			if ( is_wp_error( $tax_class ) ) {
				return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 );
			}

			do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data );

			$this->server->send_status( 201 );

			return array(
				'tax_class' => $tax_class,
			);
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a tax class
	 *
	 * @since 2.5.0
	 *
	 * @param int $slug The tax class slug
	 *
	 * @return array|WP_Error
	 */
	public function delete_tax_class( $slug ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 );
			}

			$slug      = sanitize_title( $slug );
			$tax_class = WC_Tax::get_tax_class_by( 'slug', $slug );
			$deleted   = WC_Tax::delete_tax_class_by( 'slug', $slug );

			if ( is_wp_error( $deleted ) || ! $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 );
			}

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of tax classes
	 *
	 * @since 2.5.0
	 *
	 * @return array|WP_Error
	 */
	public function get_tax_classes_count() {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 );
			}

			$total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate

			return array( 'count' => $total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
api/v3/class-wc-api-webhooks.php000064400000036363151542631340012477 0ustar00<?php
/**
 * WooCommerce API Webhooks class
 *
 * Handles requests to the /webhooks endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

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

class WC_API_Webhooks extends WC_API_Resource {

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

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

		# GET|POST /webhooks
		$routes[ $this->base ] = array(
			array( array( $this, 'get_webhooks' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_webhook' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

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

		# GET|PUT|DELETE /webhooks/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
		);

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

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

		return $routes;
	}

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

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

		$filter['page'] = $page;

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

		$webhooks = array();

		foreach ( $query['results'] as $webhook_id ) {
			$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query['headers'] );

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

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

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

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

		$webhook = wc_get_webhook( $id );

		$webhook_data = array(
			'id'           => $webhook->get_id(),
			'name'         => $webhook->get_name(),
			'status'       => $webhook->get_status(),
			'topic'        => $webhook->get_topic(),
			'resource'     => $webhook->get_resource(),
			'event'        => $webhook->get_event(),
			'hooks'        => $webhook->get_hooks(),
			'delivery_url' => $webhook->get_delivery_url(),
			'created_at'   => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'   => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
		);

		return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
	}

	/**
	 * Get the total number of webhooks
	 *
	 * @since 2.2
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_webhooks_count( $status = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
			}

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

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

			return array( 'count' => $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create an webhook
	 *
	 * @since 2.2
	 *
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function create_webhook( $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			// permission check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );

			// validate topic
			if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
			}

			// validate delivery URL
			if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
			}

			$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
				'post_type'     => 'shop_webhook',
				'post_status'   => 'publish',
				'ping_status'   => 'closed',
				'post_author'   => get_current_user_id(),
				'post_password' => 'webhook_' . wp_generate_password(),
				'post_title'    => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ),
			), $data, $this );

			$webhook = new WC_Webhook();

			$webhook->set_name( $webhook_data['post_title'] );
			$webhook->set_user_id( $webhook_data['post_author'] );
			$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
			$webhook->set_topic( $data['topic'] );
			$webhook->set_delivery_url( $data['delivery_url'] );
			$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
			$webhook->set_api_version( 'legacy_v3' );
			$webhook->save();

			$webhook->deliver_ping();

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a webhook
	 *
	 * @since 2.2
	 *
	 * @param int $id webhook ID
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function edit_webhook( $id, $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

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

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

			$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );

			$webhook = wc_get_webhook( $id );

			// update topic
			if ( ! empty( $data['topic'] ) ) {

				if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {

					$webhook->set_topic( $data['topic'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
				}
			}

			// update delivery URL
			if ( ! empty( $data['delivery_url'] ) ) {
				if ( wc_is_valid_url( $data['delivery_url'] ) ) {

					$webhook->set_delivery_url( $data['delivery_url'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
				}
			}

			// update secret
			if ( ! empty( $data['secret'] ) ) {
				$webhook->set_secret( $data['secret'] );
			}

			// update status
			if ( ! empty( $data['status'] ) ) {
				$webhook->set_status( $data['status'] );
			}

			// update name
			if ( ! empty( $data['name'] ) ) {
				$webhook->set_name( $data['name'] );
			}

			$webhook->save();

			do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a webhook
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @return array|WP_Error
	 */
	public function delete_webhook( $id ) {

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

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

		do_action( 'woocommerce_api_delete_webhook', $id, $this );

		$webhook = wc_get_webhook( $id );

		return $webhook->delete( true );
	}

	/**
	 * Helper method to get webhook post objects
	 *
	 * @since 2.2
	 * @param array $args Request arguments for filtering query.
	 * @return array
	 */
	private function query_webhooks( $args ) {
		$args = $this->merge_query_args( array(), $args );

		$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );

		if ( empty( $args['offset'] ) ) {
			$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
		}

		$page = $args['paged'];
		unset( $args['paged'], $args['posts_per_page'] );

		if ( isset( $args['s'] ) ) {
			$args['search'] = $args['s'];
			unset( $args['s'] );
		}

		// Post type to webhook status.
		if ( ! empty( $args['post_status'] ) ) {
			$args['status'] = $args['post_status'];
			unset( $args['post_status'] );
		}

		if ( ! empty( $args['post__in'] ) ) {
			$args['include'] = $args['post__in'];
			unset( $args['post__in'] );
		}

		if ( ! empty( $args['date_query'] ) ) {
			foreach ( $args['date_query'] as $date_query ) {
				if ( 'post_date_gmt' === $date_query['column'] ) {
					$args['after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
					$args['modified_after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				}
			}

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

		$args['paginate'] = true;

		// Get the webhooks.
		$data_store = WC_Data_Store::load( 'webhook' );
		$results    = $data_store->search_webhooks( $args );

		// Get total items.
		$headers              = new stdClass;
		$headers->page        = $page;
		$headers->total       = $results->total;
		$headers->is_single   = $args['limit'] > $headers->total;
		$headers->total_pages = $results->max_num_pages;

		return array(
			'results' => $results->webhooks,
			'headers' => $headers,
		);
	}

	/**
	 * Get deliveries for a webhook
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_webhook_deliveries( $webhook_id, $fields = null ) {

		// Ensure ID is valid webhook ID
		$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

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

		return array( 'webhook_deliveries' => array() );
	}

	/**
	 * Get the delivery log for the given webhook ID and delivery ID
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string $id delivery log ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
		try {
			// Validate webhook ID
			$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

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

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
			}

			$webhook = new WC_Webhook( $webhook_id );

			$log = 0;

			if ( ! $log ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
			}

			return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * 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 3.3.0
	 * @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 ) {
		$id = absint( $id );

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

		$webhook = wc_get_webhook( $id );

		if ( null === $webhook ) {
			return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
		}

		// Validate permissions.
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}
}
api/v3/interface-wc-api-handler.php000064400000001515151542631340013115 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       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 );

}
class-wc-legacy-api.php000064400000020514151542631340011010 0ustar00<?php
/**
 * WooCommerce Legacy API. Was deprecated with 2.6.0.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.6
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy API.
 */
class WC_Legacy_API {

	/**
	 * This is the major version for the REST API and takes
	 * first-order position in endpoint URLs.
	 *
	 * @deprecated 2.6.0
	 * @var string
	 */
	const VERSION = '3.1.0';

	/**
	 * The REST API server.
	 *
	 * @deprecated 2.6.0
	 * @var WC_API_Server
	 */
	public $server;

	/**
	 * REST API authentication class instance.
	 *
	 * @deprecated 2.6.0
	 * @var WC_API_Authentication
	 */
	public $authentication;

	/**
	 * Init the legacy API.
	 */
	public function init() {
		add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 );
	}

	/**
	 * Add new query vars.
	 *
	 * @since 2.0
	 * @param array $vars Vars.
	 * @return string[]
	 */
	public function add_query_vars( $vars ) {
		$vars[] = 'wc-api-version'; // Deprecated since 2.6.0.
		$vars[] = 'wc-api-route'; // Deprecated since 2.6.0.
		return $vars;
	}

	/**
	 * Add new endpoints.
	 *
	 * @since 2.0
	 */
	public static function add_endpoint() {
		// REST API, deprecated since 2.6.0.
		add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' );
		add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' );
	}

	/**
	 * Handle REST API requests.
	 *
	 * @since 2.2
	 * @deprecated 2.6.0
	 */
	public function handle_rest_api_requests() {
		global $wp;

		if ( ! empty( $_GET['wc-api-version'] ) ) {
			$wp->query_vars['wc-api-version'] = $_GET['wc-api-version'];
		}

		if ( ! empty( $_GET['wc-api-route'] ) ) {
			$wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
		}

		// REST API request.
		if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) {

			wc_maybe_define_constant( 'WC_API_REQUEST', true );
			wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) );

			// Legacy v1 API request.
			if ( 1 === WC_API_REQUEST_VERSION ) {
				$this->handle_v1_rest_api_request();
			} elseif ( 2 === WC_API_REQUEST_VERSION ) {
				$this->handle_v2_rest_api_request();
			} else {
				$this->includes();

				$this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] );

				// load API resource classes.
				$this->register_resources( $this->server );

				// Fire off the request.
				$this->server->serve_request();
			}

			exit;
		}
	}

	/**
	 * Include required files for REST API request.
	 *
	 * @since 2.1
	 * @deprecated 2.6.0
	 */
	public function includes() {

		// API server / response handlers.
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' );

		// Authentication.
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' );

		// Allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );
	}

	/**
	 * Register available API resources.
	 *
	 * @since 2.1
	 * @deprecated 2.6.0
	 * @param WC_API_Server $server the REST server.
	 */
	public function register_resources( $server ) {

		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Coupons',
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Reports',
				'WC_API_Taxes',
				'WC_API_Webhooks',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $server );
		}
	}


	/**
	 * Handle legacy v1 REST API requests.
	 *
	 * @since 2.2
	 * @deprecated 2.6.0
	 */
	private function handle_v1_rest_api_request() {

		// Include legacy required files for v1 REST API request.
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' );

		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' );

		// Allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );

		$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );

		// Register available resources for legacy v1 REST API request.
		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Coupons',
				'WC_API_Reports',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $this->server );
		}

		// Fire off the request.
		$this->server->serve_request();
	}

	/**
	 * Handle legacy v2 REST API requests.
	 *
	 * @since 2.4
	 * @deprecated 2.6.0
	 */
	private function handle_v2_rest_api_request() {
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' );

		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' );

		// allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );

		$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );

		// Register available resources for legacy v2 REST API request.
		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Coupons',
				'WC_API_Reports',
				'WC_API_Webhooks',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $this->server );
		}

		// Fire off the request.
		$this->server->serve_request();
	}

	/**
	 * Rest API Init.
	 *
	 * @deprecated 3.7.0 - REST API classes autoload.
	 */
	public function rest_api_init() {}

	/**
	 * Include REST API classes.
	 *
	 * @deprecated 3.7.0 - REST API classes autoload.
	 */
	public function rest_api_includes() {
		$this->rest_api_init();
	}
	/**
	 * Register REST API routes.
	 *
	 * @deprecated 3.7.0
	 */
	public function register_rest_routes() {
		wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' );
		$this->register_wp_admin_settings();
	}
}
class-wc-legacy-cart.php000064400000030613151542631340011171 0ustar00<?php
/**
 * Legacy cart
 *
 * Legacy and deprecated functions are here to keep the WC_Cart class clean.
 * This class will be removed in future versions.
 *
 * @version  3.2.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   Automattic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy cart class.
 */
#[AllowDynamicProperties]
abstract class WC_Legacy_Cart {

	/**
	 * Array of defaults. Not used since 3.2.
	 *
	 * @deprecated 3.2.0
	 */
	public $cart_session_data = array(
		'cart_contents_total'         => 0,
		'total'                       => 0,
		'subtotal'                    => 0,
		'subtotal_ex_tax'             => 0,
		'tax_total'                   => 0,
		'taxes'                       => array(),
		'shipping_taxes'              => array(),
		'discount_cart'               => 0,
		'discount_cart_tax'           => 0,
		'shipping_total'              => 0,
		'shipping_tax_total'          => 0,
		'coupon_discount_amounts'     => array(),
		'coupon_discount_tax_amounts' => array(),
		'fee_total'                   => 0,
		'fees'                        => array(),
	);

	/**
	 * Contains an array of coupon usage counts after they have been applied.
	 *
	 * @deprecated 3.2.0
	 * @var array
	 */
	public $coupon_applied_count = array();

	/**
	 * Map legacy variables.
	 *
	 * @param string $name Property name.
	 * @param mixed  $value Value to set.
	 */
	public function __isset( $name ) {
		$legacy_keys = array_merge(
			array(
				'dp',
				'prices_include_tax',
				'round_at_subtotal',
				'cart_contents_total',
				'total',
				'subtotal',
				'subtotal_ex_tax',
				'tax_total',
				'fee_total',
				'discount_cart',
				'discount_cart_tax',
				'shipping_total',
				'shipping_tax_total',
				'display_totals_ex_tax',
				'display_cart_ex_tax',
				'cart_contents_weight',
				'cart_contents_count',
				'coupons',
				'taxes',
				'shipping_taxes',
				'coupon_discount_amounts',
				'coupon_discount_tax_amounts',
				'fees',
				'tax',
				'discount_total',
				'tax_display_cart',
			),
			is_array( $this->cart_session_data ) ? array_keys( $this->cart_session_data ) : array()
		);

		if ( in_array( $name, $legacy_keys, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Magic getters.
	 *
	 * If you add/remove cases here please update $legacy_keys in __isset accordingly.
	 *
	 * @param string $name Property name.
	 * @return mixed
	 */
	public function &__get( $name ) {
		$value = '';

		switch ( $name ) {
			case 'dp' :
				$value = wc_get_price_decimals();
				break;
			case 'prices_include_tax' :
				$value = wc_prices_include_tax();
				break;
			case 'round_at_subtotal' :
				$value = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
				break;
			case 'cart_contents_total' :
				$value = $this->get_cart_contents_total();
				break;
			case 'total' :
				$value = $this->get_total( 'edit' );
				break;
			case 'subtotal' :
				$value = $this->get_subtotal() + $this->get_subtotal_tax();
				break;
			case 'subtotal_ex_tax' :
				$value = $this->get_subtotal();
				break;
			case 'tax_total' :
				$value = $this->get_fee_tax() + $this->get_cart_contents_tax();
				break;
			case 'fee_total' :
				$value = $this->get_fee_total();
				break;
			case 'discount_cart' :
				$value = $this->get_discount_total();
				break;
			case 'discount_cart_tax' :
				$value = $this->get_discount_tax();
				break;
			case 'shipping_total' :
				$value = $this->get_shipping_total();
				break;
			case 'shipping_tax_total' :
				$value = $this->get_shipping_tax();
				break;
			case 'display_totals_ex_tax' :
			case 'display_cart_ex_tax' :
				$value = ! $this->display_prices_including_tax();
				break;
			case 'cart_contents_weight' :
				$value = $this->get_cart_contents_weight();
				break;
			case 'cart_contents_count' :
				$value = $this->get_cart_contents_count();
				break;
			case 'coupons' :
				$value = $this->get_coupons();
				break;

			// Arrays returned by reference to allow modification without notices. TODO: Remove in 4.0.
			case 'taxes' :
				wc_deprecated_function( 'WC_Cart->taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_cart_contents_taxes()', 'WC_Cart::set_cart_contents_taxes()' ) );
				$value = &$this->totals[ 'cart_contents_taxes' ];
				break;
			case 'shipping_taxes' :
				wc_deprecated_function( 'WC_Cart->shipping_taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_shipping_taxes()', 'WC_Cart::set_shipping_taxes()' ) );
				$value = &$this->totals[ 'shipping_taxes' ];
				break;
			case 'coupon_discount_amounts' :
				$value = &$this->coupon_discount_totals;
				break;
			case 'coupon_discount_tax_amounts' :
				$value = &$this->coupon_discount_tax_totals;
				break;
			case 'fees' :
				wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::get_fees' ) );

				// Grab fees from the new API.
				$new_fees   = $this->fees_api()->get_fees();

				// Add new fees to the legacy prop so it can be adjusted via legacy property.
				$this->fees = $new_fees;

				// Return by reference.
				$value = &$this->fees;
				break;
			// Deprecated args. TODO: Remove in 4.0.
			case 'tax' :
				wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax directly' );
				$this->tax = new WC_Tax();
				$value = $this->tax;
				break;
			case 'discount_total':
				wc_deprecated_argument( 'WC_Cart->discount_total', '2.3', 'After tax coupons are no longer supported. For more information see: https://woocommerce.wordpress.com/2014/12/upcoming-coupon-changes-in-woocommerce-2-3/' );
				$value = 0;
				break;
			case 'tax_display_cart':
				wc_deprecated_argument( 'WC_Cart->tax_display_cart', '4.4', 'Use WC_Cart->get_tax_price_display_mode() instead.' );
				$value = $this->get_tax_price_display_mode();
				break;
		}
		return $value;
	}

	/**
	 * Map legacy variables to setters.
	 *
	 * @param string $name Property name.
	 * @param mixed  $value Value to set.
	 */
	public function __set( $name, $value ) {
		switch ( $name ) {
			case 'cart_contents_total' :
				$this->set_cart_contents_total( $value );
				break;
			case 'total' :
				$this->set_total( $value );
				break;
			case 'subtotal' :
				$this->set_subtotal( $value );
				break;
			case 'subtotal_ex_tax' :
				$this->set_subtotal( $value );
				break;
			case 'tax_total' :
				$this->set_cart_contents_tax( $value );
				$this->set_fee_tax( 0 );
				break;
			case 'taxes' :
				$this->set_cart_contents_taxes( $value );
				break;
			case 'shipping_taxes' :
				$this->set_shipping_taxes( $value );
				break;
			case 'fee_total' :
				$this->set_fee_total( $value );
				break;
			case 'discount_cart' :
				$this->set_discount_total( $value );
				break;
			case 'discount_cart_tax' :
				$this->set_discount_tax( $value );
				break;
			case 'shipping_total' :
				$this->set_shipping_total( $value );
				break;
			case 'shipping_tax_total' :
				$this->set_shipping_tax( $value );
				break;
			case 'coupon_discount_amounts' :
				$this->set_coupon_discount_totals( $value );
				break;
			case 'coupon_discount_tax_amounts' :
				$this->set_coupon_discount_tax_totals( $value );
				break;
			case 'fees' :
				wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::add_fee' ) );
				$this->fees = $value;
				break;
			default :
				$this->$name = $value;
				break;
		}
	}

	/**
	 * Methods moved to session class in 3.2.0.
	 */
	public function get_cart_from_session() { $this->session->get_cart_from_session(); }
	public function maybe_set_cart_cookies() { $this->session->maybe_set_cart_cookies(); }
	public function set_session() { $this->session->set_session(); }
	public function get_cart_for_session() { return $this->session->get_cart_for_session(); }
	public function persistent_cart_update() { $this->session->persistent_cart_update(); }
	public function persistent_cart_destroy() { $this->session->persistent_cart_destroy(); }

	/**
	 * Get the total of all cart discounts.
	 *
	 * @return float
	 */
	public function get_cart_discount_total() {
		return $this->get_discount_total();
	}

	/**
	 * Get the total of all cart tax discounts (used for discounts on tax inclusive prices).
	 *
	 * @return float
	 */
	public function get_cart_discount_tax_total() {
		return $this->get_discount_tax();
	}

	/**
	 * Renamed for consistency.
	 *
	 * @param string $coupon_code
	 * @return bool	True if the coupon is applied, false if it does not exist or cannot be applied.
	 */
	public function add_discount( $coupon_code ) {
		return $this->apply_coupon( $coupon_code );
	}
	/**
	 * Remove taxes.
	 *
	 * @deprecated 3.2.0 Taxes are never calculated if customer is tax except making this function unused.
	 */
	public function remove_taxes() {
		wc_deprecated_function( 'WC_Cart::remove_taxes', '3.2', '' );
	}
	/**
	 * Init.
	 *
	 * @deprecated 3.2.0 Session is loaded via hooks rather than directly.
	 */
	public function init() {
		wc_deprecated_function( 'WC_Cart::init', '3.2', '' );
		$this->get_cart_from_session();
	}

	/**
	 * Function to apply discounts to a product and get the discounted price (before tax is applied).
	 *
	 * @deprecated 3.2.0 Calculation and coupon logic is handled in WC_Cart_Totals.
	 * @param mixed $values Cart item.
	 * @param mixed $price Price of item.
	 * @param bool  $add_totals Legacy.
	 * @return float price
	 */
	public function get_discounted_price( $values, $price, $add_totals = false ) {
		wc_deprecated_function( 'WC_Cart::get_discounted_price', '3.2', '' );

		$cart_item_key = $values['key'];
		$cart_item     = $this->cart_contents[ $cart_item_key ];

		return $cart_item['line_total'];
	}

	/**
	 * Gets the url to the cart page.
	 *
	 * @deprecated 2.5.0 in favor to wc_get_cart_url()
	 * @return string url to page
	 */
	public function get_cart_url() {
		wc_deprecated_function( 'WC_Cart::get_cart_url', '2.5', 'wc_get_cart_url' );
		return wc_get_cart_url();
	}

	/**
	 * Gets the url to the checkout page.
	 *
	 * @deprecated 2.5.0 in favor to wc_get_checkout_url()
	 * @return string url to page
	 */
	public function get_checkout_url() {
		wc_deprecated_function( 'WC_Cart::get_checkout_url', '2.5', 'wc_get_checkout_url' );
		return wc_get_checkout_url();
	}

	/**
	 * Sees if we need a shipping address.
	 *
	 * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only()
	 * @return bool
	 */
	public function ship_to_billing_address_only() {
		wc_deprecated_function( 'WC_Cart::ship_to_billing_address_only', '2.5', 'wc_ship_to_billing_address_only' );
		return wc_ship_to_billing_address_only();
	}

	/**
	 * Coupons enabled function. Filterable.
	 *
	 * @deprecated 2.5.0
	 * @return bool
	 */
	public function coupons_enabled() {
		wc_deprecated_function( 'WC_Legacy_Cart::coupons_enabled', '2.5.0', 'wc_coupons_enabled' );
		return wc_coupons_enabled();
	}

	/**
	 * Gets the total (product) discount amount - these are applied before tax.
	 *
	 * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
	 * @return mixed formatted price or false if there are none.
	 */
	public function get_discounts_before_tax() {
		wc_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' );
		if ( $this->get_cart_discount_total() ) {
			$discounts_before_tax = wc_price( $this->get_cart_discount_total() );
		} else {
			$discounts_before_tax = false;
		}
		return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this );
	}

	/**
	 * Get the total of all order discounts (after tax discounts).
	 *
	 * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3.
	 * @return int
	 */
	public function get_order_discount_total() {
		wc_deprecated_function( 'get_order_discount_total', '2.3' );
		return 0;
	}

	/**
	 * Function to apply cart discounts after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 * @param $values
	 * @param $price
	 */
	public function apply_cart_discounts_after_tax( $values, $price ) {
		wc_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' );
	}

	/**
	 * Function to apply product discounts after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 *
	 * @param $values
	 * @param $price
	 */
	public function apply_product_discounts_after_tax( $values, $price ) {
		wc_deprecated_function( 'apply_product_discounts_after_tax', '2.3' );
	}

	/**
	 * Gets the order discount amount - these are applied after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 */
	public function get_discounts_after_tax() {
		wc_deprecated_function( 'get_discounts_after_tax', '2.3' );
	}
}
class-wc-legacy-coupon.php000064400000011631151542631340011542 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Coupon.
 *
 * Legacy and deprecated functions are here to keep the WC_Legacy_Coupon class clean.
 * This class will be removed in future versions.
 *
 * @class       WC_Legacy_Coupon
 * @version     3.0.0
 * @package     WooCommerce\Classes
 * @category    Class
 * @author      WooThemes
 */
abstract class WC_Legacy_Coupon extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 * @param  string $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'exists',
			'coupon_custom_fields',
			'type',
			'discount_type',
			'amount',
			'coupon_amount',
			'code',
			'individual_use',
			'product_ids',
			'exclude_product_ids',
			'usage_limit',
			'usage_limit_per_user',
			'limit_usage_to_x_items',
			'usage_count',
			'expiry_date',
			'product_categories',
			'exclude_product_categories',
			'minimum_amount',
			'maximum_amount',
			'customer_email',
		);
		if ( in_array( $key, $legacy_keys ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 * @param  string $key
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Coupon properties should not be accessed directly.', '3.0' );

		switch ( $key ) {
			case 'id' :
				$value = $this->get_id();
			break;
			case 'exists' :
				$value = $this->get_id() > 0;
			break;
			case 'coupon_custom_fields' :
				$legacy_custom_fields = array();
				$custom_fields = $this->get_id() ? $this->get_meta_data() : array();
				if ( ! empty( $custom_fields ) ) {
					foreach ( $custom_fields as  $cf_value ) {
						// legacy only supports 1 key
						$legacy_custom_fields[ $cf_value->key ][0] = $cf_value->value;
					}
				}
				$value = $legacy_custom_fields;
			break;
			case 'type' :
			case 'discount_type' :
				$value = $this->get_discount_type();
			break;
			case 'amount' :
			case 'coupon_amount' :
				$value = $this->get_amount();
			break;
			case 'code' :
				$value = $this->get_code();
			break;
			case 'individual_use' :
				$value = ( true === $this->get_individual_use() ) ? 'yes' : 'no';
			break;
			case 'product_ids' :
				$value = $this->get_product_ids();
			break;
			case 'exclude_product_ids' :
				$value = $this->get_excluded_product_ids();
			break;
			case 'usage_limit' :
				$value = $this->get_usage_limit();
			break;
			case 'usage_limit_per_user' :
				$value = $this->get_usage_limit_per_user();
			break;
			case 'limit_usage_to_x_items' :
				$value = $this->get_limit_usage_to_x_items();
			break;
			case 'usage_count' :
				$value = $this->get_usage_count();
			break;
			case 'expiry_date' :
				$value = ( $this->get_date_expires() ? $this->get_date_expires()->date( 'Y-m-d' ) : '' );
			break;
			case 'product_categories' :
				$value = $this->get_product_categories();
			break;
			case 'exclude_product_categories' :
				$value = $this->get_excluded_product_categories();
			break;
			case 'minimum_amount' :
				$value = $this->get_minimum_amount();
			break;
			case 'maximum_amount' :
				$value = $this->get_maximum_amount();
			break;
			case 'customer_email' :
				$value = $this->get_email_restrictions();
			break;
			default :
				$value = '';
			break;
		}

		return $value;
	}

	/**
	 * Format loaded data as array.
	 * @param  string|array $array
	 * @return array
	 */
	public function format_array( $array ) {
		wc_deprecated_function( 'WC_Coupon::format_array', '3.0' );
		if ( ! is_array( $array ) ) {
			if ( is_serialized( $array ) ) {
				$array = maybe_unserialize( $array );
			} else {
				$array = explode( ',', $array );
			}
		}
		return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) );
	}


	/**
	 * Check if coupon needs applying before tax.
	 *
	 * @return bool
	 */
	public function apply_before_tax() {
		wc_deprecated_function( 'WC_Coupon::apply_before_tax', '3.0' );
		return true;
	}

	/**
	 * Check if a coupon enables free shipping.
	 *
	 * @return bool
	 */
	public function enable_free_shipping() {
		wc_deprecated_function( 'WC_Coupon::enable_free_shipping', '3.0', 'WC_Coupon::get_free_shipping' );
		return $this->get_free_shipping();
	}

	/**
	 * Check if a coupon excludes sale items.
	 *
	 * @return bool
	 */
	public function exclude_sale_items() {
		wc_deprecated_function( 'WC_Coupon::exclude_sale_items', '3.0', 'WC_Coupon::get_exclude_sale_items' );
		return $this->get_exclude_sale_items();
	}

	/**
	 * Increase usage count for current coupon.
	 *
	 * @param string $used_by Either user ID or billing email
	 */
	public function inc_usage_count( $used_by = '' ) {
		$this->increase_usage_count( $used_by );
	}

	/**
	 * Decrease usage count for current coupon.
	 *
	 * @param string $used_by Either user ID or billing email
	 */
	public function dcr_usage_count( $used_by = '' ) {
		$this->decrease_usage_count( $used_by );
	}
}
class-wc-legacy-customer.php000064400000015663151542631340012111 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Customer.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooThemes
 */
abstract class WC_Legacy_Customer extends WC_Data {

	/**
	 * __isset legacy.
	 * @param mixed $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'country',
			'state',
			'postcode',
			'city',
			'address_1',
			'address',
			'address_2',
			'shipping_country',
			'shipping_state',
			'shipping_postcode',
			'shipping_city',
			'shipping_address_1',
			'shipping_address',
			'shipping_address_2',
			'is_vat_exempt',
			'calculated_shipping',
		);
		$key = $this->filter_legacy_key( $key );
		return in_array( $key, $legacy_keys );
	}

	/**
	 * __get function.
	 * @param string $key
	 * @return string
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Customer properties should not be accessed directly.', '3.0' );
		$key = $this->filter_legacy_key( $key );
		if ( in_array( $key, array( 'country', 'state', 'postcode', 'city', 'address_1', 'address', 'address_2' ) ) ) {
			$key = 'billing_' . $key;
		}
		return is_callable( array( $this, "get_{$key}" ) ) ? $this->{"get_{$key}"}() : '';
	}

	/**
	 * __set function.
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public function __set( $key, $value ) {
		wc_doing_it_wrong( $key, 'Customer properties should not be set directly.', '3.0' );
		$key = $this->filter_legacy_key( $key );

		if ( is_callable( array( $this, "set_{$key}" ) ) ) {
			$this->{"set_{$key}"}( $value );
		}
	}

	/**
	 * Address and shipping_address are aliased, so we want to get the 'real' key name.
	 * For all other keys, we can just return it.
	 * @since 3.0.0
	 * @param  string $key
	 * @return string
	 */
	private function filter_legacy_key( $key ) {
		if ( 'address' === $key ) {
			$key = 'address_1';
		}
		if ( 'shipping_address' === $key ) {
			$key = 'shipping_address_1';
		}

		return $key;
	}

	/**
	 * Sets session data for the location.
	 *
	 * @param string $country
	 * @param string $state
	 * @param string $postcode (default: '')
	 * @param string $city (default: '')
	 */
	public function set_location( $country, $state, $postcode = '', $city = '' ) {
		$this->set_billing_location( $country, $state, $postcode, $city );
		$this->set_shipping_location( $country, $state, $postcode, $city );
	}

	/**
	 * Get default country for a customer.
	 * @return string
	 */
	public function get_default_country() {
		wc_deprecated_function( 'WC_Customer::get_default_country', '3.0', 'wc_get_customer_default_location' );
		$default = wc_get_customer_default_location();
		return $default['country'];
	}

	/**
	 * Get default state for a customer.
	 * @return string
	 */
	public function get_default_state() {
		wc_deprecated_function( 'WC_Customer::get_default_state', '3.0', 'wc_get_customer_default_location' );
		$default = wc_get_customer_default_location();
		return $default['state'];
	}

	/**
	 * Set customer address to match shop base address.
	 */
	public function set_to_base() {
		wc_deprecated_function( 'WC_Customer::set_to_base', '3.0', 'WC_Customer::set_billing_address_to_base' );
		$this->set_billing_address_to_base();
	}

	/**
	 * Set customer shipping address to base address.
	 */
	public function set_shipping_to_base() {
		wc_deprecated_function( 'WC_Customer::set_shipping_to_base', '3.0', 'WC_Customer::set_shipping_address_to_base' );
		$this->set_shipping_address_to_base();
	}

	/**
	 * Calculated shipping.
	 * @param boolean $calculated
	 */
	public function calculated_shipping( $calculated = true ) {
		wc_deprecated_function( 'WC_Customer::calculated_shipping', '3.0', 'WC_Customer::set_calculated_shipping' );
		$this->set_calculated_shipping( $calculated );
	}

	/**
	 * Set default data for a customer.
	 */
	public function set_default_data() {
		wc_deprecated_function( 'WC_Customer::set_default_data', '3.0' );
	}

	/**
	 * Save data function.
	 */
	public function save_data() {
		$this->save();
	}

	/**
	 * Is the user a paying customer?
	 *
	 * @param int $user_id
	 *
	 * @return bool
	 */
	function is_paying_customer( $user_id = '' ) {
		wc_deprecated_function( 'WC_Customer::is_paying_customer', '3.0', 'WC_Customer::get_is_paying_customer' );
		if ( ! empty( $user_id ) ) {
			$user_id = get_current_user_id();
		}
		return '1' === get_user_meta( $user_id, 'paying_customer', true );
	}

	/**
	 * Legacy get address.
	 */
	function get_address() {
		wc_deprecated_function( 'WC_Customer::get_address', '3.0', 'WC_Customer::get_billing_address_1' );
		return $this->get_billing_address_1();
	}

	/**
	 * Legacy get address 2.
	 */
	function get_address_2() {
		wc_deprecated_function( 'WC_Customer::get_address_2', '3.0', 'WC_Customer::get_billing_address_2' );
		return $this->get_billing_address_2();
	}

	/**
	 * Legacy get country.
	 */
	function get_country() {
		wc_deprecated_function( 'WC_Customer::get_country', '3.0', 'WC_Customer::get_billing_country' );
		return $this->get_billing_country();
	}

	/**
	 * Legacy get state.
	 */
	function get_state() {
		wc_deprecated_function( 'WC_Customer::get_state', '3.0', 'WC_Customer::get_billing_state' );
		return $this->get_billing_state();
	}

	/**
	 * Legacy get postcode.
	 */
	function get_postcode() {
		wc_deprecated_function( 'WC_Customer::get_postcode', '3.0', 'WC_Customer::get_billing_postcode' );
		return $this->get_billing_postcode();
	}

	/**
	 * Legacy get city.
	 */
	function get_city() {
		wc_deprecated_function( 'WC_Customer::get_city', '3.0', 'WC_Customer::get_billing_city' );
		return $this->get_billing_city();
	}

	/**
	 * Legacy set country.
	 *
	 * @param string $country
	 */
	function set_country( $country ) {
		wc_deprecated_function( 'WC_Customer::set_country', '3.0', 'WC_Customer::set_billing_country' );
		$this->set_billing_country( $country );
	}

	/**
	 * Legacy set state.
	 *
	 * @param string $state
	 */
	function set_state( $state ) {
		wc_deprecated_function( 'WC_Customer::set_state', '3.0', 'WC_Customer::set_billing_state' );
		$this->set_billing_state( $state );
	}

	/**
	 * Legacy set postcode.
	 *
	 * @param string $postcode
	 */
	function set_postcode( $postcode ) {
		wc_deprecated_function( 'WC_Customer::set_postcode', '3.0', 'WC_Customer::set_billing_postcode' );
		$this->set_billing_postcode( $postcode );
	}

	/**
	 * Legacy set city.
	 *
	 * @param string $city
	 */
	function set_city( $city ) {
		wc_deprecated_function( 'WC_Customer::set_city', '3.0', 'WC_Customer::set_billing_city' );
		$this->set_billing_city( $city );
	}

	/**
	 * Legacy set address.
	 *
	 * @param string $address
	 */
	function set_address( $address ) {
		wc_deprecated_function( 'WC_Customer::set_address', '3.0', 'WC_Customer::set_billing_address' );
		$this->set_billing_address( $address );
	}

	/**
	 * Legacy set address.
	 *
	 * @param string $address
	 */
	function set_address_2( $address ) {
		wc_deprecated_function( 'WC_Customer::set_address_2', '3.0', 'WC_Customer::set_billing_address_2' );
		$this->set_billing_address_2( $address );
	}
}
class-wc-legacy-shipping-zone.php000064400000003107151542631340013030 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Shipping Zone.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooThemes
 */
abstract class WC_Legacy_Shipping_Zone extends WC_Data {

	/**
	 * Get zone ID
	 * @return int|null Null if the zone does not exist. 0 is the default zone.
	 * @deprecated 3.0
	 */
	public function get_zone_id() {
		wc_deprecated_function( 'WC_Shipping_Zone::get_zone_id', '3.0', 'WC_Shipping_Zone::get_id' );
		return $this->get_id();
	}

	/**
	 * Read a shipping zone by ID.
	 * @deprecated 3.0.0 - Init a shipping zone with an ID.
	 *
	 * @param int $zone_id
	 */
	public function read( $zone_id ) {
		wc_deprecated_function( 'WC_Shipping_Zone::read', '3.0', 'a shipping zone initialized with an ID.' );
		$this->set_id( $zone_id );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		$data_store->read( $this );
	}

	/**
	 * Update a zone.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function update() {
		wc_deprecated_function( 'WC_Shipping_Zone::update', '3.0', 'WC_Shipping_Zone::save instead.' );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		try {
			$data_store->update( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

	/**
	 * Create a zone.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function create() {
		wc_deprecated_function( 'WC_Shipping_Zone::create', '3.0', 'WC_Shipping_Zone::save instead.' );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		try {
			$data_store->create( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}


}
class-wc-legacy-webhook.php000064400000005010151542631340011667 0ustar00<?php
/**
 * Legacy Webhook
 *
 * Legacy and deprecated functions are here to keep the WC_Legacy_Webhook class clean.
 * This class will be removed in future versions.
 *
 * @version  3.2.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   Automattic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Webhook class.
 */
abstract class WC_Legacy_Webhook extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 *
	 * @param  string $key Item to check.
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'status',
			'post_data',
			'delivery_url',
			'secret',
			'topic',
			'hooks',
			'resource',
			'event',
			'failure_count',
			'api_version',
		);

		if ( in_array( $key, $legacy_keys, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 *
	 * @param  string $key Item to get.
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Webhook properties should not be accessed directly.', '3.2' );

		switch ( $key ) {
			case 'id' :
				$value = $this->get_id();
				break;
			case 'status' :
				$value = $this->get_status();
				break;
			case 'post_data' :
				$value = null;
				break;
			case 'delivery_url' :
				$value = $this->get_delivery_url();
				break;
			case 'secret' :
				$value = $this->get_secret();
				break;
			case 'topic' :
				$value = $this->get_topic();
				break;
			case 'hooks' :
				$value = $this->get_hooks();
				break;
			case 'resource' :
				$value = $this->get_resource();
				break;
			case 'event' :
				$value = $this->get_event();
				break;
			case 'failure_count' :
				$value = $this->get_failure_count();
				break;
			case 'api_version' :
				$value = $this->get_api_version();
				break;

			default :
				$value = '';
				break;
		} // End switch().

		return $value;
	}

	/**
	 * Get the post data for the webhook.
	 *
	 * @deprecated 3.2.0
	 * @since      2.2
	 * @return     null|WP_Post
	 */
	public function get_post_data() {
		wc_deprecated_function( 'WC_Webhook::get_post_data', '3.2' );

		return null;
	}

	/**
	 * Update the webhook status.
	 *
	 * @deprecated 3.2.0
	 * @since      2.2.0
	 * @param      string $status Status to set.
	 */
	public function update_status( $status ) {
		wc_deprecated_function( 'WC_Webhook::update_status', '3.2', 'WC_Webhook::set_status' );

		$this->set_status( $status );
		$this->save();
	}
}
class-jetpack-ixr-client.php000064400000011534151547223160012067 0ustar00<?php
/**
 * IXR_Client
 *
 * @package automattic/jetpack-connection
 *
 * @since 1.7.0
 * @since-jetpack 1.5
 * @since-jetpack 7.7 Moved to the jetpack-connection package.
 */

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;

if ( ! class_exists( IXR_Client::class ) ) {
	require_once ABSPATH . WPINC . '/class-IXR.php';
}

/**
 * A Jetpack implementation of the WordPress core IXR client.
 */
class Jetpack_IXR_Client extends IXR_Client {
	/**
	 * Jetpack args, used for the remote requests.
	 *
	 * @var array
	 */
	public $jetpack_args = null;

	/**
	 * Remote Response Headers.
	 *
	 * @var array
	 */
	public $response_headers = null;

	/**
	 * Holds the raw remote response from the latest call to query().
	 *
	 * @var null|array|WP_Error
	 */
	public $last_response = null;

	/**
	 * Constructor.
	 * Initialize a new Jetpack IXR client instance.
	 *
	 * @param array       $args    Jetpack args, used for the remote requests.
	 * @param string|bool $path    Path to perform the reuqest to.
	 * @param int         $port    Port number.
	 * @param int         $timeout The connection timeout, in seconds.
	 */
	public function __construct( $args = array(), $path = false, $port = 80, $timeout = 15 ) {
		$connection = new Manager();

		$defaults = array(
			'url'     => $connection->xmlrpc_api_url(),
			'user_id' => 0,
			'headers' => array(),
		);

		$args            = wp_parse_args( $args, $defaults );
		$args['headers'] = array_merge( array( 'Content-Type' => 'text/xml' ), (array) $args['headers'] );

		$this->jetpack_args = $args;

		$this->IXR_Client( $args['url'], $path, $port, $timeout );
	}

	/**
	 * Perform the IXR request.
	 *
	 * @param string[] ...$args IXR args.
	 *
	 * @return bool True if request succeeded, false otherwise.
	 */
	public function query( ...$args ) {
		$method  = array_shift( $args );
		$request = new IXR_Request( $method, $args );
		$xml     = trim( $request->getXml() );

		$response = Client::remote_request( $this->jetpack_args, $xml );

		// Store response headers.
		$this->response_headers = wp_remote_retrieve_headers( $response );

		$this->last_response = $response;
		if ( is_array( $this->last_response ) && isset( $this->last_response['http_response'] ) ) {
			// If the expected array response is received, format the data as plain arrays.
			$this->last_response            = $this->last_response['http_response']->to_array();
			$this->last_response['headers'] = $this->last_response['headers']->getAll();
		}

		if ( is_wp_error( $response ) ) {
			$this->error = new IXR_Error( -10520, sprintf( 'Jetpack: [%s] %s', $response->get_error_code(), $response->get_error_message() ) );
			return false;
		}

		if ( ! $response ) {
			$this->error = new IXR_Error( -10520, 'Jetpack: Unknown Error' );
			return false;
		}

		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
			$this->error = new IXR_Error( -32300, 'transport error - HTTP status code was not 200' );
			return false;
		}

		$content = wp_remote_retrieve_body( $response );

		// Now parse what we've got back.
		$this->message = new IXR_Message( $content );
		if ( ! $this->message->parse() ) {
			// XML error.
			$this->error = new IXR_Error( -32700, 'parse error. not well formed' );
			return false;
		}

		// Is the message a fault?
		if ( 'fault' === $this->message->messageType ) {
			$this->error = new IXR_Error( $this->message->faultCode, $this->message->faultString );
			return false;
		}

		// Message must be OK.
		return true;
	}

	/**
	 * Retrieve the Jetpack error from the result of the last request.
	 *
	 * @param int    $fault_code   Fault code.
	 * @param string $fault_string Fault string.
	 * @return WP_Error Error object.
	 */
	public function get_jetpack_error( $fault_code = null, $fault_string = null ) {
		if ( $fault_code === null ) {
			$fault_code = $this->error->code;
		}

		if ( $fault_string === null ) {
			$fault_string = $this->error->message;
		}

		if ( preg_match( '#jetpack:\s+\[(\w+)\]\s*(.*)?$#i', $fault_string, $match ) ) {
			$code    = $match[1];
			$message = $match[2];
			$status  = $fault_code;
			return new \WP_Error( $code, $message, $status );
		}

		return new \WP_Error( "IXR_{$fault_code}", $fault_string );
	}

	/**
	 * Retrieve a response header if set.
	 *
	 * @param  string $name  header name.
	 * @return string|bool Header value if set, false if not set.
	 */
	public function get_response_header( $name ) {
		if ( isset( $this->response_headers[ $name ] ) ) {
			return $this->response_headers[ $name ];
		}
		// case-insensitive header names: http://www.ietf.org/rfc/rfc2616.txt.
		if ( isset( $this->response_headers[ strtolower( $name ) ] ) ) {
			return $this->response_headers[ strtolower( $name ) ];
		}
		return false;
	}

	/**
	 * Retrieve the raw response for the last query() call.
	 *
	 * @return null|array|WP_Error
	 */
	public function get_last_response() {
		return $this->last_response;
	}
}
class-jetpack-ixr-clientmulticall.php000064400000003405151547223160013774 0ustar00<?php
/**
 * IXR_ClientMulticall
 *
 * @package automattic/jetpack-connection
 *
 * @since 1.7.0
 * @since-jetpack 1.5
 * @since-jetpack 7.7 Moved to the jetpack-connection package.
 */

/**
 * A Jetpack implementation of the WordPress core IXR client, capable of multiple calls in a single request.
 */
class Jetpack_IXR_ClientMulticall extends Jetpack_IXR_Client {
	/**
	 * Storage for the IXR calls.
	 *
	 * @var array
	 */
	public $calls = array();

	/**
	 * Add a IXR call to the client.
	 * First argument is the method name.
	 * The rest of the arguments are the params specified to the method.
	 *
	 * @param string[] ...$args IXR args.
	 */
	public function addCall( ...$args ) {
		$method_name   = array_shift( $args );
		$struct        = array(
			'methodName' => $method_name,
			'params'     => $args,
		);
		$this->calls[] = $struct;
	}

	/**
	 * Perform the IXR multicall request.
	 *
	 * @param string[] ...$args IXR args.
	 *
	 * @return bool True if request succeeded, false otherwise.
	 */
	public function query( ...$args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$this->calls = $this->sort_calls( $this->calls );

		// Prepare multicall, then call the parent::query() method.
		return parent::query( 'system.multicall', $this->calls );
	}

	/**
	 * Sort the IXR calls.
	 * Make sure syncs are always done first preserving relative order.
	 *
	 * @param array $calls Calls to sort.
	 * @return array Sorted calls.
	 */
	public function sort_calls( $calls ) {
		$sync_calls  = array();
		$other_calls = array();

		foreach ( $calls as $call ) {
			if ( 'jetpack.syncContent' === $call['methodName'] ) {
				$sync_calls[] = $call;
			} else {
				$other_calls[] = $call;
			}
		}

		return array_merge( $sync_calls, $other_calls );
	}
}
class-jetpack-options.php000064400000064422151547223160011510 0ustar00<?php
/**
 * Legacy Jetpack_Options class.
 *
 * @package automattic/jetpack-connection
 */

use Automattic\Jetpack\Constants;

/**
 * Class Jetpack_Options
 */
class Jetpack_Options {

	/**
	 * An array that maps a grouped option type to an option name.
	 *
	 * @var array
	 */
	private static $grouped_options = array(
		'compact' => 'jetpack_options',
		'private' => 'jetpack_private_options',
	);

	/**
	 * Returns an array of option names for a given type.
	 *
	 * @param string $type The type of option to return. Defaults to 'compact'.
	 *
	 * @return array
	 */
	public static function get_option_names( $type = 'compact' ) {
		switch ( $type ) {
			case 'non-compact':
			case 'non_compact':
				return array(
					'activated',
					'active_modules',
					'active_modules_initialized', // (bool) used to determine that all the default modules were activated, so we know how to act on a reconnection.
					'allowed_xsite_search_ids', // (array) Array of WP.com blog ids that are allowed to search the content of this site
					'available_modules',
					'do_activate',
					'edit_links_calypso_redirect', // (bool) Whether post/page edit links on front end should point to Calypso.
					'log',
					'slideshow_background_color',
					'widget_twitter',
					'wpcc_options',
					'relatedposts',
					'file_data',
					'autoupdate_plugins',          // (array)  An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated
					'autoupdate_plugins_translations', // (array)  An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated translation files.
					'autoupdate_themes',           // (array)  An array of theme ids ( eg. twentyfourteen ) that should be autoupdated
					'autoupdate_themes_translations', // (array)  An array of theme ids ( eg. twentyfourteen ) that should autoupdated translation files.
					'autoupdate_core',             // (bool)   Whether or not to autoupdate core
					'autoupdate_translations',     // (bool)   Whether or not to autoupdate all translations
					'json_api_full_management',    // (bool)   Allow full management (eg. Activate, Upgrade plugins) of the site via the JSON API.
					'sync_non_public_post_stati',  // (bool)   Allow synchronisation of posts and pages with non-public status.
					'site_icon_url',               // (string) url to the full site icon
					'site_icon_id',                // (int)    Attachment id of the site icon file
					'dismissed_manage_banner',     // (bool) Dismiss Jetpack manage banner allows the user to dismiss the banner permanently
					'unique_connection',           // (array)  A flag to determine a unique connection to wordpress.com two values "connected" and "disconnected" with values for how many times each has occured
					'unique_registrations',        // (integer) A counter of how many times the site was registered
					'protect_whitelist',           // (array) IP Address for the Protect module to ignore
					'sync_error_idc',              // (bool|array) false or array containing the site's home and siteurl at time of IDC error
					'sync_health_status',          // (bool|array) An array of data relating to Jetpack's sync health.
					'safe_mode_confirmed',         // (bool) True if someone confirms that this site was correctly put into safe mode automatically after an identity crisis is discovered.
					'migrate_for_idc',             // (bool) True if someone confirms that this site should migrate stats and subscribers from its previous URL
					'dismissed_connection_banner', // (bool) True if the connection banner has been dismissed
					'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner.
					'onboarding',                  // (string) Auth token to be used in the onboarding connection flow
					'tos_agreed',                  // (bool)   Whether or not the TOS for connection has been agreed upon.
					'static_asset_cdn_files',      // (array) An nested array of files that we can swap out for cdn versions.
					'mapbox_api_key',              // (string) Mapbox API Key, for use with Map block.
					'mailchimp',                   // (string) Mailchimp keyring data, for mailchimp block.
					'xmlrpc_errors',               // (array) Keys are XML-RPC signature error codes. Values are truthy.
					'dismissed_wizard_banner',     // (int) (DEPRECATED) True if the Wizard banner has been dismissed.
				);

			case 'private':
				return array(
					'blog_token',  // (string) The Client Secret/Blog Token of this site.
					'user_token',  // (string) The User Token of this site. (deprecated)
					'user_tokens',  // (array)  User Tokens for each user of this site who has connected to jetpack.wordpress.com.
					'purchase_token', // (string) Token for logged out user purchases.
					'token_lock', // (string) Token lock in format `expiration_date|||site_url`.
				);

			case 'network':
				return array(
					'onboarding',                   // (string) Auth token to be used in the onboarding connection flow
					'file_data',                     // (array) List of absolute paths to all Jetpack modules
				);
		}

		return array(
			'id',                                  // (int)    The Client ID/WP.com Blog ID of this site.
			'publicize_connections',               // (array)  An array of Publicize connections from WordPress.com.
			'master_user',                         // (int)    The local User ID of the user who connected this site to jetpack.wordpress.com.
			'version',                             // (string) Used during upgrade procedure to auto-activate new modules. version:time.
			'old_version',                         // (string) Used to determine which modules are the most recently added. previous_version:time.
			'fallback_no_verify_ssl_certs',        // (int)    Flag for determining if this host must skip SSL Certificate verification due to misconfigured SSL.
			'time_diff',                           // (int)    Offset between Jetpack server's clocks and this server's clocks. Jetpack Server Time = time() + (int) Jetpack_Options::get_option( 'time_diff' )
			'public',                              // (int|bool) If we think this site is public or not (1, 0), false if we haven't yet tried to figure it out.
			'videopress',                          // (array)  VideoPress options array.
			'is_network_site',                     // (int|bool) If we think this site is a network or a single blog (1, 0), false if we haven't yet tried to figue it out.
			'social_links',                        // (array)  The specified links for each social networking site.
			'identity_crisis_whitelist',           // (array)  An array of options, each having an array of the values whitelisted for it.
			'gplus_authors',                       // (array)  The Google+ authorship information for connected users.
			'last_heartbeat',                      // (int)    The timestamp of the last heartbeat that fired.
			'hide_jitm',                           // (array)  A list of just in time messages that we should not show because they have been dismissed by the user.
			'custom_css_4.7_migration',            // (bool)   Whether Custom CSS has scanned for and migrated any legacy CSS CPT entries to the new Core format.
			'image_widget_migration',              // (bool)   Whether any legacy Image Widgets have been converted to the new Core widget.
			'gallery_widget_migration',            // (bool)   Whether any legacy Gallery Widgets have been converted to the new Core widget.
			'sso_first_login',                     // (bool)   Is this the first time the user logins via SSO.
			'dismissed_hints',                     // (array)  Part of Plugin Search Hints. List of cards that have been dismissed.
			'first_admin_view',                    // (bool)   Set to true the first time the user views the admin. Usually after the initial connection.
			'setup_wizard_questionnaire',          // (array)  (DEPRECATED) List of user choices from the setup wizard.
			'setup_wizard_status',                 // (string) (DEPRECATED) Status of the setup wizard.
			'licensing_error',                     // (string) Last error message occurred while attaching licenses that is yet to be surfaced to the user.
			'recommendations_banner_dismissed',    // (bool) Determines if the recommendations dashboard banner is dismissed or not.
			'recommendations_banner_enabled',      // (bool)   Whether the recommendations are enabled or not.
			'recommendations_data',                // (array)  The user choice and other data for the recommendations.
			'recommendations_step',                // (string) The current step of the recommendations.
			'recommendations_conditional',         // (array)  An array of action-based recommendations.
			'licensing_activation_notice_dismiss', // (array) The `last_detached_count` and the `last_dismissed_time` for the user-license activation notice.
			'has_seen_wc_connection_modal',        // (bool) Whether the site has displayed the WooCommerce Connection modal
			'partner_coupon',                      // (string) A Jetpack partner issued coupon to promote a sale together with Jetpack.
			'partner_coupon_added',                // (string) A date for when `partner_coupon` was added, so we can auto-purge after a certain time interval.
			'dismissed_backup_review_restore',     // (bool) Determines if the component review request is dismissed for successful restore requests.
			'dismissed_backup_review_backups',     // (bool) Determines if the component review request is dismissed for successful backup requests.
		);
	}

	/**
	 * Is the option name valid?
	 *
	 * @param string      $name  The name of the option.
	 * @param string|null $group The name of the group that the option is in. Default to null, which will search non_compact.
	 *
	 * @return bool Is the option name valid?
	 */
	public static function is_valid( $name, $group = null ) {
		if ( is_array( $name ) ) {
			$compact_names = array();
			foreach ( array_keys( self::$grouped_options ) as $_group ) {
				$compact_names = array_merge( $compact_names, self::get_option_names( $_group ) );
			}

			$result = array_diff( $name, self::get_option_names( 'non_compact' ), $compact_names );

			return empty( $result );
		}

		if ( $group === null || 'non_compact' === $group ) {
			if ( in_array( $name, self::get_option_names( $group ), true ) ) {
				return true;
			}
		}

		foreach ( array_keys( self::$grouped_options ) as $_group ) {
			if ( $group === null || $group === $_group ) {
				if ( in_array( $name, self::get_option_names( $_group ), true ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks if an option must be saved for the whole network in WP Multisite
	 *
	 * @param string $option_name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
	 *
	 * @return bool
	 */
	public static function is_network_option( $option_name ) {
		if ( ! is_multisite() ) {
			return false;
		}
		return in_array( $option_name, self::get_option_names( 'network' ), true );
	}

	/**
	 * Filters the requested option.
	 * This is a wrapper around `get_option_from_database` so that we can filter the option.
	 *
	 * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
	 * @param mixed  $default (optional).
	 *
	 * @return mixed
	 */
	public static function get_option( $name, $default = false ) {
		/**
		 * Filter Jetpack Options.
		 * Can be useful in environments when Jetpack is running with a different setup
		 *
		 * @since 1.7.0
		 *
		 * @param string $value The value from the database.
		 * @param string $name Option name, _without_ `jetpack_%` prefix.
		 * @return string $value, unless the filters modify it.
		 */
		return apply_filters( 'jetpack_options', self::get_option_from_database( $name, $default ), $name );
	}

	/**
	 * Returns the requested option.  Looks in jetpack_options or jetpack_$name as appropriate.
	 *
	 * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
	 * @param mixed  $default (optional).
	 *
	 * @return mixed
	 */
	private static function get_option_from_database( $name, $default = false ) {
		if ( self::is_valid( $name, 'non_compact' ) ) {
			if ( self::is_network_option( $name ) ) {
				return get_site_option( "jetpack_$name", $default );
			}

			return get_option( "jetpack_$name", $default );
		}

		foreach ( array_keys( self::$grouped_options ) as $group ) {
			if ( self::is_valid( $name, $group ) ) {
				return self::get_grouped_option( $group, $name, $default );
			}
		}

		trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't wish to change legacy behavior.

		return $default;
	}

	/**
	 * Returns the requested option, and ensures it's autoloaded in the future.
	 * This does _not_ adjust the prefix in any way (does not prefix jetpack_%)
	 *
	 * @param string $name Option name.
	 * @param mixed  $default (optional).
	 *
	 * @return mixed
	 */
	public static function get_option_and_ensure_autoload( $name, $default ) {
		// In this function the name is not adjusted by prefixing jetpack_
		// so if it has already prefixed, we'll replace it and then
		// check if the option name is a network option or not.
		$jetpack_name      = preg_replace( '/^jetpack_/', '', $name, 1 );
		$is_network_option = self::is_network_option( $jetpack_name );
		$value             = $is_network_option ? get_site_option( $name ) : get_option( $name );

		if ( false === $value && false !== $default ) {
			if ( $is_network_option ) {
				add_site_option( $name, $default );
			} else {
				add_option( $name, $default );
			}
			$value = $default;
		}

		return $value;
	}

	/**
	 * Update grouped option
	 *
	 * @param string $group Options group.
	 * @param string $name Options name.
	 * @param mixed  $value Options value.
	 *
	 * @return bool Success or failure.
	 */
	private static function update_grouped_option( $group, $name, $value ) {
		$options = get_option( self::$grouped_options[ $group ] );
		if ( ! is_array( $options ) ) {
			$options = array();
		}
		$options[ $name ] = $value;

		return update_option( self::$grouped_options[ $group ], $options );
	}

	/**
	 * Updates the single given option.  Updates jetpack_options or jetpack_$name as appropriate.
	 *
	 * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
	 * @param mixed  $value Option value.
	 * @param string $autoload If not compact option, allows specifying whether to autoload or not.
	 *
	 * @return bool Was the option successfully updated?
	 */
	public static function update_option( $name, $value, $autoload = null ) {
		/**
		 * Fires before Jetpack updates a specific option.
		 *
		 * @since 1.1.2
		 * @since-jetpack 3.0.0
		 *
		 * @param str $name The name of the option being updated.
		 * @param mixed $value The new value of the option.
		 */
		do_action( 'pre_update_jetpack_option_' . $name, $name, $value );
		if ( self::is_valid( $name, 'non_compact' ) ) {
			if ( self::is_network_option( $name ) ) {
				return update_site_option( "jetpack_$name", $value );
			}

			return update_option( "jetpack_$name", $value, $autoload );

		}

		foreach ( array_keys( self::$grouped_options ) as $group ) {
			if ( self::is_valid( $name, $group ) ) {
				return self::update_grouped_option( $group, $name, $value );
			}
		}

		trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't want to change legacy behavior.

		return false;
	}

	/**
	 * Updates the multiple given options.  Updates jetpack_options and/or jetpack_$name as appropriate.
	 *
	 * @param array $array array( option name => option value, ... ).
	 */
	public static function update_options( $array ) {
		$names = array_keys( $array );

		foreach ( array_diff( $names, self::get_option_names(), self::get_option_names( 'non_compact' ), self::get_option_names( 'private' ) ) as $unknown_name ) {
			trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $unknown_name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't change legacy behavior.
			unset( $array[ $unknown_name ] );
		}

		foreach ( $names as $name ) {
			self::update_option( $name, $array[ $name ] );
		}
	}

	/**
	 * Deletes the given option.  May be passed multiple option names as an array.
	 * Updates jetpack_options and/or deletes jetpack_$name as appropriate.
	 *
	 * @param string|array $names Option names. They must come _without_ `jetpack_%` prefix. The method will prefix the option names.
	 *
	 * @return bool Was the option successfully deleted?
	 */
	public static function delete_option( $names ) {
		$result = true;
		$names  = (array) $names;

		if ( ! self::is_valid( $names ) ) {
			// phpcs:disable -- This line triggers a handful of errors; ignoring to avoid changing legacy behavior.
			trigger_error( sprintf( 'Invalid Jetpack option names: %s', print_r( $names, 1 ) ), E_USER_WARNING );
			// phpcs:enable
			return false;
		}

		foreach ( array_intersect( $names, self::get_option_names( 'non_compact' ) ) as $name ) {
			if ( self::is_network_option( $name ) ) {
				$result = delete_site_option( "jetpack_$name" );
			} else {
				$result = delete_option( "jetpack_$name" );
			}
		}

		foreach ( array_keys( self::$grouped_options ) as $group ) {
			if ( ! self::delete_grouped_option( $group, $names ) ) {
				$result = false;
			}
		}

		return $result;
	}

	/**
	 * Get group option.
	 *
	 * @param string $group Option group name.
	 * @param string $name Option name.
	 * @param mixed  $default Default option value.
	 *
	 * @return mixed Option.
	 */
	private static function get_grouped_option( $group, $name, $default ) {
		$options = get_option( self::$grouped_options[ $group ] );
		if ( is_array( $options ) && isset( $options[ $name ] ) ) {
			return $options[ $name ];
		}

		return $default;
	}

	/**
	 * Delete grouped option.
	 *
	 * @param string $group Option group name.
	 * @param array  $names Option names.
	 *
	 * @return bool Success or failure.
	 */
	private static function delete_grouped_option( $group, $names ) {
		$options = get_option( self::$grouped_options[ $group ], array() );

		$to_delete = array_intersect( $names, self::get_option_names( $group ), array_keys( $options ) );
		if ( $to_delete ) {
			foreach ( $to_delete as $name ) {
				unset( $options[ $name ] );
			}

			return update_option( self::$grouped_options[ $group ], $options );
		}

		return true;
	}

	/*
	 * Raw option methods allow Jetpack to get / update / delete options via direct DB queries, including options
	 * that are not created by the Jetpack plugin. This is helpful only in rare cases when we need to bypass
	 * cache and filters.
	 */

	/**
	 * Deletes an option via $wpdb query.
	 *
	 * @param string $name Option name.
	 *
	 * @return bool Is the option deleted?
	 */
	public static function delete_raw_option( $name ) {
		if ( self::bypass_raw_option( $name ) ) {
			return delete_option( $name );
		}
		global $wpdb;
		$result = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $name ) );
		return $result;
	}

	/**
	 * Updates an option via $wpdb query.
	 *
	 * @param string $name Option name.
	 * @param mixed  $value Option value.
	 * @param bool   $autoload Specifying whether to autoload or not.
	 *
	 * @return bool Is the option updated?
	 */
	public static function update_raw_option( $name, $value, $autoload = false ) {
		if ( self::bypass_raw_option( $name ) ) {
			return update_option( $name, $value, $autoload );
		}
		global $wpdb;
		$autoload_value = $autoload ? 'yes' : 'no';

		$old_value = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
				$name
			)
		);
		if ( $old_value === $value ) {
			return false;
		}

		$serialized_value = maybe_serialize( $value );
		// below we used "insert ignore" to at least suppress the resulting error.
		$updated_num = $wpdb->query(
			$wpdb->prepare(
				"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
				$serialized_value,
				$name
			)
		);

		// Try inserting the option if the value doesn't exits.
		if ( ! $updated_num ) {
			$updated_num = $wpdb->query(
				$wpdb->prepare(
					"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, %s )",
					$name,
					$serialized_value,
					$autoload_value
				)
			);
		}
		return (bool) $updated_num;
	}

	/**
	 * Gets an option via $wpdb query.
	 *
	 * @since 1.1.2
	 * @since-jetpack 5.4.0
	 *
	 * @param string $name Option name.
	 * @param mixed  $default Default option value if option is not found.
	 *
	 * @return mixed Option value, or null if option is not found and default is not specified.
	 */
	public static function get_raw_option( $name, $default = null ) {
		if ( self::bypass_raw_option( $name ) ) {
			return get_option( $name, $default );
		}

		global $wpdb;
		$value = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
				$name
			)
		);
		$value = maybe_unserialize( $value );

		if ( null === $value && null !== $default ) {
			return $default;
		}

		return $value;
	}

	/**
	 * This function checks for a constant that, if present, will disable direct DB queries Jetpack uses to manage certain options and force Jetpack to always use Options API instead.
	 * Options can be selectively managed via a blocklist by filtering option names via the jetpack_disabled_raw_option filter.
	 *
	 * @param string $name Option name.
	 *
	 * @return bool
	 */
	public static function bypass_raw_option( $name ) {

		if ( Constants::get_constant( 'JETPACK_DISABLE_RAW_OPTIONS' ) ) {
			return true;
		}
		/**
		 * Allows to disable particular raw options.
		 *
		 * @since 1.1.2
		 * @since-jetpack 5.5.0
		 *
		 * @param array $disabled_raw_options An array of option names that you can selectively blocklist from being managed via direct database queries.
		 */
		$disabled_raw_options = apply_filters( 'jetpack_disabled_raw_options', array() );
		return isset( $disabled_raw_options[ $name ] );
	}

	/**
	 * Gets all known options that are used by Jetpack and managed by Jetpack_Options.
	 *
	 * @since 1.1.2
	 * @since-jetpack 5.4.0
	 *
	 * @param boolean $strip_unsafe_options If true, and by default, will strip out options necessary for the connection to WordPress.com.
	 * @return array An array of all options managed via the Jetpack_Options class.
	 */
	public static function get_all_jetpack_options( $strip_unsafe_options = true ) {
		$jetpack_options            = self::get_option_names();
		$jetpack_options_non_compat = self::get_option_names( 'non_compact' );
		$jetpack_options_private    = self::get_option_names( 'private' );

		$all_jp_options = array_merge( $jetpack_options, $jetpack_options_non_compat, $jetpack_options_private );

		if ( $strip_unsafe_options ) {
			// Flag some Jetpack options as unsafe.
			$unsafe_options = array(
				'id',                           // (int)    The Client ID/WP.com Blog ID of this site.
				'master_user',                  // (int)    The local User ID of the user who connected this site to jetpack.wordpress.com.
				'version',                      // (string) Used during upgrade procedure to auto-activate new modules. version:time

				// non_compact.
				'activated',

				// private.
				'register',
				'blog_token',                  // (string) The Client Secret/Blog Token of this site.
				'user_token',                  // (string) The User Token of this site. (deprecated)
				'user_tokens',
			);

			// Remove the unsafe Jetpack options.
			foreach ( $unsafe_options as $unsafe_option ) {
				$key = array_search( $unsafe_option, $all_jp_options, true );
				if ( false !== $key ) {
					unset( $all_jp_options[ $key ] );
				}
			}
		}

		return $all_jp_options;
	}

	/**
	 * Get all options that are not managed by the Jetpack_Options class that are used by Jetpack.
	 *
	 * @since 1.1.2
	 * @since-jetpack 5.4.0
	 *
	 * @return array
	 */
	public static function get_all_wp_options() {
		// A manual build of the wp options.
		return array(
			'sharing-options',
			'disabled_likes',
			'disabled_reblogs',
			'jetpack_comments_likes_enabled',
			'stats_options',
			'stats_dashboard_widget',
			'safecss_preview_rev',
			'safecss_rev',
			'safecss_revision_migrated',
			'nova_menu_order',
			'jetpack_portfolio',
			'jetpack_portfolio_posts_per_page',
			'jetpack_testimonial',
			'jetpack_testimonial_posts_per_page',
			'sharedaddy_disable_resources',
			'sharing-options',
			'sharing-services',
			'site_icon_temp_data',
			'featured-content',
			'site_logo',
			'jetpack_dismissed_notices',
			'jetpack-twitter-cards-site-tag',
			'jetpack-sitemap-state',
			'jetpack_sitemap_post_types',
			'jetpack_sitemap_location',
			'jetpack_protect_key',
			'jetpack_protect_blocked_attempts',
			'jetpack_protect_activating',
			'jetpack_connection_banner_ab',
			'jetpack_active_plan',
			'jetpack_activation_source',
			'jetpack_site_products',
			'jetpack_sso_match_by_email',
			'jetpack_sso_require_two_step',
			'jetpack_sso_remove_login_form',
			'jetpack_last_connect_url_check',
			'jpo_business_address',
			'jpo_site_type',
			'jpo_homepage_format',
			'jpo_contact_page',
			'jetpack_excluded_extensions',
		);
	}

	/**
	 * Gets all options that can be safely reset by CLI.
	 *
	 * @since 1.1.2
	 * @since-jetpack 5.4.0
	 *
	 * @return array array Associative array containing jp_options which are managed by the Jetpack_Options class and wp_options which are not.
	 */
	public static function get_options_for_reset() {
		$all_jp_options = self::get_all_jetpack_options();

		$wp_options = self::get_all_wp_options();

		$options = array(
			'jp_options' => $all_jp_options,
			'wp_options' => $wp_options,
		);

		return $options;
	}

	/**
	 * Delete all known options
	 *
	 * @since 1.1.2
	 * @since-jetpack 5.4.0
	 *
	 * @return void
	 */
	public static function delete_all_known_options() {
		// Delete all compact options.
		foreach ( (array) self::$grouped_options as $option_name ) {
			delete_option( $option_name );
		}

		// Delete all non-compact Jetpack options.
		foreach ( (array) self::get_option_names( 'non-compact' ) as $option_name ) {
			self::delete_option( $option_name );
		}

		// Delete all options that can be reset via CLI, that aren't Jetpack options.
		foreach ( (array) self::get_all_wp_options() as $option_name ) {
			delete_option( $option_name );
		}
	}
}
class-jetpack-signature.php000064400000035075151547223160012020 0ustar00<?php
/**
 * The Jetpack Connection signature class file.
 *
 * @package automattic/jetpack-connection
 */

use Automattic\Jetpack\Connection\Manager as Connection_Manager;

/**
 * The Jetpack Connection signature class that is used to sign requests.
 */
class Jetpack_Signature {
	/**
	 * Token part of the access token.
	 *
	 * @access public
	 * @var string
	 */
	public $token;

	/**
	 * Access token secret.
	 *
	 * @access public
	 * @var string
	 */
	public $secret;

	/**
	 * Timezone difference (in seconds).
	 *
	 * @access public
	 * @var int
	 */
	public $time_diff;

	/**
	 * The current request URL.
	 *
	 * @access public
	 * @var string
	 */
	public $current_request_url;

	/**
	 * Constructor.
	 *
	 * @param array $access_token Access token.
	 * @param int   $time_diff    Timezone difference (in seconds).
	 */
	public function __construct( $access_token, $time_diff = 0 ) {
		$secret = explode( '.', $access_token );
		if ( 2 !== count( $secret ) ) {
			return;
		}

		$this->token     = $secret[0];
		$this->secret    = $secret[1];
		$this->time_diff = $time_diff;
	}

	/**
	 * Sign the current request.
	 *
	 * @todo Implement a proper nonce verification.
	 *
	 * @param array $override Optional arguments to override the ones from the current request.
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
	 */
	public function sign_current_request( $override = array() ) {
		if ( isset( $override['scheme'] ) ) {
			$scheme = $override['scheme'];
			if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
				return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' );
			}
		} elseif ( is_ssl() ) {
			$scheme = 'https';
		} else {
			$scheme = 'http';
		}

		$port = $this->get_current_request_port();

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidatedNotSanitized -- Sniff misses the esc_url_raw wrapper.
		$this->current_request_url = esc_url_raw( wp_unslash( "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . ( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ) ) );

		if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) {
			$body = $override['body'];
		} elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
			$body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null;

			// Convert the $_POST to the body, if the body was empty. This is how arrays are hashed
			// and encoded on the Jetpack side.
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
				// phpcs:ignore WordPress.Security.NonceVerification.Missing
				if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) {
					$body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
				}
			}
		} elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
			// This is a little strange-looking, but there doesn't seem to be another way to get the PUT body.
			$raw_put_data = file_get_contents( 'php://input' );
			parse_str( $raw_put_data, $body );

			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
				$put_data = json_decode( $raw_put_data, true );
				if ( is_array( $put_data ) && count( $put_data ) > 0 ) {
					$body = $put_data;
				}
			}
		} else {
			$body = null;
		}

		if ( empty( $body ) ) {
			$body = null;
		}

		$a = array();
		foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) {
			if ( isset( $override[ $parameter ] ) ) {
				$a[ $parameter ] = $override[ $parameter ];
			} else {
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$a[ $parameter ] = isset( $_GET[ $parameter ] ) ? filter_var( wp_unslash( $_GET[ $parameter ] ) ) : '';
			}
		}

		$method = isset( $override['method'] ) ? $override['method'] : ( isset( $_SERVER['REQUEST_METHOD'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : null );
		return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true );
	}

	/**
	 * Sign a specified request.
	 *
	 * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array?
	 * @todo Use wp_json_encode() instead of json_encode()?
	 *
	 * @param string $token            Request token.
	 * @param int    $timestamp        Timestamp of the request.
	 * @param string $nonce            Request nonce.
	 * @param string $body_hash        Request body hash.
	 * @param string $method           Request method.
	 * @param string $url              Request URL.
	 * @param mixed  $body             Request body.
	 * @param bool   $verify_body_hash Whether to verify the body hash against the body.
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
	 */
	public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) {
		if ( ! $this->secret ) {
			return new WP_Error( 'invalid_secret', 'Invalid secret' );
		}

		if ( ! $this->token ) {
			return new WP_Error( 'invalid_token', 'Invalid token' );
		}

		list( $token ) = explode( '.', $token );

		$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );

		if ( 0 !== strpos( $token, "$this->token:" ) ) {
			return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
		}

		// If we got an array at this point, let's encode it, so we can see what it looks like as a string.
		if ( is_array( $body ) ) {
			if ( count( $body ) > 0 ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
				$body = json_encode( $body );

			} else {
				$body = '';
			}
		}

		$required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' );
		if ( $body !== null ) {
			$required_parameters[] = 'body_hash';
			if ( ! is_string( $body ) ) {
				return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) );
			}
		}

		foreach ( $required_parameters as $required ) {
			if ( ! is_scalar( $$required ) ) {
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
			}

			if ( ! strlen( $$required ) ) {
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
			}
		}

		if ( empty( $body ) ) {
			if ( $body_hash ) {
				return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) );
			}
		} else {
			$connection = new Connection_Manager();
			if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) {
				return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) );
			}
		}

		$parsed = wp_parse_url( $url );
		if ( ! isset( $parsed['host'] ) ) {
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) );
		}

		if ( ! empty( $parsed['port'] ) ) {
			$port = $parsed['port'];
		} elseif ( 'http' === $parsed['scheme'] ) {
			$port = 80;
		} elseif ( 'https' === $parsed['scheme'] ) {
			$port = 443;
		} else {
			return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) );
		}

		if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug.
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) );
		}

		$local_time = $timestamp - $this->time_diff;
		if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
			return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) );
		}

		if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) {
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) );
		}

		$normalized_request_pieces = array(
			$token,
			$timestamp,
			$nonce,
			$body_hash,
			strtoupper( $method ),
			strtolower( $parsed['host'] ),
			$port,
			empty( $parsed['path'] ) ? '' : $parsed['path'],
			// Normalized Query String.
		);

		$normalized_request_pieces      = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) );
		$flat_normalized_request_pieces = array();
		foreach ( $normalized_request_pieces as $piece ) {
			if ( is_array( $piece ) ) {
				foreach ( $piece as $subpiece ) {
					$flat_normalized_request_pieces[] = $subpiece;
				}
			} else {
				$flat_normalized_request_pieces[] = $piece;
			}
		}
		$normalized_request_pieces = $flat_normalized_request_pieces;

		$normalized_request_string = join( "\n", $normalized_request_pieces ) . "\n";

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) );
	}

	/**
	 * Retrieve and normalize the parameters from a query string.
	 *
	 * @param string $query_string Query string.
	 * @return array Normalized query string parameters.
	 */
	public function normalized_query_parameters( $query_string ) {
		parse_str( $query_string, $array );

		unset( $array['signature'] );

		$names  = array_keys( $array );
		$values = array_values( $array );

		$names  = array_map( array( $this, 'encode_3986' ), $names );
		$values = array_map( array( $this, 'encode_3986' ), $values );

		$pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values );

		sort( $pairs );

		return $pairs;
	}

	/**
	 * Encodes a string or array of strings according to RFC 3986.
	 *
	 * @param string|array $string_or_array String or array to encode.
	 * @return string|array URL-encoded string or array.
	 */
	public function encode_3986( $string_or_array ) {
		if ( is_array( $string_or_array ) ) {
			return array_map( array( $this, 'encode_3986' ), $string_or_array );
		}

		return rawurlencode( $string_or_array );
	}

	/**
	 * Concatenates a parameter name and a parameter value with an equals sign between them.
	 *
	 * @param string       $name  Parameter name.
	 * @param string|array $value Parameter value.
	 * @return string|array A string pair (e.g. `name=value`) or an array of string pairs.
	 */
	public function join_with_equal_sign( $name, $value ) {
		if ( is_array( $value ) ) {
			return $this->join_array_with_equal_sign( $name, $value );
		}
		return "{$name}={$value}";
	}

	/**
	 * Helper function for join_with_equal_sign for handling arrayed values.
	 * Explicitly supports nested arrays.
	 *
	 * @param string $name  Parameter name.
	 * @param array  $value Parameter value.
	 * @return array An array of string pairs (e.g. `[ name[example]=value ]`).
	 */
	private function join_array_with_equal_sign( $name, $value ) {
		$result = array();
		foreach ( $value as $value_key => $value_value ) {
			$joined_value = $this->join_with_equal_sign( $name . '[' . $value_key . ']', $value_value );
			if ( is_array( $joined_value ) ) {
				foreach ( array_values( $joined_value ) as $individual_joined_value ) {
					$result[] = $individual_joined_value;
				}
			} elseif ( is_string( $joined_value ) ) {
				$result[] = $joined_value;
			}
		}

		sort( $result );
		return $result;
	}

	/**
	 * Gets the port that should be considered to sign the current request.
	 *
	 * It will analyze the current request, as well as some Jetpack constants, to return the string
	 * to be concatenated in the URL representing the port of the current request.
	 *
	 * @since 1.8.4
	 *
	 * @return string The port to be used in the signature
	 */
	public function get_current_request_port() {
		$host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $this->sanitize_host_post( $_SERVER['HTTP_X_FORWARDED_PORT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
		if ( '' === $host_port && isset( $_SERVER['SERVER_PORT'] ) ) {
			$host_port = $this->sanitize_host_post( $_SERVER['SERVER_PORT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
		}

		/**
		 * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test.
		 * Please update the test if any changes are made in this logic.
		 */
		if ( is_ssl() ) {
			// 443: Standard Port
			// 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites
			// with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with
			// the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a
			// site at https://example.com:80/ (which would be a strange configuration).
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
			// if the site is behind a proxy running on port 443 without
			// X-Forwarded-Port and the back end's port is *not* 80. It's better,
			// though, to configure the proxy to send X-Forwarded-Port.
			$https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTPS_PORT ) : '443';
			$port       = in_array( $host_port, array( '443', '80', $https_port ), true ) ? '' : $host_port;
		} else {
			// 80: Standard Port
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
			// if the site is behind a proxy running on port 80 without
			// X-Forwarded-Port. It's better, though, to configure the proxy to
			// send X-Forwarded-Port.
			$http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTP_PORT ) : '80';
			$port      = in_array( $host_port, array( '80', $http_port ), true ) ? '' : $host_port;
		}
		return (string) $port;
	}

	/**
	 * Sanitizes a variable checking if it's a valid port number, which can be an integer or a numeric string
	 *
	 * @since 1.8.4
	 *
	 * @param mixed $port_number Variable representing a port number.
	 * @return string Always a string with a valid port number, or an empty string if input is invalid
	 */
	public function sanitize_host_post( $port_number ) {

		if ( ! is_int( $port_number ) && ! is_string( $port_number ) ) {
			return '';
		}
		if ( is_string( $port_number ) && ! ctype_digit( $port_number ) ) {
			return '';
		}

		if ( 0 >= (int) $port_number || 65535 < $port_number ) {
			return '';
		}
		return (string) $port_number;
	}
}
class-jetpack-tracks-client.php000064400000014625151547223160012560 0ustar00<?php
/**
 * Legacy Jetpack Tracks Client
 *
 * @package automattic/jetpack-tracking
 */

use Automattic\Jetpack\Connection\Manager;

/**
 * Jetpack_Tracks_Client
 *
 * Send Tracks events on behalf of a user
 *
 * Example Usage:
```php
	require( dirname(__FILE__).'path/to/tracks/class-jetpack-tracks-client.php' );

	$result = Jetpack_Tracks_Client::record_event( array(
		'_en'        => $event_name,       // required
		'_ui'        => $user_id,          // required unless _ul is provided
		'_ul'        => $user_login,       // required unless _ui is provided

		// Optional, but recommended
		'_ts'        => $ts_in_ms,         // Default: now
		'_via_ip'    => $client_ip,        // we use it for geo, etc.

		// Possibly useful to set some context for the event
		'_via_ua'    => $client_user_agent,
		'_via_url'   => $client_url,
		'_via_ref'   => $client_referrer,

		// For user-targeted tests
		'abtest_name'        => $abtest_name,
		'abtest_variation'   => $abtest_variation,

		// Your application-specific properties
		'custom_property'    => $some_value,
	) );

	if ( is_wp_error( $result ) ) {
		// Handle the error in your app
	}
```
 */
class Jetpack_Tracks_Client {
	const PIXEL           = 'https://pixel.wp.com/t.gif';
	const BROWSER_TYPE    = 'php-agent';
	const USER_AGENT_SLUG = 'tracks-client';
	const VERSION         = '0.3';

	/**
	 * Stores the Terms of Service Object Reference.
	 *
	 * @var null
	 */
	private static $terms_of_service = null;

	/**
	 * Record an event.
	 *
	 * @param  mixed $event Event object to send to Tracks. An array will be cast to object. Required.
	 *                      Properties are included directly in the pixel query string after light validation.
	 * @return mixed         True on success, WP_Error on failure
	 */
	public static function record_event( $event ) {
		if ( ! self::$terms_of_service ) {
			self::$terms_of_service = new \Automattic\Jetpack\Terms_Of_Service();
		}

		// Don't track users who have opted out or not agreed to our TOS, or are not running an active Jetpack.
		if ( ! self::$terms_of_service->has_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) {
			return false;
		}

		if ( ! $event instanceof Jetpack_Tracks_Event ) {
			$event = new Jetpack_Tracks_Event( $event );
		}
		if ( is_wp_error( $event ) ) {
			return $event;
		}

		$pixel = $event->build_pixel_url( $event );

		if ( ! $pixel ) {
			return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
		}

		return self::record_pixel( $pixel );
	}

	/**
	 * Synchronously request the pixel.
	 *
	 * @param string $pixel The wp.com tracking pixel.
	 * @return array|bool|WP_Error True if successful. wp_remote_get response or WP_Error if not.
	 */
	public static function record_pixel( $pixel ) {
		// Add the Request Timestamp and URL terminator just before the HTTP request.
		$pixel .= '&_rt=' . self::build_timestamp() . '&_=_';

		$response = wp_remote_get(
			$pixel,
			array(
				'blocking'    => true, // The default, but being explicit here :).
				'timeout'     => 1,
				'redirection' => 2,
				'httpversion' => '1.1',
				'user-agent'  => self::get_user_agent(),
			)
		);

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

		$code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0;

		if ( 200 !== $code ) {
			return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code );
		}

		return true;
	}

	/**
	 * Get the user agent.
	 *
	 * @return string The user agent.
	 */
	public static function get_user_agent() {
		return self::USER_AGENT_SLUG . '-v' . self::VERSION;
	}

	/**
	 * Build an event and return its tracking URL
	 *
	 * @deprecated          Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead.
	 * @param  array $event Event keys and values.
	 * @return string       URL of a tracking pixel.
	 */
	public static function build_pixel_url( $event ) {
		$_event = new Jetpack_Tracks_Event( $event );
		return $_event->build_pixel_url();
	}

	/**
	 * Validate input for a tracks event.
	 *
	 * @deprecated          Instantiate a Jetpack_Tracks_Event object instead
	 * @param  array $event Event keys and values.
	 * @return mixed        Validated keys and values or WP_Error on failure
	 */
	private static function validate_and_sanitize( $event ) {
		$_event = new Jetpack_Tracks_Event( $event );
		if ( is_wp_error( $_event ) ) {
			return $_event;
		}
		return get_object_vars( $_event );
	}

	/**
	 * Builds a timestamp.
	 *
	 * Milliseconds since 1970-01-01.
	 *
	 * @return string
	 */
	public static function build_timestamp() {
		$ts = round( microtime( true ) * 1000 );
		return number_format( $ts, 0, '', '' );
	}

	/**
	 * Grabs the user's anon id from cookies, or generates and sets a new one
	 *
	 * @return string An anon id for the user
	 */
	public static function get_anon_id() {
		static $anon_id = null;

		if ( ! isset( $anon_id ) ) {

			// Did the browser send us a cookie?
			if ( isset( $_COOKIE['tk_ai'] ) && preg_match( '#^[a-z]+:[A-Za-z0-9+/=]{24}$#', $_COOKIE['tk_ai'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
				$anon_id = $_COOKIE['tk_ai']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
			} else {

				$binary = '';

				// Generate a new anonId and try to save it in the browser's cookies.
				// Note that base64-encoding an 18 character string generates a 24-character anon id.
				for ( $i = 0; $i < 18; ++$i ) {
					$binary .= chr( wp_rand( 0, 255 ) );
				}

				$anon_id = 'jetpack:' . base64_encode( $binary ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode

				if ( ! headers_sent()
					&& ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
					&& ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
				) {
					setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random value and should be fine.
				}
			}
		}

		return $anon_id;
	}

	/**
	 * Gets the WordPress.com user's Tracks identity, if connected.
	 *
	 * @return array|bool
	 */
	public static function get_connected_user_tracks_identity() {
		$user_data = ( new Manager() )->get_connected_user_data();
		if ( ! $user_data ) {
			return false;
		}

		return array(
			'blogid'      => Jetpack_Options::get_option( 'id', 0 ),
			'userid'      => $user_data['ID'],
			'username'    => $user_data['login'],
			'user_locale' => $user_data['user_locale'],
		);
	}
}
class-jetpack-tracks-event.php000064400000010530151547223160012412 0ustar00<?php
/**
 * Class Jetpack_Tracks_Event. Legacy.
 *
 * @package automattic/jetpack-sync
 */

/*
 * Example Usage:
```php
	require_once( dirname(__FILE__) . 'path/to/tracks/class-jetpack-tracks-event.php' );

	$event = new Jetpack_Tracks_Event( array(
		'_en'        => $event_name,       // required
		'_ui'        => $user_id,          // required unless _ul is provided
		'_ul'        => $user_login,       // required unless _ui is provided

		// Optional, but recommended
		'_via_ip'    => $client_ip,        // for geo, etc.

		// Possibly useful to set some context for the event
		'_via_ua'    => $client_user_agent,
		'_via_url'   => $client_url,
		'_via_ref'   => $client_referrer,

		// For user-targeted tests
		'abtest_name'        => $abtest_name,
		'abtest_variation'   => $abtest_variation,

		// Your application-specific properties
		'custom_property'    => $some_value,
	) );

	if ( is_wp_error( $event->error ) ) {
		// Handle the error in your app
	}

	$bump_and_redirect_pixel = $event->build_signed_pixel_url();
```
 */

/**
 * Class Jetpack_Tracks_Event
 */
#[AllowDynamicProperties]
class Jetpack_Tracks_Event {
	const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';
	const PROP_NAME_REGEX  = '/^[a-z_][a-z0-9_]*$/';

	/**
	 * Tracks Event Error.
	 *
	 * @var mixed Error.
	 */
	public $error;

	/**
	 * Jetpack_Tracks_Event constructor.
	 *
	 * @param object $event Tracks event.
	 */
	public function __construct( $event ) {
		$_event = self::validate_and_sanitize( $event );
		if ( is_wp_error( $_event ) ) {
			$this->error = $_event;
			return;
		}

		foreach ( $_event as $key => $value ) {
			$this->{$key} = $value;
		}
	}

	/**
	 * Record a track event.
	 */
	public function record() {
		return Jetpack_Tracks_Client::record_event( $this );
	}

	/**
	 * Annotate the event with all relevant info.
	 *
	 * @param  mixed $event Object or (flat) array.
	 * @return mixed        The transformed event array or WP_Error on failure.
	 */
	public static function validate_and_sanitize( $event ) {
		$event = (object) $event;

		// Required.
		if ( ! $event->_en ) {
			return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
		}

		// delete non-routable addresses otherwise geoip will discard the record entirely.
		if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
			unset( $event->_via_ip );
		}

		$validated = array(
			'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE,
			'_aua'         => Jetpack_Tracks_Client::get_user_agent(),
		);

		$_event = (object) array_merge( (array) $event, $validated );

		// If you want to block property names, do it here.

		// Make sure we have an event timestamp.
		if ( ! isset( $_event->_ts ) ) {
			$_event->_ts = Jetpack_Tracks_Client::build_timestamp();
		}

		return $_event;
	}

	/**
	 * Build a pixel URL that will send a Tracks event when fired.
	 * On error, returns an empty string ('').
	 *
	 * @return string A pixel URL or empty string ('') if there were invalid args.
	 */
	public function build_pixel_url() {
		if ( $this->error ) {
			return '';
		}

		$args = get_object_vars( $this );

		// Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
		unset( $args['_rt'] );
		unset( $args['_'] );

		$validated = self::validate_and_sanitize( $args );

		if ( is_wp_error( $validated ) ) {
			return '';
		}

		return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated );
	}

	/**
	 * Validate the event name.
	 *
	 * @param string $name Event name.
	 * @return false|int
	 */
	public static function event_name_is_valid( $name ) {
		return preg_match( self::EVENT_NAME_REGEX, $name );
	}

	/**
	 * Validates prop name
	 *
	 * @param string $name Property name.
	 *
	 * @return false|int Truthy value.
	 */
	public static function prop_name_is_valid( $name ) {
		return preg_match( self::PROP_NAME_REGEX, $name );
	}

	/**
	 * Scrutinize event name.
	 *
	 * @param object $event Tracks event.
	 */
	public static function scrutinize_event_names( $event ) {
		if ( ! self::event_name_is_valid( $event->_en ) ) {
			return;
		}

		$whitelisted_key_names = array(
			'anonId',
			'Browser_Type',
		);

		foreach ( array_keys( (array) $event ) as $key ) {
			if ( in_array( $key, $whitelisted_key_names, true ) ) {
				continue;
			}
			if ( ! self::prop_name_is_valid( $key ) ) {
				return;
			}
		}
	}
}
class-jetpack-xmlrpc-server.php000064400000062465151547223160012633 0ustar00<?php
/**
 * Jetpack XMLRPC Server.
 *
 * @package automattic/jetpack-connection
 */

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Secrets;
use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Roles;

/**
 * Just a sack of functions.  Not actually an IXR_Server
 */
class Jetpack_XMLRPC_Server {
	/**
	 * The current error object
	 *
	 * @var \WP_Error
	 */
	public $error = null;

	/**
	 * The current user
	 *
	 * @var \WP_User
	 */
	public $user = null;

	/**
	 * The connection manager object.
	 *
	 * @var Automattic\Jetpack\Connection\Manager
	 */
	private $connection;

	/**
	 * Creates a new XMLRPC server object.
	 */
	public function __construct() {
		$this->connection = new Connection_Manager();
	}

	/**
	 * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
	 * user is not authenticated (->login()) then the methods are never added,
	 * so they will get a "does not exist" error.
	 *
	 * @param array $core_methods Core XMLRPC methods.
	 */
	public function xmlrpc_methods( $core_methods ) {
		$jetpack_methods = array(
			'jetpack.verifyAction'     => array( $this, 'verify_action' ),
			'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ),
			'jetpack.unlinkUser'       => array( $this, 'unlink_user' ),
			'jetpack.testConnection'   => array( $this, 'test_connection' ),
		);

		$jetpack_methods = array_merge( $jetpack_methods, $this->provision_xmlrpc_methods() );

		$this->user = $this->login();

		if ( $this->user ) {
			$jetpack_methods = array_merge(
				$jetpack_methods,
				array(
					'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ),
				)
			);

			if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
				$jetpack_methods['metaWeblog.newMediaObject']      = $core_methods['metaWeblog.newMediaObject'];
				$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
			}

			/**
			 * Filters the XML-RPC methods available to Jetpack for authenticated users.
			 *
			 * @since 1.7.0
			 * @since-jetpack 1.1.0
			 *
			 * @param array    $jetpack_methods XML-RPC methods available to the Jetpack Server.
			 * @param array    $core_methods    Available core XML-RPC methods.
			 * @param \WP_User $user            Information about the user authenticated in the request.
			 */
			$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
		}

		/**
		 * Filters the XML-RPC methods available to Jetpack for requests signed both with a blog token or a user token.
		 *
		 * @since 1.7.0
		 * @since 1.9.5 Introduced the $user parameter.
		 * @since-jetpack 3.0.0
		 *
		 * @param array         $jetpack_methods XML-RPC methods available to the Jetpack Server.
		 * @param array         $core_methods    Available core XML-RPC methods.
		 * @param \WP_User|bool $user            Information about the user authenticated in the request. False if authenticated with blog token.
		 */
		return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods, $this->user );
	}

	/**
	 * Whitelist of the bootstrap XML-RPC methods
	 */
	public function bootstrap_xmlrpc_methods() {
		return array(
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
			'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
		);
	}

	/**
	 * Additional method needed for authorization calls.
	 */
	public function authorize_xmlrpc_methods() {
		return array(
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
			'jetpack.remoteRegister'  => array( $this, 'remote_already_registered' ),
		);
	}

	/**
	 * Remote provisioning methods.
	 */
	public function provision_xmlrpc_methods() {
		return array(
			'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
			'jetpack.remoteProvision' => array( $this, 'remote_provision' ),
			'jetpack.remoteConnect'   => array( $this, 'remote_connect' ),
			'jetpack.getUser'         => array( $this, 'get_user' ),
		);
	}

	/**
	 * Used to verify whether a local user exists and what role they have.
	 *
	 * @param int|string|array $request One of:
	 *                         int|string The local User's ID, username, or email address.
	 *                         array      A request array containing:
	 *                                    0: int|string The local User's ID, username, or email address.
	 *
	 * @return array|\IXR_Error Information about the user, or error if no such user found:
	 *                          roles:     string[] The user's rols.
	 *                          login:     string   The user's username.
	 *                          email_hash string[] The MD5 hash of the user's normalized email address.
	 *                          caps       string[] The user's capabilities.
	 *                          allcaps    string[] The user's granular capabilities, merged from role capabilities.
	 *                          token_key  string   The Token Key of the user's Jetpack token. Empty string if none.
	 */
	public function get_user( $request ) {
		$user_id = is_array( $request ) ? $request[0] : $request;

		if ( ! $user_id ) {
			return $this->error(
				new \WP_Error(
					'invalid_user',
					__( 'Invalid user identifier.', 'jetpack-connection' ),
					400
				),
				'get_user'
			);
		}

		$user = $this->get_user_by_anything( $user_id );

		if ( ! $user ) {
			return $this->error(
				new \WP_Error(
					'user_unknown',
					__( 'User not found.', 'jetpack-connection' ),
					404
				),
				'get_user'
			);
		}

		$user_token = ( new Tokens() )->get_access_token( $user->ID );

		if ( $user_token ) {
			list( $user_token_key ) = explode( '.', $user_token->secret );
			if ( $user_token_key === $user_token->secret ) {
				$user_token_key = '';
			}
		} else {
			$user_token_key = '';
		}

		return array(
			'id'         => $user->ID,
			'login'      => $user->user_login,
			'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
			'roles'      => $user->roles,
			'caps'       => $user->caps,
			'allcaps'    => $user->allcaps,
			'token_key'  => $user_token_key,
		);
	}

	/**
	 * Remote authorization XMLRPC method handler.
	 *
	 * @param array $request the request.
	 */
	public function remote_authorize( $request ) {
		$user = get_user_by( 'id', $request['state'] );

		/**
		 * Happens on various request handling events in the Jetpack XMLRPC server.
		 * The action combines several types of events:
		 *    - remote_authorize
		 *    - remote_provision
		 *    - get_user.
		 *
		 * @since 1.7.0
		 * @since-jetpack 8.0.0
		 *
		 * @param String  $action the action name, i.e., 'remote_authorize'.
		 * @param String  $stage  the execution stage, can be 'begin', 'success', 'error', etc.
		 * @param array   $parameters extra parameters from the event.
		 * @param WP_User $user the acting user.
		 */
		do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user );

		foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
				return $this->error(
					new \WP_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ),
					'remote_authorize'
				);
			}
		}

		if ( ! $user ) {
			return $this->error( new \WP_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' );
		}

		if ( $this->connection->has_connected_owner() && $this->connection->is_user_connected( $request['state'] ) ) {
			return $this->error( new \WP_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' );
		}

		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );

		if ( is_a( $verified, 'IXR_Error' ) ) {
			return $this->error( $verified, 'remote_authorize' );
		}

		wp_set_current_user( $request['state'] );

		$result = $this->connection->authorize( $request );

		if ( is_wp_error( $result ) ) {
			return $this->error( $result, 'remote_authorize' );
		}

		// This action is documented in class.jetpack-xmlrpc-server.php.
		do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' );

		return array(
			'result' => $result,
		);
	}

	/**
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
	 * register this site so that a plan can be provisioned.
	 *
	 * @param array $request An array containing at minimum nonce and local_user keys.
	 *
	 * @return \WP_Error|array
	 */
	public function remote_register( $request ) {
		// This action is documented in class.jetpack-xmlrpc-server.php.
		do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() );

		$user = $this->fetch_and_verify_local_user( $request );

		if ( ! $user ) {
			return $this->error(
				new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
				'remote_register'
			);
		}

		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
			return $this->error( $user, 'remote_register' );
		}

		if ( empty( $request['nonce'] ) ) {
			return $this->error(
				new \WP_Error(
					'nonce_missing',
					__( 'The required "nonce" parameter is missing.', 'jetpack-connection' ),
					400
				),
				'remote_register'
			);
		}

		$nonce = sanitize_text_field( $request['nonce'] );
		unset( $request['nonce'] );

		$api_url  = $this->connection->api_url( 'partner_provision_nonce_check' );
		$response = Client::_wp_remote_request(
			esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
			array( 'method' => 'GET' ),
			true
		);

		if (
			200 !== wp_remote_retrieve_response_code( $response ) ||
			'OK' !== trim( wp_remote_retrieve_body( $response ) )
		) {
			return $this->error(
				new \WP_Error(
					'invalid_nonce',
					__( 'There was an issue validating this request.', 'jetpack-connection' ),
					400
				),
				'remote_register'
			);
		}

		if ( ! Jetpack_Options::get_option( 'id' ) || ! ( new Tokens() )->get_access_token() || ! empty( $request['force'] ) ) {
			wp_set_current_user( $user->ID );

			// This code mostly copied from Jetpack::admin_page_load.
			if ( isset( $request['from'] ) ) {
				$this->connection->add_register_request_param( 'from', (string) $request['from'] );
			}
			$registered = $this->connection->try_registration();
			if ( is_wp_error( $registered ) ) {
				return $this->error( $registered, 'remote_register' );
			} elseif ( ! $registered ) {
				return $this->error(
					new \WP_Error(
						'registration_error',
						__( 'There was an unspecified error registering the site', 'jetpack-connection' ),
						400
					),
					'remote_register'
				);
			}
		}

		// This action is documented in class.jetpack-xmlrpc-server.php.
		do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' );

		return array(
			'client_id' => Jetpack_Options::get_option( 'id' ),
		);
	}

	/**
	 * This is a substitute for remote_register() when the blog is already registered which returns an error code
	 * signifying that state.
	 * This is an unauthorized call and we should not be responding with any data other than the error code.
	 *
	 * @return \IXR_Error
	 */
	public function remote_already_registered() {
		return $this->error(
			new \WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 ),
			'remote_register'
		);
	}

	/**
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
	 * register this site so that a plan can be provisioned.
	 *
	 * @param array $request An array containing at minimum a nonce key and a local_username key.
	 *
	 * @return \WP_Error|array
	 */
	public function remote_provision( $request ) {
		$user = $this->fetch_and_verify_local_user( $request );

		if ( ! $user ) {
			return $this->error(
				new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
				'remote_provision'
			);
		}

		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
			return $this->error( $user, 'remote_provision' );
		}

		$site_icon = get_site_icon_url();

		/**
		 * Filters the Redirect URI returned by the remote_register XMLRPC method
		 *
		 * @param string $redirect_uri The Redirect URI
		 *
		 * @since 1.9.7
		 */
		$redirect_uri = apply_filters( 'jetpack_xmlrpc_remote_register_redirect_uri', admin_url() );

		// Generate secrets.
		$roles   = new Roles();
		$role    = $roles->translate_user_to_role( $user );
		$secrets = ( new Secrets() )->generate( 'authorize', $user->ID );

		$response = array(
			'jp_version'   => Constants::get_constant( 'JETPACK__VERSION' ),
			'redirect_uri' => $redirect_uri,
			'user_id'      => $user->ID,
			'user_email'   => $user->user_email,
			'user_login'   => $user->user_login,
			'scope'        => $this->connection->sign_role( $role, $user->ID ),
			'secret'       => $secrets['secret_1'],
			'is_active'    => $this->connection->has_connected_owner(),
		);

		if ( $site_icon ) {
			$response['site_icon'] = $site_icon;
		}

		/**
		 * Filters the response of the remote_provision XMLRPC method
		 *
		 * @param array    $response The response.
		 * @param array    $request An array containing at minimum a nonce key and a local_username key.
		 * @param \WP_User $user The local authenticated user.
		 *
		 * @since 1.9.7
		 */
		$response = apply_filters( 'jetpack_remote_xmlrpc_provision_response', $response, $request, $user );

		return $response;
	}

	/**
	 * Given an array containing a local user identifier and a nonce, will attempt to fetch and set
	 * an access token for the given user.
	 *
	 * @param array       $request    An array containing local_user and nonce keys at minimum.
	 * @param \IXR_Client $ixr_client The client object, optional.
	 * @return mixed
	 */
	public function remote_connect( $request, $ixr_client = false ) {
		if ( $this->connection->has_connected_owner() ) {
			return $this->error(
				new WP_Error(
					'already_connected',
					__( 'Jetpack is already connected.', 'jetpack-connection' ),
					400
				),
				'remote_connect'
			);
		}

		$user = $this->fetch_and_verify_local_user( $request );

		if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
			return $this->error(
				new WP_Error(
					'input_error',
					__( 'Valid user is required.', 'jetpack-connection' ),
					400
				),
				'remote_connect'
			);
		}

		if ( empty( $request['nonce'] ) ) {
			return $this->error(
				new WP_Error(
					'input_error',
					__( 'A non-empty nonce must be supplied.', 'jetpack-connection' ),
					400
				),
				'remote_connect'
			);
		}

		if ( ! $ixr_client ) {
			$ixr_client = new Jetpack_IXR_Client();
		}
		// TODO: move this query into the Tokens class?
		$ixr_client->query(
			'jetpack.getUserAccessToken',
			array(
				'nonce'            => sanitize_text_field( $request['nonce'] ),
				'external_user_id' => $user->ID,
			)
		);

		$token = $ixr_client->isError() ? false : $ixr_client->getResponse();
		if ( empty( $token ) ) {
			return $this->error(
				new WP_Error(
					'token_fetch_failed',
					__( 'Failed to fetch user token from WordPress.com.', 'jetpack-connection' ),
					400
				),
				'remote_connect'
			);
		}
		$token = sanitize_text_field( $token );

		( new Tokens() )->update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );

		/**
		 * Hook fired at the end of the jetpack.remoteConnect XML-RPC callback
		 *
		 * @since 1.9.7
		 */
		do_action( 'jetpack_remote_connect_end' );

		return $this->connection->has_connected_owner();
	}

	/**
	 * Getter for the local user to act as.
	 *
	 * @param array $request the current request data.
	 */
	private function fetch_and_verify_local_user( $request ) {
		if ( empty( $request['local_user'] ) ) {
			return $this->error(
				new \WP_Error(
					'local_user_missing',
					__( 'The required "local_user" parameter is missing.', 'jetpack-connection' ),
					400
				),
				'remote_provision'
			);
		}

		// Local user is used to look up by login, email or ID.
		$local_user_info = $request['local_user'];

		return $this->get_user_by_anything( $local_user_info );
	}

	/**
	 * Gets the user object by its data.
	 *
	 * @param string $user_id can be any identifying user data.
	 */
	private function get_user_by_anything( $user_id ) {
		$user = get_user_by( 'login', $user_id );

		if ( ! $user ) {
			$user = get_user_by( 'email', $user_id );
		}

		if ( ! $user ) {
			$user = get_user_by( 'ID', $user_id );
		}

		return $user;
	}

	/**
	 * Possible error_codes:
	 *
	 * - verify_secret_1_missing
	 * - verify_secret_1_malformed
	 * - verify_secrets_missing: verification secrets are not found in database
	 * - verify_secrets_incomplete: verification secrets are only partially found in database
	 * - verify_secrets_expired: verification secrets have expired
	 * - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
	 * - state_missing: required parameter of state not found
	 * - state_malformed: state is not a digit
	 * - invalid_state: state in request does not match the stored state
	 *
	 * The 'authorize' and 'register' actions have additional error codes
	 *
	 * state_missing: a state ( user id ) was not supplied
	 * state_malformed: state is not the correct data type
	 * invalid_state: supplied state does not match the stored state
	 *
	 * @param array $params action An array of 3 parameters:
	 *     [0]: string action. Possible values are `authorize`, `publicize` and `register`.
	 *     [1]: string secret_1.
	 *     [2]: int state.
	 * @return \IXR_Error|string IXR_Error on failure, secret_2 on success.
	 */
	public function verify_action( $params ) {
		$action        = isset( $params[0] ) ? $params[0] : '';
		$verify_secret = isset( $params[1] ) ? $params[1] : '';
		$state         = isset( $params[2] ) ? $params[2] : '';

		$result = ( new Secrets() )->verify( $action, $verify_secret, $state );

		if ( is_wp_error( $result ) ) {
			return $this->error( $result );
		}

		return $result;
	}

	/**
	 * Wrapper for wp_authenticate( $username, $password );
	 *
	 * @return \WP_User|bool
	 */
	public function login() {
		$this->connection->require_jetpack_authentication();
		$user = wp_authenticate( 'username', 'password' );
		if ( is_wp_error( $user ) ) {
			if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything.
				$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
			} else {
				$this->error = $user;
			}
			return false;
		} elseif ( ! $user ) { // Shouldn't happen.
			$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
			return false;
		}

		wp_set_current_user( $user->ID );

		return $user;
	}

	/**
	 * Returns the current error as an \IXR_Error
	 *
	 * @param \WP_Error|\IXR_Error $error             The error object, optional.
	 * @param string               $event_name The event name.
	 * @param \WP_User             $user              The user object.
	 * @return bool|\IXR_Error
	 */
	public function error( $error = null, $event_name = null, $user = null ) {
		if ( null !== $event_name ) {
			// This action is documented in class.jetpack-xmlrpc-server.php.
			do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user );
		}

		if ( $error !== null ) {
			$this->error = $error;
		}

		if ( is_wp_error( $this->error ) ) {
			$code = $this->error->get_error_data();
			if ( ! $code ) {
				$code = -10520;
			}
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
			if ( ! class_exists( \IXR_Error::class ) ) {
				require_once ABSPATH . WPINC . '/class-IXR.php';
			}
			return new \IXR_Error( $code, $message );
		} elseif ( is_a( $this->error, 'IXR_Error' ) ) {
			return $this->error;
		}

		return false;
	}

	/* API Methods */

	/**
	 * Just authenticates with the given Jetpack credentials.
	 *
	 * @return string A success string. The Jetpack plugin filters it and make it return the Jetpack plugin version.
	 */
	public function test_connection() {
		/**
		 * Filters the successful response of the XMLRPC test_connection method
		 *
		 * @param string $response The response string.
		 */
		return apply_filters( 'jetpack_xmlrpc_test_connection_response', 'success' );
	}

	/**
	 * Test the API user code.
	 *
	 * @param array $args arguments identifying the test site.
	 */
	public function test_api_user_code( $args ) {
		$client_id = (int) $args[0];
		$user_id   = (int) $args[1];
		$nonce     = (string) $args[2];
		$verify    = (string) $args[3];

		if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) {
			return false;
		}

		$user = get_user_by( 'id', $user_id );
		if ( ! $user || is_wp_error( $user ) ) {
			return false;
		}

		/* phpcs:ignore
		 debugging
		error_log( "CLIENT: $client_id" );
		error_log( "USER:   $user_id" );
		error_log( "NONCE:  $nonce" );
		error_log( "VERIFY: $verify" );
		*/

		$jetpack_token = ( new Tokens() )->get_access_token( $user_id );

		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
		if ( ! $api_user_code ) {
			return false;
		}

		$hmac = hash_hmac(
			'md5',
			json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
				(object) array(
					'client_id' => (int) $client_id,
					'user_id'   => (int) $user_id,
					'nonce'     => (string) $nonce,
					'code'      => (string) $api_user_code,
				)
			),
			$jetpack_token->secret
		);

		if ( ! hash_equals( $hmac, $verify ) ) {
			return false;
		}

		return $user_id;
	}

	/**
	 * Unlink a user from WordPress.com
	 *
	 * When the request is done without any parameter, this XMLRPC callback gets an empty array as input.
	 *
	 * If $user_id is not provided, it will try to disconnect the current logged in user. This will fail if called by the Master User.
	 *
	 * If $user_id is is provided, it will try to disconnect the informed user, even if it's the Master User.
	 *
	 * @param mixed $user_id The user ID to disconnect from this site.
	 */
	public function unlink_user( $user_id = array() ) {
		$user_id = (int) $user_id;
		if ( $user_id < 1 ) {
			$user_id = null;
		}
		/**
		 * Fired when we want to log an event to the Jetpack event log.
		 *
		 * @since 1.7.0
		 * @since-jetpack 7.7.0
		 *
		 * @param string $code Unique name for the event.
		 * @param string $data Optional data about the event.
		 */
		do_action( 'jetpack_event_log', 'unlink' );
		return $this->connection->disconnect_user(
			$user_id,
			(bool) $user_id
		);
	}

	/**
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
	 * and the remote Jetpack site.
	 *
	 * @return array
	 */
	public function validate_urls_for_idc_mitigation() {
		return array(
			'home'    => Urls::home_url(),
			'siteurl' => Urls::site_url(),
		);
	}

	/**
	 * Updates the attachment parent object.
	 *
	 * @param array $args attachment and parent identifiers.
	 */
	public function update_attachment_parent( $args ) {
		$attachment_id = (int) $args[0];
		$parent_id     = (int) $args[1];

		return wp_update_post(
			array(
				'ID'          => $attachment_id,
				'post_parent' => $parent_id,
			)
		);
	}

	/**
	 * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
	 *
	 * Disconnect this blog from the connected wordpress.com account
	 *
	 * @deprecated since 1.25.0
	 * @see Jetpack_XMLRPC_Methods::disconnect_blog() in the Jetpack plugin
	 *
	 * @return boolean
	 */
	public function disconnect_blog() {
		_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::disconnect_blog()' );
		if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
			return Jetpack_XMLRPC_Methods::disconnect_blog();
		}
		return false;
	}

	/**
	 * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
	 *
	 * Returns what features are available. Uses the slug of the module files.
	 *
	 * @deprecated since 1.25.0
	 * @see Jetpack_XMLRPC_Methods::features_available() in the Jetpack plugin
	 *
	 * @return array
	 */
	public function features_available() {
		_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::features_available()' );
		if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
			return Jetpack_XMLRPC_Methods::features_available();
		}
		return array();
	}

	/**
	 * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
	 *
	 * Returns what features are enabled. Uses the slug of the modules files.
	 *
	 * @deprecated since 1.25.0
	 * @see Jetpack_XMLRPC_Methods::features_enabled() in the Jetpack plugin
	 *
	 * @return array
	 */
	public function features_enabled() {
		_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::features_enabled()' );
		if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
			return Jetpack_XMLRPC_Methods::features_enabled();
		}
		return array();
	}

	/**
	 * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
	 *
	 * Serve a JSON API request.
	 *
	 * @deprecated since 1.25.0
	 * @see Jetpack_XMLRPC_Methods::json_api() in the Jetpack plugin
	 *
	 * @param array $args request arguments.
	 */
	public function json_api( $args = array() ) {
		_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::json_api()' );
		if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
			return Jetpack_XMLRPC_Methods::json_api( $args );
		}
		return array();
	}

}