From 49a0f0f6d2a13f4a4f84c8b997a1875e6f7f6ce3 Mon Sep 17 00:00:00 2001 From: Justin Shreve Date: Fri, 21 Dec 2018 17:57:46 -0500 Subject: [PATCH] Add `reports/downloads` REST API endpoint. (https://github.com/woocommerce/woocommerce-admin/pull/1122) * First pass at downloads REST API * Handle PR feedback --- ...dmin-rest-reports-downloads-controller.php | 330 ++++++++++++++ .../includes/class-wc-admin-api-init.php | 5 + ...class-wc-admin-reports-downloads-query.php | 47 ++ .../class-wc-admin-reports-data-store.php | 59 +++ ...-wc-admin-reports-downloads-data-store.php | 383 +++++++++++++++++ .../tests/api/reports-downloads.php | 401 ++++++++++++++++++ 6 files changed, 1225 insertions(+) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php create mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php create mode 100644 plugins/woocommerce-admin/tests/api/reports-downloads.php diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php index e70a3538c30..3dc6aaf8bf9 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-downloads-controller.php @@ -30,4 +30,334 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control * @var string */ protected $rest_base = 'reports/downloads'; + + /** + * Get items. + * + * @param WP_REST_Request $request Request data. + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $args = array(); + $registered = array_keys( $this->get_collection_params() ); + foreach ( $registered as $param_name ) { + if ( isset( $request[ $param_name ] ) ) { + $args[ $param_name ] = $request[ $param_name ]; + } + } + + $reports = new WC_Admin_Reports_Downloads_Query( $args ); + $downloads_data = $reports->get_data(); + + $data = array(); + + foreach ( $downloads_data->data as $download_data ) { + $item = $this->prepare_item_for_response( $download_data, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + $response = rest_ensure_response( $data ); + + $response->header( 'X-WP-Total', (int) $downloads_data->total ); + $response->header( 'X-WP-TotalPages', (int) $downloads_data->pages ); + + $page = $downloads_data->page_no; + $max_pages = $downloads_data->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; + } + + /** + * Prepare a report object for serialization. + * + * @param Array $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $report, $request ) { + $data = $report; + + $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( $report ) ); + + $response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' ); + + // Figure out file name. + // Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197. + $product_id = intval( $data['product_id'] ); + $_product = wc_get_product( $product_id ); + $file_path = $_product->get_file_download_path( $data['download_id'] ); + $filename = basename( $file_path ); + $response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + + /** + * 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_downloads', $response, $report, $request ); + } + + /** + * Prepare links for the request. + * + * @param Array $object Object data. + * @return array Links for the given post. + */ + protected function prepare_links( $object ) { + $links = array( + 'product' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ), + 'embeddable' => true, + ), + 'user' => array( + 'href' => rest_url( 'wp/v2/users/' . $object['user_id'] ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * 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_downloads', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'ID.', 'wc-admin' ), + ), + 'product_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product ID.', 'wc-admin' ), + ), + 'date' => array( + 'description' => __( "The date of the download, in the site's timezone.", 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_gmt' => array( + 'description' => __( 'The date of the download, as GMT.', 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'download_id' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Download ID.', 'wc-admin' ), + ), + 'file_name' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'File name.', 'wc-admin' ), + ), + 'product_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product ID.', 'wc-admin' ), + ), + 'order_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Order ID.', 'wc-admin' ), + ), + 'user_id' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'User ID for the downloader.', 'wc-admin' ), + ), + 'ip_address' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'IP address for the downloader.', 'wc-admin' ), + ), + ), + ); + + 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' ) ); + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['match'] = array( + 'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'all', + 'enum' => array( + 'all', + 'any', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + + ); + $params['product_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['order_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified order ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['order_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['user_includes'] = array( + 'description' => __( 'Limit response to objects that have the specified user ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['user_excludes'] = array( + 'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['ip_address_includes'] = array( + 'description' => __( 'Limit response to objects that have a specified ip address.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'string', + ), + ); + + $params['ip_address_excludes'] = array( + 'description' => __( 'Limit response to objects that don\'t have a specified ip address.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'string', + ), + ); + + return $params; + } } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 85823327952..4838ef7a632 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -57,6 +57,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-query.php'; // Data stores. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php'; @@ -69,6 +70,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-stats-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php'; + require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-data-store.php'; // Data triggers. require_once dirname( __FILE__ ) . '/wc-admin-order-functions.php'; @@ -104,6 +106,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php'; + require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php'; $controllers = array( 'WC_Admin_REST_Admin_Notes_Controller', @@ -123,6 +126,7 @@ class WC_Admin_Api_Init { 'WC_Admin_REST_Reports_Coupons_Controller', 'WC_Admin_REST_Reports_Coupons_Stats_Controller', 'WC_Admin_REST_Reports_Stock_Controller', + 'WC_Admin_REST_Reports_Downloads_Controller', ); foreach ( $controllers as $controller ) { @@ -335,6 +339,7 @@ class WC_Admin_Api_Init { 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store', 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store', 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store', + 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store', 'admin-note' => 'WC_Admin_Notes_Data_Store', ) ); diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php new file mode 100644 index 00000000000..aaeaa25c3c0 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-downloads-query.php @@ -0,0 +1,47 @@ + '2018-07-19 00:00:00', + * 'after' => '2018-07-05 00:00:00', + * 'page' => 2, + * 'products' => array(1,2,3) + * ); + * $report = new WC_Admin_Reports_Downloads_Query( $args ); + * $mydata = $report->get_data(); + * + * @package WooCommerce Admin/Classes + */ + +defined( 'ABSPATH' ) || exit; + +/** + * WC_Admin_Reports_Downloads_Query + */ +class WC_Admin_Reports_Downloads_Query extends WC_Admin_Reports_Query { + + /** + * Valid fields for downloads report. + * + * @return array + */ + protected function get_default_query_vars() { + return array(); + } + + /** + * Get downloads data based on the current query vars. + * + * @return array + */ + public function get_data() { + $args = apply_filters( 'woocommerce_reports_downloads_query_args', $this->get_query_vars() ); + + $data_store = WC_Data_Store::load( 'report-downloads' ); + $results = $data_store->get_data( $args ); + return apply_filters( 'woocommerce_reports_downloads_select_query', $results, $args ); + } + +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php index a4b2e34ab1d..e9964d9fb08 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php @@ -634,6 +634,65 @@ class WC_Admin_Reports_Data_Store { return $excluded_coupons_str; } + /** + * Returns comma separated ids of included orders, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_orders( $query_args ) { + $included_orders_str = ''; + + if ( isset( $query_args['order_includes'] ) && is_array( $query_args['order_includes'] ) && count( $query_args['order_includes'] ) > 0 ) { + $included_orders_str = implode( ',', $query_args['order_includes'] ); + } + return $included_orders_str; + } + + /** + * Returns comma separated ids of excluded orders, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_orders( $query_args ) { + $excluded_orders_str = ''; + + if ( isset( $query_args['order_excludes'] ) && is_array( $query_args['order_excludes'] ) && count( $query_args['order_excludes'] ) > 0 ) { + $excluded_orders_str = implode( ',', $query_args['order_excludes'] ); + } + return $excluded_orders_str; + } + + /** + * Returns comma separated ids of included users, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_users( $query_args ) { + $included_users_str = ''; + + if ( isset( $query_args['user_includes'] ) && is_array( $query_args['user_includes'] ) && count( $query_args['user_includes'] ) > 0 ) { + $included_users_str = implode( ',', $query_args['user_includes'] ); + } + return $included_users_str; + } + + /** + * Returns comma separated ids of excluded users, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_users( $query_args ) { + $excluded_users_str = ''; + + if ( isset( $query_args['user_excludes'] ) && is_array( $query_args['user_excludes'] ) && count( $query_args['user_excludes'] ) > 0 ) { + $excluded_users_str = implode( ',', $query_args['user_excludes'] ); + } + return $excluded_users_str; + } /** * Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user. diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php new file mode 100644 index 00000000000..86555a8b1ec --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-data-store.php @@ -0,0 +1,383 @@ + 'intval', + 'date' => 'strval', + 'date_gmt' => 'strval', + 'download_id' => 'strval', // String because this can sometimes be a hash. + 'file_name' => 'strval', + 'product_id' => 'intval', + 'order_id' => 'intval', + 'user_id' => 'intval', + 'ip_address' => 'strval', + ); + + /** + * SQL columns to select in the db query and their mapping to SQL code. + * + * @var array + */ + protected $report_columns = array( + 'id' => 'download_log_id as id', + 'date' => 'timestamp as date_gmt', + 'download_id' => 'product_permissions.download_id', + 'product_id' => 'product_permissions.product_id', + 'order_id' => 'product_permissions.order_id', + 'user_id' => 'product_permissions.user_id', + 'ip_address' => 'user_ip_address as ip_address', + ); + + /** + * Constructor + */ + public function __construct() { + global $wpdb; + } + + /** + * Updates the database query with parameters used for downloads report. + * + * @param array $query_args Query arguments supplied by the user. + * @return array Array of parameters used for SQL query. + */ + protected function get_sql_query_params( $query_args ) { + global $wpdb; + + $lookup_table = $wpdb->prefix . self::TABLE_NAME; + $operator = $this->get_match_operator( $query_args ); + $where_filters = array(); + + $sql_query_params = $this->get_time_period_sql_params( $query_args, $lookup_table ); + $sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) ); + $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); + + $included_products = $this->get_included_products( $query_args ); + $excluded_products = $this->get_excluded_products( $query_args ); + if ( $included_products ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$included_products}) + )"; + } + + if ( $excluded_products ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$excluded_products}) + )"; + } + + $included_orders = $this->get_included_orders( $query_args ); + $excluded_orders = $this->get_excluded_orders( $query_args ); + if ( $included_orders ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$included_orders}) + )"; + } + + if ( $excluded_orders ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$excluded_orders}) + )"; + } + + $included_users = $this->get_included_users( $query_args ); + $excluded_users = $this->get_excluded_users( $query_args ); + if ( $included_users ) { + $where_filters[] = " {$lookup_table}.permission_id IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$included_users}) + )"; + } + + if ( $excluded_users ) { + $where_filters[] = " {$lookup_table}.permission_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id + FROM + {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE + {$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$excluded_users}) + )"; + } + + $included_ip_addresses = $this->get_included_ip_addresses( $query_args ); + $excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args ); + if ( $included_ip_addresses ) { + $where_filters[] = " {$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')"; + } + + if ( $excluded_ip_addresses ) { + $where_filters[] = " {$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')"; + } + + $where_subclause = implode( " $operator ", $where_filters ); + if ( $where_subclause ) { + $sql_query_params['where_clause'] .= " AND ( $where_subclause )"; + } + + $sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id"; + + return $sql_query_params; + } + + /** + * Returns comma separated ids of included ip address, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_included_ip_addresses( $query_args ) { + $included_ips_str = ''; + + if ( isset( $query_args['ip_address_includes'] ) && is_array( $query_args['ip_address_includes'] ) && count( $query_args['ip_address_includes'] ) > 0 ) { + $ip_includes = array(); + foreach ( $query_args['ip_address_includes'] as $ip ) { + $ip_includes[] = esc_sql( $ip ); + } + $included_ips_str = implode( "','", $ip_includes ); + } + return $included_ips_str; + } + + /** + * Returns comma separated ids of excluded ip address, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return string + */ + protected function get_excluded_ip_addresses( $query_args ) { + $excluded_ips_str = ''; + + if ( isset( $query_args['ip_address_excludes'] ) && is_array( $query_args['ip_address_excludes'] ) && count( $query_args['ip_address_excludes'] ) > 0 ) { + $ip_excludes = array(); + foreach ( $query_args['ip_address_excludes'] as $ip ) { + $ip_excludes[] = esc_sql( $ip ); + } + $excluded_ips_str = implode( ',', $ip_excludes ); + } + return $excluded_ips_str; + } + + + /** + * Fills WHERE clause of SQL request with date-related constraints. + * + * @param array $query_args Parameters supplied by the user. + * @param string $table_name Name of the db table relevant for the date constraint. + * @return array + */ + protected function get_time_period_sql_params( $query_args, $table_name ) { + $sql_query = array( + 'from_clause' => '', + 'where_time_clause' => '', + 'where_clause' => '', + ); + + if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) { + $datetime = new DateTime( $query_args['before'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.timestamp <= '$datetime_str'"; + + } + + if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) { + $datetime = new DateTime( $query_args['after'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.timestamp >= '$datetime_str'"; + } + + return $sql_query; + } + + /** + * Fills ORDER BY clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_order_by_sql_params( $query_args ) { + $sql_query['order_by_clause'] = ''; + if ( isset( $query_args['orderby'] ) ) { + $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); + } + + if ( isset( $query_args['order'] ) ) { + $sql_query['order_by_clause'] .= ' ' . $query_args['order']; + } else { + $sql_query['order_by_clause'] .= ' DESC'; + } + + return $sql_query; + } + + /** + * Returns the report data based on parameters supplied by the user. + * + * @param array $query_args Query parameters. + * @return stdClass|WP_Error Data. + */ + public function get_data( $query_args ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + $now = time(); + $week_back = $now - WEEK_IN_SECONDS; + + // These defaults are only partially applied when used via REST API, as that has its own defaults. + $defaults = array( + 'per_page' => get_option( 'posts_per_page' ), + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'timestamp', + 'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ), + 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), + 'fields' => '*', + ); + $query_args = wp_parse_args( $query_args, $defaults ); + + $cache_key = $this->get_cache_key( $query_args ); + $data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false === $data ) { + $data = (object) array( + 'data' => array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, + ); + + $selections = $this->selected_columns( $query_args ); + $sql_query_params = $this->get_sql_query_params( $query_args ); + + $db_records_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM ( + SELECT + {$table_name}.download_log_id + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + {$table_name}.download_log_id + ) AS tt" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); + if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { + return $data; + } + + $download_data = $wpdb->get_results( + "SELECT + {$selections} + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + {$table_name}.download_log_id + ORDER BY + {$sql_query_params['order_by_clause']} + {$sql_query_params['limit']} + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + if ( null === $download_data ) { + return $data; + } + + $download_data = array_map( array( $this, 'cast_numbers' ), $download_data ); + $data = (object) array( + 'data' => $download_data, + 'total' => $db_records_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], + ); + + wp_cache_set( $cache_key, $data, $this->cache_group ); + } + + return $data; + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); + } + + /** + * Maps ordering specified by the user to columns in the database/fields in the data. + * + * @param string $order_by Sorting criterion. + * @return string + */ + protected function normalize_order_by( $order_by ) { + global $wpdb; + + if ( 'date' === $order_by ) { + return $wpdb->prefix . 'wc_download_log.timestamp'; + } + + return $order_by; + } + +} diff --git a/plugins/woocommerce-admin/tests/api/reports-downloads.php b/plugins/woocommerce-admin/tests/api/reports-downloads.php new file mode 100644 index 00000000000..f79ad47fb4e --- /dev/null +++ b/plugins/woocommerce-admin/tests/api/reports-downloads.php @@ -0,0 +1,401 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( $this->endpoint, $routes ); + } + + /** + * Test getting report. + */ + public function test_get_report() { + global $wpdb; + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + + // Populate all of the data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 1 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 25 ); + $product->save(); + + $order = WC_Helper_Order::create_order( 1, $product ); + $order->set_status( 'completed' ); + $order->set_total( 100 ); + $order->save(); + + $download = new WC_Customer_Download(); + $download->set_user_id( $this->user ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( $this->user ); + $object->set_user_ip_address( '1.2.3.4' ); + $id = $object->save(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + + $download_report = reset( $reports ); + + $this->assertEquals( 1, $download_report['download_id'] ); + $this->assertEquals( $product->get_id(), $download_report['product_id'] ); + $this->assertEquals( $order->get_id(), $download_report['order_id'] ); + $this->assertEquals( $this->user, $download_report['user_id'] ); + $this->assertEquals( '1.2.3.4', $download_report['ip_address'] ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + } + + /** + * Does some test setup so we can filter with different options in later tests. + */ + public function filter_setup() { + global $wpdb; + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + $time = time(); + + // First set of data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 1 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 25 ); + $product->save(); + $product_1 = $product->get_id(); + + $order = WC_Helper_Order::create_order( 1, $product ); + $order->set_status( 'completed' ); + $order->set_total( 25 ); + $order->save(); + $order_1 = $order->get_id(); + + $download = new WC_Customer_Download(); + $download->set_user_id( 1 ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( 1 ); + $object->set_user_ip_address( '1.2.3.4' ); + $id = $object->save(); + + // Second set of data. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/test.png' ); + $prod_download->set_id( 2 ); + + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product 2' ); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->set_regular_price( 10 ); + $product->save(); + $product_2 = $product->get_id(); + + $order = WC_Helper_Order::create_order( 2, $product ); + $order->set_status( 'completed' ); + $order->set_total( 10 ); + $order->save(); + $order_2 = $order->get_id(); + + $download = new WC_Customer_Download(); + $download->set_user_id( 2 ); + $download->set_order_id( $order->get_id() ); + $download->set_product_id( $product->get_id() ); + $download->set_download_id( $prod_download->get_id() ); + $download->save(); + + $object = new WC_Customer_Download_Log(); + $object->set_permission_id( $download->get_id() ); + $object->set_user_id( 2 ); + $object->set_user_ip_address( '5.4.3.2.1' ); + $object->set_timestamp( date( 'Y-m-d H:00:00', $time - ( 2 * DAY_IN_SECONDS ) ) ); + $id = $object->save(); + + return array( + 'time' => $time, + 'product_1' => $product_1, + 'product_2' => $product_2, + 'order_1' => $order_1, + 'order_2' => $order_2, + ); + } + + /** + * Test getting report with date filter. + */ + public function test_get_report_with_date_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test date filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'before' => date( 'Y-m-d H:00:00', $test_info['time'] + DAY_IN_SECONDS ), + 'after' => date( 'Y-m-d H:00:00', $test_info['time'] - DAY_IN_SECONDS ), + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + } + + /** + * Test getting report with product filter. + */ + public function test_get_report_with_product_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'product_includes' => $test_info['product_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'product_excludes' => $test_info['product_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting report with order filter. + */ + public function test_get_report_with_order_filter() { + $test_info = $this->filter_setup(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $reports ) ); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'order_includes' => $test_info['order_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'order_excludes' => $test_info['order_1'], + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting report with user filter. + */ + public function test_get_report_with_user_filter() { + $test_info = $this->filter_setup(); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'user_includes' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + $this->assertEquals( 1, $download_report['user_id'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'user_excludes' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + $this->assertEquals( 2, $download_report['user_id'] ); + } + + /** + * Test getting report with ip address filter. + */ + public function test_get_report_with_ip_address_filter() { + $test_info = $this->filter_setup(); + + // Test includes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'ip_address_includes' => '1.2.3.4', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'help.png', $download_report['file_name'] ); + + // Test excludes filtering. + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'ip_address_excludes' => '1.2.3.4', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $download_report = reset( $reports ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + $this->assertEquals( 'test.png', $download_report['file_name'] ); + } + + /** + * Test getting reports without valid permissions. + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test reports schema. + */ + public function test_reports_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', $this->endpoint ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 9, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'product_id', $properties ); + $this->assertArrayHasKey( 'date', $properties ); + $this->assertArrayHasKey( 'date_gmt', $properties ); + $this->assertArrayHasKey( 'download_id', $properties ); + $this->assertArrayHasKey( 'file_name', $properties ); + $this->assertArrayHasKey( 'order_id', $properties ); + $this->assertArrayHasKey( 'user_id', $properties ); + $this->assertArrayHasKey( 'ip_address', $properties ); + } +}