Match server-side CSV export format to client-side (https://github.com/woocommerce/woocommerce-admin/pull/2987)

* Add "exportable" report interface for defining CSV export values.

* Define export values for Orders Report.

* Define export values for Products Report.

* Define export values for Categories Report.

* Define export values for Coupons Report.

* Allow commas and double quotes in CSV exported values.

* Fix in-browser export formatting of orders report products.

* Align server-side orders report export formatting with in-browser.

* Cover comma and double quote escaping in CSV export package tests.

* Define export values for Customers Report.

* Embed response links when requesting data for CSV exports.

* Define export values for Downloads Report.

* Move reusable report export functions to a trait.

* Define export values for Stock Report.

* Define export values for Taxes Report.

* Define export values for Variations Report.

* Define export values for Revenue Report.

* Always pass export row data through the filter.

* Fix formatting in test case for CSV coupon export.

* Quote escape CSV headers in client-side export.

Escape values with spaces as well.

* Check if inventory is managed at the product level before using the stock status/quantity.

* Prevent CSV injection in csv-export package.
This commit is contained in:
Jeff Stieler 2019-10-24 09:41:16 -07:00 committed by GitHub
parent dd77d25a34
commit 1ac8577fc2
21 changed files with 710 additions and 49 deletions

View File

@ -186,7 +186,11 @@ export default class OrdersReportTable extends Component {
href: product.href,
} ) )
),
value: formattedProducts.map( product => product.label ).join( ' ' ),
value: formattedProducts
.map( ( { quantity, label } ) =>
sprintf( __( '%s× %s', 'woocommerce-admin' ), quantity, label )
)
.join( ', ' ),
},
{
display: numberFormat( num_items_sold ),

View File

@ -1,3 +1,8 @@
# 1.2.0 (Unreleased)
- Properly escape values with double quotes.
- Prevent CSV injection.
# 1.1.2
- Update dependencies.

View File

@ -1,4 +1,4 @@
/** @format */
export default `Date,Orders,Description,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue
2018-04-29T00:00:00,30,lorem ipsum,200,19,19,100,19,200`;
export default `Date,Orders,Description,"Gross Revenue",Refunds,Coupons,Taxes,Shipping,"Net Revenue","Negative Number"
2018-04-29T00:00:00,30,"Lorem, ""ipsum""",200,19,19,100,19,200,'-123`;

View File

@ -37,4 +37,8 @@ export default [
label: 'Net Revenue',
key: 'net_revenue',
},
{
label: 'Negative Number',
key: 'neg_num',
},
];

View File

@ -11,8 +11,8 @@ export default [
value: '30',
},
{
display: 'Lorem, ipsum',
value: 'lorem, ipsum',
display: 'Lorem, "ipsum"',
value: 'Lorem, "ipsum"',
},
{
display: '€200.00',
@ -38,5 +38,9 @@ export default [
display: '€200.00',
value: 200,
},
{
display: '-123',
value: -123,
},
],
];

View File

