dvadf
File manager - Edit - /home/theblueo/tv/wp-includes/pomo/lib/api.tar
Back
WordPressTranslations.php 0000666 00000002716 15214164776 0011636 0 ustar 00 <?php /** * */ class Loco_api_WordPressTranslations { /** * @var array */ private $installed; /** * Wrap wp_get_available_translations * @return array */ public function getAvailableCore(){ if( ! function_exists('wp_get_available_translations') ){ require_once ABSPATH.'wp-admin/includes/translation-install.php'; } // WordPress will raise Warning if offline return wp_get_available_translations(); } /** * Check if a given locale is installed * @return bool */ public function isInstalled( $locale ){ if( ! isset($this->installed) ){ // wp-includes/l10n.php should always be included at runtime $this->installed = array( 'en_US' => 1 ) + array_flip( get_available_languages() ); } return array_key_exists( (string) $locale, $this->installed ); } /** * Get WordPress locale data by strictly well-formed language tag * @return array */ public function getLocaleData( $tag ){ $all = $this->getAvailableCore(); return isset($all[$tag]) ? $all[$tag] : null; } /** * Get and populate a Locale object from a valid language tag * @return Loco_Locale */ public function getLocale( $tag ){ $obj = Loco_Locale::parse($tag); if( $obj->isValid() ){ $obj->fetchName( $this ); } return $obj; } } WordPressFileSystem.php 0000666 00000021252 15214164776 0011235 0 ustar 00 <?php /** * Abstracts WordPress filesystem connection. * https://codex.wordpress.org/Filesystem_API */ class Loco_api_WordPressFileSystem { /** * @var WP_Filesystem_Direct */ private $fs; /** * Credentials form HTML echoed from request_filesystem_credentials * @var string */ private $form = ''; /** * Credentials posted into the API * @var array */ private $creds_in = array(); /** * Credentials returned from the API * @var array */ private $creds_out = array(); /** * Get HTML form rendered by request_filesystem_credentials * @return string */ public function getForm(){ return $this->form; } /** * Authorize for the creation of a file that does not exist * @return bool whether file system is authorized NOT necessarily whether file is creatable */ public function authorizeCreate( Loco_fs_File $file ){ if( $file->exists() ){ throw new Loco_error_WriteException( sprintf( __('%s already exists in this folder','loco'), $file->basename() ) ); } return $file->creatable() || $this->authorize($file); } /** * Authorize for the update of a file that does exist * @return bool whether file system is authorized NOT necessarily whether file is updatable */ public function authorizeUpdate( Loco_fs_File $file ){ if( ! $file->exists() ){ throw new Loco_error_WriteException("File doesn't exist, try authorizeCreate"); } return $file->writable() || $this->authorize($file); } /** * Authorize for the removal of an existing file * @return bool whether file system is authorized NOT necessarily whether file is removable */ public function authorizeDelete( Loco_fs_File $file ){ if( ! $file->exists() ){ throw new Loco_error_WriteException("Can't delete a file that doesn't exist"); } return $file->deletable() || $this->authorize($file); } /** * Authorizes update or create, depending on whether file exists * @return bool whether file system is authorized */ public function authorizeWrite( Loco_fs_File $file ){ return ( $file->exists() ? $file->writable() : $file->creatable() ) || $this->authorize($file); } /** * Authorize connection for any operations, regardless of whether direct file system is available. * @return bool whether file system is authorized */ public function authorizeConnect( Loco_fs_File $file ){ return $this->authorize($file); } /** * Wraps `request_filesystem_credentials` negotiation to obtain a remote connection and buffer WordPress form outout * Call before output started, because buffers. */ private function authorize( Loco_fs_File $file ){ $this->fs = null; $this->form = ''; $this->creds_out = array(); // observe settings held temporarily in session try { $session = Loco_data_Session::get(); if( isset($session['loco-fs']) ){ $creds = $session['loco-fs']; if( $this->tryCredentials($creds,$file) ){ $this->creds_in = array(); return true; } } } catch( Exception $e ){ // tollerate session failure } $post = Loco_mvc_PostParams::get(); $dflt = array( 'hostname' => '', 'username' => '', 'password' => '', 'public_key' => '', 'private_key' => '', 'connection_type' => ''); $this->creds_in = array_intersect_key( $post->getArrayCopy(), $dflt ); // deliberately circumventing call to `get_filesystem_method` // risk of WordPress version compatibility issues, but only sane way to force a remote connection // @codeCoverageIgnoreStart if( defined('FS_METHOD') && FS_METHOD ){ $type = FS_METHOD; // forcing direct access means request_filesystem_credentials will never give us a form :( if( 'direct' === $type ){ return false; } } else if( 'ssh' === $post->connection_type && extension_loaded('ssh2') && function_exists('stream_get_contents') ){ $type = 'ssh2'; } else if( extension_loaded('ftp') ){ $type = 'ftpext'; } else if( extension_loaded('sockets') || function_exists('fsockopen') ){ $type = 'ftpsockets'; } // @codeCoverageIgnoreEnd else { $type = ''; } // context is nonsense here as the system doesn't know what operation we're performing // testing directory write-permission when we're updating a file, for example. $context = '/ignore/this'; $type = apply_filters( 'filesystem_method', $type, $post->getArrayCopy(), $context, true ); // the only params we'll pass into form will be those used by the ajax fsConnect end point $extra = array( 'loco-nonce', 'path', 'auth' ); // capture WordPress output during negotiation. $buffer = Loco_output_Buffer::start(); if( $creds = request_filesystem_credentials( '', $type, false, $context, $extra ) ){ // credentials passed through, should allow connect if they are correct if( $this->tryCredentials( $creds?$creds:array(), $file ) ){ $this->persistCredentials(); return true; } // else there must be an error with the credentials $error = true; // pull more useful connection error for display in form if( isset($GLOBALS['wp_filesystem']) ){ $fs = $GLOBALS['wp_filesystem']; $GLOBALS['wp_filesystem'] = null; if( $fs && $fs->errors && $fs->errors->get_error_code() ){ $error = $fs->errors; } } // annoyingly WordPress moves the error notice above the navigation tabs :-/ request_filesystem_credentials( '', $type, $error, $context, $extra ); } // now have unauthorized remote connection $this->form = (string) $buffer->close(); return false; } /** * @internal * @return bool when credentials connected ok */ private function tryCredentials( array $creds, Loco_fs_File $file ){ // lazy construct the file system from current credentials if possible // in typical WordPress style, after success the object will be held in a global. if( WP_Filesystem( $creds, '/ignore/this/' ) ){ $this->fs = $GLOBALS['wp_filesystem']; // hook new file system into write context (specifying that connect has already been performed) $file->getWriteContext()->connect( $this->fs, false ); // $this->creds_out = $creds; return true; } return false; } /** * Set current credentials in session if settings allow * @return bool whether creds persisted */ private function persistCredentials(){ try { $settings = Loco_data_Settings::get(); if( $settings['fs_persist'] ){ $session = Loco_data_Session::get(); $session['loco-fs'] = $this->creds_out; $session->persist(); return true; } } catch( Exception $e ){ // tollerate session failure Loco_error_AdminNotices::debug( $e->getMessage() ); } return false; } /** * Get working credentials that resulted in connection * @return array */ public function getOutputCredentials(){ return $this->creds_out; } /** * Get input credentials from original post. * this is not the same as getCredentials. It is designed for replay only, regardless of success * Note that input to request_filesystem_credentials is not the same as the output (specifically how hostname:port is handled) */ public function getInputCredentials(){ return $this->creds_in; } /** * Get currently configured filesystem API * @return WP_Filesystem_Direct */ public function getFileSystem(){ if( ! $this->fs ){ $this->fs = new WP_Filesystem_Direct( null ); } return $this->fs; } /** * Check if filesystem access is direct * @return bool */ public function isDirect(){ return 'direct' === $this->getFileSystem()->method; } } class-wc-rest-coupons-controller.php 0000666 00000053443 15214171310 0013621 0 ustar 00 <?php /** * REST API Coupons controller * * Handles requests to the /coupons endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Coupons controller class. * * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'coupons'; /** * Post type. * * @var string */ protected $post_type = 'shop_coupon'; /** * Order refunds actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for coupons. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'required' => true, 'type' => 'string', ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Query args. * * @param array $args * @param WP_REST_Request $request * @return array */ public function query_args( $args, $request ) { global $wpdb; if ( ! empty( $request['code'] ) ) { $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $request['code'] ) ); $args['post__in'] = array( $id ); } return $args; } /** * Prepare a single coupon output for response. * * @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 ) { global $wpdb; // Get the coupon code. $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $post->ID ) ); $coupon = new WC_Coupon( $code ); $data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), 'discount_type' => $coupon->type, 'description' => $post->post_excerpt, 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'expiry_date' => $coupon->expiry_date ? wc_rest_prepare_date_response( date( 'Y-m-d', $coupon->expiry_date ) ) : null, 'usage_count' => (int) $coupon->usage_count, 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'free_shipping' => $coupon->enable_free_shipping(), 'product_categories' => array_map( 'absint', (array) $coupon->product_categories ), 'excluded_product_categories' => array_map( 'absint', (array) $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), 'email_restrictions' => $coupon->customer_email, 'used_by' => $coupon->get_used_by(), ); $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( $post ) ); /** * 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. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { global $wpdb; $data = new stdClass; // ID. if ( isset( $request['id'] ) ) { $data->ID = absint( $request['id'] ); } $schema = $this->get_item_schema(); // Validate required POST fields. if ( 'POST' === $request->get_method() && empty( $data->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 ) ); } } // Code. if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); $id = isset( $data->ID ) ? $data->ID : 0; // Check for duplicate coupon codes. $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' AND $wpdb->posts.ID != %s ", $coupon_code, $id ) ); if ( $coupon_found ) { return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); } $data->post_title = $coupon_code; } // Content. $data->post_content = ''; // Coupon description (excerpt). if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $data->post_excerpt = wp_filter_post_kses( $request['description'] ); } // Post type. $data->post_type = $this->post_type; // Post status. $data->post_status = 'publish'; // Comment status. $data->comment_status = 'closed'; // Ping status. $data->ping_status = 'closed'; /** * 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 stdClass $data An object representing a single item prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); } /** * Expiry date format. * * @param string $expiry_date * @return string */ protected function get_coupon_expiry_date( $expiry_date ) { if ( '' !== $expiry_date ) { return date( 'Y-m-d', strtotime( $expiry_date ) ); } return ''; } /** * Add post meta fields. * * @param WP_Post $post * @param WP_REST_Request $request * @return bool|WP_Error */ protected function add_post_meta_fields( $post, $request ) { $data = array_filter( $request->get_params() ); $defaults = array( 'discount_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' => '', 'free_shipping' => false, 'product_categories' => array(), 'excluded_product_categories' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'email_restrictions' => array(), 'description' => '' ); $data = wp_parse_args( $data, $defaults ); // Set coupon meta. update_post_meta( $post->ID, 'discount_type', $data['discount_type'] ); update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); update_post_meta( $post->ID, 'usage_limit', absint( $data['usage_limit'] ) ); update_post_meta( $post->ID, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); update_post_meta( $post->ID, 'free_shipping', ( true === $data['free_shipping'] ) ? 'yes' : 'no' ); update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_categories'] ) ) ); update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['excluded_product_categories'] ) ) ); update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['email_restrictions'] ) ) ); return true; } /** * Update post meta fields. * * @param WP_Post $post * @param WP_REST_Request $request * @return bool|WP_Error */ protected function update_post_meta_fields( $post, $request ) { if ( isset( $request['amount'] ) ) { update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $request['amount'] ) ); } if ( isset( $request['individual_use'] ) ) { update_post_meta( $post->ID, 'individual_use', ( true === $request['individual_use'] ) ? 'yes' : 'no' ); } if ( isset( $request['product_ids'] ) ) { update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $request['product_ids'] ) ) ) ); } if ( isset( $request['exclude_product_ids'] ) ) { update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $request['exclude_product_ids'] ) ) ) ); } if ( isset( $request['usage_limit'] ) ) { update_post_meta( $post->ID, 'usage_limit', absint( $request['usage_limit'] ) ); } if ( isset( $request['usage_limit_per_user'] ) ) { update_post_meta( $post->ID, 'usage_limit_per_user', absint( $request['usage_limit_per_user'] ) ); } if ( isset( $request['limit_usage_to_x_items'] ) ) { update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $request['limit_usage_to_x_items'] ) ); } if ( isset( $request['usage_count'] ) ) { update_post_meta( $post->ID, 'usage_count', absint( $request['usage_count'] ) ); } if ( isset( $request['expiry_date'] ) ) { update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); } if ( isset( $request['free_shipping'] ) ) { update_post_meta( $post->ID, 'free_shipping', ( true === $request['free_shipping'] ) ? 'yes' : 'no' ); } if ( isset( $request['product_categories'] ) ) { update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_categories'] ) ) ); } if ( isset( $request['excluded_product_categories'] ) ) { update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['excluded_product_categories'] ) ) ); } if ( isset( $request['exclude_sale_items'] ) ) { update_post_meta( $post->ID, 'exclude_sale_items', ( true === $request['exclude_sale_items'] ) ? 'yes' : 'no' ); } if ( isset( $request['minimum_amount'] ) ) { update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $request['minimum_amount'] ) ); } if ( isset( $request['maximum_amount'] ) ) { update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); } if ( isset( $request['email_restrictions'] ) ) { update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['email_restrictions'] ) ) ); } return true; } /** * Get the Coupon's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'Coupon description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount_type' => array( 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), 'type' => 'string', 'default' => 'fixed_cart', 'enum' => array_keys( wc_get_coupon_types() ), 'context' => array( 'view', 'edit' ), ), 'amount' => array( 'description' => __( 'The amount of discount.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'expiry_date' => array( 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'usage_count' => array( 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'individual_use' => array( 'description' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_ids' => array( 'description' => __( "List of product ID's the coupon can be used on.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'exclude_product_ids' => array( 'description' => __( "List of product ID's the coupon cannot be used on.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'usage_limit' => array( 'description' => __( 'How many times the coupon can be used.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'usage_limit_per_user' => array( 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'limit_usage_to_x_items' => array( 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'free_shipping' => array( 'description' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'product_categories' => array( 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'excluded_product_categories' => array( 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'exclude_sale_items' => array( 'description' => __( 'Define if should not apply when have sale items.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'minimum_amount' => array( 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'maximum_amount' => array( 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email_restrictions' => array( 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit' ), ), 'used_by' => array( 'description' => __( 'List of user IDs who have used the coupon.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['code'] = array( 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-products-controller.php 0000666 00000275331 15214171310 0014000 0 ustar 00 <?php /** * REST API Products controller * * Handles requests to the /products endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Products controller class. * * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products'; /** * Post type. * * @var string */ protected $post_type = 'product'; /** * Initialize product actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); } /** * Register the routes for products. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), 'type' => 'boolean', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Get post types. * * @return array */ protected function get_post_types() { return array( 'product', 'product_variation' ); } /** * Query args. * * @param array $args * @param WP_REST_Request $request * @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 ] ) ) { $terms = explode( ',', $request[ $key ] ); $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $terms, ); } } // Filter product type by slug. if ( ! empty( $request['type'] ) ) { $terms = explode( ',', $request['type'] ); $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $terms, ); } // Filter by attribute and term. if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names() ) ) { $terms = explode( ',', $request['attribute_term'] ); $tax_query[] = array( 'taxonomy' => $request['attribute'], 'field' => 'term_id', 'terms' => $terms, ); } } if ( ! empty( $tax_query ) ) { $args['tax_query'] = $tax_query; } // Filter by sku. if ( ! empty( $request['sku'] ) ) { if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); } $args['meta_query'][] = array( 'key' => '_sku', 'value' => $request['sku'], 'compare' => '=' ); } // Apply all WP_Query filters again. if ( is_array( $request['filter'] ) ) { $args = array_merge( $args, $request['filter'] ); unset( $args['filter'] ); } // Force the post_type argument, since it's not a user input variable. if ( ! empty( $request['sku'] ) ) { $args['post_type'] = $this->get_post_types(); } else { $args['post_type'] = $this->post_type; } return $args; } /** * Get the downloads for a product or product variation. * * @param WC_Product|WC_Product_Variation $product * @return array */ protected function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_files() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // MD5 hash. 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get taxonomy terms. * * @param WC_Product $product * @param string $taxonomy * @return array */ protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { $terms = array(); foreach ( wp_get_post_terms( $product->id, 'product_' . $taxonomy ) as $term ) { $terms[] = array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ); } return $terms; } /** * Get the images for a product or product variation. * * @param WC_Product|WC_Product_Variation $product * @return array */ protected function get_images( $product ) { $images = array(); $attachment_ids = array(); if ( $product->is_type( 'variation' ) ) { if ( has_post_thumbnail( $product->get_variation_id() ) ) { // Add variation image if set. $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); } elseif ( has_post_thumbnail( $product->id ) ) { // Otherwise use the parent product featured image if set. $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { // Add featured image. if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'name' => 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, 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), 'src' => wc_placeholder_img_src(), 'name' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Get attribute taxonomy label. * * @param string $name * @return string */ protected function get_attribute_taxonomy_label( $name ) { $tax = get_taxonomy( $name ); $labels = get_taxonomy_labels( $tax ); return $labels->singular_name; } /** * Get default attributes. * * @param WC_Product $product * @return array */ protected function get_default_attributes( $product ) { $default = array(); if ( $product->is_type( 'variable' ) ) { foreach ( array_filter( (array) get_post_meta( $product->id, '_default_attributes', true ), 'strlen' ) as $key => $value ) { if ( 0 === strpos( $key, 'pa_' ) ) { $default[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $key ), 'name' => $this->get_attribute_taxonomy_label( $key ), 'option' => $value, ); } else { $default[] = array( 'id' => 0, 'name' => str_replace( 'pa_', '', $key ), 'option' => $value, ); } } } return $default; } /** * 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. * * @param WC_Product|WC_Product_Variation $product * @return array */ protected function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // Variation attributes. foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { $name = str_replace( 'attribute_', '', $attribute_name ); if ( ! $attribute ) { continue; } // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { $option_term = get_term_by( 'slug', $attribute, $name ); $attributes[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $name ), 'name' => $this->get_attribute_taxonomy_label( $name ), 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, ); } else { $attributes[] = array( 'id' => 0, 'name' => str_replace( 'pa_', '', $name ), 'option' => $attribute, ); } } } else { foreach ( $product->get_attributes() as $attribute ) { if ( $attribute['is_taxonomy'] ) { $attributes[] = array( 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->id, $attribute ), ); } else { $attributes[] = array( 'id' => 0, 'name' => str_replace( 'pa_', '', $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->id, $attribute ), ); } } } return $attributes; } /** * Get product menu order. * * @param WC_Product $product * @return int */ protected function get_product_menu_order( $product ) { $menu_order = $product->get_post_data()->menu_order; if ( $product->is_type( 'variation' ) ) { $variation = get_post( $product->get_variation_id() ); $menu_order = $variation->menu_order; } return $menu_order; } /** * Get product data. * * @param WC_Product $product * @return array */ protected function get_product_data( $product ) { $data = array( 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'name' => $product->get_title(), 'slug' => $product->get_post_data()->post_name, 'permalink' => $product->get_permalink(), 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_status, 'featured' => $product->is_featured(), 'catalog_visibility' => $product->visibility, 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'sku' => $product->get_sku(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', 'date_on_sale_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', 'date_on_sale_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', 'price_html' => $product->get_price_html(), 'on_sale' => $product->is_on_sale(), 'purchasable' => $product->is_purchasable(), 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), 'virtual' => $product->is_virtual(), 'downloadable' => $product->is_downloadable(), 'downloads' => $this->get_downloads( $product ), 'download_limit' => '' !== $product->download_limit ? (int) $product->download_limit : -1, 'download_expiry' => '' !== $product->download_expiry ? (int) $product->download_expiry : -1, 'download_type' => $product->download_type ? $product->download_type : 'standard', 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'manage_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders' => $product->backorders, 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'weight' => $product->get_weight(), 'dimensions' => array( 'length' => $product->get_length(), 'width' => $product->get_width(), 'height' => $product->get_height(), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => (int) $product->get_shipping_class_id(), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => (int) $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), 'categories' => $this->get_taxonomy_terms( $product ), 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), 'images' => $this->get_images( $product ), 'attributes' => $this->get_attributes( $product ), 'default_attributes' => $this->get_default_attributes( $product ), 'variations' => array(), 'grouped_products' => array(), 'menu_order' => $this->get_product_menu_order( $product ), ); return $data; } /** * Get an individual variation's data. * * @param WC_Product $product * @return array */ protected function get_variation_data( $product ) { $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = $product->get_child( $child_id ); if ( ! $variation->exists() ) { continue; } $post_data = get_post( $variation->get_variation_id() ); $variations[] = array( 'id' => $variation->get_variation_id(), 'date_created' => wc_rest_prepare_date_response( $post_data->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $post_data->post_modified_gmt ), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price(), 'date_on_sale_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', 'on_sale' => $variation->is_on_sale(), 'purchasable' => $variation->is_purchasable(), 'visible' => $variation->is_visible(), 'virtual' => $variation->is_virtual(), 'downloadable' => $variation->is_downloadable(), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => '' !== $variation->download_limit ? (int) $variation->download_limit : -1, 'download_expiry' => '' !== $variation->download_expiry ? (int) $variation->download_expiry : -1, 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'manage_stock' => $variation->managing_stock(), 'stock_quantity' => $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backorders' => $variation->backorders, 'backorders_allowed' => $variation->backorders_allowed(), 'backordered' => $variation->is_on_backorder(), 'weight' => $variation->get_weight(), 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => $variation->get_shipping_class_id(), 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), ); } return $variations; } /** * Prepare a single product output for response. * * @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 ) { $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'] = $this->get_variation_data( $product ); } // 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 ) ); /** * 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 links for the request. * * @param WC_Product $product Product object. * @return array Links for the given product. */ protected function prepare_links( $product ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( $product->is_type( 'variation' ) && $product->parent ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->parent->id ) ), ); } elseif ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->post->post_parent ) ), ); } return $links; } /** * Prepare a single product for create or update. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { $data = new stdClass; // ID. if ( isset( $request['id'] ) ) { $data->ID = absint( $request['id'] ); } // Post title. if ( isset( $request['name'] ) ) { $data->post_title = wp_filter_post_kses( $request['name'] ); } // Post content. if ( isset( $request['description'] ) ) { $data->post_content = wp_filter_post_kses( $request['description'] ); } // Post excerpt. if ( isset( $request['short_description'] ) ) { $data->post_excerpt = wp_filter_post_kses( $request['short_description'] ); } // Post status. if ( isset( $request['status'] ) ) { $data->post_status = get_post_status_object( $request['status'] ) ? $request['status'] : 'draft'; } // Post slug. if ( isset( $request['slug'] ) ) { $data->post_name = $request['slug']; } // Menu order. if ( isset( $request['menu_order'] ) ) { $data->menu_order = (int) $request['menu_order']; } // Comment status. if ( isset( $request['reviews_allowed'] ) ) { $data->comment_status = $request['reviews_allowed'] ? 'open' : 'closed'; } // Only when creating products. if ( empty( $request['id'] ) ) { // Post type. $data->post_type = $this->post_type; // Ping status. $data->ping_status = 'closed'; } /** * 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 stdClass $data An object representing a single item prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); } /** * Save product images. * * @param int $product_id * @param array $images * @throws WC_REST_Exception */ protected function save_product_images( $product_id, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_id ); } if ( ! wp_attachment_is_image( $attachment_id ) ) { throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { set_post_thumbnail( $product_id, $attachment_id ); } else { $gallery[] = $attachment_id; } // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); } } if ( ! empty( $gallery ) ) { update_post_meta( $product_id, '_product_image_gallery', implode( ',', $gallery ) ); } } else { delete_post_meta( $product_id, '_thumbnail_id' ); update_post_meta( $product_id, '_product_image_gallery', '' ); } } /** * Save product shipping data. * * @param int $product_id * @param array $data */ private function save_product_shipping_data( $product_id, $data ) { // Virtual. if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { update_post_meta( $product_id, '_weight', '' ); update_post_meta( $product_id, '_length', '' ); update_post_meta( $product_id, '_width', '' ); update_post_meta( $product_id, '_height', '' ); } else { if ( isset( $data['weight'] ) ) { update_post_meta( $product_id, '_weight', '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); } // Height. if ( isset( $data['dimensions']['height'] ) ) { update_post_meta( $product_id, '_height', '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width. if ( isset( $data['dimensions']['width'] ) ) { update_post_meta( $product_id, '_width', '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); } // Length. if ( isset( $data['dimensions']['length'] ) ) { update_post_meta( $product_id, '_length', '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Shipping class. if ( isset( $data['shipping_class'] ) ) { wp_set_object_terms( $product_id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); } } /** * Save downloadable files. * * @param in $product_id * @param array $downloads * @param int $variation_id */ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { $files = array(); // File paths will be stored in an array keyed off md5(file path). foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( ! isset( $file['file'] ) ) { continue; } $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; if ( 0 === strpos( $file['file'], 'http' ) ) { $file_url = esc_url_raw( $file['file'] ); } else { $file_url = wc_clean( $file['file'] ); } $files[ md5( $file_url ) ] = array( 'name' => $file_name, 'file' => $file_url, ); } // Grant permission to any newly added files on any existing orders for this product prior to saving. do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); $id = ( 0 === $variation_id ) ? $product_id : $variation_id; update_post_meta( $id, '_downloadable_files', $files ); } /** * Save taxonomy terms. * * @param int $product_id * @param array $terms * @param string $taxonomy * @return array */ protected function save_taxonomy_terms( $product_id, $terms, $taxonomy = 'cat' ) { $term_ids = wp_list_pluck( $terms, 'id' ); $term_ids = array_unique( array_map( 'intval', $term_ids ) ); wp_set_object_terms( $product_id, $term_ids, 'product_' . $taxonomy ); return $terms; } /** * Save product meta. * * @param WC_Product $product * @param WP_REST_Request $request * @return bool * @throws WC_REST_Exception */ protected function save_product_meta( $product, $request ) { global $wpdb; // Product Type. $product_type = null; if ( isset( $request['type'] ) ) { $product_type = wc_clean( $request['type'] ); wp_set_object_terms( $product->id, $product_type, 'product_type' ); } else { $_product_type = get_the_terms( $product->id, 'product_type' ); if ( is_array( $_product_type ) ) { $_product_type = current( $_product_type ); $product_type = $_product_type->slug; } } // Default total sales. add_post_meta( $product->id, 'total_sales', '0', true ); // Virtual. if ( isset( $request['virtual'] ) ) { update_post_meta( $product->id, '_virtual', true === $request['virtual'] ? 'yes' : 'no' ); } // Tax status. if ( isset( $request['tax_status'] ) ) { update_post_meta( $product->id, '_tax_status', wc_clean( $request['tax_status'] ) ); } // Tax Class. if ( isset( $request['tax_class'] ) ) { update_post_meta( $product->id, '_tax_class', wc_clean( $request['tax_class'] ) ); } // Catalog Visibility. if ( isset( $request['catalog_visibility'] ) ) { update_post_meta( $product->id, '_visibility', wc_clean( $request['catalog_visibility'] ) ); } // Purchase Note. if ( isset( $request['purchase_note'] ) ) { update_post_meta( $product->id, '_purchase_note', wc_clean( $request['purchase_note'] ) ); } // Featured Product. if ( isset( $request['featured'] ) ) { update_post_meta( $product->id, '_featured', true === $request['featured'] ? 'yes' : 'no' ); } // Shipping data. $this->save_product_shipping_data( $product->id, $request ); // SKU. if ( isset( $request['sku'] ) ) { $sku = get_post_meta( $product->id, '_sku', true ); $new_sku = wc_clean( $request['sku'] ); if ( '' === $new_sku ) { update_post_meta( $product->id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product->id, $new_sku ); if ( ! $unique_sku ) { $sku_found = wc_get_product_id_by_sku( $sku ); throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) ); } else { update_post_meta( $product->id, '_sku', $new_sku ); } } else { update_post_meta( $product->id, '_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(); } // Update post terms. if ( taxonomy_exists( $attribute_name ) ) { wp_set_object_terms( $product->id, $values, $attribute_name ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attributes[ $attribute_name ] = array( 'name' => $attribute_name, 'value' => '', 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => 1, ); } } elseif ( isset( $attribute['options'] ) ) { // Array based. if ( is_array( $attribute['options'] ) ) { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); // Text based, separate by pipe. } else { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); } // Custom attribute - Add attribute to array and set the values. $attributes[ sanitize_title( $attribute_name ) ] = array( 'name' => $attribute_name, 'value' => $values, 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => 0, ); } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); update_post_meta( $product->id, '_product_attributes', $attributes ); } // Sales and prices. if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices. update_post_meta( $product->id, '_regular_price', '' ); update_post_meta( $product->id, '_sale_price', '' ); update_post_meta( $product->id, '_sale_price_dates_from', '' ); update_post_meta( $product->id, '_sale_price_dates_to', '' ); update_post_meta( $product->id, '_price', '' ); } else { // Regular Price if ( isset( $request['regular_price'] ) ) { $regular_price = ( '' === $request['regular_price'] ) ? '' : $request['regular_price']; } else { $regular_price = get_post_meta( $product->id, '_regular_price', true ); } // Sale Price if ( isset( $request['sale_price'] ) ) { $sale_price = ( '' === $request['sale_price'] ) ? '' : $request['sale_price']; } else { $sale_price = get_post_meta( $product->id, '_sale_price', true ); } if ( isset( $request['date_on_sale_from'] ) ) { $date_from = $request['date_on_sale_from']; } else { $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $request['date_on_sale_to'] ) ) { $date_to = $request['date_on_sale_to']; } else { $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $product->id, $regular_price, $sale_price, $date_from, $date_to ); } // Product parent ID for groups. $parent_id = 0; if ( isset( $request['parent_id'] ) ) { $parent_id = wp_update_post( array( 'ID' => $product->id, 'post_parent' => absint( $request['parent_id'] ) ) ); } // Update parent if grouped so price sorting works and stays in sync with the cheapest child. if ( $parent_id > 0 || 'grouped' === $product_type ) { $clear_parent_ids = array(); if ( $parent_id > 0 ) { $clear_parent_ids[] = $parent_id; } if ( 'grouped' === $product_type ) { $clear_parent_ids[] = $product->id; } if ( ! empty( $clear_parent_ids ) ) { foreach ( $clear_parent_ids as $clear_id ) { $children_by_price = get_posts( array( 'post_parent' => $clear_id, 'orderby' => 'meta_value_num', 'order' => 'asc', 'meta_key' => '_price', 'posts_per_page' => 1, 'post_type' => 'product', 'fields' => 'ids' ) ); if ( $children_by_price ) { foreach ( $children_by_price as $child ) { $child_price = get_post_meta( $child, '_price', true ); update_post_meta( $clear_id, '_price', $child_price ); } } } } } // Sold individually. if ( isset( $request['sold_individually'] ) ) { update_post_meta( $product->id, '_sold_individually', true === $request['sold_individually'] ? 'yes' : '' ); } // Stock status. if ( isset( $request['in_stock'] ) ) { $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; } else { $stock_status = get_post_meta( $product->id, '_stock_status', true ); if ( '' === $stock_status ) { $stock_status = 'instock'; } } // Stock data. if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $manage_stock = ( true === $request['manage_stock'] ) ? 'yes' : 'no'; update_post_meta( $product->id, '_manage_stock', $manage_stock ); } else { $manage_stock = get_post_meta( $product->id, '_manage_stock', true ); } // Backorders. if ( isset( $request['backorders'] ) ) { $backorders = $request['backorders']; update_post_meta( $product->id, '_backorders', $backorders ); } else { $backorders = get_post_meta( $product->id, '_backorders', true ); } if ( 'grouped' === $product_type ) { update_post_meta( $product->id, '_manage_stock', 'no' ); update_post_meta( $product->id, '_backorders', 'no' ); update_post_meta( $product->id, '_stock', '' ); wc_update_product_stock_status( $product->id, $stock_status ); } elseif ( 'external' === $product_type ) { update_post_meta( $product->id, '_manage_stock', 'no' ); update_post_meta( $product->id, '_backorders', 'no' ); update_post_meta( $product->id, '_stock', '' ); wc_update_product_stock_status( $product->id, 'instock' ); } elseif ( 'yes' === $manage_stock ) { update_post_meta( $product->id, '_backorders', $backorders ); // Stock status is always determined by children so sync later. if ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product->id, $stock_status ); } // Stock quantity. if ( isset( $request['stock_quantity'] ) ) { wc_update_product_stock( $product->id, wc_stock_amount( $request['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( get_post_meta( $product->id, '_stock', true ) ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); wc_update_product_stock( $product->id, wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. update_post_meta( $product->id, '_manage_stock', 'no' ); update_post_meta( $product->id, '_backorders', $backorders ); update_post_meta( $product->id, '_stock', '' ); wc_update_product_stock_status( $product->id, $stock_status ); } } elseif ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product->id, $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; } } update_post_meta( $product->id, '_upsell_ids', $upsells ); } else { delete_post_meta( $product->id, '_upsell_ids' ); } } // 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; } } update_post_meta( $product->id, '_crosssell_ids', $crosssells ); } else { delete_post_meta( $product->id, '_crosssell_ids' ); } } // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { $this->save_taxonomy_terms( $product->id, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { $this->save_taxonomy_terms( $product->id, $request['tags'], 'tag' ); } // Downloadable. if ( isset( $request['downloadable'] ) ) { $is_downloadable = true === $request['downloadable'] ? 'yes' : 'no'; update_post_meta( $product->id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $product->id, '_downloadable', true ); } // Downloadable options. if ( 'yes' === $is_downloadable ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { $this->save_downloadable_files( $product->id, $request['downloads'] ); } // Download limit. if ( isset( $request['download_limit'] ) ) { update_post_meta( $product->id, '_download_limit', -1 === $request['download_limit'] ? '' : absint( $request['download_limit'] ) ); } // Download expiry. if ( isset( $request['download_expiry'] ) ) { update_post_meta( $product->id, '_download_expiry', -1 === $request['download_expiry'] ? '' : absint( $request['download_expiry'] ) ); } // Download type. if ( isset( $request['download_type'] ) ) { update_post_meta( $product->id, '_download_type', 'standard' === $request['download_type'] ? '' : wc_clean( $request['download_type'] ) ); } } // Product url and button text for external products. if ( 'external' === $product_type ) { if ( isset( $request['external_url'] ) ) { update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); } if ( isset( $request['button_text'] ) ) { update_post_meta( $product->id, '_button_text', wc_clean( $request['button_text'] ) ); } } return true; } /** * Save variations. * * @param WC_Product $product * @param WP_REST_Request $request * @return bool * @throws WC_REST_Exception */ protected function save_variations_data( $product, $request ) { global $wpdb; $variations = $request['variations']; $attributes = $product->get_attributes(); foreach ( $variations as $menu_order => $variation ) { $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; // Generate a useful post title. $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $product->id ) ) ); // Update or Add post. if ( ! $variation_id ) { $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; $new_variation = array( 'post_title' => $variation_post_title, 'post_content' => '', 'post_status' => $post_status, 'post_author' => get_current_user_id(), 'post_parent' => $product->id, 'post_type' => 'product_variation', 'menu_order' => $menu_order, ); $variation_id = wp_insert_post( $new_variation ); do_action( 'woocommerce_create_product_variation', $variation_id ); } else { $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); if ( isset( $variation['visible'] ) ) { $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; $update_variation['post_status'] = $post_status; } $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); do_action( 'woocommerce_update_product_variation', $variation_id ); } // Stop with we don't have a variation ID. if ( is_wp_error( $variation_id ) ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); } // SKU. if ( isset( $variation['sku'] ) ) { $sku = get_post_meta( $variation_id, '_sku', true ); $new_sku = wc_clean( $variation['sku'] ); if ( '' === $new_sku ) { update_post_meta( $variation_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); if ( ! $unique_sku ) { throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); } else { update_post_meta( $variation_id, '_sku', $new_sku ); } } else { update_post_meta( $variation_id, '_sku', '' ); } } } // Thumbnail. if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { $image = current( $variation['image'] ); if ( $image && is_array( $image ) ) { if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); } $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); } // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image name if present. if ( ! empty( $image['name'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); } update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); } } else { delete_post_meta( $variation_id, '_thumbnail_id' ); } } // Virtual variation. if ( isset( $variation['virtual'] ) ) { $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_virtual', $is_virtual ); } // Downloadable variation. if ( isset( $variation['downloadable'] ) ) { $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); } // Shipping data. $this->save_product_shipping_data( $variation_id, $variation ); // Stock handling. if ( isset( $variation['manage_stock'] ) ) { $manage_stock = ( true === $variation['manage_stock'] ) ? 'yes' : 'no'; } else { $manage_stock = get_post_meta( $variation_id, '_manage_stock', true ); } update_post_meta( $variation_id, '_manage_stock', '' === $manage_stock ? 'no' : $manage_stock ); if ( isset( $variation['in_stock'] ) ) { $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; } else { $stock_status = get_post_meta( $variation_id, '_stock_status', true ); } wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); if ( 'yes' === $manage_stock ) { $backorders = get_post_meta( $variation_id, '_backorders', true ); if ( isset( $variation['backorders'] ) ) { $backorders = $variation['backorders']; } update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); if ( isset( $variation['stock_quantity'] ) ) { wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); } elseif ( isset( $request['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); } } else { delete_post_meta( $variation_id, '_backorders' ); delete_post_meta( $variation_id, '_stock' ); } // Regular Price. if ( isset( $variation['regular_price'] ) ) { $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; } else { $regular_price = get_post_meta( $variation_id, '_regular_price', true ); } // Sale Price. if ( isset( $variation['sale_price'] ) ) { $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; } else { $sale_price = get_post_meta( $variation_id, '_sale_price', true ); } if ( isset( $variation['date_on_sale_from'] ) ) { $date_from = $variation['date_on_sale_from']; } else { $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $variation['date_on_sale_to'] ) ) { $date_to = $variation['date_on_sale_to']; } else { $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); // Tax class. if ( isset( $variation['tax_class'] ) ) { if ( $variation['tax_class'] !== 'parent' ) { update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); } else { delete_post_meta( $variation_id, '_tax_class' ); } } // Downloads. if ( 'yes' === $is_downloadable ) { // Downloadable files. if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); } // Download limit. if ( isset( $variation['download_limit'] ) ) { update_post_meta( $variation_id, '_download_limit', -1 === $variation['download_limit'] ? '' : absint( $variation['download_limit'] ) ); } // Download expiry. if ( isset( $variation['download_expiry'] ) ) { update_post_meta( $variation_id, '_download_expiry', -1 === $variation['download_expiry'] ? '' : absint( $variation['download_expiry'] ) ); } } else { update_post_meta( $variation_id, '_download_limit', '' ); update_post_meta( $variation_id, '_download_expiry', '' ); update_post_meta( $variation_id, '_downloadable_files', '' ); } // Description. if ( isset( $variation['description'] ) ) { update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); } // Update taxonomies. if ( isset( $variation['attributes'] ) ) { $updated_attribute_keys = array(); foreach ( $variation['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( $attributes[ $attribute_name ] ) ) { $_attribute = $attributes[ $attribute_name ]; } if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); $updated_attribute_keys[] = $_attribute_key; $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( ! empty( $_attribute['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 ); } } update_post_meta( $variation_id, $_attribute_key, $attribute_value ); } } // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); foreach ( $delete_attribute_keys as $key ) { delete_post_meta( $variation_id, $key ); } } do_action( 'woocommerce_rest_save_product_variation', $variation_id, $menu_order, $variation ); } // Update parent if variable so price sorting works and stays in sync with the cheapest child. WC_Product_Variable::sync( $product->id ); // 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'] ) ) { $default_attributes = array(); foreach ( $request['default_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( $attributes[ $attribute_name ] ) ) { $_attribute = $attributes[ $attribute_name ]; if ( $_attribute['is_variation'] ) { $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; if ( ! empty( $_attribute['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', $value, $attribute_name ); if ( $term && ! is_wp_error( $term ) ) { $value = $term->slug; } else { $value = sanitize_title( $value ); } } if ( $value ) { $default_attributes[ $attribute_name ] = $value; } } } } update_post_meta( $product->id, '_default_attributes', $default_attributes ); } return true; } /** * Add post meta fields. * * @param WP_Post $post * @param WP_REST_Request $request * @return bool|WP_Error */ protected function add_post_meta_fields( $post, $request ) { try { $product = wc_get_product( $post ); // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $this->save_product_images( $product->id, $request['images'] ); } // Save product meta fields. $this->save_product_meta( $product, $request ); // Save variations. if ( isset( $request['type'] ) && 'variable' === $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { $this->save_variations_data( $product, $request ); } return true; } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } } /** * Update post meta fields. * * @param WP_Post $post * @param WP_REST_Request $request * @return bool|WP_Error */ protected function update_post_meta_fields( $post, $request ) { try { $product = wc_get_product( $post ); // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $this->save_product_images( $product->id, $request['images'] ); } // Save product meta fields. $this->save_product_meta( $product, $request ); // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { $this->save_variations_data( $product, $request ); } else { // Just sync variations. WC_Product_Variable::sync( $product->id ); WC_Product_Variable::sync_stock_status( $product->id ); } } return true; } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } } /** * Clear cache/transients. * * @param WP_Post $post Post data. */ public function clear_transients( $post ) { wc_delete_product_transients( $post->ID ); } /** * Delete post. * * @param WP_Post $post */ protected function delete_post( $post ) { // Delete product attachments. $attachments = get_children( array( 'post_parent' => $post->ID, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product. wp_delete_post( $post->ID, true ); } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = (bool) $request['force']; $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || ! in_array( $post->post_type, $this->get_post_types() ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); } $supports_trash = EMPTY_TRASH_DAYS > 0; /** * Filter whether an item is trashable. * * Return false to disable trash support for the item. * * @param boolean $supports_trash Whether the item type support trashing. * @param WP_Post $post The Post object being considered for trashing support. */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); // If we're forcing, then delete permanently. if ( $force ) { $child_product_variations = get_children( 'post_parent=' . $id . '&post_type=product_variation' ); if ( ! empty( $child_product_variations ) ) { foreach ( $child_product_variations as $child ) { wp_delete_post( $child->ID, true ); } } $child_products = get_children( 'post_parent=' . $id . '&post_type=product' ); if ( ! empty( $child_products ) ) { foreach ( $child_products as $child ) { $child_post = array(); $child_post['ID'] = $child->ID; $child_post['post_parent'] = 0; wp_update_post( $child_post ); } } $result = wp_delete_post( $id, true ); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); } // Otherwise, only trash if we haven't already. if ( 'trash' === $post->post_status ) { return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); } // (Note that internally this falls through to `wp_delete_post` if // the trash is disabled.) $result = wp_trash_post( $id ); } if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } // Delete parent product transients. if ( $parent_id = wp_get_post_parent_id( $id ) ) { wc_delete_product_transients( $parent_id ); } /** * Fires after a single item is deleted or trashed via the REST API. * * @param object $post The deleted or trashed item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); return $response; } /** * Get the Product's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $weight_unit = get_option( 'woocommerce_weight_unit' ); $dimension_unit = get_option( 'woocommerce_dimension_unit' ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'slug' => array( 'description' => __( 'Product slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'type' => array( 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'default' => 'simple', 'enum' => array_keys( wc_get_product_types() ), 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Product status (post status).', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_keys( get_post_statuses() ), 'context' => array( 'view', 'edit' ), ), 'featured' => array( 'description' => __( 'Featured product.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'catalog_visibility' => array( 'description' => __( 'Catalog visibility.', 'woocommerce' ), 'type' => 'string', 'default' => 'visible', 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Product description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'short_description' => array( 'description' => __( 'Product short description.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Product regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Product sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( 'End data of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price_html' => array( 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'on_sale' => array( 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_sales' => array( 'description' => __( 'Amount of sales.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'virtual' => array( 'description' => __( 'If the product is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the product is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File MD5 hash.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days that the customer has up to be able to download the product.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), 'download_type' => array( 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), 'type' => 'string', 'default' => 'standard', 'enum' => array( 'standard', 'application', 'music' ), 'context' => array( 'view', 'edit' ), ), 'external_url' => array( 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'button_text' => array( 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sold_individually' => array( 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'weight' => array( 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Product dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_taxable' => array( 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reviews_allowed' => array( 'description' => __( 'Allow reviews.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'average_rating' => array( 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rating_count' => array( 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'related_ids' => array( 'description' => __( 'List of related products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'upsell_ids' => array( 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'cross_sell_ids' => array( 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), 'parent_id' => array( 'description' => __( 'Product parent ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'purchase_note' => array( 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'categories' => array( 'description' => __( 'List of categories.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Category ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Category slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'tags' => array( 'description' => __( 'List of tags.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tag ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Tag slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'images' => array( 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Attribute position.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'visible' => array( 'description' => __( "Define if the attribute is visible on the \"Additional Information\" tab in the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'variation' => array( 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'options' => array( 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), ), ), ), ), 'default_attributes' => array( 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'variations' => array( 'description' => __( 'List of variations.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Variation ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'permalink' => array( 'description' => __( 'Variation URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'price' => array( 'description' => __( 'Current variation price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'regular_price' => array( 'description' => __( 'Variation regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'sale_price' => array( 'description' => __( 'Variation sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_on_sale_to' => array( 'description' => __( 'End data of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'on_sale' => array( 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'purchasable' => array( 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'visible' => array( 'description' => __( 'If the variation is visible.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ) ), 'virtual' => array( 'description' => __( 'If the variation is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloadable' => array( 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'File MD5 hash.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), 'download_limit' => array( 'description' => __( 'Amount of times the variation can be downloaded.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'context' => array( 'view', 'edit' ), ), 'download_expiry' => array( 'description' => __( 'Number of days that the customer has up to be able to download the variation.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'manage_stock' => array( 'description' => __( 'Stock management at variation level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'in_stock' => array( 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'backordered' => array( 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'weight' => array( 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'dimensions' => array( 'description' => __( 'Variation dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'length' => array( 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'image' => array( 'description' => __( 'Variation image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'position' => array( 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ), 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'option' => array( 'description' => __( 'Selected attribute term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), ), ), ), 'grouped_products' => array( 'description' => __( 'List of grouped products ID.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['slug'] = array( 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any' ), array_keys( get_post_statuses() ) ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['type'] = array( 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_types() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['category'] = array( 'description' => __( 'Limit result set to products assigned a specific category.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag'] = array( 'description' => __( 'Limit result set to products assigned a specific tag.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['shipping_class'] = array( 'description' => __( 'Limit result set to products assigned a specific shipping class.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute'] = array( 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute_term'] = array( 'description' => __( 'Limit result set to products with a specific attribute term (required an assigned attribute).', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['sku'] = array( 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-orders-controller.php 0000666 00000173017 15214171310 0013431 0 ustar 00 <?php /** * REST API Orders controller * * Handles requests to the /orders endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Orders controller class. * * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders'; /** * Post type. * * @var string */ protected $post_type = 'shop_order'; /** * Initialize orders actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for orders. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Prepare a single order output for response. * * @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 ) { global $wpdb; $order = wc_get_order( $post ); $dp = $request['dp']; $data = array( 'id' => $order->id, 'parent_id' => $post->post_parent, 'status' => $order->get_status(), 'order_key' => $order->order_key, 'number' => $order->get_order_number(), 'currency' => $order->get_order_currency(), 'version' => $order->order_version, 'prices_include_tax' => $order->prices_include_tax, 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), 'customer_id' => $order->get_user_id(), 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), 'discount_tax' => wc_format_decimal( $order->cart_discount_tax, $dp ), 'shipping_total' => wc_format_decimal( $order->get_total_shipping(), $dp ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), 'total' => wc_format_decimal( $order->get_total(), $dp ), 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), 'billing' => array(), 'shipping' => array(), 'payment_method' => $order->payment_method, 'payment_method_title' => $order->payment_method_title, 'transaction_id' => $order->get_transaction_id(), 'customer_ip_address' => $order->customer_ip_address, 'customer_user_agent' => $order->customer_user_agent, 'created_via' => $order->created_via, 'customer_note' => $order->customer_note, 'date_completed' => wc_rest_prepare_date_response( $order->completed_date ), 'date_paid' => $order->paid_date, 'cart_hash' => $order->cart_hash, 'line_items' => array(), 'tax_lines' => array(), 'shipping_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), 'refunds' => array(), ); // Add addresses. $data['billing'] = $order->get_address( 'billing' ); $data['shipping'] = $order->get_address( 'shipping' ); // Add line items. foreach ( $order->get_items() as $item_id => $item ) { $product = $order->get_product_from_item( $item ); $product_id = 0; $variation_id = 0; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = $product->id; $variation_id = $product->variation_id; $product_sku = $product->get_sku(); } $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta['key'], 'label' => $formatted_meta['label'], 'value' => $formatted_meta['value'], ); } $line_item = array( 'id' => $item_id, 'name' => $item['name'], 'sku' => $product_sku, 'product_id' => (int) $product_id, 'variation_id' => (int) $variation_id, 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'taxes' => array(), 'meta' => $item_meta, ); $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); if ( isset( $item_line_taxes['total'] ) ) { $line_tax = array(); foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ]['subtotal'] = $tax; } $line_item['taxes'] = array_values( $line_tax ); } $data['line_items'][] = $line_item; } // Add taxes. foreach ( $order->get_items( 'tax' ) as $key => $tax ) { $tax_line = array( 'id' => $key, 'rate_code' => $tax['name'], 'rate_id' => $tax['rate_id'], 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], 'compound' => (bool) $tax['compound'], 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), ); $data['tax_lines'][] = $tax_line; } // Add shipping. foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $shipping_line = array( 'id' => $shipping_item_id, 'method_title' => $shipping_item['name'], 'method_id' => $shipping_item['method_id'], 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), 'total_tax' => wc_format_decimal( '', $dp ), 'taxes' => array(), ); $shipping_taxes = maybe_unserialize( $shipping_item['taxes'] ); if ( ! empty( $shipping_taxes ) ) { $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes ), $dp ); foreach ( $shipping_taxes as $tax_rate_id => $tax ) { $shipping_line['taxes'][] = array( 'id' => $tax_rate_id, 'total' => $tax, ); } } $data['shipping_lines'][] = $shipping_line; } // Add fees. foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $fee_line = array( 'id' => $fee_item_id, 'name' => $fee_item['name'], 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', 'tax_status' => 'taxable', 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), 'taxes' => array(), ); $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); if ( isset( $fee_line_taxes['total'] ) ) { $fee_tax = array(); foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { $fee_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } if ( isset( $fee_line_taxes['subtotal'] ) ) { foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; } } $fee_line['taxes'] = array_values( $fee_tax ); } $data['fee_lines'][] = $fee_line; } // Add coupons. foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $coupon_line = array( 'id' => $coupon_item_id, 'code' => $coupon_item['name'], 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), ); $data['coupon_lines'][] = $coupon_line; } // Add refunds. foreach ( $order->get_refunds() as $refund ) { $data['refunds'][] = array( 'id' => $refund->id, 'refund' => $refund->get_refund_reason() ? $refund->get_refund_reason() : '', 'total' => '-' . wc_format_decimal( $refund->get_refund_amount(), $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 ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $order ) ); /** * 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 links for the request. * * @param WC_Order $order Order object. * @return array Links for the given order. */ protected function prepare_links( $order ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( 0 !== (int) $order->get_user_id() ) { $links['customer'] = array( 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), ); } if ( 0 !== (int) $order->post->post_parent ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->post->post_parent ) ), ); } return $links; } /** * Query args. * * @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 for create. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Object. */ protected function prepare_item_for_database( $request ) { $data = new stdClass; // Set default order args. $data->status = $request['status']; $data->customer_id = $request['customer_id']; $data->customer_note = $request['customer_note']; /** * 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 stdClass $data An object representing a single item prepared * for inserting the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); } /** * Create base WC Order object. * * @param array $data * @return WC_Order */ protected function create_base_order( $data ) { return wc_create_order( $data ); } /** * Create order. * * @param WP_REST_Request $request Full details about the request. * @return int|WP_Error */ protected function create_order( $request ) { wc_transaction_query( 'start' ); try { // Make sure customer exists. if ( 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 ); } $data = $this->prepare_item_for_database( $request ); if ( is_wp_error( $data ) ) { return $data; } $data->created_via = 'rest-api'; $order = $this->create_base_order( (array) $data ); if ( is_wp_error( $order ) ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order', sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); } // Set addresses. if ( is_array( $request['billing'] ) ) { $this->update_address( $order, $request['billing'], 'billing' ); } if ( is_array( $request['shipping'] ) ) { $this->update_address( $order, $request['shipping'], 'shipping' ); } // Set currency. update_post_meta( $order->id, '_order_currency', $request['currency'] ); // Set lines. $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( is_array( $request[ $line ] ) ) { foreach ( $request[ $line ] as $item ) { $set_item = 'set_' . $line_type; $this->$set_item( $order, $item, 'create' ); } } } // Calculate totals and set them. $order->calculate_totals(); // Set payment method. if ( ! empty( $request['payment_method'] ) ) { update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); } if ( ! empty( $request['payment_method_title'] ) ) { update_post_meta( $order->id, '_payment_method_title', $request['payment_method_title'] ); } if ( true === $request['set_paid'] ) { $order->payment_complete( $request['transaction_id'] ); } // Set meta data. if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { $this->update_meta_data( $order->id, $request['meta_data'] ); } wc_transaction_query( 'commit' ); return $order->id; } catch ( WC_REST_Exception $e ) { wc_transaction_query( 'rollback' ); return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update address. * * @param WC_Order $order * @param array $posted * @param string $type */ protected function update_address( $order, $posted, $type = 'billing' ) { $fields = $order->get_address( $type ); foreach ( array_keys( $fields ) as $field ) { if ( isset( $posted[ $field ] ) ) { $fields[ $field ] = $posted[ $field ]; } } // Set address. $order->set_address( $fields, $type ); // Update user meta. if ( $order->get_user_id() ) { foreach ( $fields as $key => $value ) { update_user_meta( $order->get_user_id(), $type . '_' . $key, $value ); } } } /** * Create or update a line item. * * @param WC_Order $order Order data. * @param array $item Line item data. * @param string $action 'create' to add line item or 'update' to update it. * @throws WC_REST_Exception Invalid data, server error. */ protected function set_line_item( $order, $item, $action = 'create' ) { $creating = 'create' === $action; $item_args = array(); // Product is always required. if ( empty( $item['product_id'] ) && empty( $item['sku'] ) && empty( $item['variation_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); } if ( ! empty( $item['sku'] ) ) { $product_id = (int) wc_get_product_id_by_sku( $item['sku'] ); } elseif ( ! empty( $item['product_id'] ) && empty( $item['variation_id'] ) ) { $product_id = (int) $item['product_id']; } elseif ( ! empty( $item['variation_id'] ) ) { $product_id = (int) $item['variation_id']; } // When updating, ensure product ID provided matches. if ( 'update' === $action && ! empty( $item['id'] ) ) { $item_product_id = (int) wc_get_order_item_meta( $item['id'], '_product_id' ); $item_variation_id = (int) wc_get_order_item_meta( $item['id'], '_variation_id' ); if ( $product_id !== $item_product_id && $product_id !== $item_variation_id ) { throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or variation ID provided does not match this line item.', 'woocommerce' ), 400 ); } } $product = wc_get_product( $product_id ); // Must be a valid WC_Product. if ( ! is_object( $product ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); } // Quantity must be positive float. if ( isset( $item['quantity'] ) && 0 >= floatval( $item['quantity'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_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_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); } // Get variation attributes. if ( method_exists( $product, 'get_variation_attributes' ) ) { $item_args['variation'] = $product->get_variation_attributes(); } // Quantity. if ( isset( $item['quantity'] ) ) { $item_args['qty'] = $item['quantity']; } // Total. if ( isset( $item['total'] ) ) { $item_args['totals']['total'] = floatval( $item['total'] ); } // Total tax. if ( isset( $item['total_tax'] ) ) { $item_args['totals']['tax'] = floatval( $item['total_tax'] ); } // Subtotal. if ( isset( $item['subtotal'] ) ) { $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); } // Subtotal tax. if ( isset( $item['subtotal_tax'] ) ) { $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); } if ( $creating ) { $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); if ( ! $item_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); } } else { $item_id = $order->update_product( $item['id'], $product, $item_args ); if ( ! $item_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_line_item', __( 'Cannot update line item, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order shipping method. * * @param WC_Order $order Order data. * @param array $shipping Item data. * @param string $action 'create' to add shipping or 'update' to update it. * @throws WC_REST_Exception Invalid data, server error. */ protected function set_shipping( $order, $shipping, $action ) { // Total must be a positive float. if ( ! empty( $shipping['total'] ) && 0 > floatval( $shipping['total'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // Method ID is required. if ( empty( $shipping['method_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_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'] ); $shipping_id = $order->add_shipping( $rate ); if ( ! $shipping_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_shipping', __( 'Cannot create shipping method, try again.', 'woocommerce' ), 500 ); } } else { $shipping_args = array(); if ( isset( $shipping['method_id'] ) ) { $shipping_args['method_id'] = $shipping['method_id']; } if ( isset( $shipping['method_title'] ) ) { $shipping_args['method_title'] = $shipping['method_title']; } if ( isset( $shipping['total'] ) ) { $shipping_args['cost'] = floatval( $shipping['total'] ); } $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); if ( ! $shipping_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order fee. * * @param WC_Order $order Order data. * @param array $fee Item data. * @param string $action 'create' to add fee or 'update' to update it. * @throws WC_REST_Exception Invalid data, server error. */ protected function set_fee( $order, $fee, $action ) { if ( 'create' === $action ) { // Fee name is required. if ( empty( $fee['name'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); } $fee_data = new stdClass(); $fee_data->id = sanitize_title( $fee['name'] ); $fee_data->name = $fee['name']; $fee_data->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; $fee_data->taxable = false; $fee_data->tax = 0; $fee_data->tax_data = array(); $fee_data->tax_class = ''; // If taxable, tax class and total are required. if ( isset( $fee['tax_status'] ) && 'taxable' === $fee['tax_status'] ) { if ( ! isset( $fee['tax_class'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); } $fee_data->taxable = true; $fee_data->tax_class = $fee['tax_class']; if ( isset( $fee['total_tax'] ) ) { $fee_data->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; } } $fee_id = $order->add_fee( $fee_data ); if ( ! $fee_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_fee', __( 'Cannot create fee, try again.', 'woocommerce' ), 500 ); } } else { $fee_args = array(); if ( isset( $fee['name'] ) ) { $fee_args['name'] = $fee['name']; } if ( isset( $fee['tax_class'] ) ) { $fee_args['tax_class'] = $fee['tax_class']; } if ( isset( $fee['total'] ) ) { $fee_args['line_total'] = floatval( $fee['total'] ); } if ( isset( $fee['total_tax'] ) ) { $fee_args['line_tax'] = floatval( $fee['total_tax'] ); } $fee_id = $order->update_fee( $fee['id'], $fee_args ); if ( ! $fee_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); } } } /** * Create or update an order coupon. * * @param WC_Order $order Order data. * @param array $coupon Item data. * @param string $action 'create' to add coupon or 'update' to update it. * @throws WC_REST_Exception Invalid data, server error. */ protected function set_coupon( $order, $coupon, $action ) { // Coupon discount must be positive float. if ( isset( $coupon['discount'] ) && 0 > floatval( $coupon['discount'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_total', __( 'Coupon discount must be a positive amount.', 'woocommerce' ), 400 ); } if ( 'create' === $action ) { // Coupon code is required. if ( empty( $coupon['code'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); } $coupon_id = $order->add_coupon( $coupon['code'], floatval( $coupon['discount'] ) ); if ( ! $coupon_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order_coupon', __( 'Cannot create coupon, try again.', 'woocommerce' ), 500 ); } } else { $coupon_args = array(); if ( isset( $coupon['code'] ) ) { $coupon_args['code'] = $coupon['code']; } if ( isset( $coupon['discount'] ) ) { $coupon_args['discount_amount'] = floatval( $coupon['discount'] ); } $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); if ( ! $coupon_id ) { throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); } } } /** * Helper method to add/update meta data, with two restrictions: * * 1) Only non-protected meta (no leading underscore) can be set * 2) Meta values must be scalar (int, string, bool) * * @param int $order_id Order ID. * @param array $meta_data Meta data in array( 'meta_key' => 'meta_value' ) format. */ protected function update_meta_data( $order_id, $meta_data ) { foreach ( $meta_data 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 ); } } } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order_id = $this->create_order( $request ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Clear transients. wc_delete_shop_order_transients( $order_id ); $post = get_post( $order_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param object $post Inserted object (not a WP_Post object). * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } /** * Wrapper method to create/update order items. * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @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_REST_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->id ) ) ); if ( is_null( $result ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); } } $this->$set_method( $order, $item, $action ); } /** * 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. * * @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; } /** * Update order. * * @param WP_REST_Request $request Full details about the request. * @param WP_Post $post Post data. * @return int|WP_Error */ protected function update_order( $request, $post ) { try { $update_totals = false; $order = wc_get_order( $post ); $order_args = array( 'order_id' => $order->id ); // Customer note. if ( isset( $request['customer_note'] ) ) { $order_args['customer_note'] = $request['customer_note']; } // Customer ID. if ( isset( $request['customer_id'] ) && $request['customer_id'] != $order->get_user_id() ) { // Make sure customer exists. if ( false === get_user_by( 'id', $request['customer_id'] ) ) { throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } update_post_meta( $order->id, '_customer_user', $request['customer_id'] ); } // Update addresses. if ( is_array( $request['billing'] ) ) { $this->update_address( $order, $request['billing'], 'billing' ); } if ( is_array( $request['shipping'] ) ) { $this->update_address( $order, $request['shipping'], 'shipping' ); } $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $request[ $line ] ) && is_array( $request[ $line ] ) ) { $update_totals = true; foreach ( $request[ $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' ); } } } } // Set payment method. if ( ! empty( $request['payment_method'] ) ) { update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); } if ( ! empty( $request['payment_method_title'] ) ) { update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); } if ( $order->needs_payment() && isset( $request['set_paid'] ) && true === $request['set_paid'] ) { $order->payment_complete( ! empty( $request['transaction_id'] ) ? $request['transaction_id'] : '' ); } // Set order currency. if ( isset( $request['currency'] ) ) { update_post_meta( $order->id, '_order_currency', $request['currency'] ); } // If items have changed, recalculate order totals. if ( $update_totals ) { $order->calculate_totals(); } // Update meta data. if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { $this->update_meta_data( $order->id, $request['meta_data'] ); } // Update the order post to set customer note/modified date. wc_update_order( $order_args ); // Order status. if ( ! empty( $request['status'] ) ) { $order->update_status( $request['status'], isset( $request['status_note'] ) ? $request['status_note'] : '' ); } return $order->id; } catch ( WC_REST_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } /** * Update a single order. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $order_id = $this->update_order( $request, $post ); if ( is_wp_error( $order_id ) ) { return $order_id; } // Clear transients. wc_delete_shop_order_transients( $order_id ); $post = get_post( $order_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param object $post Inserted object (not a WP_Post object). * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } /** * Get order statuses. * * @return array */ protected function get_order_statuses() { $order_statuses = array(); foreach ( array_keys( wc_get_order_statuses() ) as $status ) { $order_statuses[] = str_replace( 'wc-', '', $status ); } return $order_statuses; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'parent_id' => array( 'description' => __( 'Parent order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Order status.', 'woocommerce' ), 'type' => 'string', 'default' => 'pending', 'enum' => $this->get_order_statuses(), 'context' => array( 'view', 'edit' ), ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'number' => array( 'description' => __( 'Order number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'currency' => array( 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), 'type' => 'string', 'default' => get_woocommerce_currency(), 'enum' => array_keys( get_woocommerce_currencies() ), 'context' => array( 'view', 'edit' ), ), 'version' => array( 'description' => __( 'Version of WooCommerce when the order was made.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'prices_include_tax' => array( 'description' => __( 'Shows if the prices included tax during checkout.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_id' => array( 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), 'type' => 'integer', 'default' => 0, 'context' => array( 'view', 'edit' ), ), 'discount_total' => array( 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_tax' => array( 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_total' => array( 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax' => array( 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_tax' => array( 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Grand total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Sum of all taxes.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing' => array( 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'payment_method' => array( 'description' => __( 'Payment method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'payment_method_title' => array( 'description' => __( 'Payment method title.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'set_paid' => array( 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'edit' ), ), 'transaction_id' => array( 'description' => __( 'Unique transaction ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_ip_address' => array( 'description' => __( "Customer's IP address.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_user_agent' => array( 'description' => __( 'User agent of the customer.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'created_via' => array( 'description' => __( 'Shows where the order was created.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_note' => array( 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'date_completed' => array( 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_paid' => array( 'description' => __( "The date the order has been paid, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_hash' => array( 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta' => array( 'description' => __( 'Line item meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Meta label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'tax_lines' => array( 'description' => __( 'Tax lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_code' => array( 'description' => __( 'Tax rate code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rate_id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Tax rate label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'compound' => array( 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'tax_total' => array( 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax_total' => array( 'description' => __( 'Shipping tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'shipping_lines' => array( 'description' => __( 'Shipping lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'method_title' => array( 'description' => __( 'Shipping method name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'method_id' => array( 'description' => __( 'Shipping method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'fee_lines' => array( 'description' => __( 'Fee lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Fee name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'tax_status' => array( 'description' => __( 'Tax status of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), 'coupon_lines' => array( 'description' => __( 'Coupons line data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'code' => array( 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount' => array( 'description' => __( 'Discount total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'discount_tax' => array( 'description' => __( 'Discount total tax.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'refunds' => array( 'description' => __( 'List of refunds.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Refund ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'reason' => array( 'description' => __( 'Refund reason.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Refund total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); $params['customer'] = array( 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['product'] = array( 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['dp'] = array( 'default' => 2, 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-product-reviews-controller.php 0000666 00000024757 15214171310 0015303 0 ustar 00 <?php /** * REST API Product Reviews controller * * Handles requests to the /products/<product_id>/reviews endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Products controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews'; /** * Register the routes for product reviews. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'review' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Review content.', 'woocommerce' ), ), 'name' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Name of the reviewer.', 'woocommerce' ), ), 'email' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Email of the reviewer.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'product_id' => array( 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read webhook deliveries. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a webhook develivery. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $post = get_post( (int) $request['product_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all reviews from a product. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $product = get_post( (int) $request['product_id'] ); if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product id.', 'woocommerce' ), array( 'status' => 404 ) ); } $reviews = get_approved_comments( $product->ID ); $data = array(); foreach ( $reviews as $review_data ) { $review = $this->prepare_item_for_response( $review_data, $request ); $review = $this->prepare_response_for_collection( $review ); $data[] = $review; } return rest_ensure_response( $data ); } /** * Get a single product review. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $product = get_post( (int) $request['product_id'] ); if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product id.', 'woocommerce' ), array( 'status' => 404 ) ); } $review = get_comment( $id ); if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== intval( $product->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $delivery = $this->prepare_item_for_response( $review, $request ); $response = rest_ensure_response( $delivery ); return $response; } /** * Prepare a single product review output for response. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $review, $request ) { $data = array( 'id' => (int) $review->comment_ID, 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), 'review' => $review->comment_content, 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), 'name' => $review->comment_author, 'email' => $review->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), ); $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( $review, $request ) ); /** * Filter product reviews object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); } /** * Prepare links for the request. * * @param WP_Comment $review Product review object. * @param WP_REST_Request $request Request object. * @return array Links for the given product review. */ protected function prepare_links( $review, $request ) { $product_id = (int) $request['product_id']; $base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), ), ); return $links; } /** * Get the Product Review's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_review', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'rating' => array( 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Reviewer name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'email' => array( 'description' => __( 'Reviewer email.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'verified' => array( 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } class-wc-rest-reports-controller.php 0000666 00000011021 15214171310 0013613 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Reports controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Reports_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports'; /** * Register the routes for reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read reports. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { $data = array(); $reports = array( array( 'slug' => 'sales', 'description' => __( 'List of sales reports.', 'woocommerce' ), ), array( 'slug' => 'top_sellers', 'description' => __( 'List of top sellers products.', 'woocommerce' ), ), ); foreach ( $reports as $report ) { $item = $this->prepare_item_for_response( (object) $report, $request ); $data[] = $this->prepare_response_for_collection( $item ); } return rest_ensure_response( $data ); } /** * Prepare a report object for serialization. * * @param stdClass $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $report, $request ) { $data = array( 'slug' => $report->slug, 'description' => $report->description, ); $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( array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ) ); /** * Filter a report returned from the API. * * Allows modification of the report data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $report The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'description' => array( 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } class-wc-rest-exception.php 0000666 00000002535 15214171310 0011744 0 ustar 00 <?php /** * WooCommerce REST Exception Class * * Extends Exception to provide additional data. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_REST_Exception class. */ class WC_REST_Exception extends Exception { /** * Sanitized error code. * * @var string */ protected $error_code; /** * Error extra data. * * @var array */ protected $error_data; /** * Setup exception. * * @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`. * @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'. * @param int $http_status_code Proper HTTP status code to respond with, e.g. 400. * @param array $data Extra error data. */ public function __construct( $code, $message, $http_status_code = 400, $data = array() ) { $this->error_code = $code; $this->error_data = array_merge( array( 'status' => $http_status_code ), $data ); parent::__construct( $message, $http_status_code ); } /** * Returns the error code. * * @return string */ public function getErrorCode() { return $this->error_code; } /** * Returns error data. * * @return array */ public function getErrorData() { return $this->error_data; } } class-wc-rest-order-refunds-controller.php 0000666 00000042301 15214171310 0014701 0 ustar 00 <?php /** * REST API Order Refunds controller * * Handles requests to the /orders/<order_id>/refunds endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Order Refunds controller class. * * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds'; /** * Post type. * * @var string */ protected $post_type = 'shop_order_refund'; /** * Order refunds actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for order refunds. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => true, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Prepare a single order refund output for response. * * @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 ) { global $wpdb; $order = wc_get_order( (int) $request['order_id'] ); if ( ! $order ) { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } $refund = wc_get_order( $post ); if ( ! $refund || intval( $refund->post->post_parent ) !== intval( $order->id ) ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); } $dp = $request['dp']; $data = array( 'id' => $refund->id, 'date_created' => wc_rest_prepare_date_response( $refund->date ), 'amount' => wc_format_decimal( $refund->get_refund_amount(), $dp ), 'reason' => $refund->get_refund_reason(), 'line_items' => array(), ); // Add line items. foreach ( $refund->get_items() as $item_id => $item ) { $product = $refund->get_product_from_item( $item ); $product_id = 0; $variation_id = 0; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = $product->id; $variation_id = $product->variation_id; $product_sku = $product->get_sku(); } $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta['key'], 'label' => $formatted_meta['label'], 'value' => $formatted_meta['value'], ); } $line_item = array( 'id' => $item_id, 'name' => $item['name'], 'sku' => $product_sku, 'product_id' => (int) $product_id, 'variation_id' => (int) $variation_id, 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'taxes' => array(), 'meta' => $item_meta, ); $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); if ( isset( $item_line_taxes['total'] ) ) { $line_tax = array(); foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ] = array( 'id' => $tax_rate_id, 'total' => $tax, 'subtotal' => '', ); } foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { $line_tax[ $tax_rate_id ]['subtotal'] = $tax; } $line_item['taxes'] = array_values( $line_tax ); } $data['line_items'][] = $line_item; } $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( $refund ) ); /** * 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 links for the request. * * @param WC_Order_Refund $refund Comment object. * @return array Links for the given order refund. */ protected function prepare_links( $refund ) { $order_id = $refund->post->post_parent; $base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), ), ); return $links; } /** * Query args. * * @param array $args * @param WP_REST_Request $request * @return array */ public function query_args( $args, $request ) { // Set post_status. $args['post_status'] = 'any'; return $args; } /** * Create a single item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order_data = get_post( (int) $request['order_id'] ); if ( empty( $order_data ) ) { return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); } if ( 0 > $request['amount'] ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); } $api_refund = is_bool( $request['api_refund'] ) ? $request['api_refund'] : true; $data = array( 'order_id' => $order_data->ID, 'amount' => $request['amount'], 'reason' => empty( $request['reason'] ) ? null : $request['reason'], 'line_items' => $request['line_items'], ); // Create the refund. $refund = wc_create_refund( $data ); if ( ! $refund ) { return new WP_Error( 'woocommerce_rest_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_data ); if ( isset( $payment_gateways[ $order->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { $result = $payment_gateways[ $order->payment_method ]->process_refund( $order->id, $refund->get_refund_amount(), $refund->get_refund_reason() ); if ( is_wp_error( $result ) ) { return $result; } elseif ( ! $result ) { return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); } } } $post = get_post( $refund->id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param object $post Inserted object (not a WP_Post object). * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); return $response; } /** * Get the Order's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'amount' => array( 'description' => __( 'Refund amount.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'reason' => array( 'description' => __( 'Reason for refund.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'line_items' => array( 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Item ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sku' => array( 'description' => __( 'Product SKU.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'variation_id' => array( 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'quantity' => array( 'description' => __( 'Quantity ordered.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'tax_class' => array( 'description' => __( 'Tax class of product.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'price' => array( 'description' => __( 'Product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'taxes' => array( 'description' => __( 'Line taxes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Tax rate ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Tax total.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subtotal' => array( 'description' => __( 'Tax subtotal.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), 'meta' => array( 'description' => __( 'Line item meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'label' => array( 'description' => __( 'Meta label.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'value' => array( 'description' => __( 'Meta value.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['dp'] = array( 'default' => 2, 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-product-attribute-terms-controller.php 0000666 00000017125 15214171310 0016741 0 ustar 00 <?php /** * REST API Product Attribute Terms controller * * Handles requests to the products/attributes/<attribute_id>/terms endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Attribute Terms controller class. * * @package WooCommerce/API * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/attributes/(?P<attribute_id>[\d]+)/terms'; /** * Register the routes for terms. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'type' => 'string', 'description' => __( 'Name for the resource.', 'woocommerce' ), 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), )); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 'args' => array( 'attribute_id' => array( 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Prepare a single product attribute term output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { // Get term order. $menu_order = get_woocommerce_term_meta( $item->term_id, 'order_' . $this->taxonomy ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); $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( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Update term meta fields. * * @param WP_Term $term * @param WP_REST_Request $request * @return bool|WP_Error */ protected function update_term_meta_fields( $term, $request ) { $id = (int) $term->term_id; update_woocommerce_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] ); return true; } /** * Get the Attribute Term's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_attribute_term', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Term name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } class-wc-rest-customer-downloads-controller.php 0000666 00000016545 15214171310 0015766 0 ustar 00 <?php /** * REST API Customer Downloads controller * * Handles requests to the /customers/<customer_id>/downloads endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Customers controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Customer_Downloads_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'customers/(?P<customer_id>[\d]+)/downloads'; /** * Register the routes for customers. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'customer_id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { $customer = get_user_by( 'id', (int) $request['customer_id'] ); if ( ! $customer ) { return new WP_Error( "woocommerce_rest_customer_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_user_permissions( 'read', $customer->id ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all customer downloads. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] ); $data = array(); foreach ( $downloads as $download_data ) { $download = $this->prepare_item_for_response( (object) $download_data, $request ); $download = $this->prepare_response_for_collection( $download ); $data[] = $download; } return rest_ensure_response( $data ); } /** * Prepare a single download output for response. * * @param stdObject $download Download object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $download, $request ) { $data = (array) $download; $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; $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( $download, $request ) ); /** * Filter customer download data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdObject $download Download object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); } /** * Prepare links for the request. * * @param stdClass $download Download object. * @param WP_REST_Request $request Request object. * @return array Links for the given customer download. */ protected function prepare_links( $download, $request ) { $base = str_replace( '(?P<customer_id>[\d]+)', $request['customer_id'], $this->rest_base ); $links = array( 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'product' => array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ), ), 'order' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ), ), ); return $links; } /** * Get the Customer Download's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer_download', 'type' => 'object', 'properties' => array( 'download_url' => array( 'description' => __( 'Download file URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'download_id' => array( 'description' => __( 'Download ID (MD5).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Downloadable product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'download_name' => array( 'description' => __( 'Downloadable file name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'order_id' => array( 'description' => __( 'Order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'order_key' => array( 'description' => __( 'Order key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'downloads_remaining' => array( 'description' => __( 'Amount of downloads remaining.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'access_expires' => array( 'description' => __( "The date when the download access expires, in the site's timezone.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File details.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view' ), 'readonly' => true, 'properties' => array( 'name' => array( 'description' => __( 'File name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'file' => array( 'description' => __( 'File URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } class-wc-rest-product-shipping-classes-controller.php 0000666 00000007205 15214171310 0017060 0 ustar 00 <?php /** * REST API Product Shipping Classes controller * * Handles requests to the products/shipping_classes endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Shipping Classes controller class. * * @package WooCommerce/API * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/shipping_classes'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_shipping_class'; /** * Prepare a single product shipping class output for response. * * @param obj $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'count' => (int) $item->count, ); $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( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Shipping Class schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Shipping class name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } class-wc-rest-authentication.php 0000666 00000033141 15214171310 0012762 0 ustar 00 <?php /** * REST API Authentication * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } class WC_REST_Authentication { /** * Initialize authentication actions. */ public function __construct() { add_filter( 'determine_current_user', array( $this, 'authenticate' ), 100 ); add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ) ); add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); } /** * Check if is request to our REST API. * * @return bool */ protected function is_request_to_rest_api() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); // Check if our endpoint. $woocommerce = false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix . 'wc/' ); // Allow third party plugins use our authentication methods. $third_party = false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix . 'wc-' ); return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $woocommerce || $third_party ); } /** * Authenticate user. * * @param int|false $user_id User ID if one has been determined, false otherwise. * @return int|false */ public function authenticate( $user_id ) { // Do not authenticate twice and check if is a request to our endpoint in the WP REST API. if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) { return $user_id; } if ( is_ssl() ) { return $this->perform_basic_authentication(); } else { return $this->perform_oauth_authentication(); } } /** * Check for authentication error. * * @param WP_Error|null|bool $error * @return WP_Error|null|bool */ public function check_authentication_error( $error ) { global $wc_rest_authentication_error; // Passthrough other errors. if ( ! empty( $error ) ) { return $error; } return $wc_rest_authentication_error; } /** * Basic Authentication. * * 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. * * @return int|bool */ private function perform_basic_authentication() { global $wc_rest_authentication_error; $consumer_key = ''; $consumer_secret = ''; // If the $_GET parameters are present, use those first. if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { $consumer_key = $_GET['consumer_key']; $consumer_secret = $_GET['consumer_secret']; } // If the above is not present, we will do full basic auth. if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { $consumer_key = $_SERVER['PHP_AUTH_USER']; $consumer_secret = $_SERVER['PHP_AUTH_PW']; } // Stop if don't have any key. if ( ! $consumer_key || ! $consumer_secret ) { return false; } // Get user data. $user = $this->get_user_data_by_consumer_key( $consumer_key ); if ( empty( $user ) ) { return false; } // Validate user secret. if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); return false; } // Check API Key permissions. if ( ! $this->check_permissions( $user->permissions ) ) { return false; } // Update last access. $this->update_last_access( $user->key_id ); return $user->user_id; } /** * 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. * * @return int|bool */ private function perform_oauth_authentication() { global $wc_rest_authentication_error; $params = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); // Check for required OAuth parameters. foreach ( $params as $param ) { if ( empty( $_GET[ $param ] ) ) { return false; } } // Fetch WP user by consumer key $user = $this->get_user_data_by_consumer_key( $_GET['oauth_consumer_key'] ); if ( empty( $user ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); return false; } // Perform OAuth validation. $wc_rest_authentication_error = $this->check_oauth_signature( $user, $_GET ); if ( is_wp_error( $wc_rest_authentication_error ) ) { return false; } $wc_rest_authentication_error = $this->check_oauth_timestamp_and_nonce( $user, $_GET['oauth_timestamp'], $_GET['oauth_nonce'] ); if ( is_wp_error( $wc_rest_authentication_error ) ) { return false; } // Check API Key permissions. if ( ! $this->check_permissions( $user->permissions ) ) { return false; } // Update last access. $this->update_last_access( $user->key_id ); return $user->user_id; } /** * Verify that the consumer-provided request signature matches our generated signature, * this ensures the consumer has a valid key/secret. * * @param stdClass $user * @param array $params The request parameters. * @return null|WP_Error */ private function check_oauth_signature( $user, $params ) { $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); $wp_base = get_home_url( null, '/', 'relative' ); if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { $request_path = substr( $request_path, strlen( $wp_base ) ); } $base_request_uri = rawurlencode( get_home_url( null, $request_path ) ); // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. $consumer_signature = rawurldecode( $params['oauth_signature'] ); unset( $params['oauth_signature'] ); // Sort parameters. if ( ! uksort( $params, 'strcmp' ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 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 ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); $secret = $user->consumer_secret . '&'; $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); } return true; } /** * 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. * * @see rawurlencode() * @param array $parameters Un-normalized pararmeters. * @return array Normalized parameters. */ private function normalize_parameters( $parameters ) { $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); $parameters = array_combine( $keys, $values ); return $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 stdClass $user * @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 * @return bool|WP_Error */ private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { global $wpdb; $valid_window = 15 * 60; // 15 minute window. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); } $used_nonces = maybe_unserialize( $user->nonces ); if ( empty( $used_nonces ) ) { $used_nonces = array(); } if ( in_array( $nonce, $used_nonces ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 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' => $user->key_id ), array( '%s' ), array( '%d' ) ); return true; } /** * Return the user data for the given consumer_key. * * @param string $consumer_key * @return array */ private function get_user_data_by_consumer_key( $consumer_key ) { global $wpdb; $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); $user = $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 ) ); return $user; } /** * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. * * @param string $permissions * @return bool */ private function check_permissions( $permissions ) { global $wc_rest_authentication_error; $valid = true; if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { return false; } switch ( $_SERVER['REQUEST_METHOD'] ) { case 'HEAD' : case 'GET' : if ( 'read' !== $permissions && 'read_write' !== $permissions ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); $valid = false; } break; case 'POST' : case 'PUT' : case 'PATCH' : case 'DELETE' : if ( 'write' !== $permissions && 'read_write' !== $permissions ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); $valid = false; } break; } return $valid; } /** * Updated API Key last access datetime. * * @param int $key_id */ private function update_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' ) ); } /** * 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. * * @param WP_REST_Response $response Current response being served. * @return WP_REST_Response */ public function send_unauthorized_headers( $response ) { global $wc_rest_authentication_error; if ( is_wp_error( $wc_rest_authentication_error ) && is_ssl() ) { $auth_message = __( 'WooCommerce API - Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); } return $response; } } new WC_REST_Authentication(); class-wc-rest-taxes-controller.php 0000666 00000055025 15214171310 0013255 0 ustar 00 <?php /** * REST API Taxes controller * * Handles requests to the /taxes endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Taxes controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Taxes_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'taxes'; /** * Register the routes for taxes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create taxes. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access update a tax. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function update_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a tax. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function delete_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to manipule this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { global $wpdb; $prepared_args = array(); $prepared_args['exclude'] = $request['exclude']; $prepared_args['include'] = $request['include']; $prepared_args['order'] = $request['order']; $prepared_args['number'] = $request['per_page']; if ( ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } $orderby_possibles = array( 'id' => 'tax_rate_id', 'order' => 'tax_rate_order', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['class'] = $request['class']; /** * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. * * @param array $prepared_args Array of arguments for $wpdb->get_results(). * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); $query = " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE 1 = 1 "; // Filter by tax class. if ( ! empty( $prepared_args['class'] ) ) { $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; $query .= " AND tax_rate_class = '$class'"; } // Order tax rates. $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); // Pagination. $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); // Query taxes. $results = $wpdb->get_results( $query . $order_by . $pagination ); $taxes = array(); foreach ( $results as $tax ) { $data = $this->prepare_item_for_response( $tax, $request ); $taxes[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $taxes ); // Store pagation values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); // Query only for ids. $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); // Calcule totals. $total_taxes = (int) $wpdb->num_rows; $response->header( 'X-WP-Total', (int) $total_taxes ); $max_pages = ceil( $total_taxes / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Take tax data from the request and return the updated or newly created rate. * * @todo Replace with CRUD in 2.7.0 * @param WP_REST_Request $request Full details about the request. * @param stdClass|null $current Existing tax object. * @return stdClass */ protected function create_or_update_tax( $request, $current = null ) { $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); $data = array(); $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 ( $fields as $field ) { // Keys via API differ from the stored names returned by _get_tax_rate. $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); // Remove data that was not posted. if ( ! isset( $request[ $key ] ) ) { continue; } // Test new data against current data. if ( $current && $current->$field === $request[ $key ] ) { continue; } // Add to data array. switch ( $key ) { case 'tax_rate_priority' : case 'tax_rate_compound' : case 'tax_rate_shipping' : case 'tax_rate_order' : $data[ $field ] = absint( $request[ $key ] ); break; case 'tax_rate_class' : $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; break; default : $data[ $field ] = wc_clean( $request[ $key ] ); break; } } if ( $id ) { WC_Tax::_update_tax_rate( $id, $data ); } else { $id = WC_Tax::_insert_tax_rate( $data ); } // Add locales. if ( ! empty( $request['postcode'] ) ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); } if ( ! empty( $request['city'] ) ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); } return WC_Tax::_get_tax_rate( $id, OBJECT ); } /** * Create a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); } $tax = $this->create_or_update_tax( $request ); $this->update_additional_fields_for_object( $tax, $request ); /** * Fires after a tax is created or updated via the REST API. * * @param stdClass $tax Data used to create the tax. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax, false when updating tax. */ do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); return $response; } /** * Get a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax_obj ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $tax = $this->prepare_item_for_response( $tax_obj, $request ); $response = rest_ensure_response( $tax ); return $response; } /** * Update a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax_obj ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $tax = $this->create_or_update_tax( $request, $tax_obj ); $this->update_additional_fields_for_object( $tax, $request ); /** * Fires after a tax is created or updated via the REST API. * * @param stdClass $tax Data used to create the tax. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax, false when updating tax. */ do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Delete a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { global $wpdb; $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax, $request ); WC_Tax::_delete_tax_rate( $id ); if ( 0 === $wpdb->rows_affected ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a tax is deleted via the REST API. * * @param stdClass $tax The tax data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); return $response; } /** * Prepare a single tax output for response. * * @param stdClass $tax Tax object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $tax, $request ) { global $wpdb; $id = (int) $tax->tax_rate_id; $data = array( 'id' => $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 ) { $data[ $locale->location_type ] = $locale->location_code; } } $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( $tax ) ); /** * Filter tax object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $tax Tax object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); } /** * Prepare links for the request. * * @param stdClass $tax Tax object. * @return array Links for the given tax. */ protected function prepare_links( $tax ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Taxes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'tax', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'country' => array( 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'State code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postcode/ZIP.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'rate' => array( 'description' => __( 'Tax rate.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Tax rate name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'priority' => array( 'description' => __( 'Tax priority.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'context' => array( 'view', 'edit' ), ), 'compound' => array( 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), 'shipping' => array( 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), 'order' => array( 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'default' => 'standard', 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'default' => 'order', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( 'id', 'order', ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['class'] = array( 'description' => __( 'Sort by tax class.', 'woocommerce' ), 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), 'sanitize_callback' => 'sanitize_title', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-webhooks-controller.php 0000666 00000044543 15214171310 0013755 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Webhooks controller class. * * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'webhooks'; /** * Post type. * * @var string */ protected $post_type = 'shop_webhook'; /** * Initialize Webhooks actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); } /** * Register the routes for webhooks. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'topic' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Webhook topic.', 'woocommerce' ), ), 'delivery_url' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), ), 'secret' => array( 'required' => true, 'type' => 'string', 'description' => __( 'Webhook secret.', 'woocommerce' ), ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Create a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } // Validate topic. if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } // Validate delivery URL. if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } $post->post_type = $this->post_type; $post_id = wp_insert_post( $post, true ); if ( is_wp_error( $post_id ) ) { if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $post->ID = $post_id; $webhook = new WC_Webhook( $post_id ); // Set topic. $webhook->set_topic( $request['topic'] ); // Set delivery URL. $webhook->set_delivery_url( $request['delivery_url'] ); // Set secret. $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : '' ); // Set status. if ( ! empty( $request['status'] ) ) { $webhook->update_status( $request['status'] ); } $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); // Send ping. $webhook->deliver_ping(); // Clear cache. delete_transient( 'woocommerce_webhook_ids' ); return $response; } /** * Update a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } $webhook = new WC_Webhook( $id ); // Update topic. if ( ! empty( $request['topic'] ) ) { if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { $webhook->set_topic( $request['topic'] ); } else { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } } // Update delivery URL. if ( ! empty( $request['delivery_url'] ) ) { if ( wc_is_valid_url( $request['delivery_url'] ) ) { $webhook->set_delivery_url( $request['delivery_url'] ); } else { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); } } // Update secret. if ( ! empty( $request['secret'] ) ) { $webhook->set_secret( $request['secret'] ); } // Update status. if ( ! empty( $request['status'] ) ) { $webhook->update_status( $request['status'] ); } $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. $post_id = wp_update_post( (array) $post, true ); if ( is_wp_error( $post_id ) ) { if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); /** * Fires after a single item is created or updated via the REST API. * * @param WP_Post $post Inserted object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); // Clear cache. delete_transient( 'woocommerce_webhook_ids' ); return rest_ensure_response( $response ); } /** * Delete a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); $result = wp_delete_post( $id, true ); if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } /** * Fires after a single item is deleted or trashed via the REST API. * * @param object $post The deleted or trashed item. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); // Clear cache. delete_transient( 'woocommerce_webhook_ids' ); return $response; } /** * Prepare a single webhook for create or update. * * @param WP_REST_Request $request Request object. * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { global $wpdb; $data = new stdClass; // Post ID. if ( isset( $request['id'] ) ) { $data->ID = absint( $request['id'] ); } // Validate required POST fields. if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); // Post author. $data->post_author = get_current_user_id(); // Post password. $password = strlen( uniqid( 'webhook_' ) ); $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $password; // Post status. $data->post_status = 'publish'; } else { // Allow edit post title. if ( ! empty( $request['name'] ) ) { $data->post_title = $request['name']; } } // Comment status. $data->comment_status = 'closed'; // Ping status. $data->ping_status = 'closed'; /** * 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 stdClass $data An object representing a single item prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); } /** * Prepare a single webhook output for response. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $post, $request ) { $id = (int) $post->ID; $webhook = new WC_Webhook( $id ); $data = array( 'id' => $webhook->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(), 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), ); $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( $post ) ); /** * Filter webhook object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WC_Webhook $webhook Webhook object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); } /** * Query args. * * @param array $args * @param WP_REST_Request $request * @return array */ public function query_args( $args, $request ) { // Set post_status. switch ( $request['status'] ) { case 'active' : $args['post_status'] = 'publish'; break; case 'paused' : $args['post_status'] = 'draft'; break; case 'disabled' : $args['post_status'] = 'pending'; break; default : $args['post_status'] = 'any'; break; } return $args; } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Webhook status.', 'woocommerce' ), 'type' => 'string', 'default' => 'active', 'enum' => array( 'active', 'paused', 'disabled' ), 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wc_is_webhook_valid_topic', ), ), 'topic' => array( 'description' => __( 'Webhook topic.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'resource' => array( 'description' => __( 'Webhook resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'event' => array( 'description' => __( 'Webhook event.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'hooks' => array( 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'delivery_url' => array( 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'secret' => array( 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default is a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'date_created' => array( 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections of attachments. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status'] = array( 'default' => 'all', 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'all', 'active', 'paused', 'disabled' ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-report-sales-controller.php 0000666 00000030566 15214171310 0014554 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/sales endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Report Sales controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Report_Sales_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports/sales'; /** * Report instance. * * @var WC_Admin_Report */ protected $report; /** * Register the routes for sales reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read report. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get sales reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { $data = array(); $item = $this->prepare_item_for_response( null, $request ); $data[] = $this->prepare_response_for_collection( $item ); return rest_ensure_response( $data ); } /** * Prepare a report sales object for serialization. * * @param null $_ * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $_, $request ) { // Set date filtering. $filter = array( 'period' => $request['period'], 'date_min' => $request['date_min'], 'date_max' => $request['date_max'], ); $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, ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $sales_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( array( 'about' => array( 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), ), ) ); /** * Filter a report sales returned from the API. * * Allows modification of the report sales data right before it is returned. * * @param WP_REST_Response $response The response object. * @param stdClass $data The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request ); } /** * Setup the report object and parse any date filtering. * * @param array $filter date filtering */ protected 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'] = $filter['date_min']; $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; } else { // Default custom range to today. $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period']; // Change "week" period to "7day". if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; } } $this->report->calculate_current_range( $filter['period'] ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'sales_report', 'type' => 'object', 'properties' => array( 'total_sales' => array( 'description' => __( 'Gross sales in the period.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'net_sales' => array( 'description' => __( 'Net sales in the period.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'average_sales' => array( 'description' => __( 'Average net daily sales.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_orders' => array( 'description' => __( 'Total of orders placed.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_items' => array( 'description' => __( 'Total of items purchased.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Total charged for taxes.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_shipping' => array( 'description' => __( 'Total charged for shipping.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'total_refunds' => array( 'description' => __( 'Total of refunded orders.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'total_discount' => array( 'description' => __( 'Total of coupons used.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'totals_grouped_by' => array( 'description' => __( 'Group type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'totals' => array( 'description' => __( 'Totals.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'array', ), 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'period' => array( 'description' => __( 'Report period.', 'woocommerce' ), 'type' => 'string', 'enum' => array( 'week', 'month', 'last_month', 'year' ), 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), 'date_min' => array( 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), 'type' => 'string', 'format' => 'date', 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), 'date_max' => array( 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), 'type' => 'string', 'format' => 'date', 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), ); } } class-wc-rest-product-attributes-controller.php 0000666 00000050572 15214171310 0015777 0 ustar 00 <?php /** * REST API Product Attributes controller * * Handles requests to the products/attributes endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Attributes controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Product_Attributes_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/attributes'; /** * Attribute name. * * @var string */ protected $attribute = ''; /** * Register the routes for product attributes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'name' => array( 'description' => __( 'Name for the resource.', 'woocommerce' ), 'type' => 'string', 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), )); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => true, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check if a given request has access to read the attributes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to create a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to update a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to delete a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to manipule this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all attributes. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $attributes = wc_get_attribute_taxonomies(); $data = array(); foreach ( $attributes as $attribute_obj ) { $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); $attribute = $this->prepare_response_for_collection( $attribute ); $data[] = $attribute; } return rest_ensure_response( $data ); } /** * Create a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { global $wpdb; $args = array( 'attribute_label' => $request['name'], 'attribute_name' => $request['slug'], 'attribute_type' => ! empty( $request['type'] ) ? $request['type'] : 'select', 'attribute_orderby' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', 'attribute_public' => true === $request['has_archives'], ); // Set the attribute slug. if ( empty( $args['attribute_name'] ) ) { $args['attribute_name'] = wc_sanitize_taxonomy_name( stripslashes( $args['attribute_label'] ) ); } else { $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); } $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); if ( is_wp_error( $valid_slug ) ) { return $valid_slug; } $insert = $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $args, array( '%s', '%s', '%s', '%s', '%d' ) ); // Checks for errors. if ( is_wp_error( $insert ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', $insert->get_error_message(), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $wpdb->insert_id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $this->update_additional_fields_for_object( $attribute, $request ); /** * Fires after a single product attribute is created or updated via the REST API. * * @param stdObject $attribute Inserted attribute object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); // Clear transients. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); return $response; } /** * Get a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function get_item( $request ) { global $wpdb; $attribute = $this->get_attribute( (int) $request['id'] ); if ( is_wp_error( $attribute ) ) { return $attribute; } $response = $this->prepare_item_for_response( $attribute, $request ); return rest_ensure_response( $response ); } /** * Update a single term from a taxonomy. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { global $wpdb; $id = (int) $request['id']; $format = array( '%s', '%s', '%s', '%s', '%d' ); $args = array( 'attribute_label' => $request['name'], 'attribute_name' => $request['slug'], 'attribute_type' => $request['type'], 'attribute_orderby' => $request['order_by'], 'attribute_public' => $request['has_archives'], ); $i = 0; foreach ( $args as $key => $value ) { if ( empty( $value ) && ! is_bool( $value ) ) { unset( $args[ $key ] ); unset( $format[ $i ] ); } $i++; } // Set the attribute slug. if ( ! empty( $args['attribute_name'] ) ) { $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], false ); if ( is_wp_error( $valid_slug ) ) { return $valid_slug; } } $update = $wpdb->update( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $args, array( 'attribute_id' => $id ), $format, array( '%d' ) ); // Checks for errors. if ( false === $update ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Could not edit the attribute', 'woocommerce' ), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $id ); if ( is_wp_error( $attribute ) ) { return $attribute; } $this->update_additional_fields_for_object( $attribute, $request ); /** * Fires after a single product attribute is created or updated via the REST API. * * @param stdObject $attribute Inserted attribute object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); // Clear transients. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); return rest_ensure_response( $response ); } /** * Delete a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { global $wpdb; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $attribute = $this->get_attribute( (int) $request['id'] ); if ( is_wp_error( $attribute ) ) { return $attribute; } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $deleted = $wpdb->delete( $wpdb->prefix . 'woocommerce_attribute_taxonomies', array( 'attribute_id' => $attribute->attribute_id ), array( '%d' ) ); if ( false === $deleted ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } $taxonomy = wc_attribute_taxonomy_name( $attribute->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 ); } } /** * Fires after a single attribute is deleted via the REST API. * * @param stdObject $attribute The deleted attribute. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); // Fires woocommerce_attribute_deleted hook. do_action( 'woocommerce_attribute_deleted', $attribute->attribute_id, $attribute->attribute_name, $taxonomy ); // Clear transients. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); return $response; } /** * Prepare a single product attribute output for response. * * @param obj $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->attribute_id, 'name' => $item->attribute_label, 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), 'type' => $item->attribute_type, 'order_by' => $item->attribute_orderby, 'has_archives' => (bool) $item->attribute_public, ); $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( $item ) ); /** * Filter a attribute item returned from the API. * * Allows modification of the product attribute data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original attribute object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); } /** * Prepare links for the request. * * @param object $attribute Attribute object. * @return array Links for the given attribute. */ protected function prepare_links( $attribute ) { $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), ), 'collection' => array( 'href' => rest_url( $base ), ), ); return $links; } /** * Get the Attribute's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'product_attribute', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'type' => array( 'description' => __( 'Type of attribute.', 'woocommerce' ), 'type' => 'string', 'default' => 'select', 'enum' => array_keys( wc_get_attribute_types() ), 'context' => array( 'view', 'edit' ), ), 'order_by' => array( 'description' => __( 'Default sort order.', 'woocommerce' ), 'type' => 'string', 'default' => 'menu_order', 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ), 'context' => array( 'view', 'edit' ), ), 'has_archives' => array( 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections * * @return array */ public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); return $params; } /** * Get attribute name. * * @param WP_REST_Request $request Full details about the request. * @return string */ protected function get_taxonomy( $request ) { if ( '' !== $this->attribute ) { return $this->attribute; } if ( $request['id'] ) { $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); $this->attribute = $name; } return $this->attribute; } /** * Get attribute data. * * @param int $id Attribute ID. * @return stdClass|WP_Error */ protected function get_attribute( $id ) { global $wpdb; $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 ) ) { return new WP_Error( 'woocommerce_rest_attribute_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } return $attribute; } /** * Validate attribute slug. * * @param string $slug * @param bool $new_data * @return bool|WP_Error */ protected function validate_attribute_slug( $slug, $new_data = true ) { if ( strlen( $slug ) >= 28 ) { return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max).', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); } return true; } } class-wc-rest-customers-controller.php 0000666 00000076224 15214171310 0014161 0 ustar 00 <?php /** * REST API Customers controller * * Handles requests to the /customers endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Customers controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Customers_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'customers'; /** * Register the routes for customers. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'email' => array( 'required' => true, 'type' => 'string', 'description' => __( 'New user email address.', 'woocommerce' ), ), 'username' => array( 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), 'description' => __( 'New user username.', 'woocommerce' ), 'type' => 'string', ), 'password' => array( 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), 'description' => __( 'New user password.', 'woocommerce' ), 'type' => 'string', ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), 'reassign' => array( 'default' => 0, 'type' => 'integer', 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'batch_items' ), 'permission_callback' => array( $this, 'batch_items_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), 'schema' => array( $this, 'get_public_batch_schema' ), ) ); } /** * Check whether a given request has permission to read customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create customers. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access update a customer. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function update_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a customer. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function delete_item_permissions_check( $request ) { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function batch_items_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'batch' ) ) { return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to manipule this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all customers. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_items( $request ) { $prepared_args = array(); $prepared_args['exclude'] = $request['exclude']; $prepared_args['include'] = $request['include']; $prepared_args['order'] = $request['order']; $prepared_args['number'] = $request['per_page']; if ( ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } $orderby_possibles = array( 'id' => 'ID', 'include' => 'include', 'name' => 'display_name', 'registered_date' => 'registered', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['search'] = $request['search']; if ( '' !== $prepared_args['search'] ) { $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } // Filter by email. if ( ! empty( $request['email'] ) ) { $prepared_args['search'] = $request['email']; $prepared_args['search_columns'] = array( 'user_email' ); } // Filter by role. if ( 'all' !== $request['role'] ) { $prepared_args['role'] = $request['role']; } /** * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. * * @see https://developer.wordpress.org/reference/classes/wp_user_query/ * * @param array $prepared_args Array of arguments for WP_User_Query. * @param WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); $query = new WP_User_Query( $prepared_args ); $users = array(); foreach ( $query->results as $user ) { $data = $this->prepare_item_for_response( $user, $request ); $users[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $users ); // Store pagation values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $prepared_args['fields'] = 'ID'; $total_users = $query->get_total(); if ( $total_users < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'] ); unset( $prepared_args['offset'] ); $count_query = new WP_User_Query( $prepared_args ); $total_users = $count_query->get_total(); } $response->header( 'X-WP-Total', (int) $total_users ); $max_pages = ceil( $total_users / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Create a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); } // Sets the username. $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; // Sets the password. $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; // Create customer. $customer_id = wc_create_new_customer( $request['email'], $request['username'], $request['password'] ); if ( is_wp_error( $customer_id ) ) { return $customer_id; } $customer = get_user_by( 'id', $customer_id ); $this->update_additional_fields_for_object( $customer, $request ); // Add customer data. $this->update_customer_meta_fields( $customer, $request ); /** * Fires after a customer is created or updated via the REST API. * * @param WP_User $customer Data used to create the customer. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating customer, false when updating customer. */ do_action( 'woocommerce_rest_insert_customer', $customer, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer_id ) ) ); return $response; } /** * Get a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $customer = get_userdata( $id ); if ( empty( $id ) || empty( $customer->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $customer = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $customer ); return $response; } /** * Update a single user. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { $id = (int) $request['id']; $customer = get_userdata( $id ); if ( ! $customer ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->user_email ) { return new WP_Error( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $request['username'] ) && $request['username'] !== $customer->user_login ) { return new WP_Error( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), array( 'status' => 400 ) ); } // Customer email. if ( isset( $request['email'] ) ) { wp_update_user( array( 'ID' => $customer->ID, 'user_email' => sanitize_email( $request['email'] ) ) ); } // Customer password. if ( isset( $request['password'] ) ) { wp_update_user( array( 'ID' => $customer->ID, 'user_pass' => wc_clean( $request['password'] ) ) ); } $this->update_additional_fields_for_object( $customer, $request ); // Update customer data. $this->update_customer_meta_fields( $customer, $request ); /** * Fires after a customer is created or updated via the REST API. * * @param WP_User $customer Data used to create the customer. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating customer, false when updating customer. */ do_action( 'woocommerce_rest_insert_customer', $customer, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Delete a single customer. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { $id = (int) $request['id']; $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $customer = get_userdata( $id ); if ( ! $customer ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); } } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $customer, $request ); /** Include admin customer functions to get access to wp_delete_user() */ require_once ABSPATH . 'wp-admin/includes/user.php'; $result = wp_delete_user( $id, $reassign ); if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); } /** * Fires after a customer is deleted via the REST API. * * @param WP_User $customer The customer data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_customer', $customer, $response, $request ); return $response; } /** * Prepare a single customer output for response. * * @param WP_User $customer Customer object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $customer, $request ) { $last_order = wc_get_customer_last_order( $customer->ID ); $data = array( 'id' => $customer->ID, 'date_created' => wc_rest_prepare_date_response( $customer->user_registered ), 'date_modified' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'last_order' => array( 'id' => is_object( $last_order ) ? $last_order->id : null, 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->post->post_date_gmt ) : null ), 'orders_count' => wc_get_customer_order_count( $customer->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), 'avatar_url' => wc_get_customer_avatar_url( $customer->customer_email ), 'billing' => array( 'first_name' => $customer->billing_first_name, 'last_name' => $customer->billing_last_name, 'company' => $customer->billing_company, 'address_1' => $customer->billing_address_1, 'address_2' => $customer->billing_address_2, 'city' => $customer->billing_city, 'state' => $customer->billing_state, 'postcode' => $customer->billing_postcode, 'country' => $customer->billing_country, 'email' => $customer->billing_email, 'phone' => $customer->billing_phone, ), 'shipping' => array( 'first_name' => $customer->shipping_first_name, 'last_name' => $customer->shipping_last_name, 'company' => $customer->shipping_company, 'address_1' => $customer->shipping_address_1, 'address_2' => $customer->shipping_address_2, 'city' => $customer->shipping_city, 'state' => $customer->shipping_state, 'postcode' => $customer->shipping_postcode, 'country' => $customer->shipping_country, ), ); $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( $customer ) ); /** * Filter customer data returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_User $customer User object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_customer', $response, $customer, $request ); } /** * Update customer meta fields. * * @param WP_User $customer * @param WP_REST_Request $request */ protected function update_customer_meta_fields( $customer, $request ) { $schema = $this->get_item_schema(); // Customer first name. if ( isset( $request['first_name'] ) ) { update_user_meta( $customer->ID, 'first_name', wc_clean( $request['first_name'] ) ); } // Customer last name. if ( isset( $request['last_name'] ) ) { update_user_meta( $customer->ID, 'last_name', wc_clean( $request['last_name'] ) ); } // Customer billing address. if ( isset( $request['billing'] ) ) { foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $address ) { if ( isset( $request['billing'][ $address ] ) ) { update_user_meta( $customer->ID, 'billing_' . $address, wc_clean( $request['billing'][ $address ] ) ); } } } // Customer shipping address. if ( isset( $request['shipping'] ) ) { foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $address ) { if ( isset( $request['shipping'][ $address ] ) ) { update_user_meta( $customer->ID, 'shipping_' . $address, wc_clean( $request['shipping'][ $address ] ) ); } } } } /** * Prepare links for the request. * * @param WP_User $customer Customer object. * @return array Links for the given customer. */ protected function prepare_links( $customer ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Customer's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'email' => array( 'description' => __( 'The email address for the customer.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'first_name' => array( 'description' => __( 'Customer first name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Customer last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'username' => array( 'description' => __( 'Customer login name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_user', ), ), 'password' => array( 'description' => __( 'Customer password.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'last_order' => array( 'description' => __( 'Last order data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => array( 'id' => array( 'description' => __( 'Last order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date' => array( 'description' => __( 'UTC DateTime of the customer last order.', 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ), 'orders_count' => array( 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_spent' => array( 'description' => __( 'Total amount spent.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'avatar_url' => array( 'description' => __( 'Avatar URL.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing' => array( 'description' => __( 'List of billing address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'email' => array( 'description' => __( 'Email address.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'view', 'edit' ), ), 'phone' => array( 'description' => __( 'Phone number.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'shipping' => array( 'description' => __( 'List of shipping address data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( 'description' => __( 'First name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'last_name' => array( 'description' => __( 'Last name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'company' => array( 'description' => __( 'Company name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_1' => array( 'description' => __( 'Address line 1.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'address_2' => array( 'description' => __( 'Address line 2.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'state' => array( 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'postcode' => array( 'description' => __( 'Postal code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'country' => array( 'description' => __( 'ISO code of the country.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get role names. * * @return array */ protected function get_role_names() { global $wp_roles; return array_keys( $wp_roles->role_names ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { $params = parent::get_collection_params(); $params['context']['default'] = 'view'; $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['include'] = array( 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['orderby'] = array( 'default' => 'name', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( 'id', 'include', 'name', 'registered_date', ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $params['email'] = array( 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), 'type' => 'string', 'format' => 'email', 'validate_callback' => 'rest_validate_request_arg', ); $params['role'] = array( 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), 'type' => 'string', 'default' => 'customer', 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), 'validate_callback' => 'rest_validate_request_arg', ); return $params; } } class-wc-rest-order-notes-controller.php 0000666 00000034101 15214171310 0014362 0 ustar 00 <?php /** * REST API Order Notes controller * * Handles requests to the /orders/<order_id>/notes endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Order Notes controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Order_Notes_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'orders/(?P<order_id>[\d]+)/notes'; /** * Post type. * * @var string */ protected $post_type = 'shop_order'; /** * Register the routes for order notes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'note' => array( 'type' => 'string', 'description' => __( 'Order note content.', 'woocommerce' ), 'required' => true, ), ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), 'order_id' => array( 'description' => __( 'The order ID.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read order notes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create order notes. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $post = get_post( (int) $request['order_id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a order note. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function delete_item_permissions_check( $request ) { $post = get_post( (int) $request['order_id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get order notes from an order. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $order = get_post( (int) $request['order_id'] ); if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); } $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 ); $data = array(); foreach ( $notes as $note ) { $order_note = $this->prepare_item_for_response( $note, $request ); $order_note = $this->prepare_response_for_collection( $order_note ); $data[] = $order_note; } return rest_ensure_response( $data ); } /** * Create a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); } $order = get_post( (int) $request['order_id'] ); if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); } $order = wc_get_order( $order ); // Create the note. $note_id = $order->add_order_note( $request['note'], $request['customer_note'] ); if ( ! $note_id ) { return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); } $note = get_comment( $note_id ); $this->update_additional_fields_for_object( $note, $request ); /** * Fires after a order note is created or updated via the REST API. * * @param WP_Comment $note New order note object. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $note, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->id, $this->rest_base ), $note_id ) ) ); return $response; } /** * Get a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $order = get_post( (int) $request['order_id'] ); if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); } $note = get_comment( $id ); if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $order_note = $this->prepare_item_for_response( $note, $request ); $response = rest_ensure_response( $order_note ); return $response; } /** * Delete a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $id = (int) $request['id']; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $order = get_post( (int) $request['order_id'] ); if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); } $note = get_comment( $id ); if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $note, $request ); $result = wp_delete_comment( $note->comment_ID, true ); if ( ! $result ) { return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); } /** * Fires after a order note is deleted or trashed via the REST API. * * @param WP_Comment $note The deleted or trashed order note. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); return $response; } /** * Prepare a single order note output for response. * * @param WP_Comment $note Order note object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => (int) $note->comment_ID, 'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); $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( $note ) ); /** * Filter order note object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $note Order note object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); } /** * Prepare links for the request. * * @param WP_Comment $note Delivery order_note object. * @return array Links for the given order note. */ protected function prepare_links( $note ) { $order_id = (int) $note->comment_post_ID; $base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), ), ); return $links; } /** * Get the Order Notes schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'order_note', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'note' => array( 'description' => __( 'Order note.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'customer_note' => array( 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } class-wc-rest-webhook-deliveries.php 0000666 00000024131 15214171310 0013531 0 ustar 00 <?php /** * REST API Webhooks controller * * Handles requests to the /webhooks/<webhook_id>/deliveries endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Webhook Deliveries controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Webhook_Deliveries_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'webhooks/(?P<webhook_id>[\d]+)/deliveries'; /** * Register the routes for webhook deliveries. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'args' => array( 'webhook_id' => array( 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 'args' => array( 'webhook_id' => array( 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), 'type' => 'integer', ), 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read webhook deliveries. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( 'shop_webhook', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access to read a webhook develivery. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { $post = get_post( (int) $request['webhook_id'] ); if ( $post && ! wc_rest_check_post_permissions( 'shop_webhook', 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all webhook deliveries. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $webhook = new WC_Webhook( (int) $request['webhook_id'] ); if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); } $logs = $webhook->get_delivery_logs(); $data = array(); foreach ( $logs as $log ) { $delivery = $this->prepare_item_for_response( (object) $log, $request ); $delivery = $this->prepare_response_for_collection( $delivery ); $data[] = $delivery; } return rest_ensure_response( $data ); } /** * Get a single webhook delivery. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = (int) $request['id']; $webhook = new WC_Webhook( (int) $request['webhook_id'] ); if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); } $log = $webhook->get_delivery_log( $id ); if ( empty( $id ) || empty( $log ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $delivery = $this->prepare_item_for_response( (object) $log, $request ); $response = rest_ensure_response( $delivery ); return $response; } /** * Prepare a single webhook delivery output for response. * * @param stdClass $log Delivery log object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $log, $request ) { $data = (array) $log; // Add timestamp. $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); // Remove comment object. unset( $data['comment'] ); $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( $log ) ); /** * Filter webhook delivery object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $log Delivery log object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); } /** * Prepare links for the request. * * @param stdClass $log Delivery log object. * @return array Links for the given webhook delivery. */ protected function prepare_links( $log ) { $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; $base = str_replace( '(?P<webhook_id>[\d]+)', $webhook_id, $this->rest_base ); $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), ), ); return $links; } /** * Get the Webhook's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'webhook_delivery', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'duration' => array( 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'summary' => array( 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'request_url' => array( 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'request_headers' => array( 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view' ), 'readonly' => true, ), 'request_headers' => array( 'description' => __( 'Request headers.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'request_body' => array( 'description' => __( 'Request body.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_code' => array( 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_message' => array( 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'response_headers' => array( 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, 'items' => array( 'type' => 'string', ), ), 'response_body' => array( 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'date_created' => array( 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } class-wc-rest-product-categories-controller.php 0000666 00000020234 15214171310 0015726 0 ustar 00 <?php /** * REST API Product Categories controller * * Handles requests to the products/categories endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Categories controller class. * * @package WooCommerce/API * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/categories'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_cat'; /** * Prepare a single product category output for response. * * @param WP_Term $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { // Get category display type. $display_type = get_woocommerce_term_meta( $item->term_id, 'display_type' ); // Get category order. $menu_order = get_woocommerce_term_meta( $item->term_id, 'order' ); $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'parent' => (int) $item->parent, 'description' => $item->description, 'display' => $display_type ? $display_type : 'default', 'image' => array(), 'menu_order' => (int) $menu_order, 'count' => (int) $item->count, ); // Get category image. if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_id' ) ) { $attachment = get_post( $image_id ); $data['image'] = array( 'id' => (int) $image_id, 'date_created' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), 'src' => wp_get_attachment_url( $image_id ), 'title' => get_the_title( $attachment ), 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), ); } $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( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Update term meta fields. * * @param WP_Term $term * @param WP_REST_Request $request * @return bool|WP_Error */ protected function update_term_meta_fields( $term, $request ) { $id = (int) $term->term_id; if ( isset( $request['display'] ) ) { update_woocommerce_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); } if ( isset( $request['menu_order'] ) ) { update_woocommerce_term_meta( $id, 'order', $request['menu_order'] ); } if ( ! empty( $request['image'] ) ) { if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); if ( is_wp_error( $upload ) ) { return $upload; } $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); } else { $image_id = absint( $request['image']['id'] ); } // Check if image_id is a valid image attachment before updating the term meta. if ( $image_id && wp_attachment_is_image( $image_id ) ) { update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); // Set the image alt. if ( ! empty( $request['image']['alt'] ) ) { update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); } // Set the image title. if ( ! empty( $request['image']['title'] ) ) { wp_update_post( array( 'ID' => $image_id, 'post_title' => wc_clean( $request['image']['title'] ) ) ); } } } return true; } /** * Get the Category schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Category name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'parent' => array( 'description' => __( 'The id for the parent of the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'display' => array( 'description' => __( 'Category archive display type.', 'woocommerce' ), 'type' => 'string', 'default' => 'default', 'enum' => array( 'default', 'products', 'subcategories', 'both' ), 'context' => array( 'view', 'edit' ), ), 'image' => array( 'description' => __( 'Image data.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'properties' => array( 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } class-wc-rest-tax-classes-controller.php 0000666 00000024720 15214171310 0014356 0 ustar 00 <?php /** * REST API Tax Classes controller * * Handles requests to the /taxes/classes endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Tax Classes controller class. * * @package WooCommerce/API * @extends WC_REST_Controller */ class WC_REST_Tax_Classes_Controller extends WC_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'taxes/classes'; /** * Register the routes for tax classes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)', array( 'args' => array( 'slug' => array( 'description' => __( 'Unique slug for the resource.', 'woocommerce' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'default' => false, 'type' => 'boolean', 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Check whether a given request has permission to read tax classes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access create tax classes. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Check if a given request has access delete a tax. * * @param WP_REST_Request $request Full details about the request. * @return boolean */ public function delete_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get all tax classes. * * @param WP_REST_Request $request * @return array */ public function get_items( $request ) { $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[] = array( 'slug' => sanitize_title( $class ), 'name' => $class, ); } $data = array(); foreach ( $tax_classes as $tax_class ) { $class = $this->prepare_item_for_response( $tax_class, $request ); $class = $this->prepare_response_for_collection( $class ); $data[] = $class; } return rest_ensure_response( $data ); } /** * Create a single tax. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { $exists = false; $classes = WC_Tax::get_tax_classes(); $tax_class = array( 'slug' => sanitize_title( $request['name'] ), 'name' => $request['name'], ); // Check if class exists. foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $tax_class['slug'] ) { $exists = true; break; } } // Return error if tax class already exists. if ( $exists ) { return new WP_Error( 'woocommerce_rest_tax_class_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); } // Add the new class. $classes[] = $tax_class['name']; update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); $this->update_additional_fields_for_object( $tax_class, $request ); /** * Fires after a tax class is created or updated via the REST API. * * @param stdClass $tax_class Data used to create the tax class. * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating tax class, false when updating tax class. */ do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax_class, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); return $response; } /** * Delete a single tax class. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function delete_item( $request ) { global $wpdb; $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } $tax_class = array( 'slug' => sanitize_title( $request['slug'] ), 'name' => '', ); $classes = WC_Tax::get_tax_classes(); $deleted = false; foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $tax_class['slug'] ) { $tax_class['name'] = $class; unset( $classes[ $key ] ); $deleted = true; break; } } if ( ! $deleted ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); // Delete tax rate locations locations from the selected class. $wpdb->query( $wpdb->prepare( " DELETE locations.* FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations INNER JOIN {$wpdb->prefix}woocommerce_tax_rates AS rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_class = '%s' ", $tax_class['slug'] ) ); // Delete tax rates in the selected class. $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $tax_class['slug'] ), array( '%s' ) ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $tax_class, $request ); /** * Fires after a tax class is deleted via the REST API. * * @param stdClass $tax_class The tax data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); return $response; } /** * Prepare a single tax class output for response. * * @param array $tax_class Tax class data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $tax_class, $request ) { $data = $tax_class; $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() ); /** * Filter tax object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param stdClass $tax_class Tax object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); } /** * Prepare links for the request. * * @return array Links for the given tax class. */ protected function prepare_links() { $links = array( 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Get the Tax Classes schema, conforming to JSON Schema * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'tax_class', 'type' => 'object', 'properties' => array( 'slug' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Tax class name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'required' => true, 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), ), ); return $this->add_additional_fields_schema( $schema ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } } legacy/v2/class-wc-api-json-handler.php 0000666 00000003672 15214171310 0013724 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce/API * @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'] ) ) { // JSONP enabled by default if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); } // Check for invalid characters (only alphanumeric allowed) if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); } // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; } return json_encode( $data ); } } legacy/v2/class-wc-api-exception.php 0000666 00000002207 15214171310 0013327 0 ustar 00 <?php /** * WooCommerce API Exception Class * * Extends Exception to provide additional data * * @author WooThemes * @category API * @package WooCommerce/API * @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; } } legacy/v2/class-wc-api-orders.php 0000666 00000163742 15214171310 0012643 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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_post = get_post( $id ); $order_data = array( 'id' => $order->id, 'order_number' => $order->get_order_number(), 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ), 'completed_at' => $this->server->format_datetime( $order->completed_date, true ), 'status' => $order->get_status(), 'currency' => $order->get_order_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_total_shipping(), $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->payment_method, 'method_title' => $order->payment_method_title, 'paid' => isset( $order->paid_date ), ), 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), 'note' => $order->customer_note, 'customer_ip' => $order->customer_ip_address, 'customer_user_agent' => $order->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 = $order->get_product_from_item( $item ); $product_id = null; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id; $product_sku = $product->get_sku(); } $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); $hideprefix = ( isset( $filter['all_item_meta'] ) && $filter['all_item_meta'] === 'true' ) ? null : '_'; foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta['key'], 'label' => $formatted_meta['label'], 'value' => $formatted_meta['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['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => $product_id, 'sku' => $product_sku, 'meta' => $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['method_id'], 'method_title' => $shipping_item['name'], 'total' => wc_format_decimal( $shipping_item['cost'], $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['name'], 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, '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['name'], 'amount' => wc_format_decimal( $coupon_item['discount_amount'], $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 */ 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 ( $status == 'any' ) { $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 */ public function create_order( $data ) { global $wpdb; wc_transaction_query( 'start' ); 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->id, '_payment_method', $data['payment_details']['method_id'] ); update_post_meta( $order->id, '_payment_method_title', $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->id, '_order_currency', $data['currency'] ); } // set order meta if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->id, $data['order_meta'] ); } // HTTP 201 Created $this->server->send_status( 201 ); wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_api_create_order', $order->id, $data, $this ); wc_transaction_query( 'commit' ); return $this->get_order( $order->id ); } catch ( WC_API_Exception $e ) { wc_transaction_query( 'rollback' ); 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 * @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 */ 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->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->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->id, '_payment_method', $data['payment_details']['method_id'] ); } // Method title. if ( isset( $data['payment_details']['method_title'] ) ) { update_post_meta( $order->id, '_payment_method_title', $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->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->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'] ) ) { $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); } wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_api_edit_order', $order->id, $data, $this ); return $this->get_order( $id ); } 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 */ 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 ] ); } } } $order->set_address( $billing_address, 'billing' ); $order->set_address( $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 ); } } } /** * 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->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 ); $item_args = array(); // 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 ); } } $item_args['variation'] = $item['variations']; $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item_args['variation'] ); } $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 ( isset( $item['quantity'] ) ) { $item_args['qty'] = $item['quantity']; } // total if ( isset( $item['total'] ) ) { $item_args['totals']['total'] = floatval( $item['total'] ); } // total tax if ( isset( $item['total_tax'] ) ) { $item_args['totals']['tax'] = floatval( $item['total_tax'] ); } // subtotal if ( isset( $item['subtotal'] ) ) { $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); } // subtotal tax if ( isset( $item['subtotal_tax'] ) ) { $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); } if ( $creating ) { $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again', 'woocommerce' ), 500 ); } } else { $item_id = $order->update_product( $item['id'], $product, $item_args ); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_line_item', __( 'Cannot update 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 int $product_id main product ID * @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_', '', str_replace( 'pa_', '', $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_', '', str_replace( 'pa_', '', $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 */ 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'] ); $shipping_id = $order->add_shipping( $rate ); if ( ! $shipping_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_shipping', __( 'Cannot create shipping method, try again', 'woocommerce' ), 500 ); } } else { $shipping_args = array(); if ( isset( $shipping['method_id'] ) ) { $shipping_args['method_id'] = $shipping['method_id']; } if ( isset( $shipping['method_title'] ) ) { $shipping_args['method_title'] = $shipping['method_title']; } if ( isset( $shipping['total'] ) ) { $shipping_args['cost'] = floatval( $shipping['total'] ); } $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); 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 ); } $order_fee = new stdClass(); $order_fee->id = sanitize_title( $fee['title'] ); $order_fee->name = $fee['title']; $order_fee->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; $order_fee->taxable = false; $order_fee->tax = 0; $order_fee->tax_data = array(); $order_fee->tax_class = ''; // if taxable, tax class and total are required if ( isset( $fee['taxable'] ) && $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 ); } $order_fee->taxable = true; $order_fee->tax_class = $fee['tax_class']; if ( isset( $fee['total_tax'] ) ) { $order_fee->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; } if ( isset( $fee['tax_data'] ) ) { $order_fee->tax = wc_format_refund_total( array_sum( $fee['tax_data'] ) ); $order_fee->tax_data = array_map( 'wc_format_refund_total', $fee['tax_data'] ); } } $fee_id = $order->add_fee( $order_fee ); if ( ! $fee_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_fee', __( 'Cannot create fee, try again', 'woocommerce' ), 500 ); } } else { $fee_args = array(); if ( isset( $fee['title'] ) ) { $fee_args['name'] = $fee['title']; } if ( isset( $fee['tax_class'] ) ) { $fee_args['tax_class'] = $fee['tax_class']; } if ( isset( $fee['total'] ) ) { $fee_args['line_total'] = floatval( $fee['total'] ); } if ( isset( $fee['total_tax'] ) ) { $fee_args['line_tax'] = floatval( $fee['total_tax'] ); } $fee_id = $order->update_fee( $fee['id'], $fee_args ); 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 ); } $coupon_id = $order->add_coupon( $coupon['code'], isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0 ); if ( ! $coupon_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_order_coupon', __( 'Cannot create coupon, try again', 'woocommerce' ), 500 ); } } else { $coupon_args = array(); if ( isset( $coupon['code'] ) ) { $coupon_args['code'] = $coupon['code']; } if ( isset( $coupon['amount'] ) ) { $coupon_args['discount_amount'] = floatval( $coupon['amount'] ); } $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); 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 */ 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 */ 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' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, ); 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->id, $note_id ); } 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->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->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->id, $this ); return $this->get_order_note( $order->id, $note->comment_ID ); } 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 = wp_delete_comment( $note->comment_ID, true ); 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 */ 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 string|null $fields fields to limit response to * @return array */ public function get_order_refund( $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_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 = $order->get_product_from_item( $item ); $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); foreach ( $meta->get_formatted() as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $meta_key, 'label' => $formatted_meta['label'], 'value' => $formatted_meta['value'], ); } $line_items[] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), 'subtotal_tax' => wc_format_decimal( $item['line_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' => (int) $item['qty'], 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id, 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => $item_meta, 'refunded_item_id' => (int) $item['refunded_item_id'], ); } $order_refund = array( 'id' => $refund->id, 'created_at' => $this->server->format_datetime( $refund->date ), 'amount' => wc_format_decimal( $refund->get_refund_amount(), 2 ), 'reason' => $refund->get_refund_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->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { $result = $payment_gateways[ $order->payment_method ]->process_refund( $order_id, $refund->get_refund_amount(), $refund->get_refund_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->id, $order_id, $this ); return $this->get_order_refund( $order_id, $refund->id ); } 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_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 */ 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']; } } // Order don't exists / create order else { $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_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } legacy/v2/class-wc-api-customers.php 0000666 00000061030 15214171310 0013354 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Customers */ 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 */ 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 WP_User( $id ); // Get info about user's last order $last_order = $wpdb->get_row( "SELECT id, post_date_gmt FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = {$customer->ID} AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) ORDER BY posts.ID DESC " ); $roles = array_values( $customer->roles ); $customer_data = array( 'id' => $customer->ID, 'created_at' => $this->server->format_datetime( $customer->user_registered ), 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'role' => $roles[0], 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null, 'orders_count' => wc_get_customer_order_count( $customer->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), 'avatar_url' => $this->get_avatar_url( $customer->customer_email ), 'billing_address' => array( 'first_name' => $customer->billing_first_name, 'last_name' => $customer->billing_last_name, 'company' => $customer->billing_company, 'address_1' => $customer->billing_address_1, 'address_2' => $customer->billing_address_2, 'city' => $customer->billing_city, 'state' => $customer->billing_state, 'postcode' => $customer->billing_postcode, 'country' => $customer->billing_country, 'email' => $customer->billing_email, 'phone' => $customer->billing_phone, ), 'shipping_address' => array( 'first_name' => $customer->shipping_first_name, 'last_name' => $customer->shipping_last_name, 'company' => $customer->shipping_company, 'address_1' => $customer->shipping_address_1, 'address_2' => $customer->shipping_address_2, 'city' => $customer->shipping_city, 'state' => $customer->shipping_state, 'postcode' => $customer->shipping_postcode, 'country' => $customer->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 */ 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 */ 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 */ protected function update_customer_data( $id, $data ) { // Customer first name. if ( isset( $data['first_name'] ) ) { update_user_meta( $id, 'first_name', wc_clean( $data['first_name'] ) ); } // Customer last name. if ( isset( $data['last_name'] ) ) { update_user_meta( $id, 'last_name', wc_clean( $data['last_name'] ) ); } // Customer billing address. if ( isset( $data['billing_address'] ) ) { foreach ( $this->get_customer_billing_address() as $address ) { if ( isset( $data['billing_address'][ $address ] ) ) { update_user_meta( $id, 'billing_' . $address, wc_clean( $data['billing_address'][ $address ] ) ); } } } // Customer shipping address. if ( isset( $data['shipping_address'] ) ) { foreach ( $this->get_customer_shipping_address() as $address ) { if ( isset( $data['shipping_address'][ $address ] ) ) { update_user_meta( $id, 'shipping_' . $address, wc_clean( $data['shipping_address'][ $address ] ) ); } } } do_action( 'woocommerce_api_update_customer_data', $id, $data ); } /** * Create a customer * * @since 2.2 * @param array $data * @return array */ 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 ); } // Sets the username. $data['username'] = ! empty( $data['username'] ) ? $data['username'] : ''; // Sets the password. $data['password'] = ! empty( $data['password'] ) ? $data['password'] : ''; // Attempts to create the new customer $id = wc_create_new_customer( $data['email'], $data['username'], $data['password'] ); // Checks for an error in the customer creation. if ( is_wp_error( $id ) ) { throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); } // Added customer data. $this->update_customer_data( $id, $data ); do_action( 'woocommerce_api_create_customer', $id, $data ); $this->server->send_status( 201 ); return $this->get_customer( $id ); } catch ( WC_API_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 */ 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 email. if ( isset( $data['email'] ) ) { wp_update_user( array( 'ID' => $id, 'user_email' => sanitize_email( $data['email'] ) ) ); } // Customer password. if ( isset( $data['password'] ) ) { wp_update_user( array( 'ID' => $id, 'user_pass' => wc_clean( $data['password'] ) ) ); } // Update customer data. $this->update_customer_data( $id, $data ); do_action( 'woocommerce_api_edit_customer', $id, $data ); return $this->get_customer( $id ); } catch ( WC_API_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 */ 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 */ 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 = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '%s' AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) ", $id ) ); 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 */ 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[ $key ] = $download; $downloads[ $key ]['access_expires'] = $this->server->format_datetime( $downloads[ $key ]['access_expires'] ); } 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 ( $args['limit'] == -1 ) { 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']; } // Orderby 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 = ( $args['limit'] == -1 ) ? 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->customer_user ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->billing_email, 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->customer_user ) ); } 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 ) ); } } /** * Wrapper for @see get_avatar() which doesn't simply return * the URL so we need to pluck it from the HTML img tag * * Kudos to https://github.com/WP-API/WP-API for offering a better solution * * @since 2.1 * @param string $email the customer's email * @return string the URL to the customer's avatar */ private function get_avatar_url( $email ) { $avatar_html = get_avatar( $email ); // Get the URL of the avatar from the provided HTML preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { return esc_url_raw( $matches[1] ); } return null; } /** * 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 ( ! current_user_can( 'edit_users' ) ) { 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 ( ! current_user_can( 'delete_users' ) ) { 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 */ 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']; } } // Customer don't exists / create customer else { $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() ) ); } } } legacy/v2/class-wc-api-reports.php 0000666 00000023126 15214171310 0013032 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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 */ 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_title(), '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 true if the request is valid and should be processed, false otherwise */ 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; } } } legacy/v2/class-wc-api-webhooks.php 0000666 00000031600 15214171310 0013151 0 ustar 00 <?php /** * WooCommerce API Webhooks class * * Handles requests to the /webhooks endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 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->posts as $webhook_id ) { if ( ! $this->is_readable( $webhook_id ) ) { continue; } $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'webhooks' => $webhooks ); } /** * Get the webhook for the given ID * * @since 2.2 * @param int $id webhook ID * @param array $fields * @return array */ 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 = new WC_Webhook( $id ); $webhook_data = array( 'id' => $webhook->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_post_data()->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ), ); 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 */ public function get_webhooks_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_webhooks' ) ) { 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' => (int) $query->found_posts ); } 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 */ 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( 'publish_shop_webhooks' ) ) { 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' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password, 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ), $data, $this ); $webhook_id = wp_insert_post( $webhook_data ); if ( is_wp_error( $webhook_id ) || ! $webhook_id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 ); } $webhook = new WC_Webhook( $webhook_id ); // set topic, delivery URL, and optional secret $webhook->set_topic( $data['topic'] ); $webhook->set_delivery_url( $data['delivery_url'] ); // set secret if provided, defaults to API users consumer secret $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : '' ); // send ping $webhook->deliver_ping(); // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_webhook', $webhook->id, $this ); delete_transient( 'woocommerce_webhook_ids' ); return $this->get_webhook( $webhook->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 */ 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 = new WC_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->update_status( $data['status'] ); } // update user ID $webhook_data = array( 'ID' => $webhook->id, 'post_author' => get_current_user_id() ); // update name if ( ! empty( $data['name'] ) ) { $webhook_data['post_title'] = $data['name']; } // update post wp_update_post( $webhook_data ); do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this ); delete_transient( 'woocommerce_webhook_ids' ); return $this->get_webhook( $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 */ 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 ); delete_transient( 'woocommerce_webhook_ids' ); // no way to manage trashed webhooks at the moment, so force delete return $this->delete( $id, 'webhook', true ); } /** * Helper method to get webhook post objects * * @since 2.2 * @param array $args request arguments for filtering query * @return WP_Query */ private function query_webhooks( $args ) { // Set base query arguments $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_webhook', ); // Add status argument if ( ! empty( $args['status'] ) ) { switch ( $args['status'] ) { case 'active': $query_args['post_status'] = 'publish'; break; case 'paused': $query_args['post_status'] = 'draft'; break; case 'disabled': $query_args['post_status'] = 'pending'; break; default: $query_args['post_status'] = 'publish'; } unset( $args['status'] ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Get deliveries for a webhook * * @since 2.2 * @param string $webhook_id webhook ID * @param string|null $fields fields to include in response * @return array */ 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; } $webhook = new WC_Webhook( $webhook_id ); $logs = $webhook->get_delivery_logs(); $delivery_logs = array(); foreach ( $logs as $log ) { // Add timestamp $log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); // Remove comment object unset( $log['comment'] ); $delivery_logs[] = $log; } return array( 'webhook_deliveries' => $delivery_logs ); } /** * Get the delivery log for the given webhook ID and delivery ID * * @since 2.2 * @param string $webhook_id webhook ID * @param string $id delivery log ID * @param string|null $fields fields to limit response to * @return array */ 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 = $webhook->get_delivery_log( $id ); if ( ! $log ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery', 'woocommerce' ), 400 ); } $delivery_log = $log; // Add timestamp $delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); // Remove comment object unset( $delivery_log['comment'] ); return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } legacy/v2/class-wc-api-products.php 0000666 00000241314 15214171310 0013200 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.1 */ 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 */ 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->parent ) { $product_data['parent'] = $this->get_product_data( $product->parent ); } 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 */ 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 */ 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 = $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 = $data['short_description']; } $new_product = array( 'post_title' => wc_clean( $data['title'] ), 'post_status' => ( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ), 'post_type' => 'product', 'post_excerpt' => ( isset( $data['short_description'] ) ? $post_excerpt : '' ), 'post_content' => ( isset( $data['description'] ) ? $post_content : '' ), 'post_author' => get_current_user_id(), ); // Attempts to create the new product $id = wp_insert_post( $new_product, true ); // Checks for an error in the product creation if ( is_wp_error( $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'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields $this->save_product_meta( $id, $data ); // Save variations if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $id, $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_API_Exception $e ) { // Remove the product when fails $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 */ 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; } $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); // Product title. if ( isset( $data['title'] ) ) { wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) ); } // Product name (slug). if ( isset( $data['name'] ) ) { wp_update_post( array( 'ID' => $id, 'post_name' => sanitize_title( $data['name'] ) ) ); } // Product status. if ( isset( $data['status'] ) ) { wp_update_post( array( 'ID' => $id, 'post_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'] ) ? $data['short_description'] : wc_clean( $data['short_description'] ); wp_update_post( array( 'ID' => $id, 'post_excerpt' => $post_excerpt ) ); } // Product description. if ( isset( $data['description'] ) ) { // Enable description html tags. $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] ); wp_update_post( array( 'ID' => $id, 'post_content' => $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'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields $this->save_product_meta( $id, $data ); // Save variations $product = get_product( $id ); if ( $product->is_type( 'variable' ) ) { if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $id, $data ); } else { // Just sync variations WC_Product_Variable::sync( $id ); WC_Product_Variable::sync_stock_status( $id ); } } do_action( 'woocommerce_api_edit_product', $id, $data ); // Clear cache/transients wc_delete_product_transients( $id ); return $this->get_product( $id ); } 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 */ public function delete_product( $id, $force = false ) { $id = $this->validate_request( $id, 'product', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_product', $id, $this ); // If we're forcing, then delete permanently. if ( $force ) { $child_product_variations = get_children( 'post_parent=' . $id . '&post_type=product_variation' ); if ( ! empty( $child_product_variations ) ) { foreach ( $child_product_variations as $child ) { wp_delete_post( $child->ID, true ); } } $child_products = get_children( 'post_parent=' . $id . '&post_type=product' ); if ( ! empty( $child_products ) ) { foreach ( $child_products as $child ) { $child_post = array(); $child_post['ID'] = $child->ID; $child_post['post_parent'] = 0; wp_update_post( $child_post ); } } $result = wp_delete_post( $id, true ); } else { $result = wp_trash_post( $id ); } 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 */ 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 string $filter filters to include in response * @param string $status the order status to retrieve * @param $page $page page to retrieve * @return array */ 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 */ 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 */ 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_woocommerce_term_meta( $term_id, 'display_type' ); // Get category image $image = ''; if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) { $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 $product * @return WC_Product */ private function get_product_data( $product ) { $prices_precision = wc_get_price_decimals(); return array( 'title' => $product->get_title(), 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_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' => (int) $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->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->length, 'width' => $product->width, 'height' => $product->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_post_data()->post_content ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => (int) $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'parent_id' => $product->post->post_parent, 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ), 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ), 'images' => $this->get_images( $product ), 'featured_src' => (string) wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => (int) $product->download_limit, 'download_expiry' => (int) $product->download_expiry, 'download_type' => $product->download_type, 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0, '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 = $product->get_child( $child_id ); if ( ! $variation->exists() ) { continue; } $post_data = get_post( $variation->get_variation_id() ); $variations[] = array( 'id' => $variation->get_variation_id(), 'created_at' => $this->server->format_datetime( $post_data->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $post_data->post_modified_gmt ), '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->length, 'width' => $variation->width, 'height' => $variation->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->download_limit, 'download_expiry' => (int) $product->download_expiry, ); } return $variations; } /** * Save product meta * * @since 2.2 * @param int $product_id * @param array $data * @return bool * @throws WC_API_Exception */ protected function save_product_meta( $product_id, $data ) { global $wpdb; // Product Type $product_type = null; if ( isset( $data['type'] ) ) { $product_type = wc_clean( $data['type'] ); wp_set_object_terms( $product_id, $product_type, 'product_type' ); } else { $_product_type = get_the_terms( $product_id, 'product_type' ); if ( is_array( $_product_type ) ) { $_product_type = current( $_product_type ); $product_type = $_product_type->slug; } } // Default total sales. add_post_meta( $product_id, 'total_sales', '0', true ); // Virtual if ( isset( $data['virtual'] ) ) { update_post_meta( $product_id, '_virtual', ( true === $data['virtual'] ) ? 'yes' : 'no' ); } // Tax status if ( isset( $data['tax_status'] ) ) { update_post_meta( $product_id, '_tax_status', wc_clean( $data['tax_status'] ) ); } // Tax Class if ( isset( $data['tax_class'] ) ) { update_post_meta( $product_id, '_tax_class', wc_clean( $data['tax_class'] ) ); } // Catalog Visibility if ( isset( $data['catalog_visibility'] ) ) { update_post_meta( $product_id, '_visibility', wc_clean( $data['catalog_visibility'] ) ); } // Purchase Note if ( isset( $data['purchase_note'] ) ) { update_post_meta( $product_id, '_purchase_note', wc_clean( $data['purchase_note'] ) ); } // Featured Product if ( isset( $data['featured'] ) ) { update_post_meta( $product_id, '_featured', ( true === $data['featured'] ) ? 'yes' : 'no' ); } // Shipping data $this->save_product_shipping_data( $product_id, $data ); // SKU if ( isset( $data['sku'] ) ) { $sku = get_post_meta( $product_id, '_sku', true ); $new_sku = wc_clean( $data['sku'] ); if ( '' == $new_sku ) { update_post_meta( $product_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product_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 { update_post_meta( $product_id, '_sku', $new_sku ); } } else { update_post_meta( $product_id, '_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 ) { 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_id, $values, $taxonomy ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values $attributes[ $taxonomy ] = array( 'name' => $taxonomy, 'value' => '', 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } elseif ( isset( $attribute['options'] ) ) { // Array based if ( is_array( $attribute['options'] ) ) { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); // Text based, separate by pipe } else { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); } // Custom attribute - Add attribute to array and set the values $attributes[ $attribute_slug ] = array( 'name' => wc_clean( $attribute['name'] ), 'value' => $values, 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); update_post_meta( $product_id, '_product_attributes', $attributes ); } // Sales and prices if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices update_post_meta( $product_id, '_regular_price', '' ); update_post_meta( $product_id, '_sale_price', '' ); update_post_meta( $product_id, '_sale_price_dates_from', '' ); update_post_meta( $product_id, '_sale_price_dates_to', '' ); update_post_meta( $product_id, '_price', '' ); } else { // Regular Price if ( isset( $data['regular_price'] ) ) { $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; } else { $regular_price = get_post_meta( $product_id, '_regular_price', true ); } // Sale Price if ( isset( $data['sale_price'] ) ) { $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; } else { $sale_price = get_post_meta( $product_id, '_sale_price', true ); } if ( isset( $data['sale_price_dates_from'] ) ) { $date_from = $data['sale_price_dates_from']; } else { $date_from = get_post_meta( $product_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $data['sale_price_dates_to'] ) ) { $date_to = $data['sale_price_dates_to']; } else { $date_to = get_post_meta( $product_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $product_id, $regular_price, $sale_price, $date_from, $date_to ); } // Product parent ID for groups if ( isset( $data['parent_id'] ) ) { wp_update_post( array( 'ID' => $product_id, 'post_parent' => absint( $data['parent_id'] ) ) ); } // Update parent if grouped so price sorting works and stays in sync with the cheapest child $_product = wc_get_product( $product_id ); if ( $_product->post->post_parent > 0 || $product_type == 'grouped' ) { $clear_parent_ids = array(); if ( $_product->post->post_parent > 0 ) { $clear_parent_ids[] = $_product->post->post_parent; } if ( $product_type == 'grouped' ) { $clear_parent_ids[] = $product_id; } if ( ! empty( $clear_parent_ids ) ) { foreach ( $clear_parent_ids as $clear_id ) { $children_by_price = get_posts( array( 'post_parent' => $clear_id, 'orderby' => 'meta_value_num', 'order' => 'asc', 'meta_key' => '_price', 'posts_per_page' => 1, 'post_type' => 'product', 'fields' => 'ids' ) ); if ( $children_by_price ) { foreach ( $children_by_price as $child ) { $child_price = get_post_meta( $child, '_price', true ); update_post_meta( $clear_id, '_price', $child_price ); } } } } } // Sold Individually if ( isset( $data['sold_individually'] ) ) { update_post_meta( $product_id, '_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 = get_post_meta( $product_id, '_stock_status', true ); 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'; update_post_meta( $product_id, '_manage_stock', $managing_stock ); } else { $managing_stock = get_post_meta( $product_id, '_manage_stock', true ); } // Backorders if ( isset( $data['backorders'] ) ) { if ( 'notify' == $data['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; } update_post_meta( $product_id, '_backorders', $backorders ); } else { $backorders = get_post_meta( $product_id, '_backorders', true ); } if ( 'grouped' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } elseif ( 'external' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, 'instock' ); } elseif ( 'yes' == $managing_stock ) { update_post_meta( $product_id, '_backorders', $backorders ); // Stock status is always determined by children so sync later. if ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product_id, $stock_status ); } // Stock quantity if ( isset( $data['stock_quantity'] ) ) { wc_update_product_stock( $product_id, intval( $data['stock_quantity'] ) ); } } else { // Don't manage stock update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', $backorders ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } } elseif ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product_id, $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; } } update_post_meta( $product_id, '_upsell_ids', $upsells ); } else { delete_post_meta( $product_id, '_upsell_ids' ); } } // 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; } } update_post_meta( $product_id, '_crosssell_ids', $crosssells ); } else { delete_post_meta( $product_id, '_crosssell_ids' ); } } // Product categories if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { $term_ids = array_unique( array_map( 'intval', $data['categories'] ) ); wp_set_object_terms( $product_id, $term_ids, 'product_cat' ); } // Product tags if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { $term_ids = array_unique( array_map( 'intval', $data['tags'] ) ); wp_set_object_terms( $product_id, $term_ids, 'product_tag' ); } // Downloadable if ( isset( $data['downloadable'] ) ) { $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; update_post_meta( $product_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $product_id, '_downloadable', true ); } // Downloadable options if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $this->save_downloadable_files( $product_id, $data['downloads'] ); } // Download limit if ( isset( $data['download_limit'] ) ) { update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) ); } // Download expiry if ( isset( $data['download_expiry'] ) ) { update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) ); } // Download type if ( isset( $data['download_type'] ) ) { update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) ); } } // Product url if ( $product_type == 'external' ) { if ( isset( $data['product_url'] ) ) { update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) ); } if ( isset( $data['button_text'] ) ) { update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) ); } } // Reviews allowed if ( isset( $data['reviews_allowed'] ) ) { $reviews_allowed = ( true === $data['reviews_allowed'] ) ? 'open' : 'closed'; $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) ); } // Do action for product type do_action( 'woocommerce_api_process_product_meta_' . $product_type, $product_id, $data ); return true; } /** * Save variations * * @since 2.2 * @param int $id * @param array $data * @return bool * @throws WC_API_Exception */ protected function save_variations( $id, $data ) { global $wpdb; $variations = $data['variations']; $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) ); foreach ( $variations as $menu_order => $variation ) { $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; // Generate a useful post title $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) ); // Update or Add post if ( ! $variation_id ) { $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; $new_variation = array( 'post_title' => $variation_post_title, 'post_content' => '', 'post_status' => $post_status, 'post_author' => get_current_user_id(), 'post_parent' => $id, 'post_type' => 'product_variation', 'menu_order' => $menu_order ); $variation_id = wp_insert_post( $new_variation ); do_action( 'woocommerce_create_product_variation', $variation_id ); } else { $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); if ( isset( $variation['visible'] ) ) { $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; $update_variation['post_status'] = $post_status; } $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); do_action( 'woocommerce_update_product_variation', $variation_id ); } // Stop with we don't have a variation ID if ( is_wp_error( $variation_id ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); } // SKU if ( isset( $variation['sku'] ) ) { $sku = get_post_meta( $variation_id, '_sku', true ); $new_sku = wc_clean( $variation['sku'] ); if ( '' == $new_sku ) { update_post_meta( $variation_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $variation_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 { update_post_meta( $variation_id, '_sku', $new_sku ); } } else { update_post_meta( $variation_id, '_sku', '' ); } } } // Thumbnail if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { $image = current( $variation['image'] ); if ( $image && is_array( $image ) ) { if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { $upload = $this->upload_product_image( wc_clean( $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, $id ); update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); } } else { delete_post_meta( $variation_id, '_thumbnail_id' ); } } // Virtual variation if ( isset( $variation['virtual'] ) ) { $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_virtual', $is_virtual ); } // Downloadable variation if ( isset( $variation['downloadable'] ) ) { $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); } // Shipping data $this->save_product_shipping_data( $variation_id, $variation ); // Stock handling if ( isset( $variation['managing_stock'] ) ) { $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; } else { $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); } update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock ); if ( isset( $variation['in_stock'] ) ) { $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; } else { $stock_status = get_post_meta( $variation_id, '_stock_status', true ); } wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); if ( 'yes' === $managing_stock ) { $backorders = get_post_meta( $variation_id, '_backorders', true ); if ( isset( $variation['backorders'] ) ) { if ( 'notify' === $variation['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; } } update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); if ( isset( $variation['stock_quantity'] ) ) { wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); } } else { delete_post_meta( $variation_id, '_backorders' ); delete_post_meta( $variation_id, '_stock' ); } // Regular Price if ( isset( $variation['regular_price'] ) ) { $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; } else { $regular_price = get_post_meta( $variation_id, '_regular_price', true ); } // Sale Price if ( isset( $variation['sale_price'] ) ) { $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; } else { $sale_price = get_post_meta( $variation_id, '_sale_price', true ); } if ( isset( $variation['sale_price_dates_from'] ) ) { $date_from = $variation['sale_price_dates_from']; } else { $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $variation['sale_price_dates_to'] ) ) { $date_to = $variation['sale_price_dates_to']; } else { $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); // Tax class if ( isset( $variation['tax_class'] ) ) { if ( $variation['tax_class'] !== 'parent' ) { update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); } else { delete_post_meta( $variation_id, '_tax_class' ); } } // Downloads if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { $this->save_downloadable_files( $id, $variation['downloads'], $variation_id ); } // Download limit if ( isset( $variation['download_limit'] ) ) { $download_limit = absint( $variation['download_limit'] ); update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); } // Download expiry if ( isset( $variation['download_expiry'] ) ) { $download_expiry = absint( $variation['download_expiry'] ); update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); } } else { update_post_meta( $variation_id, '_download_limit', '' ); update_post_meta( $variation_id, '_download_expiry', '' ); update_post_meta( $variation_id, '_downloadable_files', '' ); } // Update taxonomies if ( isset( $variation['attributes'] ) ) { $updated_attribute_keys = array(); foreach ( $variation['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 = 'attribute_' . sanitize_title( $_attribute['name'] ); $updated_attribute_keys[] = $_attribute_key; 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'] ) ) : ''; } update_post_meta( $variation_id, $_attribute_key, $_attribute_value ); } } // Remove old taxonomies attributes so data is kept up to date - first get attribute key names $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); foreach ( $delete_attribute_keys as $key ) { delete_post_meta( $variation_id, $key ); } } do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); } // Update parent if variable so price sorting works and stays in sync with the cheapest child WC_Product_Variable::sync( $id ); // Update default attributes options setting if ( isset( $data['default_attribute'] ) ) { $data['default_attributes'] = $data['default_attribute']; } if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) { $default_attributes = array(); foreach ( $data['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; } } } } update_post_meta( $id, '_default_attributes', $default_attributes ); } return true; } /** * Save product shipping data * * @since 2.2 * @param int $id * @param array $data */ private function save_product_shipping_data( $id, $data ) { if ( isset( $data['weight'] ) ) { update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) ); } // Product dimensions if ( isset( $data['dimensions'] ) ) { // Height if ( isset( $data['dimensions']['height'] ) ) { update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width if ( isset( $data['dimensions']['width'] ) ) { update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal($data['dimensions']['width'] ) ); } // Length if ( isset( $data['dimensions']['length'] ) ) { update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Virtual if ( isset( $data['virtual'] ) ) { $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; if ( 'yes' == $virtual ) { update_post_meta( $id, '_weight', '' ); update_post_meta( $id, '_length', '' ); update_post_meta( $id, '_width', '' ); update_post_meta( $id, '_height', '' ); } } // Shipping class if ( isset( $data['shipping_class'] ) ) { wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); } } /** * Save downloadable files * * @since 2.2 * @param int $product_id * @param array $downloads * @param int $variation_id */ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { $files = array(); // File paths will be stored in an array keyed off md5(file path) foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( ! isset( $file['file'] ) ) { continue; } $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; if ( 0 === strpos( $file['file'], 'http' ) ) { $file_url = esc_url_raw( $file['file'] ); } else { $file_url = wc_clean( $file['file'] ); } $files[ md5( $file_url ) ] = array( 'name' => $file_name, 'file' => $file_url ); } // Grant permission to any newly added files on any existing orders for this product prior to saving do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); $id = ( 0 === $variation_id ) ? $product_id : $variation_id; update_post_meta( $id, '_downloadable_files', $files ); } /** * 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(); if ( $product->is_type( 'variation' ) ) { if ( has_post_thumbnail( $product->get_variation_id() ) ) { // Add variation image if set $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); } elseif ( has_post_thumbnail( $product->id ) ) { // Otherwise use the parent product featured image if set $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { // Add featured image if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } // Add gallery images $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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 array $images * @param int $id * @throws WC_API_Exception */ protected function save_product_images( $id, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { if ( isset( $image['position'] ) && $image['position'] == 0 ) { $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, $id ); } set_post_thumbnail( $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, $id ); } else { $gallery[] = $attachment_id; } } } if ( ! empty( $gallery ) ) { update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) ); } } else { delete_post_thumbnail( $id ); update_post_meta( $id, '_product_image_gallery', '' ); } } /** * Upload image from URL * * @since 2.2 * @param string $image_url * @return int|WP_Error attachment id * @throws WC_API_Exception */ public function upload_product_image( $image_url ) { $file_name = basename( current( explode( '?', $image_url ) ) ); $parsed_url = @parse_url( $image_url ); // Check parsed URL if ( ! $parsed_url || ! is_array( $parsed_url ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_image', sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ), 400 ); } // Ensure url is valid $image_url = str_replace( ' ', '%20', $image_url ); // Get the file $response = wp_safe_remote_get( $image_url, array( 'timeout' => 10 ) ); if ( is_wp_error( $response ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ), 400 ); } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), 400 ); } // Ensure we have a file name and type $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { $headers = wp_remote_retrieve_headers( $response ); if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); $disposition = sanitize_file_name( $disposition ); $file_name = $disposition; } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); } unset( $headers ); // Recheck filetype $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { throw new WC_API_Exception( 'woocommerce_api_invalid_product_image', __( 'Invalid image type.', 'woocommerce' ), 400 ); } } // Upload the file $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); if ( $upload['error'] ) { throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload['error'], 400 ); } // Get filesize $filesize = filesize( $upload['file'] ); if ( 0 == $filesize ) { @unlink( $upload['file'] ); unset( $upload ); throw new WC_API_Exception( 'woocommerce_api_product_image_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), 400 ); } unset( $response ); 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_', '', str_replace( 'pa_', '', $attribute_name ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => wc_attribute_label( $attribute['name'] ), 'slug' => str_replace( 'pa_', '', $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->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_files() 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 */ 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 */ 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 ); } else if ( 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 ); } else if ( $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 */ 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' ); $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 */ 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' ); 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 */ 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' ); 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 */ 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 */ 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 wp_delete_post( $product_id, 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 */ 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 ); } // Product exists / edit product if ( $product_id ) { $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']; } } // Product don't exists / create product else { $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() ) ); } } } legacy/v2/class-wc-api-authentication.php 0000666 00000027430 15214171310 0014355 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Authentication */ 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 WC_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( $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 ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { 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 pararmeters * @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' ) ); } } legacy/v2/class-wc-api-server.php 0000666 00000047674 15214171310 0012660 0 ustar 00 <?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/API * @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 * @return WC_API_Server */ 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 ); // API requests run under the context of the authenticated user if ( is_a( $user, 'WP_User' ) ) { wp_set_current_user( $user->ID ); } // WP_Errors are handled in serve_request() elseif ( ! is_wp_error( $user ) ) { $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 */ 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://woothemes.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 $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; } // 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() { // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; } /** * 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 * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false ) { 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; } } legacy/v2/class-wc-api-coupons.php 0000666 00000050044 15214171310 0013021 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 ) { global $wpdb; try { $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) { return $id; } // get the coupon code $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) ); if ( is_null( $code ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); } $coupon = new WC_Coupon( $code ); $coupon_post = get_post( $coupon->id ); $coupon_data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->server->format_datetime( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), 'customer_emails' => $coupon->customer_email, 'description' => $coupon_post->post_excerpt, ); 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 */ 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 */ 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 = apply_filters( 'woocommerce_coupon_code', $data['code'] ); // Check for duplicate coupon codes $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' ", $coupon_code ) ); if ( $coupon_found ) { 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, '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 ); $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 */ 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 = apply_filters( 'woocommerce_coupon_code', $data['code'] ); // Check for duplicate coupon codes $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' AND $wpdb->posts.ID != %s ", $coupon_code, $id ) ); if ( $coupon_found ) { 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'] ) ) ); } 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 ); 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 */ 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 * @return string */ protected function get_coupon_expiry_date( $expiry_date ) { if ( '' != $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 */ 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']; } } // Coupon don't exists / create coupon else { $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() ) ); } } } legacy/v2/class-wc-api-resource.php 0000666 00000033260 15214171310 0013163 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Resource */ 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 %s found with the ID equal to %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 ); } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) { // product variation meta $meta = (array) get_post_meta( $resource->get_variation_id() ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->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; } } } legacy/v2/interface-wc-api-handler.php 0000666 00000001511 15214171310 0013576 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce/API * @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 ); } legacy/v1/class-wc-api-xml-handler.php 0000666 00000016704 15214171310 0013552 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing XML request bodies and generating XML responses * * @author WooThemes * @category API * @package WooCommerce/API * @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; } } legacy/v1/class-wc-api-products.php 0000666 00000042002 15214171310 0013170 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.1 * @version 2.1 */ 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 */ 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->parent ); } 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 */ 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 * * @TODO implement in 2.2 * @param int $id the product ID * @param array $data * @return array */ 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 * * @TODO enable along with PUT/POST in 2.2 * @param int $id the product ID * @param bool $force true to permanently delete order, false to move to trash * @return array */ 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 */ 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 $product * @return array */ private function get_product_data( $product ) { return array( 'title' => $product->get_title(), 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_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' => (int) $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->visibility, 'on_sale' => $product->is_on_sale(), 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, 'dimensions' => array( 'length' => $product->length, 'width' => $product->width, 'height' => $product->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_post_data()->post_content ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => (int) $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ), 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ), 'images' => $this->get_images( $product ), 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => (int) $product->download_limit, 'download_expiry' => (int) $product->download_expiry, 'download_type' => $product->download_type, 'purchase_note' => apply_filters( 'the_content', $product->purchase_note ), 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0, '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 = $product->get_child( $child_id ); if ( ! $variation->exists() ) { continue; } $post_data = get_post( $variation->get_variation_id() ); $variations[] = array( 'id' => $variation->get_variation_id(), 'created_at' => $this->server->format_datetime( $post_data->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $post_data->post_modified_gmt ), '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->length, 'width' => $variation->width, 'height' => $variation->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->download_limit, 'download_expiry' => (int) $product->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(); if ( $product->is_type( 'variation' ) ) { if ( has_post_thumbnail( $product->get_variation_id() ) ) { // add variation image if set $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); } elseif ( has_post_thumbnail( $product->id ) ) { // otherwise use the parent product featured image if set $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { // add featured image if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } // add gallery images $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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_', '', str_replace( 'pa_', '', $attribute_name ) ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ), 'position' => $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->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_files() 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; } } legacy/v1/class-wc-api-json-handler.php 0000666 00000003716 15214171310 0013722 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce/API * @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'] ) ) { // JSONP enabled by default if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); } // Check for invalid characters (only alphanumeric allowed) if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); } // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; } return json_encode( $data ); } } legacy/v1/interface-wc-api-handler.php 0000666 00000001535 15214171310 0013603 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce/API * @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 ); } legacy/v1/class-wc-api-server.php 0000666 00000050311 15214171310 0012635 0 ustar 00 <?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/API * @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 * @return WC_API_Server */ 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 ); // WP_Errors are handled in serve_request() elseif ( ! is_wp_error( $user ) ) $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 */ 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://woothemes.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() { // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; } /** * 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 * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false ) { 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; } } legacy/v1/class-wc-api-coupons.php 0000666 00000015665 15214171310 0013032 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ public function get_coupon( $id, $fields = null ) { global $wpdb; $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) return $id; // get the coupon code $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) ); if ( is_null( $code ) ) return new WP_Error( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), array( 'status' => 404 ) ); $coupon = new WC_Coupon( $code ); $coupon_post = get_post( $coupon->id ); $coupon_data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, 'expiry_date' => $this->server->format_datetime( $coupon->expiry_date ), 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'customer_emails' => $coupon->customer_email, ); 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 */ 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 * * @TODO implement in 2.2 * @param array $data * @return array */ public function create_coupon( $data ) { return array(); } /** * Edit a coupon * * @TODO implement in 2.2 * @param int $id the coupon ID * @param array $data * @return array */ 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 * * @TODO enable along with PUT/POST in 2.2 * @param int $id the coupon ID * @param bool $force true to permanently delete coupon, false to move to trash * @return array */ 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 ); } } legacy/v1/class-wc-api-resource.php 0000666 00000027766 15214171310 0013200 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Resource */ 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 ); } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) { // product variation meta $meta = (array) get_post_meta( $resource->get_variation_id() ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->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; } } legacy/v1/class-wc-api-reports.php 0000666 00000032344 15214171310 0013033 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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 */ 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_title(), '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 bool true if the request is valid and should be processed, false otherwise */ 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; } } } legacy/v1/class-wc-api-customers.php 0000666 00000034560 15214171310 0013363 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Customers */ 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 */ 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 WP_User( $id ); // get info about user's last order $last_order = $wpdb->get_row( "SELECT id, post_date_gmt FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = {$customer->ID} AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) " ); $customer_data = array( 'id' => $customer->ID, 'created_at' => $this->server->format_datetime( $customer->user_registered ), 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null, 'orders_count' => (int) $customer->_order_count, 'total_spent' => wc_format_decimal( $customer->_money_spent, 2 ), 'avatar_url' => $this->get_avatar_url( $customer->customer_email ), 'billing_address' => array( 'first_name' => $customer->billing_first_name, 'last_name' => $customer->billing_last_name, 'company' => $customer->billing_company, 'address_1' => $customer->billing_address_1, 'address_2' => $customer->billing_address_2, 'city' => $customer->billing_city, 'state' => $customer->billing_state, 'postcode' => $customer->billing_postcode, 'country' => $customer->billing_country, 'email' => $customer->billing_email, 'phone' => $customer->billing_phone, ), 'shipping_address' => array( 'first_name' => $customer->shipping_first_name, 'last_name' => $customer->shipping_last_name, 'company' => $customer->shipping_company, 'address_1' => $customer->shipping_address_1, 'address_2' => $customer->shipping_address_2, 'city' => $customer->shipping_city, 'state' => $customer->shipping_state, 'postcode' => $customer->shipping_postcode, 'country' => $customer->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 */ 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 * * @TODO implement in 2.2 with woocommerce_create_new_customer() * @param array $data * @return array */ 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 * * @TODO implement in 2.2 * @param int $id the customer ID * @param array $data * @return array */ 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 * * @TODO enable along with PUT/POST in 2.2 * @param int $id the customer ID * @return array */ 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 */ 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 = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '%s' AND posts.post_type = 'shop_order' AND posts.post_status = IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) ", $id ) ); 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->customer_user ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->billing_email, 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->customer_user ) ); } 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 ) ); } /** * Wrapper for @see get_avatar() which doesn't simply return * the URL so we need to pluck it from the HTML img tag * * @since 2.1 * @param string $email the customer's email * @return string the URL to the customer's avatar */ private function get_avatar_url( $email ) { $avatar_html = get_avatar( $email ); // Get the URL of the avatar from the provided HTML preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { return esc_url_raw( $matches[1] ); } return null; } /** * 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' ); } } legacy/v1/class-wc-api-orders.php 0000666 00000027140 15214171310 0012631 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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_post = get_post( $id ); $order_data = array( 'id' => $order->id, 'order_number' => $order->get_order_number(), 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ), 'completed_at' => $this->server->format_datetime( $order->completed_date, true ), 'status' => $order->get_status(), 'currency' => $order->order_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_total_shipping(), 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( $order->get_cart_discount(), 2 ), 'order_discount' => wc_format_decimal( $order->get_order_discount(), 2 ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->payment_method, 'method_title' => $order->payment_method_title, 'paid' => isset( $order->paid_date ), ), 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), 'note' => $order->customer_note, 'customer_ip' => $order->customer_ip_address, 'customer_user_agent' => $order->customer_user_agent, 'customer_id' => $order->customer_user, '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 = $order->get_product_from_item( $item ); $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' => (int) $item['qty'], 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $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['method_id'], 'method_title' => $shipping_item['name'], 'total' => wc_format_decimal( $shipping_item['cost'], 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['name'], 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, '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['name'], 'amount' => wc_format_decimal( $coupon_item['discount_amount'], 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 */ 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 */ 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 * * @TODO enable along with POST in 2.2 * @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 */ 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' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, ); } 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 += ( isset( $item['line_subtotal'] ) ) ? $item['line_subtotal'] : 0; } return $subtotal; } } legacy/v1/class-wc-api-authentication.php 0000666 00000027423 15214171310 0014356 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Authentication */ 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 WC_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( $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 ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { 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 pararmeters * @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' ) ); } } legacy/v3/class-wc-api-webhooks.php 0000666 00000031730 15214171310 0013156 0 ustar 00 <?php /** * WooCommerce API Webhooks class * * Handles requests to the /webhooks endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 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->posts as $webhook_id ) { if ( ! $this->is_readable( $webhook_id ) ) { continue; } $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); } $this->server->add_pagination_headers( $query ); return array( 'webhooks' => $webhooks ); } /** * Get the webhook for the given ID * * @since 2.2 * @param int $id webhook ID * @param array $fields * @return array */ 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 = new WC_Webhook( $id ); $webhook_data = array( 'id' => $webhook->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_post_data()->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ), ); 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 */ public function get_webhooks_count( $status = null, $filter = array() ) { try { if ( ! current_user_can( 'read_private_shop_webhooks' ) ) { 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' => (int) $query->found_posts ); } 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 */ 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( 'publish_shop_webhooks' ) ) { 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' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password, 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ), $data, $this ); $webhook_id = wp_insert_post( $webhook_data ); if ( is_wp_error( $webhook_id ) || ! $webhook_id ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 ); } $webhook = new WC_Webhook( $webhook_id ); // set topic, delivery URL, and optional secret $webhook->set_topic( $data['topic'] ); $webhook->set_delivery_url( $data['delivery_url'] ); // set secret if provided, defaults to API users consumer secret $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : '' ); // send ping $webhook->deliver_ping(); // HTTP 201 Created $this->server->send_status( 201 ); do_action( 'woocommerce_api_create_webhook', $webhook->id, $this ); delete_transient( 'woocommerce_webhook_ids' ); return $this->get_webhook( $webhook->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 */ 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 = new WC_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->update_status( $data['status'] ); } // update user ID $webhook_data = array( 'ID' => $webhook->id, 'post_author' => get_current_user_id() ); // update name if ( ! empty( $data['name'] ) ) { $webhook_data['post_title'] = $data['name']; } // update post wp_update_post( $webhook_data ); do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this ); delete_transient( 'woocommerce_webhook_ids' ); return $this->get_webhook( $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 */ 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 ); delete_transient( 'woocommerce_webhook_ids' ); // no way to manage trashed webhooks at the moment, so force delete return $this->delete( $id, 'webhook', true ); } /** * Helper method to get webhook post objects * * @since 2.2 * @param array $args request arguments for filtering query. * @return WP_Query */ private function query_webhooks( $args ) { // Set base query arguments. $query_args = array( 'fields' => 'ids', 'post_type' => 'shop_webhook', ); // Add status argument. if ( ! empty( $args['status'] ) ) { switch ( $args['status'] ) { case 'active' : $query_args['post_status'] = 'publish'; break; case 'paused' : $query_args['post_status'] = 'draft'; break; case 'disabled' : $query_args['post_status'] = 'pending'; break; case 'all' : $query_args['post_status'] = 'any'; break; default : $query_args['post_status'] = 'publish'; break; } unset( $args['status'] ); } $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); } /** * Get deliveries for a webhook * * @since 2.2 * @param string $webhook_id webhook ID * @param string|null $fields fields to include in response * @return array */ 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; } $webhook = new WC_Webhook( $webhook_id ); $logs = $webhook->get_delivery_logs(); $delivery_logs = array(); foreach ( $logs as $log ) { // Add timestamp $log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); // Remove comment object unset( $log['comment'] ); $delivery_logs[] = $log; } return array( 'webhook_deliveries' => $delivery_logs ); } /** * Get the delivery log for the given webhook ID and delivery ID * * @since 2.2 * @param string $webhook_id webhook ID * @param string $id delivery log ID * @param string|null $fields fields to limit response to * @return array */ 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 = $webhook->get_delivery_log( $id ); if ( ! $log ) { throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery', 'woocommerce' ), 400 ); } $delivery_log = $log; // Add timestamp $delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); // Remove comment object unset( $delivery_log['comment'] ); return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) ); } catch ( WC_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } legacy/v3/class-wc-api-authentication.php 0000666 00000031420 15214171310 0014350 0 ustar 00 <?php /** * WooCommerce API Authentication Class * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Authentication */ 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 WC_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( $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 ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { 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 pararmeters * @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' ) ); } } legacy/v3/class-wc-api-orders.php 0000666 00000167101 15214171310 0012635 0 ustar 00 <?php /** * WooCommerce API Orders Class * * Handles requests to the /orders endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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_post = get_post( $id ); $expand = array(); if ( ! empty( $filter['expand'] ) ) { $expand = explode( ',', $filter['expand'] ); } $order_data = array( 'id' => $order->id, 'order_number' => $order->get_order_number(), 'order_key' => $order->order_key, 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ), 'completed_at' => $this->server->format_datetime( $order->completed_date, true ), 'status' => $order->get_status(), 'currency' => $order->get_order_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_total_shipping(), $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->payment_method, 'method_title' => $order->payment_method_title, 'paid' => isset( $order->paid_date ), ), 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), 'note' => $order->customer_note, 'customer_ip' => $order->customer_ip_address, 'customer_user_agent' => $order->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(), 'is_vat_exempt' => $order->is_vat_exempt === 'yes' ? true : false, ); // Add line items. foreach ( $order->get_items() as $item_id => $item ) { $product = $order->get_product_from_item( $item ); $product_id = null; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id; $product_sku = $product->get_sku(); } $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $formatted_meta['key'], 'label' => $formatted_meta['label'], 'value' => $formatted_meta['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['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => $product_id, 'sku' => $product_sku, 'meta' => $item_meta, ); if ( in_array( 'products', $expand ) ) { $_product_data = WC()->api->WC_API_Products->get_product( $product_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['method_id'], 'method_title' => $shipping_item['name'], 'total' => wc_format_decimal( $shipping_item['cost'], $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['name'], 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, '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['name'], 'amount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), ); if ( in_array( 'coupons', $expand ) ) { $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item['name'] ); 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 */ 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 ( $status == 'any' ) { $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 */ public function create_order( $data ) { global $wpdb; wc_transaction_query( 'start' ); 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->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->id, '_payment_method', $data['payment_details']['method_id'] ); update_post_meta( $order->id, '_payment_method_title', $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->id, '_order_currency', $data['currency'] ); } // set order meta if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->id, $data['order_meta'] ); } // HTTP 201 Created $this->server->send_status( 201 ); wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_api_create_order', $order->id, $data, $this ); wc_transaction_query( 'commit' ); return $this->get_order( $order->id ); } catch ( WC_API_Exception $e ) { wc_transaction_query( 'rollback' ); 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 * @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 */ 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->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->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->id, '_payment_method', $data['payment_details']['method_id'] ); } // Method title. if ( isset( $data['payment_details']['method_title'] ) ) { update_post_meta( $order->id, '_payment_method_title', $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->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->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'] ) ) { $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); } wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_api_edit_order', $order->id, $data, $this ); return $this->get_order( $id ); } 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 */ 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 ] ); } } } $order->set_address( $billing_address, 'billing' ); $order->set_address( $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 ); } } } /** * 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->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 ); $item_args = array(); // 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 ); } } $item_args['variation'] = $item['variations']; $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item_args['variation'] ); } $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 ( isset( $item['quantity'] ) ) { $item_args['qty'] = $item['quantity']; } // total if ( isset( $item['total'] ) ) { $item_args['totals']['total'] = floatval( $item['total'] ); } // total tax if ( isset( $item['total_tax'] ) ) { $item_args['totals']['tax'] = floatval( $item['total_tax'] ); } // subtotal if ( isset( $item['subtotal'] ) ) { $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); } // subtotal tax if ( isset( $item['subtotal_tax'] ) ) { $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); } $item_args = apply_filters( 'woocommerce_api_order_line_item_args', $item_args, $item, $order, $action ); if ( $creating ) { $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again', 'woocommerce' ), 500 ); } } else { $item_id = $order->update_product( $item['id'], $product, $item_args ); if ( ! $item_id ) { throw new WC_API_Exception( 'woocommerce_cannot_update_line_item', __( 'Cannot update 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 * @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_', '', str_replace( 'pa_', '', $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_', '', str_replace( 'pa_', '', $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 */ 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'] ); $shipping_id = $order->add_shipping( $rate ); if ( ! $shipping_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_shipping', __( 'Cannot create shipping method, try again', 'woocommerce' ), 500 ); } } else { $shipping_args = array(); if ( isset( $shipping['method_id'] ) ) { $shipping_args['method_id'] = $shipping['method_id']; } if ( isset( $shipping['method_title'] ) ) { $shipping_args['method_title'] = $shipping['method_title']; } if ( isset( $shipping['total'] ) ) { $shipping_args['cost'] = floatval( $shipping['total'] ); } $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); 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 ); } $order_fee = new stdClass(); $order_fee->id = sanitize_title( $fee['title'] ); $order_fee->name = $fee['title']; $order_fee->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; $order_fee->taxable = false; $order_fee->tax = 0; $order_fee->tax_data = array(); $order_fee->tax_class = ''; // if taxable, tax class and total are required if ( isset( $fee['taxable'] ) && $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 ); } $order_fee->taxable = true; $order_fee->tax_class = $fee['tax_class']; if ( isset( $fee['total_tax'] ) ) { $order_fee->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; } if ( isset( $fee['tax_data'] ) ) { $order_fee->tax = wc_format_refund_total( array_sum( $fee['tax_data'] ) ); $order_fee->tax_data = array_map( 'wc_format_refund_total', $fee['tax_data'] ); } } $fee_id = $order->add_fee( $order_fee ); if ( ! $fee_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_fee', __( 'Cannot create fee, try again', 'woocommerce' ), 500 ); } } else { $fee_args = array(); if ( isset( $fee['title'] ) ) { $fee_args['name'] = $fee['title']; } if ( isset( $fee['tax_class'] ) ) { $fee_args['tax_class'] = $fee['tax_class']; } if ( isset( $fee['total'] ) ) { $fee_args['line_total'] = floatval( $fee['total'] ); } if ( isset( $fee['total_tax'] ) ) { $fee_args['line_tax'] = floatval( $fee['total_tax'] ); } $fee_id = $order->update_fee( $fee['id'], $fee_args ); 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 ); } $coupon_id = $order->add_coupon( $coupon['code'], isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0 ); if ( ! $coupon_id ) { throw new WC_API_Exception( 'woocommerce_cannot_create_order_coupon', __( 'Cannot create coupon, try again', 'woocommerce' ), 500 ); } } else { $coupon_args = array(); if ( isset( $coupon['code'] ) ) { $coupon_args['code'] = $coupon['code']; } if ( isset( $coupon['amount'] ) ) { $coupon_args['discount_amount'] = floatval( $coupon['amount'] ); } $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); 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 */ 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 */ 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' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, ); 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->id, $note_id ); } 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->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->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->id, $this ); return $this->get_order_note( $order->id, $note->comment_ID ); } 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 = wp_delete_comment( $note->comment_ID, true ); 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 */ 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 string|null $fields fields to limit response to * @return array */ public function get_order_refund( $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_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 = $order->get_product_from_item( $item ); $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); foreach ( $meta->get_formatted() as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $meta_key, 'label' => $formatted_meta['label'], 'value' => $formatted_meta['value'], ); } $line_items[] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), 'subtotal_tax' => wc_format_decimal( $item['line_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' => (int) $item['qty'], 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id, 'sku' => is_object( $product ) ? $product->get_sku() : null, 'meta' => $item_meta, 'refunded_item_id' => (int) $item['refunded_item_id'], ); } $order_refund = array( 'id' => $refund->id, 'created_at' => $this->server->format_datetime( $refund->date ), 'amount' => wc_format_decimal( $refund->get_refund_amount(), 2 ), 'reason' => $refund->get_refund_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->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { $result = $payment_gateways[ $order->payment_method ]->process_refund( $order_id, $refund->get_refund_amount(), $refund->get_refund_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->id, $order_id, $this ); return $this->get_order_refund( $order_id, $refund->id ); } 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_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 */ 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']; } } // Order don't exists / create order else { $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_API_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } } legacy/v3/class-wc-api-customers.php 0000666 00000060231 15214171310 0013357 0 ustar 00 <?php /** * WooCommerce API Customers Class * * Handles requests to the /customers endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Customers */ 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 */ 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 WP_User( $id ); // Get info about user's last order $last_order = $wpdb->get_row( "SELECT id, post_date_gmt FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = {$customer->ID} AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) ORDER BY posts.ID DESC " ); $roles = array_values( $customer->roles ); $customer_data = array( 'id' => $customer->ID, 'created_at' => $this->server->format_datetime( $customer->user_registered ), 'last_update' => $this->server->format_datetime( get_user_meta( $customer->ID, 'last_update', true ) ), 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'role' => $roles[0], 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null, 'orders_count' => wc_get_customer_order_count( $customer->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), 'avatar_url' => $this->get_avatar_url( $customer->customer_email ), 'billing_address' => array( 'first_name' => $customer->billing_first_name, 'last_name' => $customer->billing_last_name, 'company' => $customer->billing_company, 'address_1' => $customer->billing_address_1, 'address_2' => $customer->billing_address_2, 'city' => $customer->billing_city, 'state' => $customer->billing_state, 'postcode' => $customer->billing_postcode, 'country' => $customer->billing_country, 'email' => $customer->billing_email, 'phone' => $customer->billing_phone, ), 'shipping_address' => array( 'first_name' => $customer->shipping_first_name, 'last_name' => $customer->shipping_last_name, 'company' => $customer->shipping_company, 'address_1' => $customer->shipping_address_1, 'address_2' => $customer->shipping_address_2, 'city' => $customer->shipping_city, 'state' => $customer->shipping_state, 'postcode' => $customer->shipping_postcode, 'country' => $customer->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 */ 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 */ 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 */ protected function update_customer_data( $id, $data ) { // Customer first name. if ( isset( $data['first_name'] ) ) { update_user_meta( $id, 'first_name', wc_clean( $data['first_name'] ) ); } // Customer last name. if ( isset( $data['last_name'] ) ) { update_user_meta( $id, 'last_name', wc_clean( $data['last_name'] ) ); } // Customer billing address. if ( isset( $data['billing_address'] ) ) { foreach ( $this->get_customer_billing_address() as $address ) { if ( isset( $data['billing_address'][ $address ] ) ) { update_user_meta( $id, 'billing_' . $address, wc_clean( $data['billing_address'][ $address ] ) ); } } } // Customer shipping address. if ( isset( $data['shipping_address'] ) ) { foreach ( $this->get_customer_shipping_address() as $address ) { if ( isset( $data['shipping_address'][ $address ] ) ) { update_user_meta( $id, 'shipping_' . $address, wc_clean( $data['shipping_address'][ $address ] ) ); } } } do_action( 'woocommerce_api_update_customer_data', $id, $data ); } /** * Create a customer * * @since 2.2 * @param array $data * @return array */ 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 ); } // Sets the username. $data['username'] = ! empty( $data['username'] ) ? $data['username'] : ''; // Sets the password. $data['password'] = ! empty( $data['password'] ) ? $data['password'] : ''; // Attempts to create the new customer $id = wc_create_new_customer( $data['email'], $data['username'], $data['password'] ); // Checks for an error in the customer creation. if ( is_wp_error( $id ) ) { throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); } // Added customer data. $this->update_customer_data( $id, $data ); do_action( 'woocommerce_api_create_customer', $id, $data ); $this->server->send_status( 201 ); return $this->get_customer( $id ); } catch ( WC_API_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 */ 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 email. if ( isset( $data['email'] ) ) { wp_update_user( array( 'ID' => $id, 'user_email' => sanitize_email( $data['email'] ) ) ); } // Customer password. if ( isset( $data['password'] ) ) { wp_update_user( array( 'ID' => $id, 'user_pass' => wc_clean( $data['password'] ) ) ); } // Update customer data. $this->update_customer_data( $id, $data ); do_action( 'woocommerce_api_edit_customer', $id, $data ); return $this->get_customer( $id ); } catch ( WC_API_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 */ 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 */ 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 */ 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[ $key ] = $download; $downloads[ $key ]['access_expires'] = $this->server->format_datetime( $downloads[ $key ]['access_expires'] ); } 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 ( $args['limit'] == -1 ) { 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']; } // Orderby 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 = ( $args['limit'] == -1 ) ? 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->customer_user ) { // add customer data from order $order_data['customer'] = array( 'id' => 0, 'email' => $order->billing_email, 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), ); } else { $order_data['customer'] = current( $this->get_customer( $order->customer_user ) ); } 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 ) ); } } /** * Wrapper for @see get_avatar() which doesn't simply return * the URL so we need to pluck it from the HTML img tag * * Kudos to https://github.com/WP-API/WP-API for offering a better solution * * @since 2.1 * @param string $email the customer's email * @return string the URL to the customer's avatar */ private function get_avatar_url( $email ) { $avatar_html = get_avatar( $email ); // Get the URL of the avatar from the provided HTML preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { return esc_url_raw( $matches[1] ); } return null; } /** * 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 ( ! current_user_can( 'edit_users' ) ) { 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 ( ! current_user_can( 'delete_users' ) ) { 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 */ 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']; } } // Customer don't exists / create customer else { $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() ) ); } } } legacy/v3/class-wc-api-coupons.php 0000666 00000050044 15214171310 0013022 0 ustar 00 <?php /** * WooCommerce API Coupons Class * * Handles requests to the /coupons endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 ) { global $wpdb; try { $id = $this->validate_request( $id, 'shop_coupon', 'read' ); if ( is_wp_error( $id ) ) { return $id; } // get the coupon code $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) ); if ( is_null( $code ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); } $coupon = new WC_Coupon( $code ); $coupon_post = get_post( $coupon->id ); $coupon_data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->server->format_datetime( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), 'customer_emails' => $coupon->customer_email, 'description' => $coupon_post->post_excerpt, ); 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 */ 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 */ 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 = apply_filters( 'woocommerce_coupon_code', $data['code'] ); // Check for duplicate coupon codes $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' ", $coupon_code ) ); if ( $coupon_found ) { 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, '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 ); $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 */ 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 = apply_filters( 'woocommerce_coupon_code', $data['code'] ); // Check for duplicate coupon codes $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' AND $wpdb->posts.ID != %s ", $coupon_code, $id ) ); if ( $coupon_found ) { 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'] ) ) ); } 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 ); 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 */ 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 * @return string */ protected function get_coupon_expiry_date( $expiry_date ) { if ( '' != $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 */ 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']; } } // Coupon don't exists / create coupon else { $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() ) ); } } } legacy/v3/class-wc-api-exception.php 0000666 00000002207 15214171310 0013330 0 ustar 00 <?php /** * WooCommerce API Exception Class * * Extends Exception to provide additional data * * @author WooThemes * @category API * @package WooCommerce/API * @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; } } legacy/v3/class-wc-api-taxes.php 0000666 00000046160 15214171310 0012464 0 ustar 00 <?php /** * WooCommerce API Taxes Class * * Handles requests to the /taxes endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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 */ 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 compund 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 */ 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 */ 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 = '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 = 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 */ 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'] ); } // Tax rate exists / edit tax rate if ( $tax_id ) { $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']; } } // Tax rate don't exists / create tax rate else { $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 */ 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 */ 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'] ); $slug = sanitize_title( $name ); $classes = WC_Tax::get_tax_classes(); $exists = false; // Check if class exists. foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $slug ) { $exists = true; break; } } // Return error if tax class already exists. if ( $exists ) { throw new WC_API_Exception( 'woocommerce_api_cannot_create_tax_class', __( 'Tax class already exists', 'woocommerce' ), 401 ); } // Add the new class. $classes[] = $name; update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); do_action( 'woocommerce_api_create_tax_class', $slug, $data ); $this->server->send_status( 201 ); return array( 'tax_class' => array( 'slug' => $slug, 'name' => $name ) ); } 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 */ 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 ); $classes = WC_Tax::get_tax_classes(); $deleted = false; foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $slug ) { unset( $classes[ $key ] ); $deleted = true; break; } } if ( ! $deleted ) { throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); } update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); // Delete tax rate locations locations from the selected class. $wpdb->query( $wpdb->prepare( " DELETE locations.* FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations INNER JOIN {$wpdb->prefix}woocommerce_tax_rates AS rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_class = '%s' ", $slug ) ); // Delete tax rates in the selected class. $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $slug ), array( '%s' ) ); 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 */ 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() ) ); } } } legacy/v3/class-wc-api-server.php 0000666 00000050272 15214171310 0012645 0 ustar 00 <?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/API * @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 * @return WC_API_Server */ 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 ); // API requests run under the context of the authenticated user if ( is_a( $user, 'WP_User' ) ) { wp_set_current_user( $user->ID ); } // WP_Errors are handled in serve_request() elseif ( ! is_wp_error( $user ) ) { $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 */ 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://woothemes.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; } } else if ( 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() { // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { return file_get_contents( 'php://input' ); } global $HTTP_RAW_POST_DATA; // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, // but we can do it ourself. if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } return $HTTP_RAW_POST_DATA; } /** * 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 * @return string RFC3339 datetime */ public function format_datetime( $timestamp, $convert_to_utc = false ) { 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; } } legacy/v3/class-wc-api-resource.php 0000666 00000033510 15214171310 0013162 0 ustar 00 <?php /** * WooCommerce API Resource class * * Provides shared functionality for resource-specific API classes * * @author WooThemes * @category API * @package WooCommerce/API * @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 * @return WC_API_Resource */ 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 ) { $post = get_post( $id ); if ( null === $post ) { return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %s found with the ID equal to %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'] ); } // 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 ); } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) { // product variation meta $meta = (array) get_post_meta( $resource->get_variation_id() ); } else { // coupon/order/product meta $meta = (array) get_post_meta( $resource->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 ); } } legacy/v3/class-wc-api-reports.php 0000666 00000023126 15214171310 0013033 0 ustar 00 <?php /** * WooCommerce API Reports Class * * Handles requests to the /reports endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @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 */ 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 */ 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_title(), '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 true if the request is valid and should be processed, false otherwise */ 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; } } } legacy/v3/class-wc-api-json-handler.php 0000666 00000003672 15214171310 0013725 0 ustar 00 <?php /** * WooCommerce API * * Handles parsing JSON request bodies and generating JSON responses * * @author WooThemes * @category API * @package WooCommerce/API * @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'] ) ) { // JSONP enabled by default if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); } // Check for invalid characters (only alphanumeric allowed) if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { WC()->api->server->send_status( 400 ); $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); } // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); // Prepend '/**/' to mitigate possible JSONP Flash attacks return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; } return json_encode( $data ); } } legacy/v3/interface-wc-api-handler.php 0000666 00000001511 15214171310 0013577 0 ustar 00 <?php /** * WooCommerce API * * Defines an interface that API request/response handlers should implement * * @author WooThemes * @category API * @package WooCommerce/API * @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 ); } legacy/v3/class-wc-api-products.php 0000666 00000354015 15214171310 0013204 0 ustar 00 <?php /** * WooCommerce API Products Class * * Handles requests to the /products endpoint * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.1 */ 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 */ 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->parent ) { $product_data['parent'] = $this->get_product_data( $product->parent ); } // 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' ) && ! empty( $product->post->post_parent ) ) { $_product = wc_get_product( $product->post->post_parent ); $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 */ 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 */ 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 = $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 = $data['short_description']; } $new_product = array( 'post_title' => wc_clean( $data['title'] ), 'post_status' => isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish', 'post_type' => 'product', 'post_excerpt' => isset( $data['short_description'] ) ? $post_excerpt : '', 'post_content' => isset( $data['description'] ) ? $post_content : '', 'post_author' => get_current_user_id(), 'menu_order' => isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0, ); if ( ! empty( $data['name'] ) ) { $new_product = array_merge( $new_product, array( 'post_name' => sanitize_title( $data['name'] ) ) ); } // Attempts to create the new product. $id = wp_insert_post( $new_product, true ); // Checks for an error in the product creation. if ( is_wp_error( $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'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields. $this->save_product_meta( $id, $data ); // Save variations. if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $id, $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_API_Exception $e ) { // Remove the product when fails. $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 */ 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; } $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); // Product title. if ( isset( $data['title'] ) ) { wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) ); } // Product name (slug). if ( isset( $data['name'] ) ) { wp_update_post( array( 'ID' => $id, 'post_name' => sanitize_title( $data['name'] ) ) ); } // Product status. if ( isset( $data['status'] ) ) { wp_update_post( array( 'ID' => $id, 'post_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'] ) ? $data['short_description'] : wc_clean( $data['short_description'] ); wp_update_post( array( 'ID' => $id, 'post_excerpt' => $post_excerpt ) ); } // Product description. if ( isset( $data['description'] ) ) { // Enable description html tags. $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] ); wp_update_post( array( 'ID' => $id, 'post_content' => $post_content ) ); } // Menu order. if ( isset( $data['menu_order'] ) ) { wp_update_post( array( 'ID' => $id, 'menu_order' => intval( $data['menu_order'] ) ) ); } // 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'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields. $this->save_product_meta( $id, $data ); // Save variations. $product = get_product( $id ); if ( $product->is_type( 'variable' ) ) { if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $this->save_variations( $id, $data ); } else { // Just sync variations WC_Product_Variable::sync( $id ); WC_Product_Variable::sync_stock_status( $id ); } } do_action( 'woocommerce_api_edit_product', $id, $data ); // Clear cache/transients. wc_delete_product_transients( $id ); return $this->get_product( $id ); } 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 */ public function delete_product( $id, $force = false ) { $id = $this->validate_request( $id, 'product', 'delete' ); if ( is_wp_error( $id ) ) { return $id; } do_action( 'woocommerce_api_delete_product', $id, $this ); // If we're forcing, then delete permanently. if ( $force ) { $child_product_variations = get_children( 'post_parent=' . $id . '&post_type=product_variation' ); if ( ! empty( $child_product_variations ) ) { foreach ( $child_product_variations as $child ) { wp_delete_post( $child->ID, true ); } } $child_products = get_children( 'post_parent=' . $id . '&post_type=product' ); if ( ! empty( $child_products ) ) { foreach ( $child_products as $child ) { $child_post = array(); $child_post['ID'] = $child->ID; $child_post['post_parent'] = 0; wp_update_post( $child_post ); } } $result = wp_delete_post( $id, true ); } else { $result = wp_trash_post( $id ); } 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 */ 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 string $filter filters to include in response * @param string $status the order status to retrieve * @param $page $page page to retrieve * @return array */ 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 */ 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 */ 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_woocommerce_term_meta( $term_id, 'display_type' ); // Get category image $image = ''; if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) { $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 ); } else if ( ! 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_woocommerce_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_woocommerce_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 ); } else if ( ! 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_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); } if ( ! empty( $data['display'] ) ) { update_woocommerce_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); } if ( isset( $image_id ) ) { update_woocommerce_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 ); } // When a term is deleted, delete its meta. if ( get_option( 'db_version' ) < 34370 ) { $wpdb->delete( $wpdb->woocommerce_termmeta, array( 'woocommerce_term_id' => $id ), array( '%d' ) ); } 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 Product tags */ 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 Product tag */ 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 $product * @return WC_Product */ private function get_product_data( $product ) { return array( 'title' => $product->get_title(), 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_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->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->length, 'width' => $product->width, 'height' => $product->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_post_data()->post_content ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => (int) $product->get_rating_count(), 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->post->post_parent, 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ), 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ), 'images' => $this->get_images( $product ), 'featured_src' => (string) wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => (int) $product->download_limit, 'download_expiry' => (int) $product->download_expiry, 'download_type' => $product->download_type, 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0, '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->post->menu_order; if ( $product->is_type( 'variation' ) ) { $_product = get_post( $product->get_variation_id() ); $menu_order = $_product->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 = $product->get_child( $child_id ); if ( ! $variation->exists() ) { continue; } $post_data = get_post( $variation->get_variation_id() ); $variations[] = array( 'id' => $variation->get_variation_id(), 'created_at' => $this->server->format_datetime( $post_data->post_date_gmt ), 'updated_at' => $this->server->format_datetime( $post_data->post_modified_gmt ), '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->length, 'width' => $variation->width, 'height' => $variation->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->download_limit, 'download_expiry' => (int) $product->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 = $product->get_child( $child_id ); if ( ! $_product->exists() ) { continue; } $products[] = $this->get_product_data( $_product ); } return $products; } /** * Save product meta. * * @since 2.2 * @param int $product_id * @param array $data * @return bool * @throws WC_API_Exception */ protected function save_product_meta( $product_id, $data ) { global $wpdb; // Product Type. $product_type = null; if ( isset( $data['type'] ) ) { $product_type = wc_clean( $data['type'] ); wp_set_object_terms( $product_id, $product_type, 'product_type' ); } else { $_product_type = get_the_terms( $product_id, 'product_type' ); if ( is_array( $_product_type ) ) { $_product_type = current( $_product_type ); $product_type = $_product_type->slug; } } // Default total sales. add_post_meta( $product_id, 'total_sales', '0', true ); // Virtual. if ( isset( $data['virtual'] ) ) { update_post_meta( $product_id, '_virtual', ( true === $data['virtual'] ) ? 'yes' : 'no' ); } // Tax status. if ( isset( $data['tax_status'] ) ) { update_post_meta( $product_id, '_tax_status', wc_clean( $data['tax_status'] ) ); } // Tax Class. if ( isset( $data['tax_class'] ) ) { update_post_meta( $product_id, '_tax_class', wc_clean( $data['tax_class'] ) ); } // Catalog Visibility. if ( isset( $data['catalog_visibility'] ) ) { update_post_meta( $product_id, '_visibility', wc_clean( $data['catalog_visibility'] ) ); } // Purchase Note. if ( isset( $data['purchase_note'] ) ) { update_post_meta( $product_id, '_purchase_note', wc_clean( $data['purchase_note'] ) ); } // Featured Product. if ( isset( $data['featured'] ) ) { update_post_meta( $product_id, '_featured', ( true === $data['featured'] ) ? 'yes' : 'no' ); } // Shipping data. $this->save_product_shipping_data( $product_id, $data ); // SKU. if ( isset( $data['sku'] ) ) { $sku = get_post_meta( $product_id, '_sku', true ); $new_sku = wc_clean( $data['sku'] ); if ( '' == $new_sku ) { update_post_meta( $product_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product_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 { update_post_meta( $product_id, '_sku', $new_sku ); } } else { update_post_meta( $product_id, '_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 ) { 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_id, $values, $taxonomy ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values. $attributes[ $taxonomy ] = array( 'name' => $taxonomy, 'value' => '', 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } elseif ( isset( $attribute['options'] ) ) { // Array based. if ( is_array( $attribute['options'] ) ) { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); // Text based, separate by pipe. } else { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); } // Custom attribute - Add attribute to array and set the values. $attributes[ $attribute_slug ] = array( 'name' => wc_clean( $attribute['name'] ), 'value' => $values, 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); update_post_meta( $product_id, '_product_attributes', $attributes ); } // Sales and prices. if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices. update_post_meta( $product_id, '_regular_price', '' ); update_post_meta( $product_id, '_sale_price', '' ); update_post_meta( $product_id, '_sale_price_dates_from', '' ); update_post_meta( $product_id, '_sale_price_dates_to', '' ); update_post_meta( $product_id, '_price', '' ); } else { // Regular Price if ( isset( $data['regular_price'] ) ) { $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; } else { $regular_price = get_post_meta( $product_id, '_regular_price', true ); } // Sale Price if ( isset( $data['sale_price'] ) ) { $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; } else { $sale_price = get_post_meta( $product_id, '_sale_price', true ); } if ( isset( $data['sale_price_dates_from'] ) ) { $date_from = $data['sale_price_dates_from']; } else { $date_from = get_post_meta( $product_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $data['sale_price_dates_to'] ) ) { $date_to = $data['sale_price_dates_to']; } else { $date_to = get_post_meta( $product_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $product_id, $regular_price, $sale_price, $date_from, $date_to ); } // Product parent ID for groups. if ( isset( $data['parent_id'] ) ) { wp_update_post( array( 'ID' => $product_id, 'post_parent' => absint( $data['parent_id'] ) ) ); } // Update parent if grouped so price sorting works and stays in sync with the cheapest child. $_product = wc_get_product( $product_id ); if ( $_product->post->post_parent > 0 || $product_type == 'grouped' ) { $clear_parent_ids = array(); if ( $_product->post->post_parent > 0 ) { $clear_parent_ids[] = $_product->post->post_parent; } if ( $product_type == 'grouped' ) { $clear_parent_ids[] = $product_id; } if ( ! empty( $clear_parent_ids ) ) { foreach ( $clear_parent_ids as $clear_id ) { $children_by_price = get_posts( array( 'post_parent' => $clear_id, 'orderby' => 'meta_value_num', 'order' => 'asc', 'meta_key' => '_price', 'posts_per_page' => 1, 'post_type' => 'product', 'fields' => 'ids' ) ); if ( $children_by_price ) { foreach ( $children_by_price as $child ) { $child_price = get_post_meta( $child, '_price', true ); update_post_meta( $clear_id, '_price', $child_price ); } } } } } // Sold Individually. if ( isset( $data['sold_individually'] ) ) { update_post_meta( $product_id, '_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 = get_post_meta( $product_id, '_stock_status', true ); 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'; update_post_meta( $product_id, '_manage_stock', $managing_stock ); } else { $managing_stock = get_post_meta( $product_id, '_manage_stock', true ); } // Backorders. if ( isset( $data['backorders'] ) ) { if ( 'notify' === $data['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; } update_post_meta( $product_id, '_backorders', $backorders ); } else { $backorders = get_post_meta( $product_id, '_backorders', true ); } if ( 'grouped' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } elseif ( 'external' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, 'instock' ); } elseif ( 'yes' == $managing_stock ) { update_post_meta( $product_id, '_backorders', $backorders ); // Stock status is always determined by children so sync later. if ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product_id, $stock_status ); } // Stock quantity. if ( isset( $data['stock_quantity'] ) ) { wc_update_product_stock( $product_id, wc_stock_amount( $data['stock_quantity'] ) ); } else if ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( get_post_meta( $product_id, '_stock', true ) ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); wc_update_product_stock( $product_id, wc_stock_amount( $stock_quantity ) ); } } else { // Don't manage stock. update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', $backorders ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } } elseif ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product_id, $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; } } update_post_meta( $product_id, '_upsell_ids', $upsells ); } else { delete_post_meta( $product_id, '_upsell_ids' ); } } // 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; } } update_post_meta( $product_id, '_crosssell_ids', $crosssells ); } else { delete_post_meta( $product_id, '_crosssell_ids' ); } } // Product categories. if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { $term_ids = array_unique( array_map( 'intval', $data['categories'] ) ); wp_set_object_terms( $product_id, $term_ids, 'product_cat' ); } // Product tags. if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { $term_ids = array_unique( array_map( 'intval', $data['tags'] ) ); wp_set_object_terms( $product_id, $term_ids, 'product_tag' ); } // Downloadable. if ( isset( $data['downloadable'] ) ) { $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; update_post_meta( $product_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $product_id, '_downloadable', true ); } // Downloadable options. if ( 'yes' == $is_downloadable ) { // Downloadable files. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $this->save_downloadable_files( $product_id, $data['downloads'] ); } // Download limit. if ( isset( $data['download_limit'] ) ) { update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) ); } // Download expiry. if ( isset( $data['download_expiry'] ) ) { update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) ); } // Download type. if ( isset( $data['download_type'] ) ) { update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) ); } } // Product url. if ( $product_type == 'external' ) { if ( isset( $data['product_url'] ) ) { update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) ); } if ( isset( $data['button_text'] ) ) { update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) ); } } // Reviews allowed. if ( isset( $data['reviews_allowed'] ) ) { $reviews_allowed = ( true === $data['reviews_allowed'] ) ? 'open' : 'closed'; $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) ); } // Do action for product type do_action( 'woocommerce_api_process_product_meta_' . $product_type, $product_id, $data ); return true; } /** * Save variations * * @since 2.2 * @param int $id * @param array $data * @return bool * @throws WC_API_Exception */ protected function save_variations( $id, $data ) { global $wpdb; $variations = $data['variations']; $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) ); foreach ( $variations as $menu_order => $variation ) { $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; if ( ! $variation_id && isset( $variation['sku'] ) ) { $variation_sku = wc_clean( $variation['sku'] ); $variation_id = wc_get_product_id_by_sku( $variation_sku ); } // Generate a useful post title $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) ); // Update or Add post if ( ! $variation_id ) { $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; $new_variation = array( 'post_title' => $variation_post_title, 'post_content' => '', 'post_status' => $post_status, 'post_author' => get_current_user_id(), 'post_parent' => $id, 'post_type' => 'product_variation', 'menu_order' => $menu_order ); $variation_id = wp_insert_post( $new_variation ); do_action( 'woocommerce_create_product_variation', $variation_id ); } else { $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); if ( isset( $variation['visible'] ) ) { $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; $update_variation['post_status'] = $post_status; } $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); do_action( 'woocommerce_update_product_variation', $variation_id ); } // Stop with we don't have a variation ID if ( is_wp_error( $variation_id ) ) { throw new WC_API_Exception( 'woocommerce_api_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); } // SKU if ( isset( $variation['sku'] ) ) { $sku = get_post_meta( $variation_id, '_sku', true ); $new_sku = wc_clean( $variation['sku'] ); if ( '' == $new_sku ) { update_post_meta( $variation_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $variation_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 { update_post_meta( $variation_id, '_sku', $new_sku ); } } else { update_post_meta( $variation_id, '_sku', '' ); } } } // Thumbnail. if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { $image = current( $variation['image'] ); if ( $image && is_array( $image ) ) { if ( isset( $image['position'] ) && $image['position'] == 0 ) { if ( isset( $image['src'] ) ) { $upload = $this->upload_product_image( wc_clean( $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, $id ); } else if ( isset( $image['id'] ) ) { $attachment_id = $image['id']; } // Set the image alt if present. if ( ! empty( $image['alt'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } // Set the image title if present. if ( ! empty( $image['title'] ) ) { wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); } update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); } } else { delete_post_meta( $variation_id, '_thumbnail_id' ); } } // Virtual variation if ( isset( $variation['virtual'] ) ) { $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_virtual', $is_virtual ); } // Downloadable variation if ( isset( $variation['downloadable'] ) ) { $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); } // Shipping data $this->save_product_shipping_data( $variation_id, $variation ); // Stock handling if ( isset( $variation['managing_stock'] ) ) { $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; } else { $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); } update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock ); if ( isset( $variation['in_stock'] ) ) { $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; } else { $stock_status = get_post_meta( $variation_id, '_stock_status', true ); } wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); if ( 'yes' === $managing_stock ) { $backorders = get_post_meta( $variation_id, '_backorders', true ); if ( isset( $variation['backorders'] ) ) { if ( 'notify' === $variation['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; } } update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); if ( isset( $variation['stock_quantity'] ) ) { wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); } else if ( isset( $data['inventory_delta'] ) ) { $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); } } else { delete_post_meta( $variation_id, '_backorders' ); delete_post_meta( $variation_id, '_stock' ); } // Regular Price if ( isset( $variation['regular_price'] ) ) { $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; } else { $regular_price = get_post_meta( $variation_id, '_regular_price', true ); } // Sale Price if ( isset( $variation['sale_price'] ) ) { $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; } else { $sale_price = get_post_meta( $variation_id, '_sale_price', true ); } if ( isset( $variation['sale_price_dates_from'] ) ) { $date_from = $variation['sale_price_dates_from']; } else { $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } if ( isset( $variation['sale_price_dates_to'] ) ) { $date_to = $variation['sale_price_dates_to']; } else { $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); } _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); // Tax class if ( isset( $variation['tax_class'] ) ) { if ( $variation['tax_class'] !== 'parent' ) { update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); } else { delete_post_meta( $variation_id, '_tax_class' ); } } // Downloads if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { $this->save_downloadable_files( $id, $variation['downloads'], $variation_id ); } // Download limit if ( isset( $variation['download_limit'] ) ) { $download_limit = absint( $variation['download_limit'] ); update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); } // Download expiry if ( isset( $variation['download_expiry'] ) ) { $download_expiry = absint( $variation['download_expiry'] ); update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); } } else { update_post_meta( $variation_id, '_download_limit', '' ); update_post_meta( $variation_id, '_download_expiry', '' ); update_post_meta( $variation_id, '_downloadable_files', '' ); } // Description. if ( isset( $variation['description'] ) ) { update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); } // Update taxonomies if ( isset( $variation['attributes'] ) ) { $updated_attribute_keys = array(); foreach ( $variation['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 = 'attribute_' . sanitize_title( $_attribute['name'] ); $updated_attribute_keys[] = $_attribute_key; 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'] ) ) : ''; } update_post_meta( $variation_id, $_attribute_key, $_attribute_value ); } } // Remove old taxonomies attributes so data is kept up to date - first get attribute key names $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); foreach ( $delete_attribute_keys as $key ) { delete_post_meta( $variation_id, $key ); } } do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); } // Update parent if variable so price sorting works and stays in sync with the cheapest child WC_Product_Variable::sync( $id ); // Update default attributes options setting if ( isset( $data['default_attribute'] ) ) { $data['default_attributes'] = $data['default_attribute']; } if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) { $default_attributes = array(); foreach ( $data['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; } } } } update_post_meta( $id, '_default_attributes', $default_attributes ); } return true; } /** * Save product shipping data * * @since 2.2 * @param int $id * @param array $data */ private function save_product_shipping_data( $id, $data ) { if ( isset( $data['weight'] ) ) { update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) ); } // Product dimensions if ( isset( $data['dimensions'] ) ) { // Height if ( isset( $data['dimensions']['height'] ) ) { update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width if ( isset( $data['dimensions']['width'] ) ) { update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal($data['dimensions']['width'] ) ); } // Length if ( isset( $data['dimensions']['length'] ) ) { update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Virtual if ( isset( $data['virtual'] ) ) { $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; if ( 'yes' == $virtual ) { update_post_meta( $id, '_weight', '' ); update_post_meta( $id, '_length', '' ); update_post_meta( $id, '_width', '' ); update_post_meta( $id, '_height', '' ); } } // Shipping class if ( isset( $data['shipping_class'] ) ) { wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); } } /** * Save downloadable files * * @since 2.2 * @param int $product_id * @param array $downloads * @param int $variation_id */ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { $files = array(); // File paths will be stored in an array keyed off md5(file path) foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( ! isset( $file['file'] ) ) { continue; } $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; if ( 0 === strpos( $file['file'], 'http' ) ) { $file_url = esc_url_raw( $file['file'] ); } else { $file_url = wc_clean( $file['file'] ); } $files[ md5( $file_url ) ] = array( 'name' => $file_name, 'file' => $file_url ); } // Grant permission to any newly added files on any existing orders for this product prior to saving do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); $id = ( 0 === $variation_id ) ? $product_id : $variation_id; update_post_meta( $id, '_downloadable_files', $files ); } /** * 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(); if ( $product->is_type( 'variation' ) ) { if ( has_post_thumbnail( $product->get_variation_id() ) ) { // Add variation image if set $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); } elseif ( has_post_thumbnail( $product->id ) ) { // Otherwise use the parent product featured image if set $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { // Add featured image if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } // Add gallery images $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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 array $images * @param int $id * @throws WC_API_Exception */ protected function save_product_images( $id, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { if ( isset( $image['position'] ) && $image['position'] == 0 ) { $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, $id ); } set_post_thumbnail( $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, $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 ) ) { update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) ); } } else { delete_post_thumbnail( $id ); update_post_meta( $id, '_product_image_gallery', '' ); } } /** * 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 int|WP_Error Attachment id */ protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { $file_name = basename( current( explode( '?', $image_url ) ) ); $parsed_url = @parse_url( $image_url ); // Check parsed URL. if ( ! $parsed_url || ! is_array( $parsed_url ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_' . $upload_for, sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ), 400 ); } // Ensure url is valid. $image_url = str_replace( ' ', '%20', $image_url ); // Get the file. $response = wp_safe_remote_get( $image_url, array( 'timeout' => 10 ) ); if ( is_wp_error( $response ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ), 400 ); } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { throw new WC_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), 400 ); } // Ensure we have a file name and type. $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { $headers = wp_remote_retrieve_headers( $response ); if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); $disposition = sanitize_file_name( $disposition ); $file_name = $disposition; } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); } unset( $headers ); // Recheck filetype $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { throw new WC_API_Exception( 'woocommerce_api_invalid_' . $upload_for, __( 'Invalid image type.', 'woocommerce' ), 400 ); } } // Upload the file. $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); if ( $upload['error'] ) { throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload['error'], 400 ); } // Get filesize. $filesize = filesize( $upload['file'] ); if ( 0 == $filesize ) { @unlink( $upload['file'] ); unset( $upload ); throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), 400 ); } unset( $response ); 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_', '', str_replace( 'pa_', '', $attribute_name ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { $attributes[] = array( 'name' => wc_attribute_label( $attribute['name'], $product ), 'slug' => str_replace( 'pa_', '', $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => $this->get_attribute_options( $product->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_files() 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 */ 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 */ 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 ); } else if ( 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 ); } else if ( $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 */ 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. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); $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 */ 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. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); 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 */ 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. flush_rewrite_rules(); delete_transient( 'wc_attribute_taxonomies' ); 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 */ 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 ); } $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 ); } $args = array( 'hide_empty' => false ); $orderby = wc_attribute_orderby( $taxonomy ); switch ( $orderby ) { case 'name' : $args['orderby'] = 'name'; $args['menu_order'] = false; break; case 'id' : $args['orderby'] = 'id'; $args['order'] = 'ASC'; $args['menu_order'] = false; break; case 'menu_order' : $args['menu_order'] = 'ASC'; break; } $terms = get_terms( $taxonomy, $args ); $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 */ public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { global $wpdb; try { $id = absint( $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 */ 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 */ 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 */ 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 ); } else if ( 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 */ 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 wp_delete_post( $product_id, 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 */ 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 ); } // Product exists / edit product if ( $product_id ) { $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']; } } // Product don't exists / create product else { $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() ) ); } } } class-wc-rest-report-top-sellers-controller.php 0000666 00000011133 15214171310 0015703 0 ustar 00 <?php /** * REST API Reports controller * * Handles requests to the reports/top_sellers endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Report Top Sellers controller class. * * @package WooCommerce/API * @extends WC_REST_Report_Sales_Controller */ class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Sales_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'reports/top_sellers'; /** * Get sales reports. * * @param WP_REST_Request $request * @return array|WP_Error */ public function get_items( $request ) { // Set date filtering. $filter = array( 'period' => $request['period'], 'date_min' => $request['date_min'], 'date_max' => $request['date_max'], ); $this->setup_report( $filter ); $report_data = $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 = array(); foreach ( $report_data as $item ) { $product = wc_get_product( $item->product_id ); if ( $product ) { $top_sellers[] = array( 'name' => $product->get_title(), 'product_id' => (int) $item->product_id, 'quantity' => wc_stock_amount( $item->order_item_qty ), ); } } $data = array(); foreach ( $top_sellers as $top_seller ) { $item = $this->prepare_item_for_response( (object) $top_seller, $request ); $data[] = $this->prepare_response_for_collection( $item ); } return rest_ensure_response( $data ); } /** * Prepare a report sales object for serialization. * * @param stdClass $top_seller * @param WP_REST_Request $request Request object. * @return WP_REST_Response $response Response data. */ public function prepare_item_for_response( $top_seller, $request ) { $data = array( 'name' => $top_seller->name, 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->quantity, ); $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( array( 'about' => array( 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), ), 'product' => array( 'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ), ), ) ); /** * Filter a report top sellers returned from the API. * * Allows modification of the report top sellers data right before it is returned. * * @param WP_REST_Response $response The response object. * @param stdClass $top_seller The original report object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request ); } /** * Get the Report's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'top_sellers_report', 'type' => 'object', 'properties' => array( 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), 'product_id' => array( 'description' => __( 'Product ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), 'quantity' => array( 'description' => __( 'Total number of purchases.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } } class-wc-rest-product-tags-controller.php 0000666 00000007037 15214171310 0014545 0 ustar 00 <?php /** * REST API Product Tags controller * * Handles requests to the products/tags endpoint. * * @author WooThemes * @category API * @package WooCommerce/API * @since 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * REST API Product Tags controller class. * * @package WooCommerce/API * @extends WC_REST_Terms_Controller */ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/tags'; /** * Taxonomy. * * @var string */ protected $taxonomy = 'product_tag'; /** * Prepare a single product tag output for response. * * @param obj $item Term object. * @param WP_REST_Request $request * @return WP_REST_Response $response */ public function prepare_item_for_response( $item, $request ) { $data = array( 'id' => (int) $item->term_id, 'name' => $item->name, 'slug' => $item->slug, 'description' => $item->description, 'count' => (int) $item->count, ); $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( $item, $request ) ); /** * Filter a term item returned from the API. * * Allows modification of the term data right before it is returned. * * @param WP_REST_Response $response The response object. * @param object $item The original term object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } /** * Get the Tag's schema, conforming to JSON Schema. * * @return array */ public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->taxonomy, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'name' => array( 'description' => __( 'Tag name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_title', ), ), 'description' => array( 'description' => __( 'HTML description of the resource.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'wp_filter_post_kses', ), ), 'count' => array( 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $schema ); } }
dvadf
dvadf
| ver. 1.4 |
Github
|
.
| PHP 7.0.33 | Generation time: 0.03 |
proxy
|
phpinfo
|
Settings