@ -5,17 +5,40 @@
import moment from 'moment';
import { saveAs } from 'browser-filesaver';
function escapeCSVValue( value ) {
let stringValue = value.toString();
// Prevent CSV injection.
// See: http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
// See: WC_CSV_Exporter::escape_data()
if ( [ '=', '+', '-', '@' ].includes( stringValue.charAt( 0 ) ) ) {
stringValue = "'" + stringValue;
} else if ( stringValue.match( /[,"\s]/ ) ) {
stringValue = '"' + stringValue.replace( /"/g, '""' ) + '"';
}
return stringValue;
}
function getCSVHeaders( headers ) {
return Array.isArray( headers ) ? headers.map( header => header.label ).join( ',' ) : [];
return Array.isArray( headers )
? headers
.map( header => escapeCSVValue( header.label ) )
.join( ',' )
: [];
}
function getCSVRows( rows ) {
return Array.isArray( rows )
? rows
.map( row =>
row.map( rowItem =>
rowItem.value !== undefined && rowItem.value !== null ? rowItem.value.toString().replace( /,/g, ''
) : '' ).join( ',' )
row.map( rowItem => {
if ( undefined === rowItem.value || null === rowItem.value ) {
return '';
}
return escapeCSVValue( rowItem.value );
} ).join( ',' )
)
.join( '\n' )
: [];

View File

@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports categories controller class.
*
* @package WooCommerce/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
@ -318,4 +321,35 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'category' => __( 'Category', 'woocommerce-admin' ),
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'net_revenue' => __( 'Net Revenue', 'woocommerce-admin' ),
'products_count' => __( 'Products', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'category' => $item['extended_info']['name'],
'items_sold' => $item['items_sold'],
'net_revenue' => $item['net_revenue'],
'products_count' => $item['products_count'],
'orders_count' => $item['orders_count'],
);
}
}

View File

@ -294,10 +294,20 @@ class Controller extends \WC_REST_Reports_Controller {
* @return array
*/
public function get_order_statuses() {
return array_keys( $this->get_order_status_labels() );
}
/**
* Get order statuses (and labels) without prefixes.
*
* @return array
*/
public function get_order_status_labels() {
$order_statuses = array();
foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
$order_statuses[] = str_replace( 'wc-', '', $status );
foreach ( wc_get_order_statuses() as $key => $label ) {
$new_key = str_replace( 'wc-', '', $key );
$order_statuses[ $new_key ] = $label;
}
return $order_statuses;

View File

@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports coupons controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Endpoint namespace.
@ -289,4 +291,41 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'code' => __( 'Coupon Code', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'amount' => __( 'Amount Discounted', 'woocommerce-admin' ),
'created' => __( 'Created', 'woocommerce-admin' ),
'expires' => __( 'Expires', 'woocommerce-admin' ),
'type' => __( 'Type', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$date_expires = empty( $item['extended_info']['date_expires'] )
? __( 'N/A', 'woocommerce-admin' )
: $item['extended_info']['date_expires'];
return array(
'code' => $item['extended_info']['code'],
'orders_count' => $item['orders_count'],
'amount' => $item['amount'],
'created' => $item['extended_info']['date_created'],
'expires' => $date_expires,
'type' => $item['extended_info']['discount_type'],
);
}
}

View File

@ -11,6 +11,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/**
@ -19,7 +21,11 @@ use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Endpoint namespace.
@ -221,7 +227,7 @@ class Controller extends \WC_REST_Reports_Controller {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'state' => array(
'state' => array(
'description' => __( 'Region.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
@ -525,4 +531,49 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'name' => __( 'Name', 'woocommerce-admin' ),
'username' => __( 'Username', 'woocommerce-admin' ),
'last_active' => __( 'Last Active', 'woocommerce-admin' ),
'registered' => __( 'Sign Up', 'woocommerce-admin' ),
'email' => __( 'Email', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'total_spend' => __( 'Total Spend', 'woocommerce-admin' ),
'avg_order_value' => __( 'AOV', 'woocommerce-admin' ),
'country' => __( 'Country', 'woocommerce-admin' ),
'city' => __( 'City', 'woocommerce-admin' ),
'region' => __( 'Region', 'woocommerce-admin' ),
'postcode' => __( 'Postal Code', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'name' => $item['name'],
'username' => $item['username'],
'last_active' => $item['date_last_active'],
'registered' => $item['date_registered'],
'email' => $item['email'],
'orders_count' => $item['orders_count'],
'total_spend' => self::csv_number_format( $item['total_spend'] ),
'avg_order_value' => self::csv_number_format( $item['avg_order_value'] ),
'country' => $item['country'],
'city' => $item['city'],
'region' => $item['state'],
'postcode' => $item['postcode'],
);
}
}

View File

@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports downloads controller class.
*
* @package WooCommerce/API
* @extends Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
*
@ -378,4 +381,37 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'date' => __( 'Date', 'woocommerce-admin' ),
'product' => __( 'Product Title', 'woocommerce-admin' ),
'file_name' => __( 'File Name', 'woocommerce-admin' ),
'order_number' => __( 'Order #', 'woocommerce-admin' ),
'user_id' => __( 'User Name', 'woocommerce-admin' ),
'ip_address' => __( 'IP', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'date' => $item['date'],
'product' => $item['_embedded']['product'][0]['name'],
'file_name' => $item['file_name'],
'order_number' => $item['order_number'],
'user_id' => $item['username'],
'ip_address' => $item['ip_address'],
);
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Reports Exportable Controller Interface
*
* @package WooCommerce Admin/Interface
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WooCommerce Reports exportable controller interface.
*
* @since 3.5.0
*/
interface ExportableInterface {
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns();
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Value.
*/
public function prepare_item_for_export( $item );
}

View File

@ -0,0 +1,29 @@
<?php
/**
* REST API Reports exportable traits
*
* Collection of utility methods for exportable reports.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
/**
* ExportableTraits class.
*/
trait ExportableTraits {
/**
* Format numbers for CSV using store precision setting.
*
* @param string|float $value Numeric value.
* @return string Formatted value.
*/
public static function csv_number_format( $value ) {
$decimals = wc_get_price_decimals();
// See: @woocommerce/currency: getCurrencyFormatDecimal().
return number_format( $value, $decimals, '.', '' );
}
}

View File

@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports orders controller class.
*
* @package WooCommerce/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
@ -410,4 +413,72 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
return $params;
}
/**
* Get products column export value.
*
* @param array $products Products from report row.
* @return string
*/
protected function _get_products( $products ) {
$products_list = array();
foreach ( $products as $product ) {
$products_list[] = sprintf(
/* translators: 1: numeric product quantity, 2: name of product */
__( '%1$s× %2$s', 'woocommerce-admin' ),
$product['quantity'],
$product['name']
);
}
return implode( ', ', $products_list );
}
/**
* Get coupons column export value.
*
* @param array $coupons Coupons from report row.
* @return string
*/
protected function _get_coupons( $coupons ) {
return implode( ', ', wp_list_pluck( $coupons, 'code' ) );
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'date_created' => __( 'Date', 'woocommerce-admin' ),
'order_number' => __( 'Order #', 'woocommerce-admin' ),
'status' => __( 'Status', 'woocommerce-admin' ),
'customer_type' => __( 'Customer', 'woocommerce-admin' ),
'products' => __( 'Product(s)', 'woocommerce-admin' ),
'num_items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'coupons' => __( 'Coupon(s)', 'woocommerce-admin' ),
'net_total' => __( 'N. Revenue', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'date_created' => $item['date_created'],
'order_number' => $item['order_number'],
'status' => $item['status'],
'customer_type' => $item['customer_type'],
'products' => $this->_get_products( $item['extended_info']['products'] ),
'num_items_sold' => $item['num_items_sold'],
'coupons' => $this->_get_coupons( $item['extended_info']['coupons'] ),
'net_total' => $item['net_total'],
);
}
}

View File

@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports products controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Endpoint namespace.
@ -343,4 +345,89 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get stock status column export value.
*
* @param array $status Stock status from report row.
* @return string
*/
protected function _get_stock_status( $status ) {
$statuses = wc_get_product_stock_status_options();
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
}
/**
* Get categories column export value.
*
* @param array $category_ids Category IDs from report row.
* @return string
*/
protected function _get_categories( $category_ids ) {
$category_names = get_terms(
array(
'taxonomy' => 'product_cat',
'include' => $category_ids,
'fields' => 'names',
)
);
return implode( ', ', $category_names );
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
$export_columns = array(
'product_name' => __( 'Product Title', 'woocommerce-admin' ),
'sku' => __( 'SKU', 'woocommerce-admin' ),
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'net_revenue' => __( 'N. Revenue', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'product_cat' => __( 'Category', 'woocommerce-admin' ),
'variations' => __( 'Variations', 'woocommerce-admin' ),
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_columns['stock_status'] = __( 'Status', 'woocommerce-admin' );
$export_columns['stock'] = __( 'Stock', 'woocommerce-admin' );
}
return $export_columns;
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
'product_name' => $item['extended_info']['name'],
'sku' => $item['extended_info']['sku'],
'items_sold' => $item['items_sold'],
'net_revenue' => $item['net_revenue'],
'orders_count' => $item['orders_count'],
'product_cat' => $this->_get_categories( $item['extended_info']['category_ids'] ),
'variations' => isset( $item['extended_info']['variations'] ) ? count( $item['extended_info']['variations'] ) : 0,
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
if ( $item['extended_info']['manage_stock'] ) {
$export_item['stock_status'] = $this->_get_stock_status( $item['extended_info']['stock_status'] );
$export_item['stock'] = $item['extended_info']['stock_quantity'];
} else {
$export_item['stock_status'] = __( 'N/A', 'woocommerce-admin' );
$export_item['stock'] = __( 'N/A', 'woocommerce-admin' );
}
}
return $export_item;
}
}

View File

@ -12,6 +12,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
@ -20,7 +22,11 @@ use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Endpoint namespace.
@ -60,7 +66,7 @@ class Controller extends \WC_REST_Reports_Controller {
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
@ -105,6 +111,24 @@ class Controller extends \WC_REST_Reports_Controller {
return $response;
}
/**
* Get report items for export.
*
* Returns only the interval data.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response
*/
public function get_export_items( $request ) {
$response = $this->get_items( $request );
$data = $response->get_data();
$intervals = $data['intervals'];
$response->set_data( $intervals );
return $response;
}
/**
* Prepare a report object for serialization.
*
@ -406,4 +430,43 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'date' => __( 'Date', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'gross_revenue' => __( 'Gross Revenue', 'woocommerce-admin' ),
'refunds' => __( 'Refunds', 'woocommerce-admin' ),
'coupons' => __( 'Coupons', 'woocommerce-admin' ),
'taxes' => __( 'Taxes', 'woocommerce-admin' ),
'shipping' => __( 'Shipping', 'woocommerce-admin' ),
'net_revenue' => __( 'Net Revenue', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$subtotals = (array) $item['subtotals'];
return array(
'date' => $item['date_start'],
'orders_count' => $subtotals['orders_count'],
'gross_revenue' => self::csv_number_format( $subtotals['gross_revenue'] ),
'refunds' => self::csv_number_format( $subtotals['refunds'] ),
'coupons' => self::csv_number_format( $subtotals['coupons'] ),
'taxes' => self::csv_number_format( $subtotals['taxes'] ),
'shipping' => self::csv_number_format( $subtotals['shipping'] ),
'net_revenue' => self::csv_number_format( $subtotals['net_revenue'] ),
);
}
}

View File

@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Stock;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports stock controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Endpoint namespace.
@ -514,4 +516,33 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'title' => __( 'Product / Variation', 'woocommerce-admin' ),
'sku' => __( 'SKU', 'woocommerce-admin' ),
'stock_status' => __( 'Status', 'woocommerce-admin' ),
'stock_quantity' => __( 'Stock', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'title' => $item['name'],
'sku' => $item['sku'],
'stock_status' => $item['stock_status'],
'stock_quantity' => $item['stock_quantity'],
);
}
}

View File

@ -11,13 +11,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
/**
* REST API Reports taxes controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Endpoint namespace.
@ -287,4 +294,37 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'tax_code' => __( 'Tax Code', 'woocommerce-admin' ),
'rate' => __( 'Rate', 'woocommerce-admin' ),
'total_tax' => __( 'Total Tax', 'woocommerce-admin' ),
'order_tax' => __( 'Order Tax', 'woocommerce-admin' ),
'shipping_tax' => __( 'Shipping Tax', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'tax_code' => \WC_Tax::get_rate_code( $item['tax_rate_id'] ),
'rate' => $item['tax_rate'],
'total_tax' => self::csv_number_format( $item['total_tax'] ),
'order_tax' => self::csv_number_format( $item['order_tax'] ),
'shipping_tax' => self::csv_number_format( $item['shipping_tax'] ),
'orders_count' => $item['orders_count'],
);
}
}

View File

@ -11,13 +11,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
/**
* REST API Reports products controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Endpoint namespace.
@ -327,4 +334,61 @@ class Controller extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Get stock status column export value.
*
* @param array $status Stock status from report row.
* @return string
*/
protected function _get_stock_status( $status ) {
$statuses = wc_get_product_stock_status_options();
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
$export_columns = array(
'product_name' => __( 'Product / Variation Title', 'woocommerce-admin' ),
'sku' => __( 'SKU', 'woocommerce-admin' ),
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'net_revenue' => __( 'N. Revenue', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_columns['stock_status'] = __( 'Status', 'woocommerce-admin' );
$export_columns['stock'] = __( 'Stock', 'woocommerce-admin' );
}
return $export_columns;
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
'product_name' => $item['extended_info']['name'],
'sku' => $item['extended_info']['sku'],
'items_sold' => $item['items_sold'],
'net_revenue' => self::csv_number_format( $item['net_revenue'] ),
'orders_count' => $item['orders_count'],
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_item['stock_status'] = $this->_get_stock_status( $item['extended_info']['stock_status'] );
$export_item['stock'] = $item['extended_info']['stock_quantity'];
}
return $export_item;
}
}

View File

@ -11,6 +11,8 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* Include dependencies.
*/
@ -103,6 +105,7 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
* @return bool|WC_REST_Reports_Controller Report controller instance or boolean false on error.
*/
protected function map_report_controller() {
// @todo - Add filter to this list.
$controller_map = array(
'products' => 'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
'variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
@ -113,6 +116,7 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
'stock' => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
'downloads' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
'customers' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
'revenue' => 'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
);
if ( isset( $controller_map[ $this->report_type ] ) ) {
@ -129,11 +133,17 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
}
/**
* Get the report columns from the schema.
* Get the report columns from the controller.
*
* @return array Array of report column names.
*/
protected function get_report_columns() {
// Default to the report's defined export columns.
if ( $this->controller instanceof ExportableInterface ) {
return $this->controller->get_export_columns();
}
// Fallback to generating columns from the report schema.
$report_columns = array();
$report_schema = $this->controller->get_item_schema();
@ -190,20 +200,32 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
$request->set_default_params( $defaults );
$request->set_query_params( $this->report_args );
$response = $this->controller->get_items( $request );
// Does the controller have an export-specific item retrieval method?
// @todo - Potentially revisit. This is only for /revenue/stats/.
if ( is_callable( array( $this->controller, 'get_export_items' ) ) ) {
$response = $this->controller->get_export_items( $request );
} else {
$response = $this->controller->get_items( $request );
}
// Use WP_REST_Server::response_to_data() to embed links in data.
add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
$rest_server = rest_get_server();
$report_data = $rest_server->response_to_data( $response, true );
remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );
$report_meta = $response->get_headers();
$report_data = $response->get_data();
$this->total_rows = $report_meta['X-WP-Total'];
$this->row_data = array_map( array( $this, 'generate_row_data' ), $report_data );
}
/**
* Take a report item and generate row data from it for export.
* Generate row data from a raw report item.
*
* @param object $item Report item data.
* @return array CSV row data.
*/
protected function generate_row_data( $item ) {
protected function get_raw_row_data( $item ) {
$columns = $this->get_column_names();
$row = array();
@ -236,6 +258,24 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
$row[ $column_id ] = $value;
}
return $row;
}
/**
* Get the export row for a given report item.
*
* @param object $item Report item data.
* @return array CSV row data.
*/
protected function generate_row_data( $item ) {
// Default to the report's export method.
if ( $this->controller instanceof ExportableInterface ) {
$row = $this->controller->prepare_item_for_export( $item );
} else {
// Fallback to raw report data.
$row = $this->get_raw_row_data( $item );
}
return apply_filters( "woocommerce_export_{$this->export_type}_row_data", $row, $item );
}
}

View File

@ -274,40 +274,31 @@ class WC_Tests_Reports_Coupons extends WC_Unit_Test_Case {
// Test the CSV export.
$expected_csv_columns = array(
'coupon_id',
'amount',
'orders_count',
'code',
'date_created',
'date_created_gmt',
'date_expires',
'date_expires_gmt',
'discount_type',
'"Coupon Code"',
'Orders',
'"Amount Discounted"',
'Created',
'Expires',
'Type',
);
// Expected CSV for Coupon 2.
$coupon_2_csv = array(
$coupon_2_response['coupon_id'],
$coupon_2_response['amount'],
$coupon_2_response['orders_count'],
$coupon_2_response['extended_info']['code'],
$coupon_2_response['orders_count'],
$coupon_2_response['amount'],
$coupon_2_response['extended_info']['date_created'],
$coupon_2_response['extended_info']['date_created_gmt'],
$coupon_2_response['extended_info']['date_expires'],
$coupon_2_response['extended_info']['date_expires_gmt'],
$coupon_2_response['extended_info']['date_expires'] ? : 'N/A',
$coupon_2_response['extended_info']['discount_type'],
);
// Expected CSV for Coupon 1.
$coupon_1_csv = array(
$coupon_1_response['coupon_id'],
$coupon_1_response['amount'],
$coupon_1_response['orders_count'],
$coupon_1_response['extended_info']['code'],
$coupon_1_response['orders_count'],
$coupon_1_response['amount'],
$coupon_1_response['extended_info']['date_created'],
$coupon_1_response['extended_info']['date_created_gmt'],
$coupon_1_response['extended_info']['date_expires'],
$coupon_1_response['extended_info']['date_expires_gmt'],
$coupon_1_response['extended_info']['date_expires'] ? : 'N/A',
$coupon_1_response['extended_info']['discount_type'],
);