Remove the Legacy REST API from WooCommerce core (#40627)

* Replace the Legacy REST API with a stub that always returns an error.

If the Legacy REST API plugin is installed, it will "take over"
the stub and the API will be available again without noticeable
changes in behavior.

* Move the get_endpoint_data method to WooCommerce core

This method was part of the legacy REST API package but is actually
being used to query endpoints of the new REST API, so it actually
belongs to WooCommerce core.

It has been placed in a new src/Utilities/RestApiUtil class.
The existing src/Internal/RestApiUtil class has been renamed
ro RestApiParameterUtil to avoid confussion.

* Move the register_wp_admin_settings to WooCommerce core.

This method was part of the legacy REST API package but it's needed
for certain endpoints of the current REST API, for example
settings/general/woocommerce_specific_allowed_countries

* Adjust unit tests for the removal of the legacy REST API.

Basically, some tests no longer make sense so they are removed.

* Adjust webhooks for the removal of the legacy REST API.

If a webhook is configured to compose its payload using the
legacy REST API format, and the separate legacy REST API plugin
is not installed, an exception will be thrown. This will cause
the scheduled action that does the webhook delivery to fail.

* Add warning in the webhooks settings page.

The warning will show if the legacy REST API is not available
and there's at least one plugin that is configured to be delivered
using legacy payloads. Each hook will have an exclamation symbol
next to its name in the table.

Additionally, inside the webhook edition page the legacy payload
option will display "NOT AVAILABLE" instead of "deprecated"
when the legacy REST API is not available.

* Add support for queries containing the "wc-api" variable.

These queries will return a 400 error by default, but
"woocommerce_api_request" and "woocommerce_api_(query)" actions
are available for custom processing.

* Enable the woocommerce_api_enabled option if the Legacy API plugin
is installed and the option is disabled.

Co-authored-by: Corey McKrill <916023+coreymckrill@users.noreply.github.com>

* Mark get_woocommerce_api_url function as deprecated

* Mark $api property in WooCommerce class as deprecated

* Replace call to wc()->api->get_endpoint_data in Checkout class

Replace it with just wc()->cart, which is what the invoked
endpoint was doing in the end.

* Use get_endpoint_data to get the data from /wc/store/v1/cart

---------

Co-authored-by: Corey McKrill <916023+coreymckrill@users.noreply.github.com>
This commit is contained in:
Néstor Soriano 2024-05-21 08:39:03 +02:00 committed by GitHub
parent 929564da82
commit f64aec8cc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 452 additions and 24368 deletions

View File

@ -0,0 +1,4 @@
Significance: major
Type: update
Replace the Legacy REST API with a stub that always returns an error

View File

@ -159,51 +159,23 @@ class WC_Admin_Notices {
self::maybe_add_legacy_api_removal_notice();
}
// phpcs:disable Generic.Commenting.Todo.TaskFound
/**
* Add an admin notice about the removal of the Legacy REST API if the said API is enabled,
* and a notice about soon to be unsupported webhooks with Legacy API payload if at least one of these exist.
*
* TODO: Change this method in WooCommerce 9.0 so that it checks if the Legacy REST API extension is installed, and if not, it points to the extension URL in the WordPress plugins directory.
* Add an admin notice about unsupported webhooks with Legacy API payload if at least one of these exist
* and the Legacy REST API plugin is not installed.
*/
private static function maybe_add_legacy_api_removal_notice() {
if ( is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) {
return;
}
if ( 'yes' === get_option( 'woocommerce_api_enabled' ) ) {
self::add_custom_notice(
'legacy_api_removed_in_woo_90',
sprintf(
'%s%s',
sprintf(
'<h4>%s</h4>',
esc_html__( 'The WooCommerce Legacy REST API will be removed soon', 'woocommerce' )
),
sprintf(
// translators: Placeholders are URLs.
wpautop( __( 'The WooCommerce Legacy REST API, <a href="%1$s">currently enabled in this site</a>, will be removed in WooCommerce 9.0. <a target="_blank" href="%2$s">A separate WooCommerce extension is available</a> to keep it enabled. <b><a target="_blank" href="%3$s">Learn more about this change.</a></b>', 'woocommerce' ) ),
admin_url( 'admin.php?page=wc-settings&tab=advanced&section=legacy_api' ),
'https://wordpress.org/plugins/woocommerce-legacy-rest-api/',
'https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/'
)
)
);
}
if ( wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() > 0 ) {
if ( wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() > 0 && is_null( WC()->api ) ) {
self::add_custom_notice(
'legacy_webhooks_unsupported_in_woo_90',
sprintf(
'%s%s',
sprintf(
'<h4>%s</h4>',
esc_html__( 'WooCommerce webhooks that use the Legacy REST API will be unsupported soon', 'woocommerce' )
esc_html__( 'WooCommerce webhooks that use the Legacy REST API are unsupported', 'woocommerce' )
),
sprintf(
// translators: Placeholders are URLs.
wpautop( __( 'The WooCommerce Legacy REST API will be removed in WooCommerce 9.0, and this will cause <a href="%1$s">webhooks on this site that are configured to use the Legacy REST API</a> to stop working. <a target="_blank" href="%2$s">A separate WooCommerce extension is available</a> to allow these webhooks to keep using the Legacy REST API without interruption. You can also edit these webhooks to use the current REST API version to generate the payload instead. <b><a target="_blank" href="%3$s">Learn more about this change.</a></b>', 'woocommerce' ) ),
wpautop( __( '⚠️ The WooCommerce Legacy REST API has been removed from WooCommerce, this will cause <a href="%1$s">webhooks on this site that are configured to use the Legacy REST API</a> to stop working. <a target="_blank" href="%2$s">A separate WooCommerce extension is available</a> to allow these webhooks to keep using the Legacy REST API without interruption. You can also edit these webhooks to use the current REST API version to generate the payload instead. <b><a target="_blank" href="%3$s">Learn more about this change.</a></b>', 'woocommerce' ) ),
admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&legacy=true' ),
'https://wordpress.org/plugins/woocommerce-legacy-rest-api/',
'https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/'
@ -214,31 +186,14 @@ class WC_Admin_Notices {
}
/**
* Remove the admin notice about the removal of the Legacy REST API if the said API is disabled
* or if the Legacy REST API extension is installed, and remove the notice about Legacy webhooks
* if no such webhooks exist anymore or if the Legacy REST API extension is installed.
*
* TODO: Change this method in WooCommerce 9.0 so that the notice get removed if the Legacy REST API extension is installed and active.
* Remove the admin notice about the unsupported webhooks if the Legacy REST API plugin is installed.
*/
private static function maybe_remove_legacy_api_removal_notice() {
$plugin_is_active = is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' );
if ( self::has_notice( 'legacy_api_removed_in_woo_90' ) && ( $plugin_is_active || 'yes' !== get_option( 'woocommerce_api_enabled' ) ) ) {
self::remove_notice( 'legacy_api_removed_in_woo_90' );
}
if ( self::has_notice( 'legacy_webhooks_unsupported_in_woo_90' ) && ( $plugin_is_active || 0 === wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() ) ) {
if ( self::has_notice( 'legacy_webhooks_unsupported_in_woo_90' ) && ( ! is_null( WC()->api ) || 0 === wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() ) ) {
self::remove_notice( 'legacy_webhooks_unsupported_in_woo_90' );
}
if ( self::has_notice( 'legacy_rest_api_is_incompatible_with_hpos' ) &&
! ( 'yes' === get_option( 'woocommerce_api_enabled' ) && 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) ) ) {
self::remove_notice( 'legacy_rest_api_is_incompatible_with_hpos' );
}
}
// phpcs:enable Generic.Commenting.Todo.TaskFound
/**
* Show a notice.
*

View File

@ -75,7 +75,13 @@ class WC_Admin_Webhooks_Table_List extends WP_List_Table {
$output = '';
// Title.
$output .= '<strong><a href="' . esc_url( $edit_link ) . '" class="row-title">' . esc_html( $webhook->get_name() ) . '</a></strong>';
$warning_prefix =
is_null( wc()->api ) && $this->uses_legacy_rest_api( $webhook ) ?
sprintf(
"<span title='%s'>⚠️</span> ",
esc_html__( 'This webhook is configured to be delivered using the Legacy REST API, but the Legacy REST API plugin is not installed on this site.', 'woocommerce' )
) : '';
$output .= '<strong>' . $warning_prefix . '<a href="' . esc_url( $edit_link ) . '" class="row-title">' . esc_html( $webhook->get_name() ) . '</a></strong>';
// Get actions.
$actions = array(
@ -340,4 +346,25 @@ class WC_Admin_Webhooks_Table_List extends WP_List_Table {
)
);
}
/**
* Get how many of the existing webhooks are configured to use the legacy payload format.
*
* @since 9.0.0
*
* @return int Count of existing webhooks are configured to use the legacy payload format.
*/
public function get_legacy_api_webhooks_count() {
return count( array_filter( $this->items, array( $this, 'uses_legacy_rest_api' ) ) );
}
/**
* Check if a given webhook is configured to use the legacy payload format.
*
* @param WC_Webhook $webhook Webhook object.
* @return bool True if the webhook is configured to use the legacy payload format.
*/
private function uses_legacy_rest_api( $webhook ) {
return 0 === strpos( $webhook->get_api_version(), 'legacy' );
}
}

View File

@ -302,7 +302,7 @@ class WC_Admin_Webhooks {
$num_webhooks = $data_store->get_count_webhooks_by_status();
$count = array_sum( $num_webhooks );
if ( 0 < $count ) {
if ( $count > 0 ) {
$webhooks_table_list->process_bulk_action();
$webhooks_table_list->prepare_items();
@ -310,6 +310,8 @@ class WC_Admin_Webhooks {
echo '<input type="hidden" name="tab" value="advanced" />';
echo '<input type="hidden" name="section" value="webhooks" />';
self::maybe_display_legacy_rest_api_warning();
$webhooks_table_list->views();
$webhooks_table_list->search_box( __( 'Search webhooks', 'woocommerce' ), 'webhook' );
$webhooks_table_list->display();
@ -323,6 +325,81 @@ class WC_Admin_Webhooks {
}
}
/**
* Display a warning message if the Legacy REST API extension is not installed
* and there are webhooks configured to use the legacy payload format.
*/
private static function maybe_display_legacy_rest_api_warning() {
global $webhooks_table_list;
if ( ! is_null( wc()->api ) ) {
return;
}
$legacy_api_webhooks_count = $webhooks_table_list->get_legacy_api_webhooks_count();
if ( 0 === $legacy_api_webhooks_count ) {
return;
}
?>
<div class='error inline'>
<p><strong>
<?php echo esc_html__( 'Incompatible webhooks warning', 'woocommerce' ); ?>
</strong></p>
<p>
<?php
echo wp_kses_data(
sprintf(
/* translators: %s = webhooks count */
_n(
"There's %d webhook that is configured to be delivered using the Legacy REST API, which has been removed from WooCommerce. This webhook will fail to be sent.",
'There are %d webhooks that are configured to be delivered using the Legacy REST API, which has been removed from WooCommerce. These webhooks will fail to be sent.',
$legacy_api_webhooks_count,
'woocommerce'
),
$legacy_api_webhooks_count,
'woocommerce'
)
);
?>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %s = URL */
_n(
'This webhook has the ⚠️ symbol in front of its name in the list below. Please either edit the webhook to use a different delivery format, or install and activate <a href="%s" target="_blank">the WooCommerce Legacy REST API extension</a>.',
'These webhooks have the ⚠️ symbol in front of their names in the list below. Please either edit the webhooks to use a different delivery format, or install and activate <a href="%s" target="_blank">the WooCommerce Legacy REST API extension</a>.',
$legacy_api_webhooks_count,
'woocommerce'
),
'https://wordpress.org/plugins/woocommerce-legacy-rest-api/'
),
array(
'a' => array(
'href' => array(),
'target' => array(),
),
)
);
?>
</p>
<p><strong>
<?php
echo wp_kses_data(
sprintf(
/* translators: %s is an URL */
__( "<a href='%s'>More information</a>", 'woocommerce' ),
'https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/'
)
);
?>
</strong></p>
</div>
<?php
}
/**
* Logs output.
*

View File

@ -385,26 +385,20 @@ class WC_Settings_Advanced extends WC_Settings_Page {
* @return array
*/
protected function get_settings_for_legacy_api_section() {
$enable_legacy_api_setting = array(
'title' => __( 'Legacy API', 'woocommerce' ),
'desc' => __( 'Enable the legacy REST API', 'woocommerce' ),
'id' => 'woocommerce_api_enabled',
'type' => 'checkbox',
'default' => 'no',
);
$legacy_api_setting_desc =
'yes' === get_option( 'woocommerce_api_enabled' ) ?
__( 'The legacy REST API is enabled', 'woocommerce' ) :
__( 'The legacy REST API is NOT enabled', 'woocommerce' );
if ( ! is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) {
$enable_legacy_api_setting['desc_tip'] = sprintf(
// translators: Placeholders are URLs.
__(
'⚠️ <b>The Legacy REST API will be removed in WooCommerce 9.0.</b> <a target="_blank" href="%1$s">A separate WooCommerce extension is available</a> to keep it enabled. You can check Legacy REST API usages in <b><a target="_blank" href="%2$s">the WooCommerce log files</a></b> (file names start with <code>legacy_rest_api_usages</code>). <b><a target="_blank" href="%3$s">Learn more about this change.</a></b>',
'woocommerce'
),
$legacy_api_setting_tip =
is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ?
__( ' The WooCommerce Legacy REST API extension is installed and active.', 'woocommerce' ) :
sprintf(
/* translators: placeholders are URLs */
__( '⚠️ The WooCommerce Legacy REST API has been moved to <a target=”_blank” href="%1$s">a dedicated extension</a>. <b><a target=”_blank” href="%2$s">Learn more about this change</a></b>', 'woocommerce' ),
'https://wordpress.org/plugins/woocommerce-legacy-rest-api/',
admin_url( 'admin.php?page=wc-status&tab=logs' ),
'https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/'
);
}
$settings =
array(
@ -414,7 +408,15 @@ class WC_Settings_Advanced extends WC_Settings_Page {
'desc' => '',
'id' => 'legacy_api_options',
),
$enable_legacy_api_setting,
array(
'title' => __( 'Legacy API', 'woocommerce' ),
'desc' => $legacy_api_setting_desc,
'id' => 'woocommerce_api_enabled',
'type' => 'checkbox',
'default' => 'no',
'disabled' => true,
'desc_tip' => $legacy_api_setting_tip,
),
array(
'type' => 'sectionend',
'id' => 'legacy_api_options',

View File

@ -145,7 +145,13 @@ if ( ! defined( 'ABSPATH' ) ) {
?>
</option>
<?php endforeach; ?>
<option value="legacy_v3" <?php selected( 'legacy_v3', $webhook->get_api_version(), true ); ?>><?php esc_html_e( 'Legacy API v3 (deprecated)', 'woocommerce' ); ?></option>
<?php
$legacy_api_option_name =
is_null( wc()->api ) ?
__( 'Legacy API v3 (⚠️ NOT AVAILABLE)', 'woocommerce' ) :
__( 'Legacy API v3 (deprecated)', 'woocommerce' );
?>
<option value="legacy_v3" <?php selected( 'legacy_v3', $webhook->get_api_version(), true ); ?>><?php echo esc_html( $legacy_api_option_name ); ?></option>
</select>
</td>
</tr>

View File

@ -6,12 +6,13 @@
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\RestApiUtil;
defined( 'ABSPATH' ) || exit;
global $wpdb;
$report = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
$report = wc_get_container()->get( RestApiUtil::class )->get_endpoint_data( '/wc/v3/system_status' );
$environment = $report['environment'];
$database = $report['database'];
$post_type_counts = isset( $report['post_type_counts'] ) ? $report['post_type_counts'] : array();
@ -29,18 +30,18 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, Cons
$active_plugins_count = is_countable( $active_plugins ) ? count( $active_plugins ) : 0;
$inactive_plugins_count = is_countable( $inactive_plugins ) ? count( $inactive_plugins ) : 0;
// Include necessary WordPress file to use get_plugin_data()
// Include necessary WordPress file to use get_plugin_data().
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// Define the path to the main WooCommerce plugin file using the correct constant
// Define the path to the main WooCommerce plugin file using the correct constant.
$plugin_path = WP_PLUGIN_DIR . '/woocommerce/woocommerce.php';
// Initialize the WooCommerce version variable
// Initialize the WooCommerce version variable.
$wc_version = '';
// Check if the plugin file exists before trying to access it
if (file_exists($plugin_path)) {
$plugin_data = get_plugin_data($plugin_path);
$wc_version = $plugin_data["Version"] ?? ''; // Use null coalescing operator to handle undefined index
// Check if the plugin file exists before trying to access it.
if ( file_exists( $plugin_path ) ) {
$plugin_data = get_plugin_data( $plugin_path );
$wc_version = $plugin_data['Version'] ?? ''; // Use null coalescing operator to handle undefined index.
}
?>
@ -92,20 +93,20 @@ if (file_exists($plugin_path)) {
<tr>
<td data-export-label="WC Version"><?php esc_html_e( 'WooCommerce version', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WooCommerce installed on your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
<td><?php echo esc_html( !empty($wc_version) ? $wc_version : $environment['version'] ); ?></td>
<td><?php echo esc_html( ! empty( $wc_version ) ? $wc_version : $environment['version'] ); ?></td>
</tr>
<tr>
<td data-export-label="REST API Version"><?php esc_html_e( 'WooCommerce REST API package', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce REST API package running on your site.', 'woocommerce' ) ); ?></td>
<td data-export-label="Legacy REST API Package Version"><?php esc_html_e( 'WooCommerce Legacy REST API package', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Legacy REST API plugin running on this site.', 'woocommerce' ) ); ?></td>
<td>
<?php
$version = wc()->api->get_rest_api_package_version();
if ( ! is_null( $version ) ) {
echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( wc()->api->get_rest_api_package_path() ) . '</code></mark> ';
if ( is_null( wc()->api ) ) {
echo '<mark class="info-icon"><span class="dashicons dashicons-info"></span> ' . esc_html__( 'The Legacy REST API plugin is not installed on this site.', 'woocommerce' ) . '</mark>';
} else {
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the REST API package.', 'woocommerce' ) . '</mark>';
$plugin_path = wc_get_container()->get( \Automattic\WooCommerce\Utilities\PluginUtil::class )->get_wp_plugin_id( 'woocommerce-legacy-rest-api' );
$version = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_path )['Version'] ?? '';
echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( wc()->api->get_rest_api_package_path() ) . '</code></mark> ';
}
?>
</td>
@ -420,7 +421,12 @@ if (file_exists($plugin_path)) {
</td>
</tr>
<?php
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
/**
* Filters the environment rows to show in the WooCommerce status report.
*/
$rows = apply_filters( 'woocommerce_system_status_environment_rows', array() );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceVersionComment
foreach ( $rows as $row ) {
if ( ! empty( $row['success'] ) ) {
$css_class = 'yes';
@ -860,14 +866,12 @@ if ( 0 < $mu_plugins_count ) :
/* Translators: %s: docs link. */
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'Page visibility should be <a href="%s" target="_blank">public</a>', 'woocommerce' ), 'https://wordpress.org/support/article/content-visibility/' ) ) . '</mark>';
$found_error = true;
} else {
} elseif ( $_page['shortcode_required'] || $_page['block_required'] ) {
// Shortcode and block check.
if ( $_page['shortcode_required'] || $_page['block_required'] ) {
if ( ! $_page['shortcode_present'] && ! $_page['block_present'] ) {
/* Translators: %1$s: shortcode text, %2$s: block slug. */
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
if ( ! $_page['shortcode_present'] && ! $_page['block_present'] ) {
/* Translators: %1$s: shortcode text, %2$s: block slug. */
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
}
@ -1034,7 +1038,14 @@ if ( 0 < $mu_plugins_count ) :
</tbody>
</table>
<?php do_action( 'woocommerce_system_status_report' ); ?>
<?php
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
/**
* Action fired when the WooCommerce system status report is rendered.
*/
do_action( 'woocommerce_system_status_report' );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
?>
<table class="wc_status_table widefat" cellspacing="0">
<thead>

View File

@ -1,185 +0,0 @@
<?php
/**
* WC-API endpoint handler.
*
* This handles API related functionality in WooCommerce.
* - wc-api endpoint - Commonly used by Payment gateways for callbacks.
* - Legacy REST API - Deprecated in 2.6.0. @see class-wc-legacy-api.php
* - WP REST API - The main REST API in WooCommerce which is built on top of the WP REST API.
*
* @package WooCommerce\RestApi
* @since 2.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_API class.
*/
class WC_API extends WC_Legacy_API {
/**
* Init the API by setting up action and filter hooks.
*/
public function init() {
parent::init();
add_action( 'init', array( $this, 'add_endpoint' ), 0 );
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 );
add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) );
}
/**
* Get the version of the REST API package being ran. Since API package was merged into core, this now follows WC version.
*
* @since 3.7.0
* @return string|null
*/
public function get_rest_api_package_version() {
if ( ! $this->is_rest_api_loaded() ) {
return null;
}
if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) {
$path = \Automattic\WooCommerce\RestApi\Server::get_path();
if ( 0 === strpos( $path, __DIR__ ) ) {
// We are loading API from included version.
return WC()->version;
}
}
// We are loading API from external plugin.
return \Automattic\WooCommerce\RestApi\Package::get_version();
}
/**
* Get the version of the REST API package being ran.
*
* @since 3.7.0
* @return string
*/
public function get_rest_api_package_path() {
if ( ! $this->is_rest_api_loaded() ) {
return null;
}
if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) {
// We are loading API from included version.
return \Automattic\WooCommerce\RestApi\Server::get_path();
}
// We are loading API from external plugin.
return \Automattic\WooCommerce\RestApi\Package::get_path();
}
/**
* Return if the rest API classes were already loaded.
*
* @since 3.7.0
* @return boolean
*/
protected function is_rest_api_loaded() {
return class_exists( '\Automattic\WooCommerce\RestApi\Server', false );
}
/**
* Get data from a WooCommerce API endpoint.
*
* @since 3.7.0
* @param string $endpoint Endpoint.
* @param array $params Params to pass with request.
* @return array|\WP_Error
*/
public function get_endpoint_data( $endpoint, $params = array() ) {
if ( ! $this->is_rest_api_loaded() ) {
return new WP_Error( 'rest_api_unavailable', __( 'The Rest API is unavailable.', 'woocommerce' ) );
}
$request = new \WP_REST_Request( 'GET', $endpoint );
if ( $params ) {
$request->set_query_params( $params );
}
$response = rest_do_request( $request );
$server = rest_get_server();
$json = wp_json_encode( $server->response_to_data( $response, false ) );
return json_decode( $json, true );
}
/**
* Add new query vars.
*
* @since 2.0
* @param array $vars Query vars.
* @return string[]
*/
public function add_query_vars( $vars ) {
$vars = parent::add_query_vars( $vars );
$vars[] = 'wc-api';
return $vars;
}
/**
* WC API for payment gateway IPNs, etc.
*
* @since 2.0
*/
public static function add_endpoint() {
parent::add_endpoint();
add_rewrite_endpoint( 'wc-api', EP_ALL );
}
/**
* API request - Trigger any API requests.
*
* @since 2.0
* @version 2.4
*/
public function handle_api_requests() {
global $wp;
if ( ! empty( $_GET['wc-api'] ) ) { // WPCS: input var okay, CSRF ok.
$wp->query_vars['wc-api'] = sanitize_key( wp_unslash( $_GET['wc-api'] ) ); // WPCS: input var okay, CSRF ok.
}
// wc-api endpoint requests.
if ( ! empty( $wp->query_vars['wc-api'] ) ) {
// Buffer, we won't want any output here.
ob_start();
// No cache headers.
wc_nocache_headers();
// Clean the API request.
$api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) );
// Make sure gateways are available for request.
WC()->payment_gateways();
// Trigger generic action before request hook.
do_action( 'woocommerce_api_request', $api_request );
// Is there actually something hooked into this API request? If not trigger 400 - Bad request.
status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 );
// Trigger an action which plugins can hook into to fulfill the request.
do_action( 'woocommerce_api_' . $api_request );
// Done, clear buffer and exit.
ob_end_clean();
die( '-1' );
}
}
/**
* Register WC settings from WP-API to the REST API.
*
* @since 3.0.0
*/
public function register_wp_admin_settings() {
$pages = WC_Admin_Settings::get_settings_pages();
foreach ( $pages as $page ) {
new WC_Register_WP_Admin_Settings( $page, 'page' );
}
$emails = WC_Emails::instance();
foreach ( $emails->get_emails() as $email ) {
new WC_Register_WP_Admin_Settings( $email, 'email' );
}
}
}

View File

@ -463,6 +463,7 @@ class WC_Install {
self::maybe_update_db_version();
self::maybe_set_store_id();
self::maybe_install_legacy_api_plugin();
self::maybe_activate_legacy_api_enabled_option();
delete_transient( 'wc_installing' );
@ -575,7 +576,6 @@ class WC_Install {
WC_Post_types::register_taxonomies();
WC()->query->init_query_vars();
WC()->query->add_endpoints();
WC_API::add_endpoint();
WC_Auth::add_endpoint();
TransientFilesEngine::add_endpoint();
}
@ -1316,6 +1316,17 @@ class WC_Install {
\WC_Admin_Notices::store_notices();
}
/**
* If in a previous version of WooCommerce the Legacy REST API plugin was installed manually but the core Legacy REST API was kept disabled,
* now the Legacy API is still disabled and can't be manually enabled from settings UI (the plugin, which is now in control, won't allow that),
* which is weird and confusing. So we detect this case and explicitly enable it.
*/
private static function maybe_activate_legacy_api_enabled_option() {
if ( ! self::is_new_install() && is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) && 'yes' !== get_option( 'woocommerce_api_enabled' ) ) {
update_option( 'woocommerce_api_enabled', 'yes' );
}
}
/**
* Set up the database tables which the plugin needs to function.
* WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database.

View File

@ -946,7 +946,7 @@ class WC_Tracker {
'base_state' => WC()->countries->get_base_state(),
'base_postcode' => WC()->countries->get_base_postcode(),
'selling_locations' => WC()->countries->get_allowed_countries(),
'api_enabled' => get_option( 'woocommerce_api_enabled' ),
'api_enabled' => get_option( 'woocommerce_api_enabled', 'no' ),
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
'download_method' => get_option( 'woocommerce_file_download_method' ),

View File

@ -13,7 +13,9 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil };
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\RestApiUtil;
use Automattic\WooCommerce\Utilities\LoggingUtil;
defined( 'ABSPATH' ) || exit;
@ -359,57 +361,6 @@ class WC_Webhook extends WC_Legacy_Webhook {
do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
}
/**
* Get Legacy API payload.
*
* @since 3.0.0
* @param string $resource Resource type.
* @param int $resource_id Resource ID.
* @param string $event Event type.
* @return array
*/
private function get_legacy_api_payload( $resource, $resource_id, $event ) {
// Include & load API classes.
WC()->api->includes();
WC()->api->register_resources( new WC_API_Server( '/' ) );
switch ( $resource ) {
case 'coupon':
$payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id );
break;
case 'customer':
$payload = WC()->api->WC_API_Customers->get_customer( $resource_id );
break;
case 'order':
$payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) );
break;
case 'product':
// Bulk and quick edit action hooks return a product object instead of an ID.
if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
$resource_id = $resource_id->get_id();
}
$payload = WC()->api->WC_API_Products->get_product( $resource_id );
break;
// Custom topics include the first hook argument.
case 'action':
$payload = array(
'action' => current( $this->get_hooks() ),
'arg' => $resource_id,
);
break;
default:
$payload = array();
break;
}
return $payload;
}
/**
* Get WP API integration payload.
*
@ -431,7 +382,7 @@ class WC_Webhook extends WC_Legacy_Webhook {
}
$version = str_replace( 'wp_api_', '', $this->get_api_version() );
$payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" );
$payload = wc_get_container()->get( RestApiUtil::class )->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" );
break;
// Custom topics include the first hook argument.
@ -453,9 +404,10 @@ class WC_Webhook extends WC_Legacy_Webhook {
/**
* Build the payload data for the webhook.
*
* @since 2.2.0
* @param mixed $resource_id First hook argument, typically the resource ID.
* @param mixed $resource_id First hook argument, typically the resource ID.
* @return mixed Payload data.
* @throws \Exception The webhook is configured to use the Legacy REST API, but the Legacy REST API plugin is not available.
* @since 2.2.0
*/
public function build_payload( $resource_id ) {
// Build the payload with the same user context as the user who created
@ -472,12 +424,13 @@ class WC_Webhook extends WC_Legacy_Webhook {
$payload = array(
'id' => $resource_id,
);
} else {
if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
} elseif ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
$payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
} else {
$payload = $this->get_legacy_api_payload( $resource, $resource_id, $event );
} else {
if ( is_null( wc()->api ) ) {
throw new \Exception( 'The Legacy REST API plugin is not installed on this site. More information: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ ' );
}
$payload = wc()->api->get_webhook_api_payload( $resource, $resource_id, $event );
}
// Restore the current user.
@ -783,7 +736,7 @@ class WC_Webhook extends WC_Legacy_Webhook {
public function get_api_version( $context = 'view' ) {
$version = $this->get_prop( 'api_version', $context );
return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3';
return $version > 0 ? 'wp_api_v' . $version : 'legacy_v3';
}
/**

View File

@ -22,6 +22,8 @@ use Automattic\WooCommerce\Internal\ProductImage\MatchImageBySKU;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;
use Automattic\WooCommerce\Internal\Settings\OptionSanitizer;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub;
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Internal\Admin\Marketplace;
use Automattic\WooCommerce\Proxies\LegacyProxy;
@ -36,6 +38,8 @@ use Automattic\WooCommerce\Admin\Features\Features;
*/
final class WooCommerce {
use AccessiblePrivateMethods;
/**
* WooCommerce version.
*
@ -77,6 +81,8 @@ final class WooCommerce {
/**
* API instance
*
* @deprecated 9.0.0 The Legacy REST API has been removed from WooCommerce core. This property will be null unless the WooCommerce Legacy REST API plugin is installed.
*
* @var WC_API
*/
public $api;
@ -254,6 +260,7 @@ final class WooCommerce {
add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) );
add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) );
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) );
self::add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) );
add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_remote_variant' ) );
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_remote_variant' ) );
@ -619,8 +626,6 @@ final class WooCommerce {
/**
* REST API.
*/
include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php';
include_once WC_ABSPATH . 'includes/class-wc-api.php';
include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php';
include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php';
include_once WC_ABSPATH . 'includes/class-wc-auth.php';
@ -666,8 +671,8 @@ final class WooCommerce {
$this->theme_support_includes();
$this->query = new WC_Query();
$this->api = new WC_API();
$this->api->init();
LegacyRestApiStub::setup();
}
/**
@ -1172,6 +1177,25 @@ final class WooCommerce {
return wc_get_container()->get( LegacyProxy::class )->get_global( $global_name );
}
/**
* Register WC settings from WP-API to the REST API.
*
* This method used to be part of the now removed Legacy REST API.
*
* @since 9.0.0
*/
private function register_wp_admin_settings() {
$pages = WC_Admin_Settings::get_settings_pages();
foreach ( $pages as $page ) {
new WC_Register_WP_Admin_Settings( $page, 'page' );
}
$emails = WC_Emails::instance();
foreach ( $emails->get_emails() as $email ) {
new WC_Register_WP_Admin_Settings( $email, 'email' );
}
}
/**
* Converts the WooCommerce slug to the correct slug for the current version.
* This ensures that when the plugin is installed in a different folder name, the correct slug is used so that dependent plugins can be installed/activated.

View File

@ -1,164 +0,0 @@
<?php
/**
* REST API Legacy Coupons controller
*
* Handles requests to the /coupons endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Legacy Coupons controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller {
/**
* Query args.
*
* @deprecated 3.0.0
*
* @param array $args Query args
* @param WP_REST_Request $request Request data.
* @return array
*/
public function query_args( $args, $request ) {
if ( ! empty( $request['code'] ) ) {
$id = wc_get_coupon_id_by_code( $request['code'] );
$args['post__in'] = array( $id );
}
return $args;
}
/**
* Prepare a single coupon output for response.
*
* @deprecated 3.0.0
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $data
*/
public function prepare_item_for_response( $post, $request ) {
$coupon = new WC_Coupon( (int) $post->ID );
$data = $coupon->get_data();
$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
$format_date = array( 'date_created', 'date_modified', 'date_expires' );
$format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
}
// Format date values.
foreach ( $format_date as $key ) {
$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null;
}
// Format null values.
foreach ( $format_null as $key ) {
$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $post, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Prepare a single coupon for create or update.
*
* @deprecated 3.0.0
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|stdClass $data Post object.
*/
protected function prepare_item_for_database( $request ) {
global $wpdb;
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$coupon = new WC_Coupon( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Validate required POST fields.
if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
if ( empty( $request['code'] ) ) {
return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
}
}
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'code' :
$coupon_code = wc_format_coupon_code( $value );
$id = $coupon->get_id() ? $coupon->get_id() : 0;
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
}
$coupon->set_code( $coupon_code );
break;
case 'meta_data' :
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
case 'description' :
$coupon->set_description( wp_filter_post_kses( $value ) );
break;
default :
if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
$coupon->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filter the query_vars used in `get_items` for the constructed query.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for insertion.
*
* @param WC_Coupon $coupon The coupon object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
}
}

View File

@ -1,306 +0,0 @@
<?php
/**
* REST API Legacy Orders controller
*
* Handles requests to the /orders endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Legacy Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Query args.
*
* @deprecated 3.0
*
* @param array $args
* @param WP_REST_Request $request
* @return array
*/
public function query_args( $args, $request ) {
global $wpdb;
// Set post_status.
if ( 'any' !== $request['status'] ) {
$args['post_status'] = 'wc-' . $request['status'];
} else {
$args['post_status'] = 'any';
}
if ( ! empty( $request['customer'] ) ) {
if ( ! empty( $args['meta_query'] ) ) {
$args['meta_query'] = array();
}
$args['meta_query'][] = array(
'key' => '_customer_user',
'value' => $request['customer'],
'type' => 'NUMERIC',
);
}
// Search by product.
if ( ! empty( $request['product'] ) ) {
$order_ids = $wpdb->get_col( $wpdb->prepare( "
SELECT order_id
FROM {$wpdb->prefix}woocommerce_order_items
WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
AND order_item_type = 'line_item'
", $request['product'] ) );
// Force WP_Query return empty if don't found any order.
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
$args['post__in'] = $order_ids;
}
// Search.
if ( ! empty( $args['s'] ) ) {
$order_ids = wc_order_search( $args['s'] );
if ( ! empty( $order_ids ) ) {
unset( $args['s'] );
$args['post__in'] = array_merge( $order_ids, array( 0 ) );
}
}
return $args;
}
/**
* Prepare a single order output for response.
*
* @deprecated 3.0
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $data
*/
public function prepare_item_for_response( $post, $request ) {
$this->request = $request;
$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
$statuses = wc_get_order_statuses();
$order = wc_get_order( $post );
$data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() );
$format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
$format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
}
// Format date values.
foreach ( $format_date as $key ) {
$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false;
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
// Format line items.
foreach ( $format_line_items as $key ) {
$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
}
// Refunds.
$data['refunds'] = array();
foreach ( $order->get_refunds() as $refund ) {
$data['refunds'][] = array(
'id' => $refund->get_id(),
'refund' => $refund->get_reason() ? $refund->get_reason() : '',
'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $order, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Prepare a single order for create.
*
* @deprecated 3.0
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|WC_Order $data Object.
*/
protected function prepare_item_for_database( $request ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$order = new WC_Order( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Handle all writable props
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'billing' :
case 'shipping' :
$this->update_address( $order, $value, $key );
break;
case 'line_items' :
case 'shipping_lines' :
case 'fee_lines' :
case 'coupon_lines' :
if ( is_array( $value ) ) {
foreach ( $value as $item ) {
if ( is_array( $item ) ) {
if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
$order->remove_item( $item['id'] );
} else {
$this->set_item( $order, $key, $item );
}
}
}
}
break;
case 'meta_data' :
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
default :
if ( is_callable( array( $order, "set_{$key}" ) ) ) {
$order->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filter the data for the insert.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WC_Order $order The Order object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
}
/**
* Create base WC Order object.
*
* @deprecated 3.0.0
*
* @param array $data
* @return WC_Order
*/
protected function create_base_order( $data ) {
return wc_create_order( $data );
}
/**
* Create order.
*
* @deprecated 3.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return int|WP_Error
*/
protected function create_order( $request ) {
try {
// Make sure customer exists.
if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
}
// Make sure customer is part of blog.
if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
}
$order = $this->prepare_item_for_database( $request );
$order->set_created_via( 'rest-api' );
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
$order->calculate_totals();
$order->save();
// Handle set paid.
if ( true === $request['set_paid'] ) {
$order->payment_complete( $request['transaction_id'] );
}
return $order->get_id();
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Update order.
*
* @deprecated 3.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return int|WP_Error
*/
protected function update_order( $request ) {
try {
$order = $this->prepare_item_for_database( $request );
$order->save();
// Handle set paid.
if ( $order->needs_payment() && true === $request['set_paid'] ) {
$order->payment_complete( $request['transaction_id'] );
}
// If items have changed, recalculate order totals.
if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
$order->calculate_totals();
}
return $order->get_id();
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,804 +0,0 @@
<?php
/**
* REST API Legacy Products controller
*
* Handles requests to the /products endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Legacy Products controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Query args.
*
* @deprecated 3.0.0
*
* @param array $args Request args.
* @param WP_REST_Request $request Request data.
* @return array
*/
public function query_args( $args, $request ) {
// Set post_status.
$args['post_status'] = $request['status'];
// Taxonomy query to filter products by type, category,
// tag, shipping class, and attribute.
$tax_query = array();
// Map between taxonomy name and arg's key.
$taxonomies = array(
'product_cat' => 'category',
'product_tag' => 'tag',
'product_shipping_class' => 'shipping_class',
);
// Set tax_query for each passed arg.
foreach ( $taxonomies as $taxonomy => $key ) {
if ( ! empty( $request[ $key ] ) ) {
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $request[ $key ],
);
}
}
// Filter product type by slug.
if ( ! empty( $request['type'] ) ) {
$tax_query[] = array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $request['type'],
);
}
// Filter by attribute and term.
if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
$tax_query[] = array(
'taxonomy' => $request['attribute'],
'field' => 'term_id',
'terms' => $request['attribute_term'],
);
}
}
if ( ! empty( $tax_query ) ) {
$args['tax_query'] = $tax_query;
}
// Filter featured.
if ( is_bool( $request['featured'] ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'featured',
'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
);
}
// Filter by sku.
if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $request['sku'];
}
$args['meta_query'] = $this->add_meta_query( $args, array(
'key' => '_sku',
'value' => $skus,
'compare' => 'IN',
) );
}
// Filter by tax class.
if ( ! empty( $request['tax_class'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, array(
'key' => '_tax_class',
'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
) );
}
// Price filter.
if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );
}
// Filter product in stock or out of stock.
if ( is_bool( $request['in_stock'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, array(
'key' => '_stock_status',
'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
) );
}
// Filter by on sale products.
if ( is_bool( $request['on_sale'] ) ) {
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
$args[ $on_sale_key ] += wc_get_product_ids_on_sale();
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = $this->post_type;
}
return $args;
}
/**
* Prepare a single product output for response.
*
* @deprecated 3.0.0
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $post, $request ) {
$product = wc_get_product( $post );
$data = $this->get_product_data( $product );
// Add variations to variable products.
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
$data['variations'] = $product->get_children();
}
// Add grouped products data.
if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
$data['grouped_products'] = $product->get_children();
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $product, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Get product menu order.
*
* @deprecated 3.0.0
* @param WC_Product $product Product instance.
* @return int
*/
protected function get_product_menu_order( $product ) {
return $product->get_menu_order();
}
/**
* Save product meta.
*
* @deprecated 3.0.0
* @param WC_Product $product
* @param WP_REST_Request $request
* @return bool
* @throws WC_REST_Exception
*/
protected function save_product_meta( $product, $request ) {
$product = $this->set_product_meta( $product, $request );
$product->save();
return true;
}
/**
* Set product meta.
*
* @deprecated 3.0.0
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
* @return WC_Product
*/
protected function set_product_meta( $product, $request ) {
// Virtual.
if ( isset( $request['virtual'] ) ) {
$product->set_virtual( $request['virtual'] );
}
// Tax status.
if ( isset( $request['tax_status'] ) ) {
$product->set_tax_status( $request['tax_status'] );
}
// Tax Class.
if ( isset( $request['tax_class'] ) ) {
$product->set_tax_class( $request['tax_class'] );
}
// Catalog Visibility.
if ( isset( $request['catalog_visibility'] ) ) {
$product->set_catalog_visibility( $request['catalog_visibility'] );
}
// Purchase Note.
if ( isset( $request['purchase_note'] ) ) {
$product->set_purchase_note( wc_clean( $request['purchase_note'] ) );
}
// Featured Product.
if ( isset( $request['featured'] ) ) {
$product->set_featured( $request['featured'] );
}
// Shipping data.
$product = $this->save_product_shipping_data( $product, $request );
// SKU.
if ( isset( $request['sku'] ) ) {
$product->set_sku( wc_clean( $request['sku'] ) );
}
// Attributes.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = wc_clean( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( $attribute_id ) {
if ( isset( $attribute['options'] ) ) {
$options = $attribute['options'];
if ( ! is_array( $attribute['options'] ) ) {
// Text based attributes - Posted values are term names.
$options = explode( WC_DELIMITER, $options );
}
$values = array_map( 'wc_sanitize_term_text_based', $options );
$values = array_filter( $values, 'strlen' );
} else {
$values = array();
}
if ( ! empty( $values ) ) {
// Add attribute to array, but don't set values.
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_id( $attribute_id );
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
} elseif ( isset( $attribute['options'] ) ) {
// Custom attribute - Add attribute to array and set the values.
if ( is_array( $attribute['options'] ) ) {
$values = $attribute['options'];
} else {
$values = explode( WC_DELIMITER, $attribute['options'] );
}
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
}
$product->set_attributes( $attributes );
}
// Sales and prices.
if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
$product->set_regular_price( '' );
$product->set_sale_price( '' );
$product->set_date_on_sale_to( '' );
$product->set_date_on_sale_from( '' );
$product->set_price( '' );
} else {
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$product->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$product->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to'] );
}
}
// Product parent ID for groups.
if ( isset( $request['parent_id'] ) ) {
$product->set_parent_id( $request['parent_id'] );
}
// Sold individually.
if ( isset( $request['sold_individually'] ) ) {
$product->set_sold_individually( $request['sold_individually'] );
}
// Stock status.
if ( isset( $request['in_stock'] ) ) {
$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
} else {
$stock_status = $product->get_stock_status();
}
// Stock data.
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
// Manage stock.
if ( isset( $request['manage_stock'] ) ) {
$product->set_manage_stock( $request['manage_stock'] );
}
// Backorders.
if ( isset( $request['backorders'] ) ) {
$product->set_backorders( $request['backorders'] );
}
if ( $product->is_type( 'grouped' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
} elseif ( $product->is_type( 'external' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( 'instock' );
} elseif ( $product->get_manage_stock() ) {
// Stock status is always determined by children so sync later.
if ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Stock quantity.
if ( isset( $request['stock_quantity'] ) ) {
$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $product->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
}
} else {
// Don't manage stock.
$product->set_manage_stock( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
}
} elseif ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Upsells.
if ( isset( $request['upsell_ids'] ) ) {
$upsells = array();
$ids = $request['upsell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$upsells[] = $id;
}
}
}
$product->set_upsell_ids( $upsells );
}
// Cross sells.
if ( isset( $request['cross_sell_ids'] ) ) {
$crosssells = array();
$ids = $request['cross_sell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$crosssells[] = $id;
}
}
}
$product->set_cross_sell_ids( $crosssells );
}
// Product categories.
if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['categories'] );
}
// Product tags.
if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
}
// Downloadable.
if ( isset( $request['downloadable'] ) ) {
$product->set_downloadable( $request['downloadable'] );
}
// Downloadable options.
if ( $product->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$product = $this->save_downloadable_files( $product, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$product->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$product->set_download_expiry( $request['download_expiry'] );
}
}
// Product url and button text for external products.
if ( $product->is_type( 'external' ) ) {
if ( isset( $request['external_url'] ) ) {
$product->set_product_url( $request['external_url'] );
}
if ( isset( $request['button_text'] ) ) {
$product->set_button_text( $request['button_text'] );
}
}
// Save default attributes for variable products.
if ( $product->is_type( 'variable' ) ) {
$product = $this->save_default_attributes( $product, $request );
}
return $product;
}
/**
* Save variations.
*
* @deprecated 3.0.0
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
* @return bool
*/
protected function save_variations_data( $product, $request ) {
foreach ( $request['variations'] as $menu_order => $data ) {
$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );
// Create initial name and status.
if ( ! $variation->get_slug() ) {
/* translators: 1: variation id 2: product name */
$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
}
// Parent ID.
$variation->set_parent_id( $product->get_id() );
// Menu order.
$variation->set_menu_order( $menu_order );
// Status.
if ( isset( $data['visible'] ) ) {
$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
}
// SKU.
if ( isset( $data['sku'] ) ) {
$variation->set_sku( wc_clean( $data['sku'] ) );
}
// Thumbnail.
if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
$image = $data['image'];
$image = current( $image );
if ( is_array( $image ) ) {
$image['position'] = 0;
}
$variation = $this->set_product_images( $variation, array( $image ) );
}
// Virtual variation.
if ( isset( $data['virtual'] ) ) {
$variation->set_virtual( $data['virtual'] );
}
// Downloadable variation.
if ( isset( $data['downloadable'] ) ) {
$variation->set_downloadable( $data['downloadable'] );
}
// Downloads.
if ( $variation->get_downloadable() ) {
// Downloadable files.
if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
}
// Download limit.
if ( isset( $data['download_limit'] ) ) {
$variation->set_download_limit( $data['download_limit'] );
}
// Download expiry.
if ( isset( $data['download_expiry'] ) ) {
$variation->set_download_expiry( $data['download_expiry'] );
}
}
// Shipping data.
$variation = $this->save_product_shipping_data( $variation, $data );
// Stock handling.
if ( isset( $data['manage_stock'] ) ) {
$variation->set_manage_stock( $data['manage_stock'] );
}
if ( isset( $data['in_stock'] ) ) {
$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
}
if ( isset( $data['backorders'] ) ) {
$variation->set_backorders( $data['backorders'] );
}
if ( $variation->get_manage_stock() ) {
if ( isset( $data['stock_quantity'] ) ) {
$variation->set_stock_quantity( $data['stock_quantity'] );
} elseif ( isset( $data['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
$variation->set_stock_quantity( $stock_quantity );
}
} else {
$variation->set_backorders( 'no' );
$variation->set_stock_quantity( '' );
}
// Regular Price.
if ( isset( $data['regular_price'] ) ) {
$variation->set_regular_price( $data['regular_price'] );
}
// Sale Price.
if ( isset( $data['sale_price'] ) ) {
$variation->set_sale_price( $data['sale_price'] );
}
if ( isset( $data['date_on_sale_from'] ) ) {
$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
}
if ( isset( $data['date_on_sale_to'] ) ) {
$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
}
// Tax class.
if ( isset( $data['tax_class'] ) ) {
$variation->set_tax_class( $data['tax_class'] );
}
// Description.
if ( isset( $data['description'] ) ) {
$variation->set_description( wp_kses_post( $data['description'] ) );
}
// Update taxonomies.
if ( isset( $data['attributes'] ) ) {
$attributes = array();
$parent_attributes = $product->get_attributes();
foreach ( $data['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
continue;
}
$attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $attribute_value, $attribute_name );
if ( $term && ! is_wp_error( $term ) ) {
$attribute_value = $term->slug;
} else {
$attribute_value = sanitize_title( $attribute_value );
}
}
$attributes[ $attribute_key ] = $attribute_value;
}
$variation->set_attributes( $attributes );
}
$variation->save();
do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
}
return true;
}
/**
* Add post meta fields.
*
* @deprecated 3.0.0
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request data.
* @return bool|WP_Error
*/
protected function add_post_meta_fields( $post, $request ) {
return $this->update_post_meta_fields( $post, $request );
}
/**
* Update post meta fields.
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request data.
* @return bool|WP_Error
*/
protected function update_post_meta_fields( $post, $request ) {
$product = wc_get_product( $post );
// Check for featured/gallery images, upload it and set it.
if ( isset( $request['images'] ) ) {
$product = $this->set_product_images( $product, $request['images'] );
}
// Save product meta fields.
$product = $this->set_product_meta( $product, $request );
// Save the product data.
$product->save();
// Save variations.
if ( $product->is_type( 'variable' ) ) {
if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
$this->save_variations_data( $product, $request );
}
}
// Clear caches here so in sync with any new variations/children.
wc_delete_product_transients( $product->get_id() );
wp_cache_delete( 'product-' . $product->get_id(), 'products' );
return true;
}
/**
* Delete post.
*
* @deprecated 3.0.0
*
* @param int|WP_Post $id Post ID or WP_Post instance.
*/
protected function delete_post( $id ) {
if ( ! empty( $id->ID ) ) {
$id = $id->ID;
} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
return;
}
// Delete product attachments.
$attachments = get_posts( array(
'post_parent' => $id,
'post_status' => 'any',
'post_type' => 'attachment',
) );
foreach ( (array) $attachments as $attachment ) {
wp_delete_attachment( $attachment->ID, true );
}
// Delete product.
$product = wc_get_product( $id );
$product->delete( true );
}
/**
* Get post types.
*
* @deprecated 3.0.0
*
* @return array
*/
protected function get_post_types() {
return array( 'product', 'product_variation' );
}
/**
* Save product images.
*
* @deprecated 3.0.0
*
* @param int $product_id
* @param array $images
* @throws WC_REST_Exception
*/
protected function save_product_images( $product_id, $images ) {
$product = wc_get_product( $product_id );
return set_product_images( $product, $images );
}
}

View File

@ -1,410 +0,0 @@
<?php
/**
* WooCommerce API Authentication Class
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1.0
* @version 2.4.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Authentication {
/**
* Setup class
*
* @since 2.1
*/
public function __construct() {
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
}
/**
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
*
* @since 2.1
* @param WP_User $user
* @return null|WP_Error|WP_User
*/
public function authenticate( $user ) {
// Allow access to the index by default
if ( '/' === WC()->api->server->path ) {
return new WP_User( 0 );
}
try {
if ( is_ssl() ) {
$keys = $this->perform_ssl_authentication();
} else {
$keys = $this->perform_oauth_authentication();
}
// Check API key-specific permission
$this->check_api_key_permissions( $keys['permissions'] );
$user = $this->get_user_by_id( $keys['user_id'] );
$this->update_api_key_last_access( $keys['key_id'] );
} catch ( Exception $e ) {
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
}
return $user;
}
/**
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
* attacks, so the request can be authenticated by simply looking up the user
* associated with the given consumer key and confirming the consumer secret
* provided is valid
*
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_ssl_authentication() {
$params = WC()->api->server->params['GET'];
// Get consumer key
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
// Should be in HTTP Auth header by default
$consumer_key = $_SERVER['PHP_AUTH_USER'];
} elseif ( ! empty( $params['consumer_key'] ) ) {
// Allow a query string parameter as a fallback
$consumer_key = $params['consumer_key'];
} else {
throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
}
// Get consumer secret
if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
// Should be in HTTP Auth header by default
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
} elseif ( ! empty( $params['consumer_secret'] ) ) {
// Allow a query string parameter as a fallback
$consumer_secret = $params['consumer_secret'];
} else {
throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
}
$keys = $this->get_keys_by_consumer_key( $consumer_key );
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
/**
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
*
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
*
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
*
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
*
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
* This is because there is no cross-OS function within PHP to get the raw Authorization header
*
* @link http://tools.ietf.org/html/rfc5849 for the full spec
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_oauth_authentication() {
$params = WC()->api->server->params['GET'];
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
// Check for required OAuth parameters
foreach ( $param_names as $param_name ) {
if ( empty( $params[ $param_name ] ) ) {
/* translators: %s: parameter name */
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
}
}
// Fetch WP user by consumer key
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
// Perform OAuth validation
$this->check_oauth_signature( $keys, $params );
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
// Authentication successful, return user
return $keys;
}
/**
* Return the keys for the given consumer key
*
* @since 2.4.0
* @param string $consumer_key
* @return array
* @throws Exception
*/
private function get_keys_by_consumer_key( $consumer_key ) {
global $wpdb;
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
$keys = $wpdb->get_row( $wpdb->prepare( "
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
FROM {$wpdb->prefix}woocommerce_api_keys
WHERE consumer_key = '%s'
", $consumer_key ), ARRAY_A );
if ( empty( $keys ) ) {
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
/**
* Get user by ID
*
* @since 2.4.0
* @param int $user_id
* @return WP_User
*
* @throws Exception
*/
private function get_user_by_id( $user_id ) {
$user = get_user_by( 'id', $user_id );
if ( ! $user ) {
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
}
return $user;
}
/**
* Check if the consumer secret provided for the given user is valid
*
* @since 2.1
* @param string $keys_consumer_secret
* @param string $consumer_secret
* @return bool
*/
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
return hash_equals( $keys_consumer_secret, $consumer_secret );
}
/**
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
* has a valid key/secret
*
* @param array $keys
* @param array $params the request parameters
* @throws Exception
*/
private function check_oauth_signature( $keys, $params ) {
$http_method = strtoupper( WC()->api->server->method );
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
unset( $params['oauth_signature'] );
// Remove filters and convert them from array to strings to void normalize issues
if ( isset( $params['filter'] ) ) {
$filters = $params['filter'];
unset( $params['filter'] );
foreach ( $filters as $filter => $filter_value ) {
$params[ 'filter[' . $filter . ']' ] = $filter_value;
}
}
// Normalize parameter key/values
$params = $this->normalize_parameters( $params );
// Sort parameters
if ( ! uksort( $params, 'strcmp' ) ) {
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
}
// Form query string
$query_params = array();
foreach ( $params as $param_key => $param_value ) {
$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
}
$query_string = implode( '%26', $query_params ); // join with ampersand
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
}
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );
if ( ! hash_equals( $signature, $consumer_signature ) ) {
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
}
}
/**
* Normalize each parameter by assuming each parameter may have already been
* encoded, so attempt to decode, and then re-encode according to RFC 3986
*
* Note both the key and value is normalized so a filter param like:
*
* 'filter[period]' => 'week'
*
* is encoded to:
*
* 'filter%5Bperiod%5D' => 'week'
*
* This conforms to the OAuth 1.0a spec which indicates the entire query string
* should be URL encoded
*
* @since 2.1
* @see rawurlencode()
* @param array $parameters un-normalized parameters
* @return array normalized parameters
*/
private function normalize_parameters( $parameters ) {
$normalized_parameters = array();
foreach ( $parameters as $key => $value ) {
// Percent symbols (%) must be double-encoded
$key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
$normalized_parameters[ $key ] = $value;
}
return $normalized_parameters;
}
/**
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
* an attacker could attempt to re-send an intercepted request at a later time.
*
* - A timestamp is valid if it is within 15 minutes of now
* - A nonce is valid if it has not been used within the last 15 minutes
*
* @param array $keys
* @param int $timestamp the unix timestamp for when the request was made
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
* @throws Exception
*/
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
global $wpdb;
$valid_window = 15 * 60; // 15 minute window
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) );
}
$used_nonces = maybe_unserialize( $keys['nonces'] );
if ( empty( $used_nonces ) ) {
$used_nonces = array();
}
if ( in_array( $nonce, $used_nonces ) ) {
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
}
$used_nonces[ $timestamp ] = $nonce;
// Remove expired nonces
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
unset( $used_nonces[ $nonce_timestamp ] );
}
}
$used_nonces = maybe_serialize( $used_nonces );
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'nonces' => $used_nonces ),
array( 'key_id' => $keys['key_id'] ),
array( '%s' ),
array( '%d' )
);
}
/**
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
*
* @param string $key_permissions
* @throws Exception if the permission check fails
*/
public function check_api_key_permissions( $key_permissions ) {
switch ( WC()->api->server->method ) {
case 'HEAD':
case 'GET':
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
}
break;
}
}
/**
* Updated API Key last access datetime
*
* @since 2.4.0
*
* @param int $key_id
*/
private function update_api_key_last_access( $key_id ) {
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'last_access' => current_time( 'mysql' ) ),
array( 'key_id' => $key_id ),
array( '%s' ),
array( '%d' )
);
}
}

View File

@ -1,247 +0,0 @@
<?php
/**
* WooCommerce API Coupons Class
*
* Handles requests to the /coupons endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Coupons extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/coupons';
/**
* Register the routes for this class
*
* GET /coupons
* GET /coupons/count
* GET /coupons/<id>
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /coupons
$routes[ $this->base ] = array(
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
);
# GET /coupons/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
);
# GET /coupons/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
);
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all coupons
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_coupons( $filter );
$coupons = array();
foreach ( $query->posts as $coupon_id ) {
if ( ! $this->is_readable( $coupon_id ) ) {
continue;
}
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'coupons' => $coupons );
}
/**
* Get the coupon for the given ID
*
* @since 2.1
*
* @param int $id the coupon ID
* @param string $fields fields to include in response
*
* @return array|WP_Error
* @throws WC_API_Exception
*/
public function get_coupon( $id, $fields = null ) {
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$coupon = new WC_Coupon( $id );
if ( 0 === $coupon->get_id() ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
}
$coupon_data = array(
'id' => $coupon->get_id(),
'code' => $coupon->get_code(),
'type' => $coupon->get_discount_type(),
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
'individual_use' => $coupon->get_individual_use(),
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
'usage_count' => (int) $coupon->get_usage_count(),
'expiry_date' => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times.
'enable_free_shipping' => $coupon->get_free_shipping(),
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
'customer_emails' => $coupon->get_email_restrictions(),
);
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
}
/**
* Get the total number of coupons
*
* @since 2.1
*
* @param array $filter
*
* @return array|WP_Error
*/
public function get_coupons_count( $filter = array() ) {
$query = $this->query_coupons( $filter );
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) );
}
return array( 'count' => (int) $query->found_posts );
}
/**
* Get the coupon for the given code
*
* @since 2.1
* @param string $code the coupon code
* @param string $fields fields to include in response
* @return int|WP_Error
*/
public function get_coupon_by_code( $code, $fields = null ) {
global $wpdb;
$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );
if ( is_null( $id ) ) {
return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) );
}
return $this->get_coupon( $id, $fields );
}
/**
* Create a coupon
*
* @param array $data
* @return array
*/
public function create_coupon( $data ) {
return array();
}
/**
* Edit a coupon
*
* @param int $id the coupon ID
* @param array $data
* @return array|WP_Error
*/
public function edit_coupon( $id, $data ) {
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
return $this->get_coupon( $id );
}
/**
* Delete a coupon
*
* @param int $id the coupon ID
* @param bool $force true to permanently delete coupon, false to move to trash
* @return array|WP_Error
*/
public function delete_coupon( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
}
/**
* Helper method to get coupon post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_coupons( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
);
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
}

View File

@ -1,481 +0,0 @@
<?php
/**
* WooCommerce API Customers Class
*
* Handles requests to the /customers endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Customers extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/customers';
/** @var string $created_at_min for date filtering */
private $created_at_min = null;
/** @var string $created_at_max for date filtering */
private $created_at_max = null;
/**
* Setup class, overridden to provide customer data to order response
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
parent::__construct( $server );
// add customer data to order responses
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
// modify WP_User_Query to support created_at date filtering
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
}
/**
* Register the routes for this class
*
* GET /customers
* GET /customers/count
* GET /customers/<id>
* GET /customers/<id>/orders
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /customers
$routes[ $this->base ] = array(
array( array( $this, 'get_customers' ), WC_API_Server::READABLE ),
);
# GET /customers/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_customers_count' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_customer' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>/orders
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
array( array( $this, 'get_customer_orders' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all customers
*
* @since 2.1
* @param array $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_customers( $filter );
$customers = array();
foreach ( $query->get_results() as $user_id ) {
if ( ! $this->is_readable( $user_id ) ) {
continue;
}
$customers[] = current( $this->get_customer( $user_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'customers' => $customers );
}
/**
* Get the customer for the given ID
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields
* @return array|WP_Error
*/
public function get_customer( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$customer = new WC_Customer( $id );
$last_order = $customer->get_last_order();
$customer_data = array(
'id' => $customer->get_id(),
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'email' => $customer->get_email(),
'first_name' => $customer->get_first_name(),
'last_name' => $customer->get_last_name(),
'username' => $customer->get_username(),
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
'orders_count' => $customer->get_order_count(),
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
'avatar_url' => $customer->get_avatar_url(),
'billing_address' => array(
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
),
);
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
}
/**
* Get the total number of customers
*
* @since 2.1
* @param array $filter
* @return array|WP_Error
*/
public function get_customers_count( $filter = array() ) {
$query = $this->query_customers( $filter );
if ( ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) );
}
return array( 'count' => count( $query->get_results() ) );
}
/**
* Create a customer
*
* @param array $data
* @return array|WP_Error
*/
public function create_customer( $data ) {
if ( ! current_user_can( 'create_users' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
}
return array();
}
/**
* Edit a customer
*
* @param int $id the customer ID
* @param array $data
* @return array|WP_Error
*/
public function edit_customer( $id, $data ) {
$id = $this->validate_request( $id, 'customer', 'edit' );
if ( ! is_wp_error( $id ) ) {
return $id;
}
return $this->get_customer( $id );
}
/**
* Delete a customer
*
* @param int $id the customer ID
* @return array|WP_Error
*/
public function delete_customer( $id ) {
$id = $this->validate_request( $id, 'customer', 'delete' );
if ( ! is_wp_error( $id ) ) {
return $id;
}
return $this->delete( $id, 'customer' );
}
/**
* Get the orders for a customer
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_customer_orders( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$order_ids = wc_get_orders( array(
'customer' => $id,
'limit' => -1,
'orderby' => 'date',
'order' => 'ASC',
'return' => 'ids',
) );
if ( empty( $order_ids ) ) {
return array( 'orders' => array() );
}
$orders = array();
foreach ( $order_ids as $order_id ) {
$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
}
return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
}
/**
* Helper method to get customer user objects
*
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
* pagination support
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_User_Query
*/
private function query_customers( $args = array() ) {
// default users per page
$users_per_page = get_option( 'posts_per_page' );
// set base query arguments
$query_args = array(
'fields' => 'ID',
'role' => 'customer',
'orderby' => 'registered',
'number' => $users_per_page,
);
// search
if ( ! empty( $args['q'] ) ) {
$query_args['search'] = $args['q'];
}
// limit number of users returned
if ( ! empty( $args['limit'] ) ) {
$query_args['number'] = absint( $args['limit'] );
$users_per_page = absint( $args['limit'] );
}
// page
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
// offset
if ( ! empty( $args['offset'] ) ) {
$query_args['offset'] = absint( $args['offset'] );
} else {
$query_args['offset'] = $users_per_page * ( $page - 1 );
}
// created date
if ( ! empty( $args['created_at_min'] ) ) {
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
}
if ( ! empty( $args['created_at_max'] ) ) {
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
}
$query = new WP_User_Query( $query_args );
// helper members for pagination headers
$query->total_pages = ceil( $query->get_total() / $users_per_page );
$query->page = $page;
return $query;
}
/**
* Add customer data to orders
*
* @since 2.1
* @param $order_data
* @param $order
* @return array
*/
public function add_customer_data( $order_data, $order ) {
if ( 0 == $order->get_user_id() ) {
// add customer data from order
$order_data['customer'] = array(
'id' => 0,
'email' => $order->get_billing_email(),
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'billing_address' => array(
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
),
);
} else {
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
}
return $order_data;
}
/**
* Modify the WP_User_Query to support filtering on the date the customer was created
*
* @since 2.1
* @param WP_User_Query $query
*/
public function modify_user_query( $query ) {
if ( $this->created_at_min ) {
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
}
if ( $this->created_at_max ) {
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid WP_User
* 3) the current user has the proper permissions
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param string|int $id the customer ID
* @param string $type the request type, unused because this method overrides the parent class
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
$id = absint( $id );
// validate ID
if ( empty( $id ) ) {
return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
}
// non-existent IDs return a valid WP_User object with the user ID = 0
$customer = new WP_User( $id );
if ( 0 === $customer->ID ) {
return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
}
// validate permissions
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( ! current_user_can( 'edit_users' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( ! current_user_can( 'delete_users' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
}
break;
}
return $id;
}
/**
* Check if the current user can read users
*
* @since 2.1
* @see WC_API_Resource::is_readable()
* @param int|WP_Post $post unused
* @return bool true if the current user can read users, false otherwise
*/
protected function is_readable( $post ) {
return current_user_can( 'list_users' );
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles parsing JSON request bodies and generating JSON responses
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_JSON_Handler implements WC_API_Handler {
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $body the raw request body
* @return array|mixed
*/
public function parse_body( $body ) {
return json_decode( $body, true );
}
/**
* Generate a JSON response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
if ( isset( $_GET['_jsonp'] ) ) {
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
}
$jsonp_callback = $_GET['_jsonp'];
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
}
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
}
return wp_json_encode( $data );
}
}

View File

@ -1,396 +0,0 @@
<?php
/**
* WooCommerce API Orders Class
*
* Handles requests to the /orders endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Orders extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/orders';
/**
* Register the routes for this class
*
* GET /orders
* GET /orders/count
* GET|PUT /orders/<id>
* GET /orders/<id>/notes
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /orders
$routes[ $this->base ] = array(
array( array( $this, 'get_orders' ), WC_API_Server::READABLE ),
);
# GET /orders/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
);
# GET|PUT /orders/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_order' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /orders/<id>/notes
$routes[ $this->base . '/(?P<id>\d+)/notes' ] = array(
array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all orders
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param string $status
* @param int $page
* @return array
*/
public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$filter['page'] = $page;
$query = $this->query_orders( $filter );
$orders = array();
foreach ( $query->posts as $order_id ) {
if ( ! $this->is_readable( $order_id ) ) {
continue;
}
$orders[] = current( $this->get_order( $order_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'orders' => $orders );
}
/**
* Get the order for the given ID
*
* @since 2.1
* @param int $id the order ID
* @param array $fields
* @return array|WP_Error
*/
public function get_order( $id, $fields = null ) {
// ensure order ID is valid & user has permission to read
$id = $this->validate_request( $id, 'shop_order', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$order = wc_get_order( $id );
$order_data = array(
'id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
'status' => $order->get_status(),
'currency' => $order->get_currency(),
'total' => wc_format_decimal( $order->get_total(), 2 ),
'subtotal' => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ),
'total_line_items_quantity' => $order->get_item_count(),
'total_tax' => wc_format_decimal( $order->get_total_tax(), 2 ),
'total_shipping' => wc_format_decimal( $order->get_shipping_total(), 2 ),
'cart_tax' => wc_format_decimal( $order->get_cart_tax(), 2 ),
'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), 2 ),
'total_discount' => wc_format_decimal( $order->get_total_discount(), 2 ),
'cart_discount' => wc_format_decimal( 0, 2 ),
'order_discount' => wc_format_decimal( 0, 2 ),
'shipping_methods' => $order->get_shipping_method(),
'payment_details' => array(
'method_id' => $order->get_payment_method(),
'method_title' => $order->get_payment_method_title(),
'paid' => ! is_null( $order->get_date_paid() ),
),
'billing_address' => array(
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
),
'note' => $order->get_customer_note(),
'customer_ip' => $order->get_customer_ip_address(),
'customer_user_agent' => $order->get_customer_user_agent(),
'customer_id' => $order->get_user_id(),
'view_order_url' => $order->get_view_order_url(),
'line_items' => array(),
'shipping_lines' => array(),
'tax_lines' => array(),
'fee_lines' => array(),
'coupon_lines' => array(),
);
// add line items
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$order_data['line_items'][] = array(
'id' => $item_id,
'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ),
'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ),
'quantity' => $item->get_quantity(),
'tax_class' => $item->get_tax_class(),
'name' => $item->get_name(),
'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
'sku' => is_object( $product ) ? $product->get_sku() : null,
);
}
// add shipping
foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
$order_data['shipping_lines'][] = array(
'id' => $shipping_item_id,
'method_id' => $shipping_item->get_method_id(),
'method_title' => $shipping_item->get_name(),
'total' => wc_format_decimal( $shipping_item->get_total(), 2 ),
);
}
// add taxes
foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
$order_data['tax_lines'][] = array(
'code' => $tax_code,
'title' => $tax->label,
'total' => wc_format_decimal( $tax->amount, 2 ),
'compound' => (bool) $tax->is_compound,
);
}
// add fees
foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
$order_data['fee_lines'][] = array(
'id' => $fee_item_id,
'title' => $fee_item->get_name(),
'tax_class' => $fee_item->get_tax_class(),
'total' => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ),
'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ),
);
}
// add coupons
foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
$order_data['coupon_lines'][] = array(
'id' => $coupon_item_id,
'code' => $coupon_item->get_code(),
'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ),
);
}
return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
}
/**
* Get the total number of orders
*
* @since 2.1
*
* @param string $status
* @param array $filter
*
* @return array|WP_Error
*/
public function get_orders_count( $status = null, $filter = array() ) {
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$query = $this->query_orders( $filter );
if ( ! current_user_can( 'read_private_shop_orders' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) );
}
return array( 'count' => (int) $query->found_posts );
}
/**
* Edit an order
*
* API v1 only allows updating the status of an order
*
* @since 2.1
* @param int $id the order ID
* @param array $data
* @return array|WP_Error
*/
public function edit_order( $id, $data ) {
$id = $this->validate_request( $id, 'shop_order', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
$order = wc_get_order( $id );
if ( ! empty( $data['status'] ) ) {
$order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' );
}
return $this->get_order( $id );
}
/**
* Delete an order
*
* @param int $id the order ID
* @param bool $force true to permanently delete order, false to move to trash
* @return array
*/
public function delete_order( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_order', 'delete' );
return $this->delete( $id, 'order', ( 'true' === $force ) );
}
/**
* Get the admin order notes for an order
*
* @since 2.1
* @param int $id the order ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_order_notes( $id, $fields = null ) {
// ensure ID is valid order ID
$id = $this->validate_request( $id, 'shop_order', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$args = array(
'post_id' => $id,
'approve' => 'approve',
'type' => 'order_note',
);
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$order_notes = array();
foreach ( $notes as $note ) {
$order_notes[] = array(
'id' => $note->comment_ID,
'created_at' => $this->server->format_datetime( $note->comment_date_gmt ),
'note' => $note->comment_content,
'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
);
}
return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) );
}
/**
* Helper method to get order post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_orders( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_order',
'post_status' => array_keys( wc_get_order_statuses() ),
);
// add status argument
if ( ! empty( $args['status'] ) ) {
$statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
$statuses = explode( ',', $statuses );
$query_args['post_status'] = $statuses;
unset( $args['status'] );
}
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
/**
* Helper method to get the order subtotal
*
* @since 2.1
* @param WC_Order $order
* @return float
*/
private function get_order_subtotal( $order ) {
$subtotal = 0;
// subtotal
foreach ( $order->get_items() as $item ) {
$subtotal += $item->get_subtotal();
}
return $subtotal;
}
}

View File

@ -1,548 +0,0 @@
<?php
/**
* WooCommerce API Products Class
*
* Handles requests to the /products endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 3.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Products extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/products';
/**
* Register the routes for this class
*
* GET /products
* GET /products/count
* GET /products/<id>
* GET /products/<id>/reviews
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /products
$routes[ $this->base ] = array(
array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
);
# GET /products/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
);
# GET /products/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
);
# GET /products/<id>/reviews
$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all products
*
* @since 2.1
* @param string $fields
* @param string $type
* @param array $filter
* @param int $page
* @return array
*/
public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {
if ( ! empty( $type ) ) {
$filter['type'] = $type;
}
$filter['page'] = $page;
$query = $this->query_products( $filter );
$products = array();
foreach ( $query->posts as $product_id ) {
if ( ! $this->is_readable( $product_id ) ) {
continue;
}
$products[] = current( $this->get_product( $product_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'products' => $products );
}
/**
* Get the product for the given ID
*
* @since 2.1
* @param int $id the product ID
* @param string $fields
* @return array|WP_Error
*/
public function get_product( $id, $fields = null ) {
$id = $this->validate_request( $id, 'product', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$product = wc_get_product( $id );
// add data that applies to every product type
$product_data = $this->get_product_data( $product );
// add variations to variable products
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
$product_data['variations'] = $this->get_variation_data( $product );
}
// add the parent product data to an individual variation
if ( $product->is_type( 'variation' ) ) {
$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
}
return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
}
/**
* Get the total number of orders
*
* @since 2.1
*
* @param string $type
* @param array $filter
*
* @return array|WP_Error
*/
public function get_products_count( $type = null, $filter = array() ) {
if ( ! empty( $type ) ) {
$filter['type'] = $type;
}
if ( ! current_user_can( 'read_private_products' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) );
}
$query = $this->query_products( $filter );
return array( 'count' => (int) $query->found_posts );
}
/**
* Edit a product
*
* @param int $id the product ID
* @param array $data
* @return array|WP_Error
*/
public function edit_product( $id, $data ) {
$id = $this->validate_request( $id, 'product', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
return $this->get_product( $id );
}
/**
* Delete a product
*
* @param int $id the product ID
* @param bool $force true to permanently delete order, false to move to trash
* @return array|WP_Error
*/
public function delete_product( $id, $force = false ) {
$id = $this->validate_request( $id, 'product', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
return $this->delete( $id, 'product', ( 'true' === $force ) );
}
/**
* Get the reviews for a product
*
* @since 2.1
* @param int $id the product ID to get reviews for
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_product_reviews( $id, $fields = null ) {
$id = $this->validate_request( $id, 'product', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$args = array(
'post_id' => $id,
'approve' => 'approve',
);
$comments = get_comments( $args );
$reviews = array();
foreach ( $comments as $comment ) {
$reviews[] = array(
'id' => $comment->comment_ID,
'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ),
'review' => $comment->comment_content,
'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ),
'reviewer_name' => $comment->comment_author,
'reviewer_email' => $comment->comment_author_email,
'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ),
);
}
return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
}
/**
* Helper method to get product post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_products( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'product',
'post_status' => 'publish',
'meta_query' => array(),
);
if ( ! empty( $args['type'] ) ) {
$types = explode( ',', $args['type'] );
$query_args['tax_query'] = array(
array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $types,
),
);
unset( $args['type'] );
}
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
/**
* Get standard product data that applies to every product type
*
* @since 2.1
* @param WC_Product|int $product
* @return array
*/
private function get_product_data( $product ) {
if ( is_numeric( $product ) ) {
$product = wc_get_product( $product );
}
if ( ! is_a( $product, 'WC_Product' ) ) {
return array();
}
return array(
'title' => $product->get_name(),
'id' => $product->get_id(),
'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ),
'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ),
'type' => $product->get_type(),
'status' => $product->get_status(),
'downloadable' => $product->is_downloadable(),
'virtual' => $product->is_virtual(),
'permalink' => $product->get_permalink(),
'sku' => $product->get_sku(),
'price' => wc_format_decimal( $product->get_price(), 2 ),
'regular_price' => wc_format_decimal( $product->get_regular_price(), 2 ),
'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null,
'price_html' => $product->get_price_html(),
'taxable' => $product->is_taxable(),
'tax_status' => $product->get_tax_status(),
'tax_class' => $product->get_tax_class(),
'managing_stock' => $product->managing_stock(),
'stock_quantity' => $product->get_stock_quantity(),
'in_stock' => $product->is_in_stock(),
'backorders_allowed' => $product->backorders_allowed(),
'backordered' => $product->is_on_backorder(),
'sold_individually' => $product->is_sold_individually(),
'purchaseable' => $product->is_purchasable(),
'featured' => $product->is_featured(),
'visible' => $product->is_visible(),
'catalog_visibility' => $product->get_catalog_visibility(),
'on_sale' => $product->is_on_sale(),
'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
'dimensions' => array(
'length' => $product->get_length(),
'width' => $product->get_width(),
'height' => $product->get_height(),
'unit' => get_option( 'woocommerce_dimension_unit' ),
),
'shipping_required' => $product->needs_shipping(),
'shipping_taxable' => $product->is_shipping_taxable(),
'shipping_class' => $product->get_shipping_class(),
'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
'description' => apply_filters( 'the_content', $product->get_description() ),
'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
'reviews_allowed' => $product->get_reviews_allowed(),
'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ),
'rating_count' => $product->get_rating_count(),
'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ),
'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ),
'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
'images' => $this->get_images( $product ),
'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
'attributes' => $this->get_attributes( $product ),
'downloads' => $this->get_downloads( $product ),
'download_limit' => $product->get_download_limit(),
'download_expiry' => $product->get_download_expiry(),
'download_type' => 'standard',
'purchase_note' => apply_filters( 'the_content', $product->get_purchase_note() ),
'total_sales' => $product->get_total_sales(),
'variations' => array(),
'parent' => array(),
);
}
/**
* Get an individual variation's data
*
* @since 2.1
* @param WC_Product $product
* @return array
*/
private function get_variation_data( $product ) {
$variations = array();
foreach ( $product->get_children() as $child_id ) {
$variation = wc_get_product( $child_id );
if ( ! $variation || ! $variation->exists() ) {
continue;
}
$variations[] = array(
'id' => $variation->get_id(),
'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ),
'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
'downloadable' => $variation->is_downloadable(),
'virtual' => $variation->is_virtual(),
'permalink' => $variation->get_permalink(),
'sku' => $variation->get_sku(),
'price' => wc_format_decimal( $variation->get_price(), 2 ),
'regular_price' => wc_format_decimal( $variation->get_regular_price(), 2 ),
'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null,
'taxable' => $variation->is_taxable(),
'tax_status' => $variation->get_tax_status(),
'tax_class' => $variation->get_tax_class(),
'stock_quantity' => (int) $variation->get_stock_quantity(),
'in_stock' => $variation->is_in_stock(),
'backordered' => $variation->is_on_backorder(),
'purchaseable' => $variation->is_purchasable(),
'visible' => $variation->variation_is_visible(),
'on_sale' => $variation->is_on_sale(),
'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
'dimensions' => array(
'length' => $variation->get_length(),
'width' => $variation->get_width(),
'height' => $variation->get_height(),
'unit' => get_option( 'woocommerce_dimension_unit' ),
),
'shipping_class' => $variation->get_shipping_class(),
'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
'image' => $this->get_images( $variation ),
'attributes' => $this->get_attributes( $variation ),
'downloads' => $this->get_downloads( $variation ),
'download_limit' => (int) $product->get_download_limit(),
'download_expiry' => (int) $product->get_download_expiry(),
);
}
return $variations;
}
/**
* Get the images for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_images( $product ) {
$images = $attachment_ids = array();
$product_image = $product->get_image_id();
// Add featured image.
if ( ! empty( $product_image ) ) {
$attachment_ids[] = $product_image;
}
// add gallery images.
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
// Build image data.
foreach ( $attachment_ids as $position => $attachment_id ) {
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) ) {
continue;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
continue;
}
$images[] = array(
'id' => (int) $attachment_id,
'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
'src' => current( $attachment ),
'title' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
'position' => $position,
);
}
// Set a placeholder image if the product has no images set.
if ( empty( $images ) ) {
$images[] = array(
'id' => 0,
'created_at' => $this->server->format_datetime( time() ), // default to now
'updated_at' => $this->server->format_datetime( time() ),
'src' => wc_placeholder_img_src(),
'title' => __( 'Placeholder', 'woocommerce' ),
'alt' => __( 'Placeholder', 'woocommerce' ),
'position' => 0,
);
}
return $images;
}
/**
* Get attribute options.
*
* @param int $product_id
* @param array $attribute
* @return array
*/
protected function get_attribute_options( $product_id, $attribute ) {
if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
} elseif ( isset( $attribute['value'] ) ) {
return array_map( 'trim', explode( '|', $attribute['value'] ) );
}
return array();
}
/**
* Get the attributes for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_attributes( $product ) {
$attributes = array();
if ( $product->is_type( 'variation' ) ) {
// variation attributes
foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
$attributes[] = array(
'name' => ucwords( str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ),
'option' => $attribute,
);
}
} else {
foreach ( $product->get_attributes() as $attribute ) {
$attributes[] = array(
'name' => ucwords( wc_attribute_taxonomy_slug( $attribute['name'] ) ),
'position' => $attribute['position'],
'visible' => (bool) $attribute['is_visible'],
'variation' => (bool) $attribute['is_variation'],
'options' => $this->get_attribute_options( $product->get_id(), $attribute ),
);
}
}
return $attributes;
}
/**
* Get the downloads for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_downloads( $product ) {
$downloads = array();
if ( $product->is_downloadable() ) {
foreach ( $product->get_downloads() as $file_id => $file ) {
$downloads[] = array(
'id' => $file_id, // do not cast as int as this is a hash
'name' => $file['name'],
'file' => $file['file'],
);
}
}
return $downloads;
}
}

View File

@ -1,482 +0,0 @@
<?php
/**
* WooCommerce API Reports Class
*
* Handles requests to the /reports endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Reports extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/reports';
/** @var WC_Admin_Report instance */
private $report;
/**
* Register the routes for this class
*
* GET /reports
* GET /reports/sales
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /reports
$routes[ $this->base ] = array(
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
);
# GET /reports/sales
$routes[ $this->base . '/sales' ] = array(
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
);
# GET /reports/sales/top_sellers
$routes[ $this->base . '/sales/top_sellers' ] = array(
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get a simple listing of available reports
*
* @since 2.1
* @return array
*/
public function get_reports() {
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
}
/**
* Get the sales report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_sales_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
// total sales, taxes, shipping, and order count
$totals = $this->report->get_order_report_data( array(
'data' => array(
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'sales',
),
'_order_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'tax',
),
'_order_shipping_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'shipping_tax',
),
'_order_shipping' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'shipping',
),
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'order_count',
),
),
'filter_range' => true,
) );
// total items ordered
$total_items = absint( $this->report->get_order_report_data( array(
'data' => array(
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty',
),
),
'query_type' => 'get_var',
'filter_range' => true,
) ) );
// total discount used
$total_discount = $this->report->get_order_report_data( array(
'data' => array(
'discount_amount' => array(
'type' => 'order_item_meta',
'order_item_type' => 'coupon',
'function' => 'SUM',
'name' => 'discount_amount',
),
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'coupon',
'operator' => '=',
),
),
'query_type' => 'get_var',
'filter_range' => true,
) );
// new customers
$users_query = new WP_User_Query(
array(
'fields' => array( 'user_registered' ),
'role' => 'customer',
)
);
$customers = $users_query->get_results();
foreach ( $customers as $key => $customer ) {
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
unset( $customers[ $key ] );
}
}
$total_customers = count( $customers );
// get order totals grouped by period
$orders = $this->report->get_order_report_data( array(
'data' => array(
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_sales',
),
'_order_shipping' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_shipping',
),
'_order_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_tax',
),
'_order_shipping_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_shipping_tax',
),
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'total_orders',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
),
'group_by' => $this->report->group_by_query,
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
// get order item totals grouped by period
$order_items = $this->report->get_order_report_data( array(
'data' => array(
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_count',
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'line_item',
'operator' => '=',
),
),
'group_by' => $this->report->group_by_query,
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
// get discount totals grouped by period
$discounts = $this->report->get_order_report_data( array(
'data' => array(
'discount_amount' => array(
'type' => 'order_item_meta',
'order_item_type' => 'coupon',
'function' => 'SUM',
'name' => 'discount_amount',
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'coupon',
'operator' => '=',
),
),
'group_by' => $this->report->group_by_query . ', order_item_name',
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
$period_totals = array();
// setup period totals by ensuring each period in the interval has data
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
switch ( $this->report->chart_groupby ) {
case 'day' :
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
break;
case 'month' :
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
break;
}
// set the customer signups for each period
$customer_count = 0;
foreach ( $customers as $customer ) {
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
$customer_count++;
}
}
$period_totals[ $time ] = array(
'sales' => wc_format_decimal( 0.00, 2 ),
'orders' => 0,
'items' => 0,
'tax' => wc_format_decimal( 0.00, 2 ),
'shipping' => wc_format_decimal( 0.00, 2 ),
'discount' => wc_format_decimal( 0.00, 2 ),
'customers' => $customer_count,
);
}
// add total sales, total order count, total tax and total shipping for each period
foreach ( $orders as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
$period_totals[ $time ]['orders'] = (int) $order->total_orders;
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
}
// add total order items for each period
foreach ( $order_items as $order_item ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
}
// add total discount for each period
foreach ( $discounts as $discount ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
}
$sales_data = array(
'total_sales' => wc_format_decimal( $totals->sales, 2 ),
'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ),
'total_orders' => (int) $totals->order_count,
'total_items' => $total_items,
'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ),
'total_shipping' => wc_format_decimal( $totals->shipping, 2 ),
'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ),
'totals_grouped_by' => $this->report->chart_groupby,
'totals' => $period_totals,
'total_customers' => $total_customers,
);
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
}
/**
* Get the top sellers report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_top_sellers_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
$top_sellers = $this->report->get_order_report_data( array(
'data' => array(
'_product_id' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => '',
'name' => 'product_id',
),
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty',
),
),
'order_by' => 'order_item_qty DESC',
'group_by' => 'product_id',
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
'query_type' => 'get_results',
'filter_range' => true,
) );
$top_sellers_data = array();
foreach ( $top_sellers as $top_seller ) {
$product = wc_get_product( $top_seller->product_id );
$top_sellers_data[] = array(
'title' => $product->get_name(),
'product_id' => $top_seller->product_id,
'quantity' => $top_seller->order_item_qty,
);
}
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
}
/**
* Setup the report object and parse any date filtering
*
* @since 2.1
* @param array $filter date filtering
*/
private function setup_report( $filter ) {
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
$this->report = new WC_Admin_Report();
if ( empty( $filter['period'] ) ) {
// custom date range
$filter['period'] = 'custom';
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
} else {
// default custom range to today
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
}
} else {
// ensure period is valid
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
$filter['period'] = 'week';
}
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
// allow "week" for period instead of "7day"
if ( 'week' === $filter['period'] ) {
$filter['period'] = '7day';
}
}
$this->report->calculate_current_range( $filter['period'] );
}
/**
* Verify that the current user has permission to view reports
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param null $id unused
* @param null $type unused
* @param null $context unused
* @return true|WP_Error
*/
protected function validate_request( $id = null, $type = null, $context = null ) {
if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
} else {
return true;
}
}
}

View File

@ -1,409 +0,0 @@
<?php
/**
* WooCommerce API Resource class
*
* Provides shared functionality for resource-specific API classes
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Resource {
/** @var WC_API_Server the API server */
protected $server;
/** @var string sub-classes override this to set a resource-specific base route */
protected $base;
/**
* Setup class
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
$this->server = $server;
// automatically register routes for sub-classes
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
// remove fields from responses when requests specify certain fields
// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
// still has the fields filtered properly
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid post object and matches the provided post type
* 3) the current user has the proper permissions to read/edit/delete the post
*
* @since 2.1
* @param string|int $id the post ID
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
$id = absint( $id );
// validate ID
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
}
// only custom post types have per-post type/permission checks
if ( 'customer' !== $type ) {
$post = get_post( $id );
// for checking permissions, product variations are the same as the product post type
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
// validate post type
if ( $type !== $post_type ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
}
// validate permissions
switch ( $context ) {
case 'read':
if ( ! $this->is_readable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( ! $this->is_editable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( ! $this->is_deletable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
}
}
return $id;
}
/**
* Add common request arguments to argument list before WP_Query is run
*
* @since 2.1
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
* @param array $request_args arguments provided in the request
* @return array
*/
protected function merge_query_args( $base_args, $request_args ) {
$args = array();
// date
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'] = array();
// resources created after specified date
if ( ! empty( $request_args['created_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
}
// resources created before specified date
if ( ! empty( $request_args['created_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
}
// resources updated after specified date
if ( ! empty( $request_args['updated_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
}
// resources updated before specified date
if ( ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
}
}
// search
if ( ! empty( $request_args['q'] ) ) {
$args['s'] = $request_args['q'];
}
// resources per response
if ( ! empty( $request_args['limit'] ) ) {
$args['posts_per_page'] = $request_args['limit'];
}
// resource offset
if ( ! empty( $request_args['offset'] ) ) {
$args['offset'] = $request_args['offset'];
}
// resource page
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
return array_merge( $base_args, $args );
}
/**
* Add meta to resources when requested by the client. Meta is added as a top-level
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
*
* @since 2.1
* @param array $data the resource data
* @param object $resource the resource object (e.g WC_Order)
* @return mixed
*/
public function maybe_add_meta( $data, $resource ) {
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
// don't attempt to add meta more than once
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
return $data;
}
// define the top-level property name for the meta
switch ( get_class( $resource ) ) {
case 'WC_Order':
$meta_name = 'order_meta';
break;
case 'WC_Coupon':
$meta_name = 'coupon_meta';
break;
case 'WP_User':
$meta_name = 'customer_meta';
break;
default:
$meta_name = 'product_meta';
break;
}
if ( is_a( $resource, 'WP_User' ) ) {
// customer meta
$meta = (array) get_user_meta( $resource->ID );
} else {
// coupon/order/product meta
$meta = (array) get_post_meta( $resource->get_id() );
}
foreach ( $meta as $meta_key => $meta_value ) {
// don't add hidden meta by default
if ( ! is_protected_meta( $meta_key ) ) {
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
}
}
}
return $data;
}
/**
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
*
* @since 2.1
* @param array $data the response data
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
* @param array|string the requested list of fields to include in the response
* @return array response data
*/
public function filter_response_fields( $data, $resource, $fields ) {
if ( ! is_array( $data ) || empty( $fields ) ) {
return $data;
}
$fields = explode( ',', $fields );
$sub_fields = array();
// get sub fields
foreach ( $fields as $field ) {
if ( false !== strpos( $field, '.' ) ) {
list( $name, $value ) = explode( '.', $field );
$sub_fields[ $name ] = $value;
}
}
// iterate through top-level fields
foreach ( $data as $data_field => $data_value ) {
// if a field has sub-fields and the top-level field has sub-fields to filter
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
// iterate through each sub-field
foreach ( $data_value as $sub_field => $sub_field_value ) {
// remove non-matching sub-fields
if ( ! in_array( $sub_field, $sub_fields ) ) {
unset( $data[ $data_field ][ $sub_field ] );
}
}
} else {
// remove non-matching top-level fields
if ( ! in_array( $data_field, $fields ) ) {
unset( $data[ $data_field ] );
}
}
}
return $data;
}
/**
* Delete a given resource
*
* @since 2.1
* @param int $id the resource ID
* @param string $type the resource post type, or `customer`
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
* @return array|WP_Error
*/
protected function delete( $id, $type, $force = false ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
if ( 'customer' === $type ) {
$result = wp_delete_user( $id );
if ( $result ) {
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
} else {
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
}
} else {
// delete order/coupon/product
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
if ( ! $result ) {
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
}
if ( $force ) {
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
} else {
$this->server->send_status( '202' );
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
}
}
}
/**
* Checks if the given post is readable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_readable( $post ) {
return $this->check_permission( $post, 'read' );
}
/**
* Checks if the given post is editable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_editable( $post ) {
return $this->check_permission( $post, 'edit' );
}
/**
* Checks if the given post is deletable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_deletable( $post ) {
return $this->check_permission( $post, 'delete' );
}
/**
* Checks the permissions for the current user given a post and context
*
* @since 2.1
* @param WP_Post|int $post
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
* @return bool true if the current user has the permissions to perform the context on the post
*/
private function check_permission( $post, $context ) {
if ( ! is_a( $post, 'WP_Post' ) ) {
$post = get_post( $post );
}
if ( is_null( $post ) ) {
return false;
}
$post_type = get_post_type_object( $post->post_type );
if ( 'read' === $context ) {
return current_user_can( $post_type->cap->read_private_posts, $post->ID );
} elseif ( 'edit' === $context ) {
return current_user_can( $post_type->cap->edit_post, $post->ID );
} elseif ( 'delete' === $context ) {
return current_user_can( $post_type->cap->delete_post, $post->ID );
} else {
return false;
}
}
}

View File

@ -1,767 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles REST API requests
*
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
* Many thanks to Ryan McCue and any other contributors!
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once ABSPATH . 'wp-admin/includes/admin.php';
class WC_API_Server {
const METHOD_GET = 1;
const METHOD_POST = 2;
const METHOD_PUT = 4;
const METHOD_PATCH = 8;
const METHOD_DELETE = 16;
const READABLE = 1; // GET
const CREATABLE = 2; // POST
const EDITABLE = 14; // POST | PUT | PATCH
const DELETABLE = 16; // DELETE
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
/**
* Does the endpoint accept a raw request body?
*/
const ACCEPT_RAW_DATA = 64;
/** Does the endpoint accept a request body? (either JSON or XML) */
const ACCEPT_DATA = 128;
/**
* Should we hide this endpoint from the index?
*/
const HIDDEN_ENDPOINT = 256;
/**
* Map of HTTP verbs to constants
* @var array
*/
public static $method_map = array(
'HEAD' => self::METHOD_GET,
'GET' => self::METHOD_GET,
'POST' => self::METHOD_POST,
'PUT' => self::METHOD_PUT,
'PATCH' => self::METHOD_PATCH,
'DELETE' => self::METHOD_DELETE,
);
/**
* Requested path (relative to the API root, wp-json.php)
*
* @var string
*/
public $path = '';
/**
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
*
* @var string
*/
public $method = 'HEAD';
/**
* Request parameters
*
* This acts as an abstraction of the superglobals
* (GET => $_GET, POST => $_POST)
*
* @var array
*/
public $params = array( 'GET' => array(), 'POST' => array() );
/**
* Request headers
*
* @var array
*/
public $headers = array();
/**
* Request files (matches $_FILES)
*
* @var array
*/
public $files = array();
/**
* Request/Response handler, either JSON by default
* or XML if requested by client
*
* @var WC_API_Handler
*/
public $handler;
/**
* Setup class and set request/response handler
*
* @since 2.1
* @param $path
*/
public function __construct( $path ) {
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else {
$path = '/';
}
}
$this->path = $path;
$this->method = $_SERVER['REQUEST_METHOD'];
$this->params['GET'] = $_GET;
$this->params['POST'] = $_POST;
$this->headers = $this->get_headers( $_SERVER );
$this->files = $_FILES;
// Compatibility for clients that can't use PUT/PATCH/DELETE
if ( isset( $_GET['_method'] ) ) {
$this->method = strtoupper( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
// determine type of request/response and load handler, JSON by default
if ( $this->is_json_request() ) {
$handler_class = 'WC_API_JSON_Handler';
} elseif ( $this->is_xml_request() ) {
$handler_class = 'WC_API_XML_Handler';
} else {
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
}
$this->handler = new $handler_class();
}
/**
* Check authentication for the request
*
* @since 2.1
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
*/
public function check_authentication() {
// allow plugins to remove default authentication or add their own authentication
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
// API requests run under the context of the authenticated user
if ( is_a( $user, 'WP_User' ) ) {
wp_set_current_user( $user->ID );
} elseif ( ! is_wp_error( $user ) ) {
// WP_Errors are handled in serve_request()
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
}
return $user;
}
/**
* Convert an error to an array
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behaviour, as it is represented as a
* list in JSON rather than an object/map
*
* @since 2.1
* @param WP_Error $error
* @return array List of associative arrays with code and message keys
*/
protected function error_to_array( $error ) {
$errors = array();
foreach ( (array) $error->errors as $code => $messages ) {
foreach ( (array) $messages as $message ) {
$errors[] = array( 'code' => $code, 'message' => $message );
}
}
return array( 'errors' => $errors );
}
/**
* Handle serving an API request
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 2.1
* @uses WC_API_Server::dispatch()
*/
public function serve_request() {
do_action( 'woocommerce_api_server_before_serve', $this );
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
// the API is enabled by default
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
$this->send_status( 404 );
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
return;
}
$result = $this->check_authentication();
// if authorization check was successful, dispatch the request
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch();
}
// handle any dispatch errors
if ( is_wp_error( $result ) ) {
$data = $result->get_error_data();
if ( is_array( $data ) && isset( $data['status'] ) ) {
$this->send_status( $data['status'] );
}
$result = $this->error_to_array( $result );
}
// This is a filter rather than an action, since this is designed to be
// re-entrant if needed
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
if ( ! $served ) {
if ( 'HEAD' === $this->method ) {
return;
}
echo $this->handler->generate_response( $result );
}
}
/**
* Retrieve the route map
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 2.1
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
*/
public function get_routes() {
// index added by default
$endpoints = array(
'/' => array( array( $this, 'get_index' ), self::READABLE ),
);
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
// Normalise the endpoints
foreach ( $endpoints as $route => &$handlers ) {
if ( is_array( $handlers ) && count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
$handlers = array( $handlers );
}
}
return $endpoints;
}
/**
* Match the request to a callback and call it
*
* @since 2.1
* @return mixed The value returned by the callback, or a WP_Error instance
*/
public function dispatch() {
switch ( $this->method ) {
case 'HEAD':
case 'GET':
$method = self::METHOD_GET;
break;
case 'POST':
$method = self::METHOD_POST;
break;
case 'PUT':
$method = self::METHOD_PUT;
break;
case 'PATCH':
$method = self::METHOD_PATCH;
break;
case 'DELETE':
$method = self::METHOD_DELETE;
break;
default:
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
}
foreach ( $this->get_routes() as $route => $handlers ) {
foreach ( $handlers as $handler ) {
$callback = $handler[0];
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
if ( ! ( $supported & $method ) ) {
continue;
}
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
if ( ! $match ) {
continue;
}
if ( ! is_callable( $callback ) ) {
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
}
$args = array_merge( $args, $this->params['GET'] );
if ( $method & self::METHOD_POST ) {
$args = array_merge( $args, $this->params['POST'] );
}
if ( $supported & self::ACCEPT_DATA ) {
$data = $this->handler->parse_body( $this->get_raw_data() );
$args = array_merge( $args, array( 'data' => $data ) );
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
$data = $this->get_raw_data();
$args = array_merge( $args, array( 'data' => $data ) );
}
$args['_method'] = $method;
$args['_route'] = $route;
$args['_path'] = $this->path;
$args['_headers'] = $this->headers;
$args['_files'] = $this->files;
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
// Allow plugins to halt the request via this filter
if ( is_wp_error( $args ) ) {
return $args;
}
$params = $this->sort_callback_params( $callback, $args );
if ( is_wp_error( $params ) ) {
return $params;
}
return call_user_func_array( $callback, $params );
}
}
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
}
/**
* Sort parameters by order specified in method declaration
*
* Takes a callback and a list of available params, then filters and sorts
* by the parameters the method actually needs, using the Reflection API
*
* @since 2.1
*
* @param callable|array $callback the endpoint callback
* @param array $provided the provided request parameters
*
* @return array|WP_Error
*/
protected function sort_callback_params( $callback, $provided ) {
if ( is_array( $callback ) ) {
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
} else {
$ref_func = new ReflectionFunction( $callback );
}
$wanted = $ref_func->getParameters();
$ordered_parameters = array();
foreach ( $wanted as $param ) {
if ( isset( $provided[ $param->getName() ] ) ) {
// We have this parameters in the list to choose from
$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
} elseif ( $param->isDefaultValueAvailable() ) {
// We don't have this parameter, but it's optional
$ordered_parameters[] = $param->getDefaultValue();
} else {
// We don't have this parameter and it wasn't optional, abort!
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
}
}
return $ordered_parameters;
}
/**
* Get the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 2.1
* @return array Index entity
*/
public function get_index() {
// General site data
$available = array(
'store' => array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'URL' => get_option( 'siteurl' ),
'wc_version' => WC()->version,
'routes' => array(),
'meta' => array(
'timezone' => wc_timezone_string(),
'currency' => get_woocommerce_currency(),
'currency_format' => get_woocommerce_currency_symbol(),
'tax_included' => wc_prices_include_tax(),
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
'links' => array(
'help' => 'https://woocommerce.github.io/woocommerce/rest-api/',
),
),
),
);
// Find the available routes
foreach ( $this->get_routes() as $route => $callbacks ) {
$data = array();
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
$methods = array();
foreach ( self::$method_map as $name => $bitmask ) {
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
continue 3;
}
if ( $callback[1] & $bitmask ) {
$data['supports'][] = $name;
}
if ( $callback[1] & self::ACCEPT_DATA ) {
$data['accepts_data'] = true;
}
// For non-variable routes, generate links
if ( strpos( $route, '<' ) === false ) {
$data['meta'] = array(
'self' => get_woocommerce_api_url( $route ),
);
}
}
}
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
}
return apply_filters( 'woocommerce_api_index', $available );
}
/**
* Send a HTTP status code
*
* @since 2.1
* @param int $code HTTP status
*/
public function send_status( $code ) {
status_header( $code );
}
/**
* Send a HTTP header
*
* @since 2.1
* @param string $key Header key
* @param string $value Header value
* @param boolean $replace Should we replace the existing header?
*/
public function header( $key, $value, $replace = true ) {
header( sprintf( '%s: %s', $key, $value ), $replace );
}
/**
* Send a Link header
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple
*
* @link http://tools.ietf.org/html/rfc5988
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
*
* @since 2.1
* @param string $rel Link relation. Either a registered type, or an absolute URL
* @param string $link Target IRI for the link
* @param array $other Other parameters to send, as an associative array
*/
public function link_header( $rel, $link, $other = array() ) {
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
foreach ( $other as $key => $value ) {
if ( 'title' == $key ) {
$value = '"' . $value . '"';
}
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Send pagination headers for resources
*
* @since 2.1
* @param WP_Query|WP_User_Query $query
*/
public function add_pagination_headers( $query ) {
// WP_User_Query
if ( is_a( $query, 'WP_User_Query' ) ) {
$page = $query->page;
$single = count( $query->get_results() ) == 1;
$total = $query->get_total();
$total_pages = $query->total_pages;
// WP_Query
} else {
$page = $query->get( 'paged' );
$single = $query->is_single();
$total = $query->found_posts;
$total_pages = $query->max_num_pages;
}
if ( ! $page ) {
$page = 1;
}
$next_page = absint( $page ) + 1;
if ( ! $single ) {
// first/prev
if ( $page > 1 ) {
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
}
// next
if ( $next_page <= $total_pages ) {
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
}
// last
if ( $page != $total_pages ) {
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
}
}
$this->header( 'X-WC-Total', $total );
$this->header( 'X-WC-TotalPages', $total_pages );
do_action( 'woocommerce_api_pagination_headers', $this, $query );
}
/**
* Returns the request URL with the page query parameter set to the specified page
*
* @since 2.1
* @param int $page
* @return string
*/
private function get_paginated_url( $page ) {
// remove existing page query param
$request = remove_query_arg( 'page' );
// add provided page query param
$request = urldecode( add_query_arg( 'page', $page, $request ) );
// get the home host
$host = parse_url( get_home_url(), PHP_URL_HOST );
return set_url_scheme( "http://{$host}{$request}" );
}
/**
* Retrieve the raw request entity (body)
*
* @since 2.1
* @return string
*/
public function get_raw_data() {
return file_get_contents( 'php://input' );
}
/**
* Parse an RFC3339 datetime into a MySQl datetime
*
* Invalid dates default to unix epoch
*
* @since 2.1
* @param string $datetime RFC3339 datetime
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
*/
public function parse_datetime( $datetime ) {
// Strip millisecond precision (a full stop followed by one or more digits)
if ( strpos( $datetime, '.' ) !== false ) {
$datetime = preg_replace( '/\.\d+/', '', $datetime );
}
// default timezone to UTC
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
try {
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
} catch ( Exception $e ) {
$datetime = new DateTime( '@0' );
}
return $datetime->format( 'Y-m-d H:i:s' );
}
/**
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
*
* @since 2.1
* @param int|string $timestamp unix timestamp or MySQL datetime
* @param bool $convert_to_utc
* @param bool $convert_to_gmt Use GMT timezone.
* @return string RFC3339 datetime
*/
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
if ( $convert_to_gmt ) {
if ( is_numeric( $timestamp ) ) {
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
}
$timestamp = get_gmt_from_date( $timestamp );
}
if ( $convert_to_utc ) {
$timezone = new DateTimeZone( wc_timezone_string() );
} else {
$timezone = new DateTimeZone( 'UTC' );
}
try {
if ( is_numeric( $timestamp ) ) {
$date = new DateTime( "@{$timestamp}" );
} else {
$date = new DateTime( $timestamp, $timezone );
}
// convert to UTC by adjusting the time based on the offset of the site's timezone
if ( $convert_to_utc ) {
$date->modify( -1 * $date->getOffset() . ' seconds' );
}
} catch ( Exception $e ) {
$date = new DateTime( '@0' );
}
return $date->format( 'Y-m-d\TH:i:s\Z' );
}
/**
* Extract headers from a PHP-style $_SERVER array
*
* @since 2.1
* @param array $server Associative array similar to $_SERVER
* @return array Headers extracted from the input
*/
public function get_headers( $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
foreach ( $server as $key => $value ) {
if ( strpos( $key, 'HTTP_' ) === 0 ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
/**
* Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or
* the HTTP ACCEPT header
*
* @since 2.1
* @return bool
*/
private function is_json_request() {
// check path
if ( false !== stripos( $this->path, '.json' ) ) {
return true;
}
// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627
if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) {
return true;
}
return false;
}
/**
* Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or
* the HTTP ACCEPT header
*
* @since 2.1
* @return bool
*/
private function is_xml_request() {
// check path
if ( false !== stripos( $this->path, '.xml' ) ) {
return true;
}
// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376
if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) {
return true;
}
return false;
}
}

View File

@ -1,308 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles parsing XML request bodies and generating XML responses
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_XML_Handler implements WC_API_Handler {
/** @var XMLWriter instance */
private $xml;
/**
* Add some response filters
*
* @since 2.1
*/
public function __construct() {
// tweak sales report response data
add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 );
// tweak product response data
add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 );
}
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return 'application/xml; charset=' . get_option( 'blog_charset' );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $data the raw request body
* @return array
*/
public function parse_body( $data ) {
// TODO: implement simpleXML parsing
}
/**
* Generate an XML response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
$this->xml = new XMLWriter();
$this->xml->openMemory();
$this->xml->setIndent( true );
$this->xml->startDocument( '1.0', 'UTF-8' );
$root_element = key( $data );
$data = $data[ $root_element ];
switch ( $root_element ) {
case 'orders':
$data = array( 'order' => $data );
break;
case 'order_notes':
$data = array( 'order_note' => $data );
break;
case 'customers':
$data = array( 'customer' => $data );
break;
case 'coupons':
$data = array( 'coupon' => $data );
break;
case 'products':
$data = array( 'product' => $data );
break;
case 'product_reviews':
$data = array( 'product_review' => $data );
break;
default:
$data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element );
break;
}
// generate xml starting with the root element and recursively generating child elements
$this->array_to_xml( $root_element, $data );
$this->xml->endDocument();
return $this->xml->outputMemory();
}
/**
* Convert array into XML by recursively generating child elements
*
* @since 2.1
* @param string|array $element_key - name for element, e.g. <OrderID>
* @param string|array $element_value - value for element, e.g. 1234
* @return string - generated XML
*/
private function array_to_xml( $element_key, $element_value = array() ) {
if ( is_array( $element_value ) ) {
// handle attributes
if ( '@attributes' === $element_key ) {
foreach ( $element_value as $attribute_key => $attribute_value ) {
$this->xml->startAttribute( $attribute_key );
$this->xml->text( $attribute_value );
$this->xml->endAttribute();
}
return;
}
// handle multi-elements (e.g. multiple <Order> elements)
if ( is_numeric( key( $element_value ) ) ) {
// recursively generate child elements
foreach ( $element_value as $child_element_key => $child_element_value ) {
$this->xml->startElement( $element_key );
foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
$this->array_to_xml( $sibling_element_key, $sibling_element_value );
}
$this->xml->endElement();
}
} else {
// start root element
$this->xml->startElement( $element_key );
// recursively generate child elements
foreach ( $element_value as $child_element_key => $child_element_value ) {
$this->array_to_xml( $child_element_key, $child_element_value );
}
// end root element
$this->xml->endElement();
}
} else {
// handle single elements
if ( '@value' == $element_key ) {
$this->xml->text( $element_value );
} else {
// wrap element in CDATA tags if it contains illegal characters
if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {
$this->xml->startElement( $element_key );
$this->xml->writeCdata( $element_value );
$this->xml->endElement();
} else {
$this->xml->writeElement( $element_key, $element_value );
}
}
return;
}
}
/**
* Adjust the sales report array format to change totals keyed with the sales date to become an
* attribute for the totals element instead
*
* @since 2.1
* @param array $data
* @return array
*/
public function format_sales_report_data( $data ) {
if ( ! empty( $data['totals'] ) ) {
foreach ( $data['totals'] as $date => $totals ) {
unset( $data['totals'][ $date ] );
$data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals );
}
}
return $data;
}
/**
* Adjust the product data to handle options for attributes without a named child element and other
* fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) )
*
* Note that the parent product data for variations is also adjusted in the same manner as needed
*
* @since 2.1
* @param array $data
* @return array
*/
public function format_product_data( $data ) {
// handle attribute values
if ( ! empty( $data['attributes'] ) ) {
foreach ( $data['attributes'] as $attribute_key => $attribute ) {
if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
foreach ( $attribute['options'] as $option_key => $option ) {
unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] );
$data['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
}
}
}
}
// simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data
// array to define a child element name for each field
$fields_to_fix = array(
'related_ids' => 'related_id',
'upsell_ids' => 'upsell_id',
'cross_sell_ids' => 'cross_sell_id',
'categories' => 'category',
'tags' => 'tag',
);
foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
if ( ! empty( $data[ $parent_field_name ] ) ) {
foreach ( $data[ $parent_field_name ] as $field_key => $field ) {
unset( $data[ $parent_field_name ][ $field_key ] );
$data[ $parent_field_name ][ $child_field_name ][] = array( $field );
}
}
}
// handle adjusting the parent product for variations
if ( ! empty( $data['parent'] ) ) {
// attributes
if ( ! empty( $data['parent']['attributes'] ) ) {
foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) {
if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
foreach ( $attribute['options'] as $option_key => $option ) {
unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] );
$data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
}
}
}
}
// fields
foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
if ( ! empty( $data['parent'][ $parent_field_name ] ) ) {
foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) {
unset( $data['parent'][ $parent_field_name ][ $field_key ] );
$data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field );
}
}
}
}
return $data;
}
}

View File

@ -1,48 +0,0 @@
<?php
/**
* WooCommerce API
*
* Defines an interface that API request/response handlers should implement
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
interface WC_API_Handler {
/**
* Get the content type for the response
*
* This should return the proper HTTP content-type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type();
/**
* Parse the raw request body entity into an array
*
* @since 2.1
* @param string $data
* @return array
*/
public function parse_body( $data );
/**
* Generate a response from an array of data
*
* @since 2.1
* @param array $data
* @return string
*/
public function generate_response( $data );
}

View File

@ -1,408 +0,0 @@
<?php
/**
* WooCommerce API Authentication Class
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1.0
* @version 2.4.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Authentication {
/**
* Setup class
*
* @since 2.1
*/
public function __construct() {
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
}
/**
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
*
* @since 2.1
* @param WP_User $user
* @return null|WP_Error|WP_User
*/
public function authenticate( $user ) {
// Allow access to the index by default
if ( '/' === WC()->api->server->path ) {
return new WP_User( 0 );
}
try {
if ( is_ssl() ) {
$keys = $this->perform_ssl_authentication();
} else {
$keys = $this->perform_oauth_authentication();
}
// Check API key-specific permission
$this->check_api_key_permissions( $keys['permissions'] );
$user = $this->get_user_by_id( $keys['user_id'] );
$this->update_api_key_last_access( $keys['key_id'] );
} catch ( Exception $e ) {
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
}
return $user;
}
/**
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
* attacks, so the request can be authenticated by simply looking up the user
* associated with the given consumer key and confirming the consumer secret
* provided is valid
*
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_ssl_authentication() {
$params = WC()->api->server->params['GET'];
// Get consumer key
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
// Should be in HTTP Auth header by default
$consumer_key = $_SERVER['PHP_AUTH_USER'];
} elseif ( ! empty( $params['consumer_key'] ) ) {
// Allow a query string parameter as a fallback
$consumer_key = $params['consumer_key'];
} else {
throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
}
// Get consumer secret
if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
// Should be in HTTP Auth header by default
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
} elseif ( ! empty( $params['consumer_secret'] ) ) {
// Allow a query string parameter as a fallback
$consumer_secret = $params['consumer_secret'];
} else {
throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
}
$keys = $this->get_keys_by_consumer_key( $consumer_key );
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
/**
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
*
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
*
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
*
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
*
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
* This is because there is no cross-OS function within PHP to get the raw Authorization header
*
* @link http://tools.ietf.org/html/rfc5849 for the full spec
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_oauth_authentication() {
$params = WC()->api->server->params['GET'];
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
// Check for required OAuth parameters
foreach ( $param_names as $param_name ) {
if ( empty( $params[ $param_name ] ) ) {
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
}
}
// Fetch WP user by consumer key
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
// Perform OAuth validation
$this->check_oauth_signature( $keys, $params );
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
// Authentication successful, return user
return $keys;
}
/**
* Return the keys for the given consumer key
*
* @since 2.4.0
* @param string $consumer_key
* @return array
* @throws Exception
*/
private function get_keys_by_consumer_key( $consumer_key ) {
global $wpdb;
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
$keys = $wpdb->get_row( $wpdb->prepare( "
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
FROM {$wpdb->prefix}woocommerce_api_keys
WHERE consumer_key = '%s'
", $consumer_key ), ARRAY_A );
if ( empty( $keys ) ) {
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
/**
* Get user by ID
*
* @since 2.4.0
* @param int $user_id
* @return WP_User
* @throws Exception
*/
private function get_user_by_id( $user_id ) {
$user = get_user_by( 'id', $user_id );
if ( ! $user ) {
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
}
return $user;
}
/**
* Check if the consumer secret provided for the given user is valid
*
* @since 2.1
* @param string $keys_consumer_secret
* @param string $consumer_secret
* @return bool
*/
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
return hash_equals( $keys_consumer_secret, $consumer_secret );
}
/**
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
* has a valid key/secret
*
* @param array $keys
* @param array $params the request parameters
* @throws Exception
*/
private function check_oauth_signature( $keys, $params ) {
$http_method = strtoupper( WC()->api->server->method );
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
unset( $params['oauth_signature'] );
// Remove filters and convert them from array to strings to void normalize issues
if ( isset( $params['filter'] ) ) {
$filters = $params['filter'];
unset( $params['filter'] );
foreach ( $filters as $filter => $filter_value ) {
$params[ 'filter[' . $filter . ']' ] = $filter_value;
}
}
// Normalize parameter key/values
$params = $this->normalize_parameters( $params );
// Sort parameters
if ( ! uksort( $params, 'strcmp' ) ) {
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
}
// Form query string
$query_params = array();
foreach ( $params as $param_key => $param_value ) {
$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
}
$query_string = implode( '%26', $query_params ); // join with ampersand
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
}
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );
if ( ! hash_equals( $signature, $consumer_signature ) ) {
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
}
}
/**
* Normalize each parameter by assuming each parameter may have already been
* encoded, so attempt to decode, and then re-encode according to RFC 3986
*
* Note both the key and value is normalized so a filter param like:
*
* 'filter[period]' => 'week'
*
* is encoded to:
*
* 'filter%5Bperiod%5D' => 'week'
*
* This conforms to the OAuth 1.0a spec which indicates the entire query string
* should be URL encoded
*
* @since 2.1
* @see rawurlencode()
* @param array $parameters un-normalized parameters
* @return array normalized parameters
*/
private function normalize_parameters( $parameters ) {
$normalized_parameters = array();
foreach ( $parameters as $key => $value ) {
// Percent symbols (%) must be double-encoded
$key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
$normalized_parameters[ $key ] = $value;
}
return $normalized_parameters;
}
/**
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
* an attacker could attempt to re-send an intercepted request at a later time.
*
* - A timestamp is valid if it is within 15 minutes of now
* - A nonce is valid if it has not been used within the last 15 minutes
*
* @param array $keys
* @param int $timestamp the unix timestamp for when the request was made
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
* @throws Exception
*/
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
global $wpdb;
$valid_window = 15 * 60; // 15 minute window
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
}
$used_nonces = maybe_unserialize( $keys['nonces'] );
if ( empty( $used_nonces ) ) {
$used_nonces = array();
}
if ( in_array( $nonce, $used_nonces ) ) {
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
}
$used_nonces[ $timestamp ] = $nonce;
// Remove expired nonces
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
unset( $used_nonces[ $nonce_timestamp ] );
}
}
$used_nonces = maybe_serialize( $used_nonces );
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'nonces' => $used_nonces ),
array( 'key_id' => $keys['key_id'] ),
array( '%s' ),
array( '%d' )
);
}
/**
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
*
* @param string $key_permissions
* @throws Exception if the permission check fails
*/
public function check_api_key_permissions( $key_permissions ) {
switch ( WC()->api->server->method ) {
case 'HEAD':
case 'GET':
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
}
break;
}
}
/**
* Updated API Key last access datetime
*
* @since 2.4.0
*
* @param int $key_id
*/
private function update_api_key_last_access( $key_id ) {
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'last_access' => current_time( 'mysql' ) ),
array( 'key_id' => $key_id ),
array( '%s' ),
array( '%d' )
);
}
}

View File

@ -1,575 +0,0 @@
<?php
/**
* WooCommerce API Coupons Class
*
* Handles requests to the /coupons endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Coupons extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/coupons';
/**
* Register the routes for this class
*
* GET /coupons
* GET /coupons/count
* GET /coupons/<id>
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET/POST /coupons
$routes[ $this->base ] = array(
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /coupons/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
);
# GET/PUT/DELETE /coupons/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_coupon' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_coupon' ), WC_API_Server::DELETABLE ),
);
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
);
# POST|PUT /coupons/bulk
$routes[ $this->base . '/bulk' ] = array(
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
return $routes;
}
/**
* Get all coupons
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_coupons( $filter );
$coupons = array();
foreach ( $query->posts as $coupon_id ) {
if ( ! $this->is_readable( $coupon_id ) ) {
continue;
}
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'coupons' => $coupons );
}
/**
* Get the coupon for the given ID
*
* @since 2.1
* @param int $id the coupon ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_coupon( $id, $fields = null ) {
try {
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$coupon = new WC_Coupon( $id );
if ( 0 === $coupon->get_id() ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
}
$coupon_data = array(
'id' => $coupon->get_id(),
'code' => $coupon->get_code(),
'type' => $coupon->get_discount_type(),
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
'individual_use' => $coupon->get_individual_use(),
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
'usage_count' => (int) $coupon->get_usage_count(),
'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
'enable_free_shipping' => $coupon->get_free_shipping(),
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
'customer_emails' => $coupon->get_email_restrictions(),
'description' => $coupon->get_description(),
);
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of coupons
*
* @since 2.1
*
* @param array $filter
*
* @return array|WP_Error
*/
public function get_coupons_count( $filter = array() ) {
try {
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
}
$query = $this->query_coupons( $filter );
return array( 'count' => (int) $query->found_posts );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the coupon for the given code
*
* @since 2.1
* @param string $code the coupon code
* @param string $fields fields to include in response
* @return int|WP_Error
*/
public function get_coupon_by_code( $code, $fields = null ) {
global $wpdb;
try {
$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );
if ( is_null( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
}
return $this->get_coupon( $id, $fields );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create a coupon
*
* @since 2.2
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_coupon( $data ) {
global $wpdb;
try {
if ( ! isset( $data['coupon'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
}
$data = $data['coupon'];
// Check user permission
if ( ! current_user_can( 'publish_shop_coupons' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );
// Check if coupon code is specified
if ( ! isset( $data['code'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
}
$coupon_code = wc_format_coupon_code( $data['code'] );
$id_from_code = wc_get_coupon_id_by_code( $coupon_code );
if ( $id_from_code ) {
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
}
$defaults = array(
'type' => 'fixed_cart',
'amount' => 0,
'individual_use' => false,
'product_ids' => array(),
'exclude_product_ids' => array(),
'usage_limit' => '',
'usage_limit_per_user' => '',
'limit_usage_to_x_items' => '',
'usage_count' => '',
'expiry_date' => '',
'enable_free_shipping' => false,
'product_category_ids' => array(),
'exclude_product_category_ids' => array(),
'exclude_sale_items' => false,
'minimum_amount' => '',
'maximum_amount' => '',
'customer_emails' => array(),
'description' => '',
);
$coupon_data = wp_parse_args( $data, $defaults );
// Validate coupon types
if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
}
$new_coupon = array(
'post_title' => $coupon_code,
'post_content' => '',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_type' => 'shop_coupon',
'post_excerpt' => $coupon_data['description'],
);
$id = wp_insert_post( $new_coupon, true );
if ( is_wp_error( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
}
// Set coupon meta
update_post_meta( $id, 'discount_type', $coupon_data['type'] );
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );
do_action( 'woocommerce_api_create_coupon', $id, $data );
do_action( 'woocommerce_new_coupon', $id );
$this->server->send_status( 201 );
return $this->get_coupon( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a coupon
*
* @since 2.2
*
* @param int $id the coupon ID
* @param array $data
*
* @return array|WP_Error
*/
public function edit_coupon( $id, $data ) {
try {
if ( ! isset( $data['coupon'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
}
$data = $data['coupon'];
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );
if ( isset( $data['code'] ) ) {
global $wpdb;
$coupon_code = wc_format_coupon_code( $data['code'] );
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
}
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );
if ( 0 === $updated ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
}
}
if ( isset( $data['description'] ) ) {
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );
if ( 0 === $updated ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
}
}
if ( isset( $data['type'] ) ) {
// Validate coupon types
if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
}
update_post_meta( $id, 'discount_type', $data['type'] );
}
if ( isset( $data['amount'] ) ) {
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
}
if ( isset( $data['individual_use'] ) ) {
update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
}
if ( isset( $data['product_ids'] ) ) {
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
}
if ( isset( $data['exclude_product_ids'] ) ) {
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
}
if ( isset( $data['usage_limit'] ) ) {
update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
}
if ( isset( $data['usage_limit_per_user'] ) ) {
update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
}
if ( isset( $data['limit_usage_to_x_items'] ) ) {
update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
}
if ( isset( $data['usage_count'] ) ) {
update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
}
if ( isset( $data['expiry_date'] ) ) {
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
}
if ( isset( $data['enable_free_shipping'] ) ) {
update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
}
if ( isset( $data['product_category_ids'] ) ) {
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
}
if ( isset( $data['exclude_product_category_ids'] ) ) {
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
}
if ( isset( $data['exclude_sale_items'] ) ) {
update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
}
if ( isset( $data['minimum_amount'] ) ) {
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
}
if ( isset( $data['maximum_amount'] ) ) {
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
}
if ( isset( $data['customer_emails'] ) ) {
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
}
do_action( 'woocommerce_api_edit_coupon', $id, $data );
do_action( 'woocommerce_update_coupon', $id );
return $this->get_coupon( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a coupon
*
* @since 2.2
* @param int $id the coupon ID
* @param bool $force true to permanently delete coupon, false to move to trash
* @return array|WP_Error
*/
public function delete_coupon( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_coupon', $id, $this );
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
}
/**
* expiry_date format
*
* @since 2.3.0
* @param string $expiry_date
* @param bool $as_timestamp (default: false)
* @return string|int
*/
protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
if ( '' != $expiry_date ) {
if ( $as_timestamp ) {
return strtotime( $expiry_date );
}
return date( 'Y-m-d', strtotime( $expiry_date ) );
}
return '';
}
/**
* Helper method to get coupon post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_coupons( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
);
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
/**
* Bulk update or insert coupons
* Accepts an array with coupons in the formats supported by
* WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
*
* @since 2.4.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function bulk( $data ) {
try {
if ( ! isset( $data['coupons'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
}
$data = $data['coupons'];
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );
// Limit bulk operation
if ( count( $data ) > $limit ) {
throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
}
$coupons = array();
foreach ( $data as $_coupon ) {
$coupon_id = 0;
// Try to get the coupon ID
if ( isset( $_coupon['id'] ) ) {
$coupon_id = intval( $_coupon['id'] );
}
// Coupon exists / edit coupon
if ( $coupon_id ) {
$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );
if ( is_wp_error( $edit ) ) {
$coupons[] = array(
'id' => $coupon_id,
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
);
} else {
$coupons[] = $edit['coupon'];
}
} else {
// Coupon don't exists / create coupon
$new = $this->create_coupon( array( 'coupon' => $_coupon ) );
if ( is_wp_error( $new ) ) {
$coupons[] = array(
'id' => $coupon_id,
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
);
} else {
$coupons[] = $new['coupon'];
}
}
}
return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,837 +0,0 @@
<?php
/**
* WooCommerce API Customers Class
*
* Handles requests to the /customers endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Customers extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/customers';
/** @var string $created_at_min for date filtering */
private $created_at_min = null;
/** @var string $created_at_max for date filtering */
private $created_at_max = null;
/**
* Setup class, overridden to provide customer data to order response
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
parent::__construct( $server );
// add customer data to order responses
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
// modify WP_User_Query to support created_at date filtering
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
}
/**
* Register the routes for this class
*
* GET /customers
* GET /customers/count
* GET /customers/<id>
* GET /customers/<id>/orders
*
* @since 2.2
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET/POST /customers
$routes[ $this->base ] = array(
array( array( $this, 'get_customers' ), WC_API_Server::READABLE ),
array( array( $this, 'create_customer' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /customers/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_customers_count' ), WC_API_Server::READABLE ),
);
# GET/PUT/DELETE /customers/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_customer' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_customer' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_customer' ), WC_API_Server::DELETABLE ),
);
# GET /customers/email/<email>
$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
array( array( $this, 'get_customer_by_email' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>/orders
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
array( array( $this, 'get_customer_orders' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>/downloads
$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
array( array( $this, 'get_customer_downloads' ), WC_API_Server::READABLE ),
);
# POST|PUT /customers/bulk
$routes[ $this->base . '/bulk' ] = array(
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
return $routes;
}
/**
* Get all customers
*
* @since 2.1
* @param array $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_customers( $filter );
$customers = array();
foreach ( $query->get_results() as $user_id ) {
if ( ! $this->is_readable( $user_id ) ) {
continue;
}
$customers[] = current( $this->get_customer( $user_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'customers' => $customers );
}
/**
* Get the customer for the given ID
*
* @since 2.1
* @param int $id the customer ID
* @param array $fields
* @return array|WP_Error
*/
public function get_customer( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$customer = new WC_Customer( $id );
$last_order = $customer->get_last_order();
$customer_data = array(
'id' => $customer->get_id(),
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'email' => $customer->get_email(),
'first_name' => $customer->get_first_name(),
'last_name' => $customer->get_last_name(),
'username' => $customer->get_username(),
'role' => $customer->get_role(),
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
'orders_count' => $customer->get_order_count(),
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
'avatar_url' => $customer->get_avatar_url(),
'billing_address' => array(
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
),
);
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
}
/**
* Get the customer for the given email
*
* @since 2.1
*
* @param string $email the customer email
* @param array $fields
*
* @return array|WP_Error
*/
public function get_customer_by_email( $email, $fields = null ) {
try {
if ( is_email( $email ) ) {
$customer = get_user_by( 'email', $email );
if ( ! is_object( $customer ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
}
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
}
return $this->get_customer( $customer->ID, $fields );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of customers
*
* @since 2.1
*
* @param array $filter
*
* @return array|WP_Error
*/
public function get_customers_count( $filter = array() ) {
try {
if ( ! current_user_can( 'list_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
}
$query = $this->query_customers( $filter );
return array( 'count' => $query->get_total() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get customer billing address fields.
*
* @since 2.2
* @return array
*/
protected function get_customer_billing_address() {
$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
'email',
'phone',
) );
return $billing_address;
}
/**
* Get customer shipping address fields.
*
* @since 2.2
* @return array
*/
protected function get_customer_shipping_address() {
$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
) );
return $shipping_address;
}
/**
* Add/Update customer data.
*
* @since 2.2
* @param int $id the customer ID
* @param array $data
* @param WC_Customer $customer
*/
protected function update_customer_data( $id, $data, $customer ) {
// Customer first name.
if ( isset( $data['first_name'] ) ) {
$customer->set_first_name( wc_clean( $data['first_name'] ) );
}
// Customer last name.
if ( isset( $data['last_name'] ) ) {
$customer->set_last_name( wc_clean( $data['last_name'] ) );
}
// Customer billing address.
if ( isset( $data['billing_address'] ) ) {
foreach ( $this->get_customer_billing_address() as $field ) {
if ( isset( $data['billing_address'][ $field ] ) ) {
if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
} else {
$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
}
}
}
}
// Customer shipping address.
if ( isset( $data['shipping_address'] ) ) {
foreach ( $this->get_customer_shipping_address() as $field ) {
if ( isset( $data['shipping_address'][ $field ] ) ) {
if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
} else {
$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
}
}
}
}
do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
}
/**
* Create a customer
*
* @since 2.2
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_customer( $data ) {
try {
if ( ! isset( $data['customer'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
}
$data = $data['customer'];
// Checks with can create new users.
if ( ! current_user_can( 'create_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );
// Checks with the email is missing.
if ( ! isset( $data['email'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
}
// Create customer.
$customer = new WC_Customer;
$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
$customer->set_email( $data['email'] );
$customer->save();
if ( ! $customer->get_id() ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
}
// Added customer data.
$this->update_customer_data( $customer->get_id(), $data, $customer );
$customer->save();
do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );
$this->server->send_status( 201 );
return $this->get_customer( $customer->get_id() );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a customer
*
* @since 2.2
*
* @param int $id the customer ID
* @param array $data
*
* @return array|WP_Error
*/
public function edit_customer( $id, $data ) {
try {
if ( ! isset( $data['customer'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
}
$data = $data['customer'];
// Validate the customer ID.
$id = $this->validate_request( $id, 'customer', 'edit' );
// Return the validate error.
if ( is_wp_error( $id ) ) {
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
}
$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );
$customer = new WC_Customer( $id );
// Customer email.
if ( isset( $data['email'] ) ) {
$customer->set_email( $data['email'] );
}
// Customer password.
if ( isset( $data['password'] ) ) {
$customer->set_password( $data['password'] );
}
// Update customer data.
$this->update_customer_data( $customer->get_id(), $data, $customer );
$customer->save();
do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );
return $this->get_customer( $customer->get_id() );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a customer
*
* @since 2.2
* @param int $id the customer ID
* @return array|WP_Error
*/
public function delete_customer( $id ) {
// Validate the customer ID.
$id = $this->validate_request( $id, 'customer', 'delete' );
// Return the validate error.
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_customer', $id, $this );
return $this->delete( $id, 'customer' );
}
/**
* Get the orders for a customer
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_customer_orders( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$order_ids = wc_get_orders( array(
'customer' => $id,
'limit' => -1,
'orderby' => 'date',
'order' => 'ASC',
'return' => 'ids',
) );
if ( empty( $order_ids ) ) {
return array( 'orders' => array() );
}
$orders = array();
foreach ( $order_ids as $order_id ) {
$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
}
return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
}
/**
* Get the available downloads for a customer
*
* @since 2.2
* @param int $id the customer ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_customer_downloads( $id, $fields = null ) {
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$downloads = array();
$_downloads = wc_get_customer_available_downloads( $id );
foreach ( $_downloads as $key => $download ) {
$downloads[] = array(
'download_url' => $download['download_url'],
'download_id' => $download['download_id'],
'product_id' => $download['product_id'],
'download_name' => $download['download_name'],
'order_id' => $download['order_id'],
'order_key' => $download['order_key'],
'downloads_remaining' => $download['downloads_remaining'],
'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
'file' => $download['file'],
);
}
return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
}
/**
* Helper method to get customer user objects
*
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
* pagination support
*
* The filter for role can only be a single role in a string.
*
* @since 2.3
* @param array $args request arguments for filtering query
* @return WP_User_Query
*/
private function query_customers( $args = array() ) {
// default users per page
$users_per_page = get_option( 'posts_per_page' );
// Set base query arguments
$query_args = array(
'fields' => 'ID',
'role' => 'customer',
'orderby' => 'registered',
'number' => $users_per_page,
);
// Custom Role
if ( ! empty( $args['role'] ) ) {
$query_args['role'] = $args['role'];
}
// Search
if ( ! empty( $args['q'] ) ) {
$query_args['search'] = $args['q'];
}
// Limit number of users returned
if ( ! empty( $args['limit'] ) ) {
if ( -1 == $args['limit'] ) {
unset( $query_args['number'] );
} else {
$query_args['number'] = absint( $args['limit'] );
$users_per_page = absint( $args['limit'] );
}
} else {
$args['limit'] = $query_args['number'];
}
// Page
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
// Offset
if ( ! empty( $args['offset'] ) ) {
$query_args['offset'] = absint( $args['offset'] );
} else {
$query_args['offset'] = $users_per_page * ( $page - 1 );
}
// Created date
if ( ! empty( $args['created_at_min'] ) ) {
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
}
if ( ! empty( $args['created_at_max'] ) ) {
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
}
// Order (ASC or DESC, ASC by default)
if ( ! empty( $args['order'] ) ) {
$query_args['order'] = $args['order'];
}
// Order by
if ( ! empty( $args['orderby'] ) ) {
$query_args['orderby'] = $args['orderby'];
// Allow sorting by meta value
if ( ! empty( $args['orderby_meta_key'] ) ) {
$query_args['meta_key'] = $args['orderby_meta_key'];
}
}
$query = new WP_User_Query( $query_args );
// Helper members for pagination headers
$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
$query->page = $page;
return $query;
}
/**
* Add customer data to orders
*
* @since 2.1
* @param $order_data
* @param $order
* @return array
*/
public function add_customer_data( $order_data, $order ) {
if ( 0 == $order->get_user_id() ) {
// add customer data from order
$order_data['customer'] = array(
'id' => 0,
'email' => $order->get_billing_email(),
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'billing_address' => array(
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
),
);
} else {
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
}
return $order_data;
}
/**
* Modify the WP_User_Query to support filtering on the date the customer was created
*
* @since 2.1
* @param WP_User_Query $query
*/
public function modify_user_query( $query ) {
if ( $this->created_at_min ) {
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
}
if ( $this->created_at_max ) {
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid WP_User
* 3) the current user has the proper permissions
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param integer $id the customer ID
* @param string $type the request type, unused because this method overrides the parent class
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
try {
$id = absint( $id );
// validate ID
if ( empty( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
}
// non-existent IDs return a valid WP_User object with the user ID = 0
$customer = new WP_User( $id );
if ( 0 === $customer->ID ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
}
// validate permissions
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'list_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
}
break;
case 'edit':
if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
}
break;
case 'delete':
if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
}
break;
}
return $id;
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Check if the current user can read users
*
* @since 2.1
* @see WC_API_Resource::is_readable()
* @param int|WP_Post $post unused
* @return bool true if the current user can read users, false otherwise
*/
protected function is_readable( $post ) {
return current_user_can( 'list_users' );
}
/**
* Bulk update or insert customers
* Accepts an array with customers in the formats supported by
* WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
*
* @since 2.4.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function bulk( $data ) {
try {
if ( ! isset( $data['customers'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
}
$data = $data['customers'];
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );
// Limit bulk operation
if ( count( $data ) > $limit ) {
throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
}
$customers = array();
foreach ( $data as $_customer ) {
$customer_id = 0;
// Try to get the customer ID
if ( isset( $_customer['id'] ) ) {
$customer_id = intval( $_customer['id'] );
}
// Customer exists / edit customer
if ( $customer_id ) {
$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );
if ( is_wp_error( $edit ) ) {
$customers[] = array(
'id' => $customer_id,
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
);
} else {
$customers[] = $edit['customer'];
}
} else {
// Customer don't exists / create customer
$new = $this->create_customer( array( 'customer' => $_customer ) );
if ( is_wp_error( $new ) ) {
$customers[] = array(
'id' => $customer_id,
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
);
} else {
$customers[] = $new['customer'];
}
}
}
return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,48 +0,0 @@
<?php
/**
* WooCommerce API Exception Class
*
* Extends Exception to provide additional data
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Exception extends Exception {
/** @var string sanitized error code */
protected $error_code;
/**
* Setup exception, requires 3 params:
*
* error code - machine-readable, e.g. `woocommerce_invalid_product_id`
* error message - friendly message, e.g. 'Product ID is invalid'
* http status code - proper HTTP status code to respond with, e.g. 400
*
* @since 2.2
* @param string $error_code
* @param string $error_message user-friendly translated error message
* @param int $http_status_code HTTP status code to respond with
*/
public function __construct( $error_code, $error_message, $http_status_code ) {
$this->error_code = $error_code;
parent::__construct( $error_message, $http_status_code );
}
/**
* Returns the error code
*
* @since 2.2
* @return string
*/
public function getErrorCode() {
return $this->error_code;
}
}

View File

@ -1,73 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles parsing JSON request bodies and generating JSON responses
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_JSON_Handler implements WC_API_Handler {
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $body the raw request body
* @return array|mixed
*/
public function parse_body( $body ) {
return json_decode( $body, true );
}
/**
* Generate a JSON response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
if ( isset( $_GET['_jsonp'] ) ) {
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
}
$jsonp_callback = $_GET['_jsonp'];
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
}
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
}
return wp_json_encode( $data );
}
}

View File

@ -1,329 +0,0 @@
<?php
/**
* WooCommerce API Reports Class
*
* Handles requests to the /reports endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Reports extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/reports';
/** @var WC_Admin_Report instance */
private $report;
/**
* Register the routes for this class
*
* GET /reports
* GET /reports/sales
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /reports
$routes[ $this->base ] = array(
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
);
# GET /reports/sales
$routes[ $this->base . '/sales' ] = array(
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
);
# GET /reports/sales/top_sellers
$routes[ $this->base . '/sales/top_sellers' ] = array(
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get a simple listing of available reports
*
* @since 2.1
* @return array
*/
public function get_reports() {
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
}
/**
* Get the sales report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_sales_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
// check for WP_Error
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
// new customers
$users_query = new WP_User_Query(
array(
'fields' => array( 'user_registered' ),
'role' => 'customer',
)
);
$customers = $users_query->get_results();
foreach ( $customers as $key => $customer ) {
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
unset( $customers[ $key ] );
}
}
$total_customers = count( $customers );
$report_data = $this->report->get_report_data();
$period_totals = array();
// setup period totals by ensuring each period in the interval has data
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
switch ( $this->report->chart_groupby ) {
case 'day' :
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
break;
default :
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
break;
}
// set the customer signups for each period
$customer_count = 0;
foreach ( $customers as $customer ) {
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
$customer_count++;
}
}
$period_totals[ $time ] = array(
'sales' => wc_format_decimal( 0.00, 2 ),
'orders' => 0,
'items' => 0,
'tax' => wc_format_decimal( 0.00, 2 ),
'shipping' => wc_format_decimal( 0.00, 2 ),
'discount' => wc_format_decimal( 0.00, 2 ),
'customers' => $customer_count,
);
}
// add total sales, total order count, total tax and total shipping for each period
foreach ( $report_data->orders as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
}
foreach ( $report_data->order_counts as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['orders'] = (int) $order->count;
}
// add total order items for each period
foreach ( $report_data->order_items as $order_item ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
}
// add total discount for each period
foreach ( $report_data->coupons as $discount ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
}
$sales_data = array(
'total_sales' => $report_data->total_sales,
'net_sales' => $report_data->net_sales,
'average_sales' => $report_data->average_sales,
'total_orders' => $report_data->total_orders,
'total_items' => $report_data->total_items,
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
'total_shipping' => $report_data->total_shipping,
'total_refunds' => $report_data->total_refunds,
'total_discount' => $report_data->total_coupons,
'totals_grouped_by' => $this->report->chart_groupby,
'totals' => $period_totals,
'total_customers' => $total_customers,
);
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
}
/**
* Get the top sellers report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_top_sellers_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
$top_sellers = $this->report->get_order_report_data( array(
'data' => array(
'_product_id' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => '',
'name' => 'product_id',
),
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty',
),
),
'order_by' => 'order_item_qty DESC',
'group_by' => 'product_id',
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
'query_type' => 'get_results',
'filter_range' => true,
) );
$top_sellers_data = array();
foreach ( $top_sellers as $top_seller ) {
$product = wc_get_product( $top_seller->product_id );
if ( $product ) {
$top_sellers_data[] = array(
'title' => $product->get_name(),
'product_id' => $top_seller->product_id,
'quantity' => $top_seller->order_item_qty,
);
}
}
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
}
/**
* Setup the report object and parse any date filtering
*
* @since 2.1
* @param array $filter date filtering
*/
private function setup_report( $filter ) {
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
$this->report = new WC_Report_Sales_By_Date();
if ( empty( $filter['period'] ) ) {
// custom date range
$filter['period'] = 'custom';
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
} else {
// default custom range to today
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
}
} else {
// ensure period is valid
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
$filter['period'] = 'week';
}
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
// allow "week" for period instead of "7day"
if ( 'week' === $filter['period'] ) {
$filter['period'] = '7day';
}
}
$this->report->calculate_current_range( $filter['period'] );
}
/**
* Verify that the current user has permission to view reports
*
* @since 2.1
* @see WC_API_Resource::validate_request()
*
* @param null $id unused
* @param null $type unused
* @param null $context unused
*
* @return bool|WP_Error
*/
protected function validate_request( $id = null, $type = null, $context = null ) {
if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
} else {
return true;
}
}
}

View File

@ -1,466 +0,0 @@
<?php
/**
* WooCommerce API Resource class
*
* Provides shared functionality for resource-specific API classes
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Resource {
/** @var WC_API_Server the API server */
protected $server;
/** @var string sub-classes override this to set a resource-specific base route */
protected $base;
/**
* Setup class
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
$this->server = $server;
// automatically register routes for sub-classes
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
// maybe add meta to top-level resource responses
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
}
$response_names = array(
'order',
'coupon',
'customer',
'product',
'report',
'customer_orders',
'customer_downloads',
'order_note',
'order_refund',
'product_reviews',
'product_category',
);
foreach ( $response_names as $name ) {
/**
* Remove fields from responses when requests specify certain fields
* note these are hooked at a later priority so data added via
* filters (e.g. customer data to the order response) still has the
* fields filtered properly
*/
add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid post object and matches the provided post type
* 3) the current user has the proper permissions to read/edit/delete the post
*
* @since 2.1
* @param string|int $id the post ID
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
$id = absint( $id );
// Validate ID
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
}
// Only custom post types have per-post type/permission checks
if ( 'customer' !== $type ) {
$post = get_post( $id );
if ( null === $post ) {
return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
}
// For checking permissions, product variations are the same as the product post type
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
// Validate post type
if ( $type !== $post_type ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
}
// Validate permissions
switch ( $context ) {
case 'read':
if ( ! $this->is_readable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( ! $this->is_editable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( ! $this->is_deletable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
}
}
return $id;
}
/**
* Add common request arguments to argument list before WP_Query is run
*
* @since 2.1
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
* @param array $request_args arguments provided in the request
* @return array
*/
protected function merge_query_args( $base_args, $request_args ) {
$args = array();
// date
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'] = array();
// resources created after specified date
if ( ! empty( $request_args['created_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
}
// resources created before specified date
if ( ! empty( $request_args['created_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
}
// resources updated after specified date
if ( ! empty( $request_args['updated_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
}
// resources updated before specified date
if ( ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
}
}
// search
if ( ! empty( $request_args['q'] ) ) {
$args['s'] = $request_args['q'];
}
// resources per response
if ( ! empty( $request_args['limit'] ) ) {
$args['posts_per_page'] = $request_args['limit'];
}
// resource offset
if ( ! empty( $request_args['offset'] ) ) {
$args['offset'] = $request_args['offset'];
}
// order (ASC or DESC, ASC by default)
if ( ! empty( $request_args['order'] ) ) {
$args['order'] = $request_args['order'];
}
// orderby
if ( ! empty( $request_args['orderby'] ) ) {
$args['orderby'] = $request_args['orderby'];
// allow sorting by meta value
if ( ! empty( $request_args['orderby_meta_key'] ) ) {
$args['meta_key'] = $request_args['orderby_meta_key'];
}
}
// allow post status change
if ( ! empty( $request_args['post_status'] ) ) {
$args['post_status'] = $request_args['post_status'];
unset( $request_args['post_status'] );
}
// filter by a list of post id
if ( ! empty( $request_args['in'] ) ) {
$args['post__in'] = explode( ',', $request_args['in'] );
unset( $request_args['in'] );
}
// filter by a list of post id
if ( ! empty( $request_args['in'] ) ) {
$args['post__in'] = explode( ',', $request_args['in'] );
unset( $request_args['in'] );
}
// resource page
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );
return array_merge( $base_args, $args );
}
/**
* Add meta to resources when requested by the client. Meta is added as a top-level
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
*
* @since 2.1
* @param array $data the resource data
* @param object $resource the resource object (e.g WC_Order)
* @return mixed
*/
public function maybe_add_meta( $data, $resource ) {
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
// don't attempt to add meta more than once
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
return $data;
}
// define the top-level property name for the meta
switch ( get_class( $resource ) ) {
case 'WC_Order':
$meta_name = 'order_meta';
break;
case 'WC_Coupon':
$meta_name = 'coupon_meta';
break;
case 'WP_User':
$meta_name = 'customer_meta';
break;
default:
$meta_name = 'product_meta';
break;
}
if ( is_a( $resource, 'WP_User' ) ) {
// customer meta
$meta = (array) get_user_meta( $resource->ID );
} else {
// coupon/order/product meta
$meta = (array) get_post_meta( $resource->get_id() );
}
foreach ( $meta as $meta_key => $meta_value ) {
// don't add hidden meta by default
if ( ! is_protected_meta( $meta_key ) ) {
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
}
}
}
return $data;
}
/**
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
*
* @since 2.1
* @param array $data the response data
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
* @param array|string the requested list of fields to include in the response
* @return array response data
*/
public function filter_response_fields( $data, $resource, $fields ) {
if ( ! is_array( $data ) || empty( $fields ) ) {
return $data;
}
$fields = explode( ',', $fields );
$sub_fields = array();
// get sub fields
foreach ( $fields as $field ) {
if ( false !== strpos( $field, '.' ) ) {
list( $name, $value ) = explode( '.', $field );
$sub_fields[ $name ] = $value;
}
}
// iterate through top-level fields
foreach ( $data as $data_field => $data_value ) {
// if a field has sub-fields and the top-level field has sub-fields to filter
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
// iterate through each sub-field
foreach ( $data_value as $sub_field => $sub_field_value ) {
// remove non-matching sub-fields
if ( ! in_array( $sub_field, $sub_fields ) ) {
unset( $data[ $data_field ][ $sub_field ] );
}
}
} else {
// remove non-matching top-level fields
if ( ! in_array( $data_field, $fields ) ) {
unset( $data[ $data_field ] );
}
}
}
return $data;
}
/**
* Delete a given resource
*
* @since 2.1
* @param int $id the resource ID
* @param string $type the resource post type, or `customer`
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
* @return array|WP_Error
*/
protected function delete( $id, $type, $force = false ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
if ( 'customer' === $type ) {
$result = wp_delete_user( $id );
if ( $result ) {
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
} else {
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
}
} else {
// delete order/coupon/webhook
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
if ( ! $result ) {
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
}
if ( $force ) {
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
} else {
$this->server->send_status( '202' );
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
}
}
}
/**
* Checks if the given post is readable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_readable( $post ) {
return $this->check_permission( $post, 'read' );
}
/**
* Checks if the given post is editable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_editable( $post ) {
return $this->check_permission( $post, 'edit' );
}
/**
* Checks if the given post is deletable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_deletable( $post ) {
return $this->check_permission( $post, 'delete' );
}
/**
* Checks the permissions for the current user given a post and context
*
* @since 2.1
* @param WP_Post|int $post
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
* @return bool true if the current user has the permissions to perform the context on the post
*/
private function check_permission( $post, $context ) {
if ( ! is_a( $post, 'WP_Post' ) ) {
$post = get_post( $post );
}
if ( is_null( $post ) ) {
return false;
}
$post_type = get_post_type_object( $post->post_type );
if ( 'read' === $context ) {
return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ) );
} elseif ( 'edit' === $context ) {
return current_user_can( $post_type->cap->edit_post, $post->ID );
} elseif ( 'delete' === $context ) {
return current_user_can( $post_type->cap->delete_post, $post->ID );
} else {
return false;
}
}
}

View File

@ -1,760 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles REST API requests
*
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
* Many thanks to Ryan McCue and any other contributors!
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once ABSPATH . 'wp-admin/includes/admin.php';
class WC_API_Server {
const METHOD_GET = 1;
const METHOD_POST = 2;
const METHOD_PUT = 4;
const METHOD_PATCH = 8;
const METHOD_DELETE = 16;
const READABLE = 1; // GET
const CREATABLE = 2; // POST
const EDITABLE = 14; // POST | PUT | PATCH
const DELETABLE = 16; // DELETE
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
/**
* Does the endpoint accept a raw request body?
*/
const ACCEPT_RAW_DATA = 64;
/** Does the endpoint accept a request body? (either JSON or XML) */
const ACCEPT_DATA = 128;
/**
* Should we hide this endpoint from the index?
*/
const HIDDEN_ENDPOINT = 256;
/**
* Map of HTTP verbs to constants
* @var array
*/
public static $method_map = array(
'HEAD' => self::METHOD_GET,
'GET' => self::METHOD_GET,
'POST' => self::METHOD_POST,
'PUT' => self::METHOD_PUT,
'PATCH' => self::METHOD_PATCH,
'DELETE' => self::METHOD_DELETE,
);
/**
* Requested path (relative to the API root, wp-json.php)
*
* @var string
*/
public $path = '';
/**
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
*
* @var string
*/
public $method = 'HEAD';
/**
* Request parameters
*
* This acts as an abstraction of the superglobals
* (GET => $_GET, POST => $_POST)
*
* @var array
*/
public $params = array( 'GET' => array(), 'POST' => array() );
/**
* Request headers
*
* @var array
*/
public $headers = array();
/**
* Request files (matches $_FILES)
*
* @var array
*/
public $files = array();
/**
* Request/Response handler, either JSON by default
* or XML if requested by client
*
* @var WC_API_Handler
*/
public $handler;
/**
* Setup class and set request/response handler
*
* @since 2.1
* @param $path
*/
public function __construct( $path ) {
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else {
$path = '/';
}
}
$this->path = $path;
$this->method = $_SERVER['REQUEST_METHOD'];
$this->params['GET'] = $_GET;
$this->params['POST'] = $_POST;
$this->headers = $this->get_headers( $_SERVER );
$this->files = $_FILES;
// Compatibility for clients that can't use PUT/PATCH/DELETE
if ( isset( $_GET['_method'] ) ) {
$this->method = strtoupper( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
// load response handler
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
$this->handler = new $handler_class();
}
/**
* Check authentication for the request
*
* @since 2.1
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
*/
public function check_authentication() {
// allow plugins to remove default authentication or add their own authentication
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
if ( is_a( $user, 'WP_User' ) ) {
// API requests run under the context of the authenticated user
wp_set_current_user( $user->ID );
} elseif ( ! is_wp_error( $user ) ) {
// WP_Errors are handled in serve_request()
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
}
return $user;
}
/**
* Convert an error to an array
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behaviour, as it is represented as a
* list in JSON rather than an object/map
*
* @since 2.1
* @param WP_Error $error
* @return array List of associative arrays with code and message keys
*/
protected function error_to_array( $error ) {
$errors = array();
foreach ( (array) $error->errors as $code => $messages ) {
foreach ( (array) $messages as $message ) {
$errors[] = array( 'code' => $code, 'message' => $message );
}
}
return array( 'errors' => $errors );
}
/**
* Handle serving an API request
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 2.1
* @uses WC_API_Server::dispatch()
*/
public function serve_request() {
do_action( 'woocommerce_api_server_before_serve', $this );
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
// the API is enabled by default
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
$this->send_status( 404 );
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
return;
}
$result = $this->check_authentication();
// if authorization check was successful, dispatch the request
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch();
}
// handle any dispatch errors
if ( is_wp_error( $result ) ) {
$data = $result->get_error_data();
if ( is_array( $data ) && isset( $data['status'] ) ) {
$this->send_status( $data['status'] );
}
$result = $this->error_to_array( $result );
}
// This is a filter rather than an action, since this is designed to be
// re-entrant if needed
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
if ( ! $served ) {
if ( 'HEAD' === $this->method ) {
return;
}
echo $this->handler->generate_response( $result );
}
}
/**
* Retrieve the route map
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 2.1
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
*/
public function get_routes() {
// index added by default
$endpoints = array(
'/' => array( array( $this, 'get_index' ), self::READABLE ),
);
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
// Normalise the endpoints
foreach ( $endpoints as $route => &$handlers ) {
if ( is_array( $handlers ) && count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
$handlers = array( $handlers );
}
}
return $endpoints;
}
/**
* Match the request to a callback and call it
*
* @since 2.1
* @return mixed The value returned by the callback, or a WP_Error instance
*/
public function dispatch() {
switch ( $this->method ) {
case 'HEAD' :
case 'GET' :
$method = self::METHOD_GET;
break;
case 'POST' :
$method = self::METHOD_POST;
break;
case 'PUT' :
$method = self::METHOD_PUT;
break;
case 'PATCH' :
$method = self::METHOD_PATCH;
break;
case 'DELETE' :
$method = self::METHOD_DELETE;
break;
default :
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
}
foreach ( $this->get_routes() as $route => $handlers ) {
foreach ( $handlers as $handler ) {
$callback = $handler[0];
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
if ( ! ( $supported & $method ) ) {
continue;
}
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
if ( ! $match ) {
continue;
}
if ( ! is_callable( $callback ) ) {
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
}
$args = array_merge( $args, $this->params['GET'] );
if ( $method & self::METHOD_POST ) {
$args = array_merge( $args, $this->params['POST'] );
}
if ( $supported & self::ACCEPT_DATA ) {
$data = $this->handler->parse_body( $this->get_raw_data() );
$args = array_merge( $args, array( 'data' => $data ) );
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
$data = $this->get_raw_data();
$args = array_merge( $args, array( 'data' => $data ) );
}
$args['_method'] = $method;
$args['_route'] = $route;
$args['_path'] = $this->path;
$args['_headers'] = $this->headers;
$args['_files'] = $this->files;
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
// Allow plugins to halt the request via this filter
if ( is_wp_error( $args ) ) {
return $args;
}
$params = $this->sort_callback_params( $callback, $args );
if ( is_wp_error( $params ) ) {
return $params;
}
return call_user_func_array( $callback, $params );
}
}
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
}
/**
* urldecode deep.
*
* @since 2.2
* @param string|array $value Data to decode with urldecode.
* @return string|array Decoded data.
*/
protected function urldecode_deep( $value ) {
if ( is_array( $value ) ) {
return array_map( array( $this, 'urldecode_deep' ), $value );
} else {
return urldecode( $value );
}
}
/**
* Sort parameters by order specified in method declaration
*
* Takes a callback and a list of available params, then filters and sorts
* by the parameters the method actually needs, using the Reflection API
*
* @since 2.2
*
* @param callable|array $callback the endpoint callback
* @param array $provided the provided request parameters
*
* @return array|WP_Error
*/
protected function sort_callback_params( $callback, $provided ) {
if ( is_array( $callback ) ) {
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
} else {
$ref_func = new ReflectionFunction( $callback );
}
$wanted = $ref_func->getParameters();
$ordered_parameters = array();
foreach ( $wanted as $param ) {
if ( isset( $provided[ $param->getName() ] ) ) {
// We have this parameters in the list to choose from
if ( 'data' == $param->getName() ) {
$ordered_parameters[] = $provided[ $param->getName() ];
continue;
}
$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
} elseif ( $param->isDefaultValueAvailable() ) {
// We don't have this parameter, but it's optional
$ordered_parameters[] = $param->getDefaultValue();
} else {
// We don't have this parameter and it wasn't optional, abort!
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
}
}
return $ordered_parameters;
}
/**
* Get the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 2.3
* @return array Index entity
*/
public function get_index() {
// General site data
$available = array(
'store' => array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'URL' => get_option( 'siteurl' ),
'wc_version' => WC()->version,
'routes' => array(),
'meta' => array(
'timezone' => wc_timezone_string(),
'currency' => get_woocommerce_currency(),
'currency_format' => get_woocommerce_currency_symbol(),
'currency_position' => get_option( 'woocommerce_currency_pos' ),
'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ),
'price_num_decimals' => wc_get_price_decimals(),
'tax_included' => wc_prices_include_tax(),
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
'links' => array(
'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
),
),
),
);
// Find the available routes
foreach ( $this->get_routes() as $route => $callbacks ) {
$data = array();
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
foreach ( self::$method_map as $name => $bitmask ) {
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
continue 3;
}
if ( $callback[1] & $bitmask ) {
$data['supports'][] = $name;
}
if ( $callback[1] & self::ACCEPT_DATA ) {
$data['accepts_data'] = true;
}
// For non-variable routes, generate links
if ( strpos( $route, '<' ) === false ) {
$data['meta'] = array(
'self' => get_woocommerce_api_url( $route ),
);
}
}
}
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
}
return apply_filters( 'woocommerce_api_index', $available );
}
/**
* Send a HTTP status code
*
* @since 2.1
* @param int $code HTTP status
*/
public function send_status( $code ) {
status_header( $code );
}
/**
* Send a HTTP header
*
* @since 2.1
* @param string $key Header key
* @param string $value Header value
* @param boolean $replace Should we replace the existing header?
*/
public function header( $key, $value, $replace = true ) {
header( sprintf( '%s: %s', $key, $value ), $replace );
}
/**
* Send a Link header
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple
*
* @link http://tools.ietf.org/html/rfc5988
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
*
* @since 2.1
* @param string $rel Link relation. Either a registered type, or an absolute URL
* @param string $link Target IRI for the link
* @param array $other Other parameters to send, as an associative array
*/
public function link_header( $rel, $link, $other = array() ) {
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
foreach ( $other as $key => $value ) {
if ( 'title' == $key ) {
$value = '"' . $value . '"';
}
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Send pagination headers for resources
*
* @since 2.1
* @param WP_Query|WP_User_Query|stdClass $query
*/
public function add_pagination_headers( $query ) {
// WP_User_Query
if ( is_a( $query, 'WP_User_Query' ) ) {
$single = count( $query->get_results() ) == 1;
$total = $query->get_total();
if ( $query->get( 'number' ) > 0 ) {
$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
$total_pages = ceil( $total / $query->get( 'number' ) );
} else {
$page = 1;
$total_pages = 1;
}
} elseif ( is_a( $query, 'stdClass' ) ) {
$page = $query->page;
$single = $query->is_single;
$total = $query->total;
$total_pages = $query->total_pages;
// WP_Query
} else {
$page = $query->get( 'paged' );
$single = $query->is_single();
$total = $query->found_posts;
$total_pages = $query->max_num_pages;
}
if ( ! $page ) {
$page = 1;
}
$next_page = absint( $page ) + 1;
if ( ! $single ) {
// first/prev
if ( $page > 1 ) {
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
}
// next
if ( $next_page <= $total_pages ) {
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
}
// last
if ( $page != $total_pages ) {
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
}
}
$this->header( 'X-WC-Total', $total );
$this->header( 'X-WC-TotalPages', $total_pages );
do_action( 'woocommerce_api_pagination_headers', $this, $query );
}
/**
* Returns the request URL with the page query parameter set to the specified page
*
* @since 2.1
* @param int $page
* @return string
*/
private function get_paginated_url( $page ) {
// remove existing page query param
$request = remove_query_arg( 'page' );
// add provided page query param
$request = urldecode( add_query_arg( 'page', $page, $request ) );
// get the home host
$host = parse_url( get_home_url(), PHP_URL_HOST );
return set_url_scheme( "http://{$host}{$request}" );
}
/**
* Retrieve the raw request entity (body)
*
* @since 2.1
* @return string
*/
public function get_raw_data() {
return file_get_contents( 'php://input' );
}
/**
* Parse an RFC3339 datetime into a MySQl datetime
*
* Invalid dates default to unix epoch
*
* @since 2.1
* @param string $datetime RFC3339 datetime
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
*/
public function parse_datetime( $datetime ) {
// Strip millisecond precision (a full stop followed by one or more digits)
if ( strpos( $datetime, '.' ) !== false ) {
$datetime = preg_replace( '/\.\d+/', '', $datetime );
}
// default timezone to UTC
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
try {
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
} catch ( Exception $e ) {
$datetime = new DateTime( '@0' );
}
return $datetime->format( 'Y-m-d H:i:s' );
}
/**
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
*
* @since 2.1
* @param int|string $timestamp unix timestamp or MySQL datetime
* @param bool $convert_to_utc
* @param bool $convert_to_gmt Use GMT timezone.
* @return string RFC3339 datetime
*/
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
if ( $convert_to_gmt ) {
if ( is_numeric( $timestamp ) ) {
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
}
$timestamp = get_gmt_from_date( $timestamp );
}
if ( $convert_to_utc ) {
$timezone = new DateTimeZone( wc_timezone_string() );
} else {
$timezone = new DateTimeZone( 'UTC' );
}
try {
if ( is_numeric( $timestamp ) ) {
$date = new DateTime( "@{$timestamp}" );
} else {
$date = new DateTime( $timestamp, $timezone );
}
// convert to UTC by adjusting the time based on the offset of the site's timezone
if ( $convert_to_utc ) {
$date->modify( -1 * $date->getOffset() . ' seconds' );
}
} catch ( Exception $e ) {
$date = new DateTime( '@0' );
}
return $date->format( 'Y-m-d\TH:i:s\Z' );
}
/**
* Extract headers from a PHP-style $_SERVER array
*
* @since 2.1
* @param array $server Associative array similar to $_SERVER
* @return array Headers extracted from the input
*/
public function get_headers( $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
foreach ( $server as $key => $value ) {
if ( strpos( $key, 'HTTP_' ) === 0 ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
}

View File

@ -1,509 +0,0 @@
<?php
/**
* WooCommerce API Webhooks class
*
* Handles requests to the /webhooks endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Webhooks extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/webhooks';
/**
* Register the routes for this class
*
* @since 2.2
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET|POST /webhooks
$routes[ $this->base ] = array(
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /webhooks/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /webhooks/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
);
# GET /webhooks/<id>/deliveries
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
);
# GET /webhooks/<webhook_id>/deliveries/<id>
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all webhooks
*
* @since 2.2
*
* @param array $fields
* @param array $filter
* @param string $status
* @param int $page
*
* @return array
*/
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$filter['page'] = $page;
$query = $this->query_webhooks( $filter );
$webhooks = array();
foreach ( $query['results'] as $webhook_id ) {
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
}
$this->server->add_pagination_headers( $query['headers'] );
return array( 'webhooks' => $webhooks );
}
/**
* Get the webhook for the given ID
*
* @since 2.2
* @param int $id webhook ID
* @param array $fields
* @return array|WP_Error
*/
public function get_webhook( $id, $fields = null ) {
// ensure webhook ID is valid & user has permission to read
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$webhook = wc_get_webhook( $id );
$webhook_data = array(
'id' => $webhook->get_id(),
'name' => $webhook->get_name(),
'status' => $webhook->get_status(),
'topic' => $webhook->get_topic(),
'resource' => $webhook->get_resource(),
'event' => $webhook->get_event(),
'hooks' => $webhook->get_hooks(),
'delivery_url' => $webhook->get_delivery_url(),
'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
);
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
}
/**
* Get the total number of webhooks
*
* @since 2.2
*
* @param string $status
* @param array $filter
*
* @return array|WP_Error
*/
public function get_webhooks_count( $status = null, $filter = array() ) {
try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
}
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$query = $this->query_webhooks( $filter );
return array( 'count' => $query['headers']->total );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create an webhook
*
* @since 2.2
*
* @param array $data parsed webhook data
*
* @return array|WP_Error
*/
public function create_webhook( $data ) {
try {
if ( ! isset( $data['webhook'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
}
$data = $data['webhook'];
// permission check
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
// validate topic
if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
}
// validate delivery URL
if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
}
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
'post_type' => 'shop_webhook',
'post_status' => 'publish',
'ping_status' => 'closed',
'post_author' => get_current_user_id(),
'post_password' => 'webhook_' . wp_generate_password(),
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ),
), $data, $this );
$webhook = new WC_Webhook();
$webhook->set_name( $webhook_data['post_title'] );
$webhook->set_user_id( $webhook_data['post_author'] );
$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
$webhook->set_topic( $data['topic'] );
$webhook->set_delivery_url( $data['delivery_url'] );
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
$webhook->set_api_version( 'legacy_v3' );
$webhook->save();
$webhook->deliver_ping();
// HTTP 201 Created
$this->server->send_status( 201 );
do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );
return $this->get_webhook( $webhook->get_id() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a webhook
*
* @since 2.2
*
* @param int $id webhook ID
* @param array $data parsed webhook data
*
* @return array|WP_Error
*/
public function edit_webhook( $id, $data ) {
try {
if ( ! isset( $data['webhook'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
}
$data = $data['webhook'];
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
$webhook = wc_get_webhook( $id );
// update topic
if ( ! empty( $data['topic'] ) ) {
if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
$webhook->set_topic( $data['topic'] );
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
}
}
// update delivery URL
if ( ! empty( $data['delivery_url'] ) ) {
if ( wc_is_valid_url( $data['delivery_url'] ) ) {
$webhook->set_delivery_url( $data['delivery_url'] );
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
}
}
// update secret
if ( ! empty( $data['secret'] ) ) {
$webhook->set_secret( $data['secret'] );
}
// update status
if ( ! empty( $data['status'] ) ) {
$webhook->set_status( $data['status'] );
}
// update name
if ( ! empty( $data['name'] ) ) {
$webhook->set_name( $data['name'] );
}
$webhook->save();
do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );
return $this->get_webhook( $webhook->get_id() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a webhook
*
* @since 2.2
* @param int $id webhook ID
* @return array|WP_Error
*/
public function delete_webhook( $id ) {
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_webhook', $id, $this );
$webhook = wc_get_webhook( $id );
return $webhook->delete( true );
}
/**
* Helper method to get webhook post objects
*
* @since 2.2
* @param array $args Request arguments for filtering query.
* @return array
*/
private function query_webhooks( $args ) {
$args = $this->merge_query_args( array(), $args );
$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );
if ( empty( $args['offset'] ) ) {
$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
}
$page = $args['paged'];
unset( $args['paged'], $args['posts_per_page'] );
if ( isset( $args['s'] ) ) {
$args['search'] = $args['s'];
unset( $args['s'] );
}
// Post type to webhook status.
if ( ! empty( $args['post_status'] ) ) {
$args['status'] = $args['post_status'];
unset( $args['post_status'] );
}
if ( ! empty( $args['post__in'] ) ) {
$args['include'] = $args['post__in'];
unset( $args['post__in'] );
}
if ( ! empty( $args['date_query'] ) ) {
foreach ( $args['date_query'] as $date_query ) {
if ( 'post_date_gmt' === $date_query['column'] ) {
$args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
$args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
}
}
unset( $args['date_query'] );
}
$args['paginate'] = true;
// Get the webhooks.
$data_store = WC_Data_Store::load( 'webhook' );
$results = $data_store->search_webhooks( $args );
// Get total items.
$headers = new stdClass;
$headers->page = $page;
$headers->total = $results->total;
$headers->is_single = $args['limit'] > $headers->total;
$headers->total_pages = $results->max_num_pages;
return array(
'results' => $results->webhooks,
'headers' => $headers,
);
}
/**
* Get deliveries for a webhook
*
* @since 2.2
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @param string $webhook_id webhook ID
* @param string|null $fields fields to include in response
* @return array|WP_Error
*/
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
// Ensure ID is valid webhook ID
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
if ( is_wp_error( $webhook_id ) ) {
return $webhook_id;
}
return array( 'webhook_deliveries' => array() );
}
/**
* Get the delivery log for the given webhook ID and delivery ID
*
* @since 2.2
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @param string $webhook_id webhook ID
* @param string $id delivery log ID
* @param string|null $fields fields to limit response to
*
* @return array|WP_Error
*/
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
try {
// Validate webhook ID
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
if ( is_wp_error( $webhook_id ) ) {
return $webhook_id;
}
$id = absint( $id );
if ( empty( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
}
$webhook = new WC_Webhook( $webhook_id );
$log = 0;
if ( ! $log ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
}
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer.
* 2) the ID returns a valid post object and matches the provided post type.
* 3) the current user has the proper permissions to read/edit/delete the post.
*
* @since 3.3.0
* @param string|int $id The post ID
* @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
* @param string $context The context of the request, either `read`, `edit` or `delete`.
* @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
*/
protected function validate_request( $id, $type, $context ) {
$id = absint( $id );
// Validate ID.
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
}
$webhook = wc_get_webhook( $id );
if ( null === $webhook ) {
return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
}
// Validate permissions.
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
}
return $id;
}
}

View File

@ -1,47 +0,0 @@
<?php
/**
* WooCommerce API
*
* Defines an interface that API request/response handlers should implement
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
interface WC_API_Handler {
/**
* Get the content type for the response
*
* This should return the proper HTTP content-type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type();
/**
* Parse the raw request body entity into an array
*
* @since 2.1
* @param string $data
* @return array
*/
public function parse_body( $data );
/**
* Generate a response from an array of data
*
* @since 2.1
* @param array $data
* @return string
*/
public function generate_response( $data );
}

View File

@ -1,414 +0,0 @@
<?php
/**
* WooCommerce API Authentication Class
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1.0
* @version 2.4.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Authentication {
/**
* Setup class
*
* @since 2.1
*/
public function __construct() {
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
}
/**
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
*
* @since 2.1
* @param WP_User $user
* @return null|WP_Error|WP_User
*/
public function authenticate( $user ) {
// Allow access to the index by default
if ( '/' === WC()->api->server->path ) {
return new WP_User( 0 );
}
try {
if ( is_ssl() ) {
$keys = $this->perform_ssl_authentication();
} else {
$keys = $this->perform_oauth_authentication();
}
// Check API key-specific permission
$this->check_api_key_permissions( $keys['permissions'] );
$user = $this->get_user_by_id( $keys['user_id'] );
$this->update_api_key_last_access( $keys['key_id'] );
} catch ( Exception $e ) {
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
}
return $user;
}
/**
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
* attacks, so the request can be authenticated by simply looking up the user
* associated with the given consumer key and confirming the consumer secret
* provided is valid
*
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_ssl_authentication() {
$params = WC()->api->server->params['GET'];
// if the $_GET parameters are present, use those first
if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) {
$keys = $this->get_keys_by_consumer_key( $params['consumer_key'] );
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) {
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
// if the above is not present, we will do full basic auth
if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) {
$this->exit_with_unauthorized_headers();
}
$keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] );
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) {
$this->exit_with_unauthorized_headers();
}
return $keys;
}
/**
* If the consumer_key and consumer_secret $_GET parameters are NOT provided
* and the Basic auth headers are either not present or the consumer secret does not match the consumer
* key provided, then return the correct Basic headers and an error message.
*
* @since 2.4
*/
private function exit_with_unauthorized_headers() {
$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' );
header( 'HTTP/1.0 401 Unauthorized' );
throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 );
}
/**
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
*
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
*
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
*
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
*
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
* This is because there is no cross-OS function within PHP to get the raw Authorization header
*
* @link http://tools.ietf.org/html/rfc5849 for the full spec
* @since 2.1
* @return array
* @throws Exception
*/
private function perform_oauth_authentication() {
$params = WC()->api->server->params['GET'];
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
// Check for required OAuth parameters
foreach ( $param_names as $param_name ) {
if ( empty( $params[ $param_name ] ) ) {
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
}
}
// Fetch WP user by consumer key
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
// Perform OAuth validation
$this->check_oauth_signature( $keys, $params );
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
// Authentication successful, return user
return $keys;
}
/**
* Return the keys for the given consumer key
*
* @since 2.4.0
* @param string $consumer_key
* @return array
* @throws Exception
*/
private function get_keys_by_consumer_key( $consumer_key ) {
global $wpdb;
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
$keys = $wpdb->get_row( $wpdb->prepare( "
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
FROM {$wpdb->prefix}woocommerce_api_keys
WHERE consumer_key = '%s'
", $consumer_key ), ARRAY_A );
if ( empty( $keys ) ) {
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
}
return $keys;
}
/**
* Get user by ID
*
* @since 2.4.0
*
* @param int $user_id
*
* @return WP_User
* @throws Exception
*/
private function get_user_by_id( $user_id ) {
$user = get_user_by( 'id', $user_id );
if ( ! $user ) {
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
}
return $user;
}
/**
* Check if the consumer secret provided for the given user is valid
*
* @since 2.1
* @param string $keys_consumer_secret
* @param string $consumer_secret
* @return bool
*/
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
return hash_equals( $keys_consumer_secret, $consumer_secret );
}
/**
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
* has a valid key/secret
*
* @param array $keys
* @param array $params the request parameters
* @throws Exception
*/
private function check_oauth_signature( $keys, $params ) {
$http_method = strtoupper( WC()->api->server->method );
$server_path = WC()->api->server->path;
// if the requested URL has a trailingslash, make sure our base URL does as well
if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) {
$server_path .= '/';
}
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path );
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
unset( $params['oauth_signature'] );
// Sort parameters
if ( ! uksort( $params, 'strcmp' ) ) {
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
}
// Normalize parameter key/values
$params = $this->normalize_parameters( $params );
$query_parameters = array();
foreach ( $params as $param_key => $param_value ) {
if ( is_array( $param_value ) ) {
foreach ( $param_value as $param_key_inner => $param_value_inner ) {
$query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
}
} else {
$query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign
}
}
$query_string = implode( '%26', $query_parameters ); // join with ampersand
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
}
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
$secret = $keys['consumer_secret'] . '&';
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );
if ( ! hash_equals( $signature, $consumer_signature ) ) {
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
}
}
/**
* Normalize each parameter by assuming each parameter may have already been
* encoded, so attempt to decode, and then re-encode according to RFC 3986
*
* Note both the key and value is normalized so a filter param like:
*
* 'filter[period]' => 'week'
*
* is encoded to:
*
* 'filter%5Bperiod%5D' => 'week'
*
* This conforms to the OAuth 1.0a spec which indicates the entire query string
* should be URL encoded
*
* @since 2.1
* @see rawurlencode()
* @param array $parameters un-normalized parameters
* @return array normalized parameters
*/
private function normalize_parameters( $parameters ) {
$keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) );
$values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) );
$parameters = array_combine( $keys, $values );
return $parameters;
}
/**
* Encodes a value according to RFC 3986. Supports multidimensional arrays.
*
* @since 2.4
* @param string|array $value The value to encode
* @return string|array Encoded values
*/
public static function urlencode_rfc3986( $value ) {
if ( is_array( $value ) ) {
return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value );
} else {
// Percent symbols (%) must be double-encoded
return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
}
}
/**
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
* an attacker could attempt to re-send an intercepted request at a later time.
*
* - A timestamp is valid if it is within 15 minutes of now
* - A nonce is valid if it has not been used within the last 15 minutes
*
* @param array $keys
* @param int $timestamp the unix timestamp for when the request was made
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
* @throws Exception
*/
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
global $wpdb;
$valid_window = 15 * 60; // 15 minute window
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
}
$used_nonces = maybe_unserialize( $keys['nonces'] );
if ( empty( $used_nonces ) ) {
$used_nonces = array();
}
if ( in_array( $nonce, $used_nonces ) ) {
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
}
$used_nonces[ $timestamp ] = $nonce;
// Remove expired nonces
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
unset( $used_nonces[ $nonce_timestamp ] );
}
}
$used_nonces = maybe_serialize( $used_nonces );
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'nonces' => $used_nonces ),
array( 'key_id' => $keys['key_id'] ),
array( '%s' ),
array( '%d' )
);
}
/**
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
*
* @param string $key_permissions
* @throws Exception if the permission check fails
*/
public function check_api_key_permissions( $key_permissions ) {
switch ( WC()->api->server->method ) {
case 'HEAD':
case 'GET':
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
}
break;
}
}
/**
* Updated API Key last access datetime
*
* @since 2.4.0
*
* @param int $key_id
*/
private function update_api_key_last_access( $key_id ) {
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'last_access' => current_time( 'mysql' ) ),
array( 'key_id' => $key_id ),
array( '%s' ),
array( '%d' )
);
}
}

View File

@ -1,576 +0,0 @@
<?php
/**
* WooCommerce API Coupons Class
*
* Handles requests to the /coupons endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Coupons extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/coupons';
/**
* Register the routes for this class
*
* GET /coupons
* GET /coupons/count
* GET /coupons/<id>
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET/POST /coupons
$routes[ $this->base ] = array(
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /coupons/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
);
# GET/PUT/DELETE /coupons/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_coupon' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_coupon' ), WC_API_Server::DELETABLE ),
);
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
);
# POST|PUT /coupons/bulk
$routes[ $this->base . '/bulk' ] = array(
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
return $routes;
}
/**
* Get all coupons
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_coupons( $filter );
$coupons = array();
foreach ( $query->posts as $coupon_id ) {
if ( ! $this->is_readable( $coupon_id ) ) {
continue;
}
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'coupons' => $coupons );
}
/**
* Get the coupon for the given ID
*
* @since 2.1
* @param int $id the coupon ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_coupon( $id, $fields = null ) {
try {
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$coupon = new WC_Coupon( $id );
if ( 0 === $coupon->get_id() ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
}
$coupon_data = array(
'id' => $coupon->get_id(),
'code' => $coupon->get_code(),
'type' => $coupon->get_discount_type(),
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
'individual_use' => $coupon->get_individual_use(),
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
'usage_count' => (int) $coupon->get_usage_count(),
'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
'enable_free_shipping' => $coupon->get_free_shipping(),
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
'customer_emails' => $coupon->get_email_restrictions(),
'description' => $coupon->get_description(),
);
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of coupons
*
* @since 2.1
* @param array $filter
* @return array|WP_Error
*/
public function get_coupons_count( $filter = array() ) {
try {
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
}
$query = $this->query_coupons( $filter );
return array( 'count' => (int) $query->found_posts );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the coupon for the given code
*
* @since 2.1
* @param string $code the coupon code
* @param string $fields fields to include in response
* @return int|WP_Error
*/
public function get_coupon_by_code( $code, $fields = null ) {
global $wpdb;
try {
$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );
if ( is_null( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
}
return $this->get_coupon( $id, $fields );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create a coupon
*
* @since 2.2
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_coupon( $data ) {
global $wpdb;
try {
if ( ! isset( $data['coupon'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
}
$data = $data['coupon'];
// Check user permission
if ( ! current_user_can( 'publish_shop_coupons' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );
// Check if coupon code is specified
if ( ! isset( $data['code'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
}
$coupon_code = wc_format_coupon_code( $data['code'] );
$id_from_code = wc_get_coupon_id_by_code( $coupon_code );
if ( $id_from_code ) {
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
}
$defaults = array(
'type' => 'fixed_cart',
'amount' => 0,
'individual_use' => false,
'product_ids' => array(),
'exclude_product_ids' => array(),
'usage_limit' => '',
'usage_limit_per_user' => '',
'limit_usage_to_x_items' => '',
'usage_count' => '',
'expiry_date' => '',
'enable_free_shipping' => false,
'product_category_ids' => array(),
'exclude_product_category_ids' => array(),
'exclude_sale_items' => false,
'minimum_amount' => '',
'maximum_amount' => '',
'customer_emails' => array(),
'description' => '',
);
$coupon_data = wp_parse_args( $data, $defaults );
// Validate coupon types
if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
}
$new_coupon = array(
'post_title' => $coupon_code,
'post_content' => '',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_type' => 'shop_coupon',
'post_excerpt' => $coupon_data['description'],
);
$id = wp_insert_post( $new_coupon, true );
if ( is_wp_error( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
}
// Set coupon meta
update_post_meta( $id, 'discount_type', $coupon_data['type'] );
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );
do_action( 'woocommerce_api_create_coupon', $id, $data );
do_action( 'woocommerce_new_coupon', $id );
$this->server->send_status( 201 );
return $this->get_coupon( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a coupon
*
* @since 2.2
*
* @param int $id the coupon ID
* @param array $data
*
* @return array|WP_Error
*/
public function edit_coupon( $id, $data ) {
try {
if ( ! isset( $data['coupon'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
}
$data = $data['coupon'];
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );
if ( isset( $data['code'] ) ) {
global $wpdb;
$coupon_code = wc_format_coupon_code( $data['code'] );
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
}
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );
if ( 0 === $updated ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
}
}
if ( isset( $data['description'] ) ) {
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );
if ( 0 === $updated ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
}
}
if ( isset( $data['type'] ) ) {
// Validate coupon types
if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
}
update_post_meta( $id, 'discount_type', $data['type'] );
}
if ( isset( $data['amount'] ) ) {
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
}
if ( isset( $data['individual_use'] ) ) {
update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
}
if ( isset( $data['product_ids'] ) ) {
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
}
if ( isset( $data['exclude_product_ids'] ) ) {
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
}
if ( isset( $data['usage_limit'] ) ) {
update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
}
if ( isset( $data['usage_limit_per_user'] ) ) {
update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
}
if ( isset( $data['limit_usage_to_x_items'] ) ) {
update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
}
if ( isset( $data['usage_count'] ) ) {
update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
}
if ( isset( $data['expiry_date'] ) ) {
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
}
if ( isset( $data['enable_free_shipping'] ) ) {
update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
}
if ( isset( $data['product_category_ids'] ) ) {
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
}
if ( isset( $data['exclude_product_category_ids'] ) ) {
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
}
if ( isset( $data['exclude_sale_items'] ) ) {
update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
}
if ( isset( $data['minimum_amount'] ) ) {
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
}
if ( isset( $data['maximum_amount'] ) ) {
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
}
if ( isset( $data['customer_emails'] ) ) {
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
}
do_action( 'woocommerce_api_edit_coupon', $id, $data );
do_action( 'woocommerce_update_coupon', $id );
return $this->get_coupon( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a coupon
*
* @since 2.2
*
* @param int $id the coupon ID
* @param bool $force true to permanently delete coupon, false to move to trash
*
* @return array|int|WP_Error
*/
public function delete_coupon( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_coupon', $id, $this );
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
}
/**
* expiry_date format
*
* @since 2.3.0
* @param string $expiry_date
* @param bool $as_timestamp (default: false)
* @return string|int
*/
protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
if ( '' != $expiry_date ) {
if ( $as_timestamp ) {
return strtotime( $expiry_date );
}
return date( 'Y-m-d', strtotime( $expiry_date ) );
}
return '';
}
/**
* Helper method to get coupon post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_coupons( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
);
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
/**
* Bulk update or insert coupons
* Accepts an array with coupons in the formats supported by
* WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
*
* @since 2.4.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function bulk( $data ) {
try {
if ( ! isset( $data['coupons'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
}
$data = $data['coupons'];
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );
// Limit bulk operation
if ( count( $data ) > $limit ) {
throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
}
$coupons = array();
foreach ( $data as $_coupon ) {
$coupon_id = 0;
// Try to get the coupon ID
if ( isset( $_coupon['id'] ) ) {
$coupon_id = intval( $_coupon['id'] );
}
if ( $coupon_id ) {
// Coupon exists / edit coupon
$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );
if ( is_wp_error( $edit ) ) {
$coupons[] = array(
'id' => $coupon_id,
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
);
} else {
$coupons[] = $edit['coupon'];
}
} else {
// Coupon don't exists / create coupon
$new = $this->create_coupon( array( 'coupon' => $_coupon ) );
if ( is_wp_error( $new ) ) {
$coupons[] = array(
'id' => $coupon_id,
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
);
} else {
$coupons[] = $new['coupon'];
}
}
}
return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,829 +0,0 @@
<?php
/**
* WooCommerce API Customers Class
*
* Handles requests to the /customers endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Customers extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/customers';
/** @var string $created_at_min for date filtering */
private $created_at_min = null;
/** @var string $created_at_max for date filtering */
private $created_at_max = null;
/**
* Setup class, overridden to provide customer data to order response
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
parent::__construct( $server );
// add customer data to order responses
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
// modify WP_User_Query to support created_at date filtering
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
}
/**
* Register the routes for this class
*
* GET /customers
* GET /customers/count
* GET /customers/<id>
* GET /customers/<id>/orders
*
* @since 2.2
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET/POST /customers
$routes[ $this->base ] = array(
array( array( $this, 'get_customers' ), WC_API_Server::READABLE ),
array( array( $this, 'create_customer' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /customers/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_customers_count' ), WC_API_Server::READABLE ),
);
# GET/PUT/DELETE /customers/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_customer' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_customer' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_customer' ), WC_API_Server::DELETABLE ),
);
# GET /customers/email/<email>
$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
array( array( $this, 'get_customer_by_email' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>/orders
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
array( array( $this, 'get_customer_orders' ), WC_API_Server::READABLE ),
);
# GET /customers/<id>/downloads
$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
array( array( $this, 'get_customer_downloads' ), WC_API_Server::READABLE ),
);
# POST|PUT /customers/bulk
$routes[ $this->base . '/bulk' ] = array(
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
return $routes;
}
/**
* Get all customers
*
* @since 2.1
* @param array $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_customers( $filter );
$customers = array();
foreach ( $query->get_results() as $user_id ) {
if ( ! $this->is_readable( $user_id ) ) {
continue;
}
$customers[] = current( $this->get_customer( $user_id, $fields ) );
}
$this->server->add_pagination_headers( $query );
return array( 'customers' => $customers );
}
/**
* Get the customer for the given ID
*
* @since 2.1
* @param int $id the customer ID
* @param array $fields
* @return array|WP_Error
*/
public function get_customer( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$customer = new WC_Customer( $id );
$last_order = $customer->get_last_order();
$customer_data = array(
'id' => $customer->get_id(),
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
'last_update' => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
'email' => $customer->get_email(),
'first_name' => $customer->get_first_name(),
'last_name' => $customer->get_last_name(),
'username' => $customer->get_username(),
'role' => $customer->get_role(),
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
'orders_count' => $customer->get_order_count(),
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
'avatar_url' => $customer->get_avatar_url(),
'billing_address' => array(
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
),
);
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
}
/**
* Get the customer for the given email
*
* @since 2.1
*
* @param string $email the customer email
* @param array $fields
*
* @return array|WP_Error
*/
public function get_customer_by_email( $email, $fields = null ) {
try {
if ( is_email( $email ) ) {
$customer = get_user_by( 'email', $email );
if ( ! is_object( $customer ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
}
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
}
return $this->get_customer( $customer->ID, $fields );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of customers
*
* @since 2.1
*
* @param array $filter
*
* @return array|WP_Error
*/
public function get_customers_count( $filter = array() ) {
try {
if ( ! current_user_can( 'list_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
}
$query = $this->query_customers( $filter );
return array( 'count' => $query->get_total() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get customer billing address fields.
*
* @since 2.2
* @return array
*/
protected function get_customer_billing_address() {
$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
'email',
'phone',
) );
return $billing_address;
}
/**
* Get customer shipping address fields.
*
* @since 2.2
* @return array
*/
protected function get_customer_shipping_address() {
$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
) );
return $shipping_address;
}
/**
* Add/Update customer data.
*
* @since 2.2
* @param int $id the customer ID
* @param array $data
* @param WC_Customer $customer
*/
protected function update_customer_data( $id, $data, $customer ) {
// Customer first name.
if ( isset( $data['first_name'] ) ) {
$customer->set_first_name( wc_clean( $data['first_name'] ) );
}
// Customer last name.
if ( isset( $data['last_name'] ) ) {
$customer->set_last_name( wc_clean( $data['last_name'] ) );
}
// Customer billing address.
if ( isset( $data['billing_address'] ) ) {
foreach ( $this->get_customer_billing_address() as $field ) {
if ( isset( $data['billing_address'][ $field ] ) ) {
if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
} else {
$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
}
}
}
}
// Customer shipping address.
if ( isset( $data['shipping_address'] ) ) {
foreach ( $this->get_customer_shipping_address() as $field ) {
if ( isset( $data['shipping_address'][ $field ] ) ) {
if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
} else {
$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
}
}
}
}
do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
}
/**
* Create a customer
*
* @since 2.2
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_customer( $data ) {
try {
if ( ! isset( $data['customer'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
}
$data = $data['customer'];
// Checks with can create new users.
if ( ! current_user_can( 'create_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );
// Checks with the email is missing.
if ( ! isset( $data['email'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
}
// Create customer.
$customer = new WC_Customer;
$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
$customer->set_email( $data['email'] );
$customer->save();
if ( ! $customer->get_id() ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
}
// Added customer data.
$this->update_customer_data( $customer->get_id(), $data, $customer );
$customer->save();
do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );
$this->server->send_status( 201 );
return $this->get_customer( $customer->get_id() );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a customer
*
* @since 2.2
*
* @param int $id the customer ID
* @param array $data
*
* @return array|WP_Error
*/
public function edit_customer( $id, $data ) {
try {
if ( ! isset( $data['customer'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
}
$data = $data['customer'];
// Validate the customer ID.
$id = $this->validate_request( $id, 'customer', 'edit' );
// Return the validate error.
if ( is_wp_error( $id ) ) {
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
}
$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );
$customer = new WC_Customer( $id );
// Customer email.
if ( isset( $data['email'] ) ) {
$customer->set_email( $data['email'] );
}
// Customer password.
if ( isset( $data['password'] ) ) {
$customer->set_password( $data['password'] );
}
// Update customer data.
$this->update_customer_data( $customer->get_id(), $data, $customer );
$customer->save();
do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );
return $this->get_customer( $customer->get_id() );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a customer
*
* @since 2.2
* @param int $id the customer ID
* @return array|WP_Error
*/
public function delete_customer( $id ) {
// Validate the customer ID.
$id = $this->validate_request( $id, 'customer', 'delete' );
// Return the validate error.
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_customer', $id, $this );
return $this->delete( $id, 'customer' );
}
/**
* Get the orders for a customer
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields fields to include in response
* @param array $filter filters
* @return array|WP_Error
*/
public function get_customer_orders( $id, $fields = null, $filter = array() ) {
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$filter['customer_id'] = $id;
$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 );
return $orders;
}
/**
* Get the available downloads for a customer
*
* @since 2.2
* @param int $id the customer ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_customer_downloads( $id, $fields = null ) {
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$downloads = array();
$_downloads = wc_get_customer_available_downloads( $id );
foreach ( $_downloads as $key => $download ) {
$downloads[] = array(
'download_url' => $download['download_url'],
'download_id' => $download['download_id'],
'product_id' => $download['product_id'],
'download_name' => $download['download_name'],
'order_id' => $download['order_id'],
'order_key' => $download['order_key'],
'downloads_remaining' => $download['downloads_remaining'],
'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
'file' => $download['file'],
);
}
return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
}
/**
* Helper method to get customer user objects
*
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
* pagination support
*
* The filter for role can only be a single role in a string.
*
* @since 2.3
* @param array $args request arguments for filtering query
* @return WP_User_Query
*/
private function query_customers( $args = array() ) {
// default users per page
$users_per_page = get_option( 'posts_per_page' );
// Set base query arguments
$query_args = array(
'fields' => 'ID',
'role' => 'customer',
'orderby' => 'registered',
'number' => $users_per_page,
);
// Custom Role
if ( ! empty( $args['role'] ) ) {
$query_args['role'] = $args['role'];
// Show users on all roles
if ( 'all' === $query_args['role'] ) {
unset( $query_args['role'] );
}
}
// Search
if ( ! empty( $args['q'] ) ) {
$query_args['search'] = $args['q'];
}
// Limit number of users returned
if ( ! empty( $args['limit'] ) ) {
if ( -1 == $args['limit'] ) {
unset( $query_args['number'] );
} else {
$query_args['number'] = absint( $args['limit'] );
$users_per_page = absint( $args['limit'] );
}
} else {
$args['limit'] = $query_args['number'];
}
// Page
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
// Offset
if ( ! empty( $args['offset'] ) ) {
$query_args['offset'] = absint( $args['offset'] );
} else {
$query_args['offset'] = $users_per_page * ( $page - 1 );
}
// Created date
if ( ! empty( $args['created_at_min'] ) ) {
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
}
if ( ! empty( $args['created_at_max'] ) ) {
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
}
// Order (ASC or DESC, ASC by default)
if ( ! empty( $args['order'] ) ) {
$query_args['order'] = $args['order'];
}
// Order by
if ( ! empty( $args['orderby'] ) ) {
$query_args['orderby'] = $args['orderby'];
// Allow sorting by meta value
if ( ! empty( $args['orderby_meta_key'] ) ) {
$query_args['meta_key'] = $args['orderby_meta_key'];
}
}
$query = new WP_User_Query( $query_args );
// Helper members for pagination headers
$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
$query->page = $page;
return $query;
}
/**
* Add customer data to orders
*
* @since 2.1
* @param $order_data
* @param $order
* @return array
*/
public function add_customer_data( $order_data, $order ) {
if ( 0 == $order->get_user_id() ) {
// add customer data from order
$order_data['customer'] = array(
'id' => 0,
'email' => $order->get_billing_email(),
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'billing_address' => array(
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
),
);
} else {
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
}
return $order_data;
}
/**
* Modify the WP_User_Query to support filtering on the date the customer was created
*
* @since 2.1
* @param WP_User_Query $query
*/
public function modify_user_query( $query ) {
if ( $this->created_at_min ) {
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
}
if ( $this->created_at_max ) {
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid WP_User
* 3) the current user has the proper permissions
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param integer $id the customer ID
* @param string $type the request type, unused because this method overrides the parent class
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
try {
$id = absint( $id );
// validate ID
if ( empty( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
}
// non-existent IDs return a valid WP_User object with the user ID = 0
$customer = new WP_User( $id );
if ( 0 === $customer->ID ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
}
// validate permissions
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'list_users' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
}
break;
case 'edit':
if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
}
break;
case 'delete':
if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
}
break;
}
return $id;
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Check if the current user can read users
*
* @since 2.1
* @see WC_API_Resource::is_readable()
* @param int|WP_Post $post unused
* @return bool true if the current user can read users, false otherwise
*/
protected function is_readable( $post ) {
return current_user_can( 'list_users' );
}
/**
* Bulk update or insert customers
* Accepts an array with customers in the formats supported by
* WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
*
* @since 2.4.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function bulk( $data ) {
try {
if ( ! isset( $data['customers'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
}
$data = $data['customers'];
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );
// Limit bulk operation
if ( count( $data ) > $limit ) {
throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
}
$customers = array();
foreach ( $data as $_customer ) {
$customer_id = 0;
// Try to get the customer ID
if ( isset( $_customer['id'] ) ) {
$customer_id = intval( $_customer['id'] );
}
if ( $customer_id ) {
// Customer exists / edit customer
$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );
if ( is_wp_error( $edit ) ) {
$customers[] = array(
'id' => $customer_id,
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
);
} else {
$customers[] = $edit['customer'];
}
} else {
// Customer don't exists / create customer
$new = $this->create_customer( array( 'customer' => $_customer ) );
if ( is_wp_error( $new ) ) {
$customers[] = array(
'id' => $customer_id,
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
);
} else {
$customers[] = $new['customer'];
}
}
}
return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,48 +0,0 @@
<?php
/**
* WooCommerce API Exception Class
*
* Extends Exception to provide additional data
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Exception extends Exception {
/** @var string sanitized error code */
protected $error_code;
/**
* Setup exception, requires 3 params:
*
* error code - machine-readable, e.g. `woocommerce_invalid_product_id`
* error message - friendly message, e.g. 'Product ID is invalid'
* http status code - proper HTTP status code to respond with, e.g. 400
*
* @since 2.2
* @param string $error_code
* @param string $error_message user-friendly translated error message
* @param int $http_status_code HTTP status code to respond with
*/
public function __construct( $error_code, $error_message, $http_status_code ) {
$this->error_code = $error_code;
parent::__construct( $error_message, $http_status_code );
}
/**
* Returns the error code
*
* @since 2.2
* @return string
*/
public function getErrorCode() {
return $this->error_code;
}
}

View File

@ -1,73 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles parsing JSON request bodies and generating JSON responses
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_JSON_Handler implements WC_API_Handler {
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $body the raw request body
* @return array|mixed
*/
public function parse_body( $body ) {
return json_decode( $body, true );
}
/**
* Generate a JSON response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
if ( isset( $_GET['_jsonp'] ) ) {
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
}
$jsonp_callback = $_GET['_jsonp'];
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
WC()->api->server->send_status( 400 );
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
}
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
}
return wp_json_encode( $data );
}
}

View File

@ -1,330 +0,0 @@
<?php
/**
* WooCommerce API Reports Class
*
* Handles requests to the /reports endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Reports extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/reports';
/** @var WC_Admin_Report instance */
private $report;
/**
* Register the routes for this class
*
* GET /reports
* GET /reports/sales
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /reports
$routes[ $this->base ] = array(
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
);
# GET /reports/sales
$routes[ $this->base . '/sales' ] = array(
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
);
# GET /reports/sales/top_sellers
$routes[ $this->base . '/sales/top_sellers' ] = array(
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get a simple listing of available reports
*
* @since 2.1
* @return array
*/
public function get_reports() {
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
}
/**
* Get the sales report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_sales_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
// check for WP_Error
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
// new customers
$users_query = new WP_User_Query(
array(
'fields' => array( 'user_registered' ),
'role' => 'customer',
)
);
$customers = $users_query->get_results();
foreach ( $customers as $key => $customer ) {
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
unset( $customers[ $key ] );
}
}
$total_customers = count( $customers );
$report_data = $this->report->get_report_data();
$period_totals = array();
// setup period totals by ensuring each period in the interval has data
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
switch ( $this->report->chart_groupby ) {
case 'day' :
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
break;
default :
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
break;
}
// set the customer signups for each period
$customer_count = 0;
foreach ( $customers as $customer ) {
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
$customer_count++;
}
}
$period_totals[ $time ] = array(
'sales' => wc_format_decimal( 0.00, 2 ),
'orders' => 0,
'items' => 0,
'tax' => wc_format_decimal( 0.00, 2 ),
'shipping' => wc_format_decimal( 0.00, 2 ),
'discount' => wc_format_decimal( 0.00, 2 ),
'customers' => $customer_count,
);
}
// add total sales, total order count, total tax and total shipping for each period
foreach ( $report_data->orders as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
}
foreach ( $report_data->order_counts as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['orders'] = (int) $order->count;
}
// add total order items for each period
foreach ( $report_data->order_items as $order_item ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
}
// add total discount for each period
foreach ( $report_data->coupons as $discount ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
}
$sales_data = array(
'total_sales' => $report_data->total_sales,
'net_sales' => $report_data->net_sales,
'average_sales' => $report_data->average_sales,
'total_orders' => $report_data->total_orders,
'total_items' => $report_data->total_items,
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
'total_shipping' => $report_data->total_shipping,
'total_refunds' => $report_data->total_refunds,
'total_discount' => $report_data->total_coupons,
'totals_grouped_by' => $this->report->chart_groupby,
'totals' => $period_totals,
'total_customers' => $total_customers,
);
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
}
/**
* Get the top sellers report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array|WP_Error
*/
public function get_top_sellers_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
if ( is_wp_error( $check ) ) {
return $check;
}
// set date filtering
$this->setup_report( $filter );
$top_sellers = $this->report->get_order_report_data( array(
'data' => array(
'_product_id' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => '',
'name' => 'product_id',
),
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty',
),
),
'order_by' => 'order_item_qty DESC',
'group_by' => 'product_id',
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
'query_type' => 'get_results',
'filter_range' => true,
) );
$top_sellers_data = array();
foreach ( $top_sellers as $top_seller ) {
$product = wc_get_product( $top_seller->product_id );
if ( $product ) {
$top_sellers_data[] = array(
'title' => $product->get_name(),
'product_id' => $top_seller->product_id,
'quantity' => $top_seller->order_item_qty,
);
}
}
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
}
/**
* Setup the report object and parse any date filtering
*
* @since 2.1
* @param array $filter date filtering
*/
private function setup_report( $filter ) {
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
$this->report = new WC_Report_Sales_By_Date();
if ( empty( $filter['period'] ) ) {
// custom date range
$filter['period'] = 'custom';
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
} else {
// default custom range to today
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
}
} else {
// ensure period is valid
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
$filter['period'] = 'week';
}
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
// allow "week" for period instead of "7day"
if ( 'week' === $filter['period'] ) {
$filter['period'] = '7day';
}
}
$this->report->calculate_current_range( $filter['period'] );
}
/**
* Verify that the current user has permission to view reports
*
* @since 2.1
* @see WC_API_Resource::validate_request()
*
* @param null $id unused
* @param null $type unused
* @param null $context unused
*
* @return true|WP_Error
*/
protected function validate_request( $id = null, $type = null, $context = null ) {
if ( current_user_can( 'view_woocommerce_reports' ) ) {
return true;
}
return new WP_Error(
'woocommerce_api_user_cannot_read_report',
__( 'You do not have permission to read this report', 'woocommerce' ),
array( 'status' => 401 )
);
}
}

View File

@ -1,477 +0,0 @@
<?php
/**
* WooCommerce API Resource class
*
* Provides shared functionality for resource-specific API classes
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Resource {
/** @var WC_API_Server the API server */
protected $server;
/** @var string sub-classes override this to set a resource-specific base route */
protected $base;
/**
* Setup class
*
* @since 2.1
* @param WC_API_Server $server
*/
public function __construct( WC_API_Server $server ) {
$this->server = $server;
// automatically register routes for sub-classes
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
// maybe add meta to top-level resource responses
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
}
$response_names = array(
'order',
'coupon',
'customer',
'product',
'report',
'customer_orders',
'customer_downloads',
'order_note',
'order_refund',
'product_reviews',
'product_category',
'tax',
'tax_class',
);
foreach ( $response_names as $name ) {
/**
* Remove fields from responses when requests specify certain fields
* note these are hooked at a later priority so data added via
* filters (e.g. customer data to the order response) still has the
* fields filtered properly
*/
add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid post object and matches the provided post type
* 3) the current user has the proper permissions to read/edit/delete the post
*
* @since 2.1
* @param string|int $id the post ID
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
$id = absint( $id );
// Validate ID
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
}
// Only custom post types have per-post type/permission checks
if ( 'customer' === $type ) {
return $id;
}
$post = get_post( $id );
// Orders request are a special case.
$is_invalid_orders_request = ( 'shop_order' === $type && ( ! $post || ! is_a ( $post, 'WP_Post' ) || 'shop_order' !== $post->post_type ) && ! wc_rest_check_post_permissions( 'shop_order', 'read' ) );
if ( ! $is_invalid_orders_request ) {
if ( null === $post ) {
return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
}
// For checking permissions, product variations are the same as the product post type
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
// Validate post type
if ( $type !== $post_type ) {
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
}
}
// Validate permissions
switch ( $context ) {
case 'read':
if ( $is_invalid_orders_request || ! $this->is_readable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( $is_invalid_orders_request || ! $this->is_editable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( $is_invalid_orders_request || ! $this->is_deletable( $post ) ) {
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
}
break;
}
return $id;
}
/**
* Add common request arguments to argument list before WP_Query is run
*
* @since 2.1
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
* @param array $request_args arguments provided in the request
* @return array
*/
protected function merge_query_args( $base_args, $request_args ) {
$args = array();
// date
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'] = array();
// resources created after specified date
if ( ! empty( $request_args['created_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
}
// resources created before specified date
if ( ! empty( $request_args['created_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
}
// resources updated after specified date
if ( ! empty( $request_args['updated_at_min'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
}
// resources updated before specified date
if ( ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
}
}
// search
if ( ! empty( $request_args['q'] ) ) {
$args['s'] = $request_args['q'];
}
// resources per response
if ( ! empty( $request_args['limit'] ) ) {
$args['posts_per_page'] = $request_args['limit'];
}
// resource offset
if ( ! empty( $request_args['offset'] ) ) {
$args['offset'] = $request_args['offset'];
}
// order (ASC or DESC, ASC by default)
if ( ! empty( $request_args['order'] ) ) {
$args['order'] = $request_args['order'];
}
// orderby
if ( ! empty( $request_args['orderby'] ) ) {
$args['orderby'] = $request_args['orderby'];
// allow sorting by meta value
if ( ! empty( $request_args['orderby_meta_key'] ) ) {
$args['meta_key'] = $request_args['orderby_meta_key'];
}
}
// allow post status change
if ( ! empty( $request_args['post_status'] ) ) {
$args['post_status'] = $request_args['post_status'];
unset( $request_args['post_status'] );
}
// filter by a list of post id
if ( ! empty( $request_args['in'] ) ) {
$args['post__in'] = explode( ',', $request_args['in'] );
unset( $request_args['in'] );
}
// exclude by a list of post id
if ( ! empty( $request_args['not_in'] ) ) {
$args['post__not_in'] = explode( ',', $request_args['not_in'] );
unset( $request_args['not_in'] );
}
// resource page
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );
return array_merge( $base_args, $args );
}
/**
* Add meta to resources when requested by the client. Meta is added as a top-level
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
*
* @since 2.1
* @param array $data the resource data
* @param object $resource the resource object (e.g WC_Order)
* @return mixed
*/
public function maybe_add_meta( $data, $resource ) {
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
// don't attempt to add meta more than once
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
return $data;
}
// define the top-level property name for the meta
switch ( get_class( $resource ) ) {
case 'WC_Order':
$meta_name = 'order_meta';
break;
case 'WC_Coupon':
$meta_name = 'coupon_meta';
break;
case 'WP_User':
$meta_name = 'customer_meta';
break;
default:
$meta_name = 'product_meta';
break;
}
if ( is_a( $resource, 'WP_User' ) ) {
// customer meta
$meta = (array) get_user_meta( $resource->ID );
} else {
// coupon/order/product meta
$meta = (array) get_post_meta( $resource->get_id() );
}
foreach ( $meta as $meta_key => $meta_value ) {
// don't add hidden meta by default
if ( ! is_protected_meta( $meta_key ) ) {
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
}
}
}
return $data;
}
/**
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
*
* @since 2.1
* @param array $data the response data
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
* @param array|string the requested list of fields to include in the response
* @return array response data
*/
public function filter_response_fields( $data, $resource, $fields ) {
if ( ! is_array( $data ) || empty( $fields ) ) {
return $data;
}
$fields = explode( ',', $fields );
$sub_fields = array();
// get sub fields
foreach ( $fields as $field ) {
if ( false !== strpos( $field, '.' ) ) {
list( $name, $value ) = explode( '.', $field );
$sub_fields[ $name ] = $value;
}
}
// iterate through top-level fields
foreach ( $data as $data_field => $data_value ) {
// if a field has sub-fields and the top-level field has sub-fields to filter
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
// iterate through each sub-field
foreach ( $data_value as $sub_field => $sub_field_value ) {
// remove non-matching sub-fields
if ( ! in_array( $sub_field, $sub_fields ) ) {
unset( $data[ $data_field ][ $sub_field ] );
}
}
} else {
// remove non-matching top-level fields
if ( ! in_array( $data_field, $fields ) ) {
unset( $data[ $data_field ] );
}
}
}
return $data;
}
/**
* Delete a given resource
*
* @since 2.1
* @param int $id the resource ID
* @param string $type the resource post type, or `customer`
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
* @return array|WP_Error
*/
protected function delete( $id, $type, $force = false ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
$resource_name = str_replace( 'shop_', '', $type );
} else {
$resource_name = $type;
}
if ( 'customer' === $type ) {
$result = wp_delete_user( $id );
if ( $result ) {
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
} else {
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
}
} else {
// delete order/coupon/product/webhook
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
if ( ! $result ) {
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
}
if ( $force ) {
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
} else {
$this->server->send_status( '202' );
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
}
}
}
/**
* Checks if the given post is readable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_readable( $post ) {
return $this->check_permission( $post, 'read' );
}
/**
* Checks if the given post is editable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_editable( $post ) {
return $this->check_permission( $post, 'edit' );
}
/**
* Checks if the given post is deletable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_deletable( $post ) {
return $this->check_permission( $post, 'delete' );
}
/**
* Checks the permissions for the current user given a post and context
*
* @since 2.1
* @param WP_Post|int $post
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
* @return bool true if the current user has the permissions to perform the context on the post
*/
private function check_permission( $post, $context ) {
$permission = false;
if ( ! is_a( $post, 'WP_Post' ) ) {
$post = get_post( $post );
}
if ( is_null( $post ) ) {
return $permission;
}
$post_type = get_post_type_object( $post->post_type );
if ( 'read' === $context ) {
$permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID );
} elseif ( 'edit' === $context ) {
$permission = current_user_can( $post_type->cap->edit_post, $post->ID );
} elseif ( 'delete' === $context ) {
$permission = current_user_can( $post_type->cap->delete_post, $post->ID );
}
return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type );
}
}

View File

@ -1,762 +0,0 @@
<?php
/**
* WooCommerce API
*
* Handles REST API requests
*
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
* Many thanks to Ryan McCue and any other contributors!
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once ABSPATH . 'wp-admin/includes/admin.php';
class WC_API_Server {
const METHOD_GET = 1;
const METHOD_POST = 2;
const METHOD_PUT = 4;
const METHOD_PATCH = 8;
const METHOD_DELETE = 16;
const READABLE = 1; // GET
const CREATABLE = 2; // POST
const EDITABLE = 14; // POST | PUT | PATCH
const DELETABLE = 16; // DELETE
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
/**
* Does the endpoint accept a raw request body?
*/
const ACCEPT_RAW_DATA = 64;
/** Does the endpoint accept a request body? (either JSON or XML) */
const ACCEPT_DATA = 128;
/**
* Should we hide this endpoint from the index?
*/
const HIDDEN_ENDPOINT = 256;
/**
* Map of HTTP verbs to constants
* @var array
*/
public static $method_map = array(
'HEAD' => self::METHOD_GET,
'GET' => self::METHOD_GET,
'POST' => self::METHOD_POST,
'PUT' => self::METHOD_PUT,
'PATCH' => self::METHOD_PATCH,
'DELETE' => self::METHOD_DELETE,
);
/**
* Requested path (relative to the API root, wp-json.php)
*
* @var string
*/
public $path = '';
/**
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
*
* @var string
*/
public $method = 'HEAD';
/**
* Request parameters
*
* This acts as an abstraction of the superglobals
* (GET => $_GET, POST => $_POST)
*
* @var array
*/
public $params = array( 'GET' => array(), 'POST' => array() );
/**
* Request headers
*
* @var array
*/
public $headers = array();
/**
* Request files (matches $_FILES)
*
* @var array
*/
public $files = array();
/**
* Request/Response handler, either JSON by default
* or XML if requested by client
*
* @var WC_API_Handler
*/
public $handler;
/**
* Setup class and set request/response handler
*
* @since 2.1
* @param $path
*/
public function __construct( $path ) {
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else {
$path = '/';
}
}
$this->path = $path;
$this->method = $_SERVER['REQUEST_METHOD'];
$this->params['GET'] = $_GET;
$this->params['POST'] = $_POST;
$this->headers = $this->get_headers( $_SERVER );
$this->files = $_FILES;
// Compatibility for clients that can't use PUT/PATCH/DELETE
if ( isset( $_GET['_method'] ) ) {
$this->method = strtoupper( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
// load response handler
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
$this->handler = new $handler_class();
}
/**
* Check authentication for the request
*
* @since 2.1
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
*/
public function check_authentication() {
// allow plugins to remove default authentication or add their own authentication
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
if ( is_a( $user, 'WP_User' ) ) {
// API requests run under the context of the authenticated user
wp_set_current_user( $user->ID );
} elseif ( ! is_wp_error( $user ) ) {
// WP_Errors are handled in serve_request()
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
}
return $user;
}
/**
* Convert an error to an array
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behaviour, as it is represented as a
* list in JSON rather than an object/map
*
* @since 2.1
* @param WP_Error $error
* @return array List of associative arrays with code and message keys
*/
protected function error_to_array( $error ) {
$errors = array();
foreach ( (array) $error->errors as $code => $messages ) {
foreach ( (array) $messages as $message ) {
$errors[] = array( 'code' => $code, 'message' => $message );
}
}
return array( 'errors' => $errors );
}
/**
* Handle serving an API request
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 2.1
* @uses WC_API_Server::dispatch()
*/
public function serve_request() {
do_action( 'woocommerce_api_server_before_serve', $this );
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
// the API is enabled by default
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
$this->send_status( 404 );
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
return;
}
$result = $this->check_authentication();
// if authorization check was successful, dispatch the request
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch();
}
// handle any dispatch errors
if ( is_wp_error( $result ) ) {
$data = $result->get_error_data();
if ( is_array( $data ) && isset( $data['status'] ) ) {
$this->send_status( $data['status'] );
}
$result = $this->error_to_array( $result );
}
// This is a filter rather than an action, since this is designed to be
// re-entrant if needed
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
if ( ! $served ) {
if ( 'HEAD' === $this->method ) {
return;
}
echo $this->handler->generate_response( $result );
}
}
/**
* Retrieve the route map
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 2.1
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
*/
public function get_routes() {
// index added by default
$endpoints = array(
'/' => array( array( $this, 'get_index' ), self::READABLE ),
);
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
// Normalise the endpoints
foreach ( $endpoints as $route => &$handlers ) {
if ( is_array( $handlers ) && count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
$handlers = array( $handlers );
}
}
return $endpoints;
}
/**
* Match the request to a callback and call it
*
* @since 2.1
* @return mixed The value returned by the callback, or a WP_Error instance
*/
public function dispatch() {
switch ( $this->method ) {
case 'HEAD' :
case 'GET' :
$method = self::METHOD_GET;
break;
case 'POST' :
$method = self::METHOD_POST;
break;
case 'PUT' :
$method = self::METHOD_PUT;
break;
case 'PATCH' :
$method = self::METHOD_PATCH;
break;
case 'DELETE' :
$method = self::METHOD_DELETE;
break;
default :
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
}
foreach ( $this->get_routes() as $route => $handlers ) {
foreach ( $handlers as $handler ) {
$callback = $handler[0];
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
if ( ! ( $supported & $method ) ) {
continue;
}
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
if ( ! $match ) {
continue;
}
if ( ! is_callable( $callback ) ) {
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
}
$args = array_merge( $args, $this->params['GET'] );
if ( $method & self::METHOD_POST ) {
$args = array_merge( $args, $this->params['POST'] );
}
if ( $supported & self::ACCEPT_DATA ) {
$data = $this->handler->parse_body( $this->get_raw_data() );
$args = array_merge( $args, array( 'data' => $data ) );
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
$data = $this->get_raw_data();
$args = array_merge( $args, array( 'data' => $data ) );
}
$args['_method'] = $method;
$args['_route'] = $route;
$args['_path'] = $this->path;
$args['_headers'] = $this->headers;
$args['_files'] = $this->files;
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
// Allow plugins to halt the request via this filter
if ( is_wp_error( $args ) ) {
return $args;
}
$params = $this->sort_callback_params( $callback, $args );
if ( is_wp_error( $params ) ) {
return $params;
}
return call_user_func_array( $callback, $params );
}
}
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
}
/**
* urldecode deep.
*
* @since 2.2
* @param string|array $value Data to decode with urldecode.
*
* @return string|array Decoded data.
*/
protected function urldecode_deep( $value ) {
if ( is_array( $value ) ) {
return array_map( array( $this, 'urldecode_deep' ), $value );
} else {
return urldecode( $value );
}
}
/**
* Sort parameters by order specified in method declaration
*
* Takes a callback and a list of available params, then filters and sorts
* by the parameters the method actually needs, using the Reflection API
*
* @since 2.2
*
* @param callable|array $callback the endpoint callback
* @param array $provided the provided request parameters
*
* @return array|WP_Error
*/
protected function sort_callback_params( $callback, $provided ) {
if ( is_array( $callback ) ) {
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
} else {
$ref_func = new ReflectionFunction( $callback );
}
$wanted = $ref_func->getParameters();
$ordered_parameters = array();
foreach ( $wanted as $param ) {
if ( isset( $provided[ $param->getName() ] ) ) {
// We have this parameters in the list to choose from
if ( 'data' == $param->getName() ) {
$ordered_parameters[] = $provided[ $param->getName() ];
continue;
}
$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
} elseif ( $param->isDefaultValueAvailable() ) {
// We don't have this parameter, but it's optional
$ordered_parameters[] = $param->getDefaultValue();
} else {
// We don't have this parameter and it wasn't optional, abort!
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
}
}
return $ordered_parameters;
}
/**
* Get the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 2.3
* @return array Index entity
*/
public function get_index() {
// General site data
$available = array(
'store' => array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'URL' => get_option( 'siteurl' ),
'wc_version' => WC()->version,
'version' => WC_API::VERSION,
'routes' => array(),
'meta' => array(
'timezone' => wc_timezone_string(),
'currency' => get_woocommerce_currency(),
'currency_format' => get_woocommerce_currency_symbol(),
'currency_position' => get_option( 'woocommerce_currency_pos' ),
'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ),
'price_num_decimals' => wc_get_price_decimals(),
'tax_included' => wc_prices_include_tax(),
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ),
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
'links' => array(
'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
),
),
),
);
// Find the available routes
foreach ( $this->get_routes() as $route => $callbacks ) {
$data = array();
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
foreach ( self::$method_map as $name => $bitmask ) {
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
continue 3;
}
if ( $callback[1] & $bitmask ) {
$data['supports'][] = $name;
}
if ( $callback[1] & self::ACCEPT_DATA ) {
$data['accepts_data'] = true;
}
// For non-variable routes, generate links
if ( strpos( $route, '<' ) === false ) {
$data['meta'] = array(
'self' => get_woocommerce_api_url( $route ),
);
}
}
}
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
}
return apply_filters( 'woocommerce_api_index', $available );
}
/**
* Send a HTTP status code
*
* @since 2.1
* @param int $code HTTP status
*/
public function send_status( $code ) {
status_header( $code );
}
/**
* Send a HTTP header
*
* @since 2.1
* @param string $key Header key
* @param string $value Header value
* @param boolean $replace Should we replace the existing header?
*/
public function header( $key, $value, $replace = true ) {
header( sprintf( '%s: %s', $key, $value ), $replace );
}
/**
* Send a Link header
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple
*
* @link http://tools.ietf.org/html/rfc5988
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
*
* @since 2.1
* @param string $rel Link relation. Either a registered type, or an absolute URL
* @param string $link Target IRI for the link
* @param array $other Other parameters to send, as an associative array
*/
public function link_header( $rel, $link, $other = array() ) {
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
foreach ( $other as $key => $value ) {
if ( 'title' == $key ) {
$value = '"' . $value . '"';
}
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Send pagination headers for resources
*
* @since 2.1
* @param WP_Query|WP_User_Query|stdClass $query
*/
public function add_pagination_headers( $query ) {
// WP_User_Query
if ( is_a( $query, 'WP_User_Query' ) ) {
$single = count( $query->get_results() ) == 1;
$total = $query->get_total();
if ( $query->get( 'number' ) > 0 ) {
$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
$total_pages = ceil( $total / $query->get( 'number' ) );
} else {
$page = 1;
$total_pages = 1;
}
} elseif ( is_a( $query, 'stdClass' ) ) {
$page = $query->page;
$single = $query->is_single;
$total = $query->total;
$total_pages = $query->total_pages;
// WP_Query
} else {
$page = $query->get( 'paged' );
$single = $query->is_single();
$total = $query->found_posts;
$total_pages = $query->max_num_pages;
}
if ( ! $page ) {
$page = 1;
}
$next_page = absint( $page ) + 1;
if ( ! $single ) {
// first/prev
if ( $page > 1 ) {
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
}
// next
if ( $next_page <= $total_pages ) {
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
}
// last
if ( $page != $total_pages ) {
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
}
}
$this->header( 'X-WC-Total', $total );
$this->header( 'X-WC-TotalPages', $total_pages );
do_action( 'woocommerce_api_pagination_headers', $this, $query );
}
/**
* Returns the request URL with the page query parameter set to the specified page
*
* @since 2.1
* @param int $page
* @return string
*/
private function get_paginated_url( $page ) {
// remove existing page query param
$request = remove_query_arg( 'page' );
// add provided page query param
$request = urldecode( add_query_arg( 'page', $page, $request ) );
// get the home host
$host = parse_url( get_home_url(), PHP_URL_HOST );
return set_url_scheme( "http://{$host}{$request}" );
}
/**
* Retrieve the raw request entity (body)
*
* @since 2.1
* @return string
*/
public function get_raw_data() {
return file_get_contents( 'php://input' );
}
/**
* Parse an RFC3339 datetime into a MySQl datetime
*
* Invalid dates default to unix epoch
*
* @since 2.1
* @param string $datetime RFC3339 datetime
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
*/
public function parse_datetime( $datetime ) {
// Strip millisecond precision (a full stop followed by one or more digits)
if ( strpos( $datetime, '.' ) !== false ) {
$datetime = preg_replace( '/\.\d+/', '', $datetime );
}
// default timezone to UTC
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
try {
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
} catch ( Exception $e ) {
$datetime = new DateTime( '@0' );
}
return $datetime->format( 'Y-m-d H:i:s' );
}
/**
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
*
* @since 2.1
* @param int|string $timestamp unix timestamp or MySQL datetime
* @param bool $convert_to_utc
* @param bool $convert_to_gmt Use GMT timezone.
* @return string RFC3339 datetime
*/
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
if ( $convert_to_gmt ) {
if ( is_numeric( $timestamp ) ) {
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
}
$timestamp = get_gmt_from_date( $timestamp );
}
if ( $convert_to_utc ) {
$timezone = new DateTimeZone( wc_timezone_string() );
} else {
$timezone = new DateTimeZone( 'UTC' );
}
try {
if ( is_numeric( $timestamp ) ) {
$date = new DateTime( "@{$timestamp}" );
} else {
$date = new DateTime( $timestamp, $timezone );
}
// convert to UTC by adjusting the time based on the offset of the site's timezone
if ( $convert_to_utc ) {
$date->modify( -1 * $date->getOffset() . ' seconds' );
}
} catch ( Exception $e ) {
$date = new DateTime( '@0' );
}
return $date->format( 'Y-m-d\TH:i:s\Z' );
}
/**
* Extract headers from a PHP-style $_SERVER array
*
* @since 2.1
* @param array $server Associative array similar to $_SERVER
* @return array Headers extracted from the input
*/
public function get_headers( $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
foreach ( $server as $key => $value ) {
if ( strpos( $key, 'HTTP_' ) === 0 ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
}

View File

@ -1,649 +0,0 @@
<?php
/**
* WooCommerce API Taxes Class
*
* Handles requests to the /taxes endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Taxes extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/taxes';
/**
* Register the routes for this class
*
* GET /taxes
* GET /taxes/count
* GET /taxes/<id>
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET/POST /taxes
$routes[ $this->base ] = array(
array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ),
array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /taxes/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ),
);
# GET/PUT/DELETE /taxes/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_tax' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_tax' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_tax' ), WC_API_Server::DELETABLE ),
);
# GET/POST /taxes/classes
$routes[ $this->base . '/classes' ] = array(
array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ),
array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /taxes/classes/count
$routes[ $this->base . '/classes/count' ] = array(
array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ),
);
# GET /taxes/classes/<slug>
$routes[ $this->base . '/classes/(?P<slug>\w[\w\s\-]*)' ] = array(
array( array( $this, 'delete_tax_class' ), WC_API_Server::DELETABLE ),
);
# POST|PUT /taxes/bulk
$routes[ $this->base . '/bulk' ] = array(
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
);
return $routes;
}
/**
* Get all taxes
*
* @since 2.5.0
*
* @param string $fields
* @param array $filter
* @param string $class
* @param int $page
*
* @return array
*/
public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) {
if ( ! empty( $class ) ) {
$filter['tax_rate_class'] = $class;
}
$filter['page'] = $page;
$query = $this->query_tax_rates( $filter );
$taxes = array();
foreach ( $query['results'] as $tax ) {
$taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) );
}
// Set pagination headers
$this->server->add_pagination_headers( $query['headers'] );
return array( 'taxes' => $taxes );
}
/**
* Get the tax for the given ID
*
* @since 2.5.0
*
* @param int $id The tax ID
* @param string $fields fields to include in response
*
* @return array|WP_Error
*/
public function get_tax( $id, $fields = null ) {
global $wpdb;
try {
$id = absint( $id );
// Permissions check
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 );
}
// Get tax rate details
$tax = WC_Tax::_get_tax_rate( $id );
if ( is_wp_error( $tax ) || empty( $tax ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 );
}
$tax_data = array(
'id' => (int) $tax['tax_rate_id'],
'country' => $tax['tax_rate_country'],
'state' => $tax['tax_rate_state'],
'postcode' => '',
'city' => '',
'rate' => $tax['tax_rate'],
'name' => $tax['tax_rate_name'],
'priority' => (int) $tax['tax_rate_priority'],
'compound' => (bool) $tax['tax_rate_compound'],
'shipping' => (bool) $tax['tax_rate_shipping'],
'order' => (int) $tax['tax_rate_order'],
'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard',
);
// Get locales from a tax rate
$locales = $wpdb->get_results( $wpdb->prepare( "
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
", $id ) );
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
$tax_data[ $locale->location_type ] = $locale->location_code;
}
}
return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create a tax
*
* @since 2.5.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_tax( $data ) {
try {
if ( ! isset( $data['tax'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 );
}
// Check permissions
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this );
$tax_data = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '',
'tax_rate_name' => '',
'tax_rate_priority' => 1,
'tax_rate_compound' => 0,
'tax_rate_shipping' => 1,
'tax_rate_order' => 0,
'tax_rate_class' => '',
);
foreach ( $tax_data as $key => $value ) {
$new_key = str_replace( 'tax_rate_', '', $key );
$new_key = 'tax_rate' === $new_key ? 'rate' : $new_key;
if ( isset( $data[ $new_key ] ) ) {
if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) {
$tax_data[ $key ] = $data[ $new_key ] ? 1 : 0;
} else {
$tax_data[ $key ] = $data[ $new_key ];
}
}
}
// Create tax rate
$id = WC_Tax::_insert_tax_rate( $tax_data );
// Add locales
if ( ! empty( $data['postcode'] ) ) {
WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
}
if ( ! empty( $data['city'] ) ) {
WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
}
do_action( 'woocommerce_api_create_tax', $id, $data );
$this->server->send_status( 201 );
return $this->get_tax( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a tax
*
* @since 2.5.0
*
* @param int $id The tax ID
* @param array $data
*
* @return array|WP_Error
*/
public function edit_tax( $id, $data ) {
try {
if ( ! isset( $data['tax'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 );
}
// Check permissions
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 );
}
$data = $data['tax'];
// Get current tax rate data
$tax = $this->get_tax( $id );
if ( is_wp_error( $tax ) ) {
$error_data = $tax->get_error_data();
throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] );
}
$current_data = $tax['tax'];
$data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this );
$tax_data = array();
$default_fields = array(
'tax_rate_country',
'tax_rate_state',
'tax_rate',
'tax_rate_name',
'tax_rate_priority',
'tax_rate_compound',
'tax_rate_shipping',
'tax_rate_order',
'tax_rate_class',
);
foreach ( $data as $key => $value ) {
$new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key;
// Check if the key is valid
if ( ! in_array( $new_key, $default_fields ) ) {
continue;
}
// Test new data against current data
if ( $value === $current_data[ $key ] ) {
continue;
}
// Fix compound and shipping values
if ( in_array( $key, array( 'compound', 'shipping' ) ) ) {
$value = $value ? 1 : 0;
}
$tax_data[ $new_key ] = $value;
}
// Update tax rate
WC_Tax::_update_tax_rate( $id, $tax_data );
// Update locales
if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) {
WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
}
if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) {
WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
}
do_action( 'woocommerce_api_edit_tax_rate', $id, $data );
return $this->get_tax( $id );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a tax
*
* @since 2.5.0
*
* @param int $id The tax ID
*
* @return array|WP_Error
*/
public function delete_tax( $id ) {
global $wpdb;
try {
// Check permissions
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 );
}
$id = absint( $id );
WC_Tax::_delete_tax_rate( $id );
if ( 0 === $wpdb->rows_affected ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 );
}
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of taxes
*
* @since 2.5.0
*
* @param string $class
* @param array $filter
*
* @return array|WP_Error
*/
public function get_taxes_count( $class = null, $filter = array() ) {
try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 );
}
if ( ! empty( $class ) ) {
$filter['tax_rate_class'] = $class;
}
$query = $this->query_tax_rates( $filter, true );
return array( 'count' => (int) $query['headers']->total );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Helper method to get tax rates objects
*
* @since 2.5.0
*
* @param array $args
* @param bool $count_only
*
* @return array
*/
protected function query_tax_rates( $args, $count_only = false ) {
global $wpdb;
$results = '';
// Set args
$args = $this->merge_query_args( $args, array() );
$query = "
SELECT tax_rate_id
FROM {$wpdb->prefix}woocommerce_tax_rates
WHERE 1 = 1
";
// Filter by tax class
if ( ! empty( $args['tax_rate_class'] ) ) {
$tax_rate_class = esc_sql( 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : '' );
$query .= " AND tax_rate_class = '$tax_rate_class'";
}
// Order tax rates
$order_by = ' ORDER BY tax_rate_order';
// Pagination
$per_page = absint( isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ) );
$offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0;
$pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page );
if ( ! $count_only ) {
$results = $wpdb->get_results( $query . $order_by . $pagination );
}
$wpdb->get_results( $query );
$headers = new stdClass;
$headers->page = $args['paged'];
$headers->total = (int) $wpdb->num_rows;
$headers->is_single = $per_page > $headers->total;
$headers->total_pages = ceil( $headers->total / $per_page );
return array(
'results' => $results,
'headers' => $headers,
);
}
/**
* Bulk update or insert taxes
* Accepts an array with taxes in the formats supported by
* WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax()
*
* @since 2.5.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function bulk( $data ) {
try {
if ( ! isset( $data['taxes'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 );
}
$data = $data['taxes'];
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' );
// Limit bulk operation
if ( count( $data ) > $limit ) {
throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
}
$taxes = array();
foreach ( $data as $_tax ) {
$tax_id = 0;
// Try to get the tax rate ID
if ( isset( $_tax['id'] ) ) {
$tax_id = intval( $_tax['id'] );
}
if ( $tax_id ) {
// Tax rate exists / edit tax rate
$edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) );
if ( is_wp_error( $edit ) ) {
$taxes[] = array(
'id' => $tax_id,
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
);
} else {
$taxes[] = $edit['tax'];
}
} else {
// Tax rate don't exists / create tax rate
$new = $this->create_tax( array( 'tax' => $_tax ) );
if ( is_wp_error( $new ) ) {
$taxes[] = array(
'id' => $tax_id,
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
);
} else {
$taxes[] = $new['tax'];
}
}
}
return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get all tax classes
*
* @since 2.5.0
*
* @param string $fields
*
* @return array|WP_Error
*/
public function get_tax_classes( $fields = null ) {
try {
// Permissions check
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 );
}
$tax_classes = array();
// Add standard class
$tax_classes[] = array(
'slug' => 'standard',
'name' => __( 'Standard rate', 'woocommerce' ),
);
$classes = WC_Tax::get_tax_classes();
foreach ( $classes as $class ) {
$tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array(
'slug' => sanitize_title( $class ),
'name' => $class,
), $class, $fields, $this );
}
return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create a tax class.
*
* @since 2.5.0
*
* @param array $data
*
* @return array|WP_Error
*/
public function create_tax_class( $data ) {
try {
if ( ! isset( $data['tax_class'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 );
}
// Check permissions
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 );
}
$data = $data['tax_class'];
if ( empty( $data['name'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
}
$name = sanitize_text_field( $data['name'] );
$tax_class = WC_Tax::create_tax_class( $name );
if ( is_wp_error( $tax_class ) ) {
return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 );
}
do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data );
$this->server->send_status( 201 );
return array(
'tax_class' => $tax_class,
);
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a tax class
*
* @since 2.5.0
*
* @param int $slug The tax class slug
*
* @return array|WP_Error
*/
public function delete_tax_class( $slug ) {
global $wpdb;
try {
// Check permissions
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 );
}
$slug = sanitize_title( $slug );
$tax_class = WC_Tax::get_tax_class_by( 'slug', $slug );
$deleted = WC_Tax::delete_tax_class_by( 'slug', $slug );
if ( is_wp_error( $deleted ) || ! $deleted ) {
throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 );
}
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the total number of tax classes
*
* @since 2.5.0
*
* @return array|WP_Error
*/
public function get_tax_classes_count() {
try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 );
}
$total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate
return array( 'count' => $total );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
}

View File

@ -1,509 +0,0 @@
<?php
/**
* WooCommerce API Webhooks class
*
* Handles requests to the /webhooks endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Webhooks extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/webhooks';
/**
* Register the routes for this class
*
* @since 2.2
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET|POST /webhooks
$routes[ $this->base ] = array(
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /webhooks/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /webhooks/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
);
# GET /webhooks/<id>/deliveries
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
);
# GET /webhooks/<webhook_id>/deliveries/<id>
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all webhooks
*
* @since 2.2
*
* @param array $fields
* @param array $filter
* @param string $status
* @param int $page
*
* @return array
*/
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$filter['page'] = $page;
$query = $this->query_webhooks( $filter );
$webhooks = array();
foreach ( $query['results'] as $webhook_id ) {
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
}
$this->server->add_pagination_headers( $query['headers'] );
return array( 'webhooks' => $webhooks );
}
/**
* Get the webhook for the given ID
*
* @since 2.2
* @param int $id webhook ID
* @param array $fields
* @return array|WP_Error
*/
public function get_webhook( $id, $fields = null ) {
// ensure webhook ID is valid & user has permission to read
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$webhook = wc_get_webhook( $id );
$webhook_data = array(
'id' => $webhook->get_id(),
'name' => $webhook->get_name(),
'status' => $webhook->get_status(),
'topic' => $webhook->get_topic(),
'resource' => $webhook->get_resource(),
'event' => $webhook->get_event(),
'hooks' => $webhook->get_hooks(),
'delivery_url' => $webhook->get_delivery_url(),
'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
);
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
}
/**
* Get the total number of webhooks
*
* @since 2.2
*
* @param string $status
* @param array $filter
*
* @return array|WP_Error
*/
public function get_webhooks_count( $status = null, $filter = array() ) {
try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
}
if ( ! empty( $status ) ) {
$filter['status'] = $status;
}
$query = $this->query_webhooks( $filter );
return array( 'count' => $query['headers']->total );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create an webhook
*
* @since 2.2
*
* @param array $data parsed webhook data
*
* @return array|WP_Error
*/
public function create_webhook( $data ) {
try {
if ( ! isset( $data['webhook'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
}
$data = $data['webhook'];
// permission check
if ( ! current_user_can( 'manage_woocommerce' ) ) {
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
}
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
// validate topic
if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
}
// validate delivery URL
if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
}
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
'post_type' => 'shop_webhook',
'post_status' => 'publish',
'ping_status' => 'closed',
'post_author' => get_current_user_id(),
'post_password' => 'webhook_' . wp_generate_password(),
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ),
), $data, $this );
$webhook = new WC_Webhook();
$webhook->set_name( $webhook_data['post_title'] );
$webhook->set_user_id( $webhook_data['post_author'] );
$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
$webhook->set_topic( $data['topic'] );
$webhook->set_delivery_url( $data['delivery_url'] );
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
$webhook->set_api_version( 'legacy_v3' );
$webhook->save();
$webhook->deliver_ping();
// HTTP 201 Created
$this->server->send_status( 201 );
do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );
return $this->get_webhook( $webhook->get_id() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Edit a webhook
*
* @since 2.2
*
* @param int $id webhook ID
* @param array $data parsed webhook data
*
* @return array|WP_Error
*/
public function edit_webhook( $id, $data ) {
try {
if ( ! isset( $data['webhook'] ) ) {
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
}
$data = $data['webhook'];
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
if ( is_wp_error( $id ) ) {
return $id;
}
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
$webhook = wc_get_webhook( $id );
// update topic
if ( ! empty( $data['topic'] ) ) {
if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
$webhook->set_topic( $data['topic'] );
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
}
}
// update delivery URL
if ( ! empty( $data['delivery_url'] ) ) {
if ( wc_is_valid_url( $data['delivery_url'] ) ) {
$webhook->set_delivery_url( $data['delivery_url'] );
} else {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
}
}
// update secret
if ( ! empty( $data['secret'] ) ) {
$webhook->set_secret( $data['secret'] );
}
// update status
if ( ! empty( $data['status'] ) ) {
$webhook->set_status( $data['status'] );
}
// update name
if ( ! empty( $data['name'] ) ) {
$webhook->set_name( $data['name'] );
}
$webhook->save();
do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );
return $this->get_webhook( $webhook->get_id() );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a webhook
*
* @since 2.2
* @param int $id webhook ID
* @return array|WP_Error
*/
public function delete_webhook( $id ) {
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
if ( is_wp_error( $id ) ) {
return $id;
}
do_action( 'woocommerce_api_delete_webhook', $id, $this );
$webhook = wc_get_webhook( $id );
return $webhook->delete( true );
}
/**
* Helper method to get webhook post objects
*
* @since 2.2
* @param array $args Request arguments for filtering query.
* @return array
*/
private function query_webhooks( $args ) {
$args = $this->merge_query_args( array(), $args );
$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );
if ( empty( $args['offset'] ) ) {
$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
}
$page = $args['paged'];
unset( $args['paged'], $args['posts_per_page'] );
if ( isset( $args['s'] ) ) {
$args['search'] = $args['s'];
unset( $args['s'] );
}
// Post type to webhook status.
if ( ! empty( $args['post_status'] ) ) {
$args['status'] = $args['post_status'];
unset( $args['post_status'] );
}
if ( ! empty( $args['post__in'] ) ) {
$args['include'] = $args['post__in'];
unset( $args['post__in'] );
}
if ( ! empty( $args['date_query'] ) ) {
foreach ( $args['date_query'] as $date_query ) {
if ( 'post_date_gmt' === $date_query['column'] ) {
$args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
$args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
}
}
unset( $args['date_query'] );
}
$args['paginate'] = true;
// Get the webhooks.
$data_store = WC_Data_Store::load( 'webhook' );
$results = $data_store->search_webhooks( $args );
// Get total items.
$headers = new stdClass;
$headers->page = $page;
$headers->total = $results->total;
$headers->is_single = $args['limit'] > $headers->total;
$headers->total_pages = $results->max_num_pages;
return array(
'results' => $results->webhooks,
'headers' => $headers,
);
}
/**
* Get deliveries for a webhook
*
* @since 2.2
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @param string $webhook_id webhook ID
* @param string|null $fields fields to include in response
* @return array|WP_Error
*/
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
// Ensure ID is valid webhook ID
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
if ( is_wp_error( $webhook_id ) ) {
return $webhook_id;
}
return array( 'webhook_deliveries' => array() );
}
/**
* Get the delivery log for the given webhook ID and delivery ID
*
* @since 2.2
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @param string $webhook_id webhook ID
* @param string $id delivery log ID
* @param string|null $fields fields to limit response to
*
* @return array|WP_Error
*/
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
try {
// Validate webhook ID
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
if ( is_wp_error( $webhook_id ) ) {
return $webhook_id;
}
$id = absint( $id );
if ( empty( $id ) ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
}
$webhook = new WC_Webhook( $webhook_id );
$log = 0;
if ( ! $log ) {
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
}
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer.
* 2) the ID returns a valid post object and matches the provided post type.
* 3) the current user has the proper permissions to read/edit/delete the post.
*
* @since 3.3.0
* @param string|int $id The post ID
* @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
* @param string $context The context of the request, either `read`, `edit` or `delete`.
* @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
*/
protected function validate_request( $id, $type, $context ) {
$id = absint( $id );
// Validate ID.
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
}
$webhook = wc_get_webhook( $id );
if ( null === $webhook ) {
return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
}
// Validate permissions.
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
case 'edit':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
case 'delete':
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
}
break;
}
return $id;
}
}

View File

@ -1,47 +0,0 @@
<?php
/**
* WooCommerce API
*
* Defines an interface that API request/response handlers should implement
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
interface WC_API_Handler {
/**
* Get the content type for the response
*
* This should return the proper HTTP content-type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type();
/**
* Parse the raw request body entity into an array
*
* @since 2.1
* @param string $data
* @return array
*/
public function parse_body( $data );
/**
* Generate a response from an array of data
*
* @since 2.1
* @param array $data
* @return string
*/
public function generate_response( $data );
}

View File

@ -1,405 +0,0 @@
<?php
/**
* WooCommerce Legacy API. Was deprecated with 2.6.0.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.6
*/
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Legacy API.
*/
class WC_Legacy_API {
use AccessiblePrivateMethods;
/**
* This is the major version for the REST API and takes
* first-order position in endpoint URLs.
*
* @deprecated 2.6.0
* @var string
*/
const VERSION = '3.1.0';
/**
* The REST API server.
*
* @deprecated 2.6.0
* @var WC_API_Server
*/
public $server;
/**
* REST API authentication class instance.
*
* @deprecated 2.6.0
* @var WC_API_Authentication
*/
public $authentication;
/**
* Init the legacy API.
*/
public function init() {
add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 );
$this->mark_method_as_accessible( 'maybe_display_legacy_wc_api_usage_notice' );
self::add_action( 'admin_notices', array( $this, 'maybe_display_legacy_wc_api_usage_notice' ), 0 );
}
/**
* Add new query vars.
*
* @since 2.0
* @param array $vars Vars.
* @return string[]
*/
public function add_query_vars( $vars ) {
$vars[] = 'wc-api-version'; // Deprecated since 2.6.0.
$vars[] = 'wc-api-route'; // Deprecated since 2.6.0.
return $vars;
}
/**
* Write a log entry and update the last usage options, for a Legacy REST API request.
*
* @param string $route The Legacy REST API route requested.
* @param string|null $user_agent The content of the user agent HTTP header in the request, null if not available.
*/
private function maybe_log_rest_api_request( string $route, ?string $user_agent ) {
if ( is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) {
return;
}
$user_agent = $user_agent ?? 'unknown';
$current_date = wp_date( 'Y-m-d H:i:s' );
$stored_api_accesses = get_transient( 'wc_legacy_rest_api_usages' );
if ( false === $stored_api_accesses ) {
$stored_api_accesses = array(
'user_agents' => array(),
'first_usage' => $current_date,
'total_count' => 0,
);
}
$stored_api_accesses_for_user_agent = $stored_api_accesses['user_agents'][ $user_agent ] ?? null;
if ( is_null( $stored_api_accesses_for_user_agent ) ) {
$stored_api_accesses['user_agents'][ $user_agent ] = array(
'first_date' => $current_date,
'last_date' => $current_date,
'count' => 1,
);
} else {
$stored_api_accesses['user_agents'][ $user_agent ]['count']++;
$stored_api_accesses['user_agents'][ $user_agent ]['last_date'] = $current_date;
}
$stored_api_accesses['total_count']++;
set_transient( 'wc_legacy_rest_api_usages', $stored_api_accesses, time() + 2 * WEEK_IN_SECONDS );
/**
* This filter allows disabling the logging of Legacy REST API usages.
*
* @param bool $do_logging True to enable the logging of all the Legacy REST API usages (default), false to disable.
*
* @since 8.5.0
*/
if ( apply_filters( 'woocommerce_log_legacy_rest_api_usages', true ) ) {
$user_agent = sanitize_text_field( wp_unslash( $user_agent ) );
$route = sanitize_text_field( wp_unslash( $route ) );
$info = 'Version: ' . WC_API_REQUEST_VERSION . ", Route: $route, User agent: $user_agent";
$ip_address = WC_Geolocation::get_ip_address();
if( '' !== $ip_address ) {
$info .= ", IP: $ip_address";
}
wc_get_logger()->info( $info, array( 'source' => 'legacy_rest_api_usages' ) );
}
}
/**
* Add new endpoints.
*
* @since 2.0
*/
public static function add_endpoint() {
// REST API, deprecated since 2.6.0.
add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' );
add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' );
}
/**
* Handle REST API requests.
*
* @since 2.2
* @deprecated 2.6.0
*/
public function handle_rest_api_requests() {
global $wp;
if ( ! empty( $_GET['wc-api-version'] ) ) {
$wp->query_vars['wc-api-version'] = $_GET['wc-api-version'];
}
if ( ! empty( $_GET['wc-api-route'] ) ) {
$wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
}
if ( empty( $wp->query_vars['wc-api-version'] ) || empty( $wp->query_vars['wc-api-route'] ) ) {
return;
}
// REST API request.
wc_maybe_define_constant( 'WC_API_REQUEST', true );
wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) );
$route = $wp->query_vars['wc-api-route'];
$this->maybe_log_rest_api_request( $route, $_SERVER['HTTP_USER_AGENT'] ?? null );
// Legacy v1 API request.
if ( 1 === WC_API_REQUEST_VERSION ) {
$this->handle_v1_rest_api_request();
} elseif ( 2 === WC_API_REQUEST_VERSION ) {
$this->handle_v2_rest_api_request();
} else {
$this->includes();
$this->server = new WC_API_Server( $route );
// load API resource classes.
$this->register_resources( $this->server );
// Fire off the request.
$this->server->serve_request();
}
exit;
}
/**
* Display an admin notice with information about the last Legacy REST API usage,
* if the corresponding transient is available and unless the Legacy REST API
* extension is installed or the user has dismissed the notice.
*/
private function maybe_display_legacy_wc_api_usage_notice(): void {
$legacy_api_usages = get_transient( 'wc_legacy_rest_api_usages' );
if ( false === $legacy_api_usages || is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) || 'yes' !== get_option( 'woocommerce_api_enabled' ) ) {
if ( WC_Admin_Notices::has_notice( 'legacy_api_usages_detected' ) ) {
WC_Admin_Notices::remove_notice( 'legacy_api_usages_detected' );
}
} elseif ( ! WC_Admin_Notices::user_has_dismissed_notice( 'legacy_api_usages_detected' ) ) {
unset( $legacy_api_usages['user_agents']['unknown'] );
$user_agents = array_keys( $legacy_api_usages['user_agents'] );
WC_Admin_Notices::add_custom_notice(
'legacy_api_usages_detected',
sprintf(
'%s%s',
sprintf(
'<h4>%s</h4>',
esc_html__( 'WooCommerce Legacy REST API access detected', 'woocommerce' )
),
sprintf(
// translators: %1$d = count of Legacy REST API usages recorded, %2$s = date and time of first access, %3$d = count of known user agents registered, %4$s = URL.
wpautop( wp_kses_data( __( '<p>The WooCommerce Legacy REST API has been accessed <b>%1$d</b> time(s) since <b>%2$s</b>. There are <b>%3$d</b> known user agent(s) registered. There are more details in <b><a target="_blank" href="%4$s">the WooCommerce log files</a></b> (file names start with <code>legacy_rest_api_usages</code>).', 'woocommerce' ) ) ),
$legacy_api_usages['total_count'],
$legacy_api_usages['first_usage'],
count( $user_agents ),
admin_url( 'admin.php?page=wc-status&tab=logs' ),
)
)
);
}
}
/**
* Include required files for REST API request.
*
* @since 2.1
* @deprecated 2.6.0
*/
public function includes() {
// API server / response handlers.
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php';
// Authentication.
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php';
// Allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
}
/**
* Register available API resources.
*
* @since 2.1
* @deprecated 2.6.0
* @param WC_API_Server $server the REST server.
*/
public function register_resources( $server ) {
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Coupons',
'WC_API_Customers',
'WC_API_Orders',
'WC_API_Products',
'WC_API_Reports',
'WC_API_Taxes',
'WC_API_Webhooks',
)
);
foreach ( $api_classes as $api_class ) {
$this->$api_class = new $api_class( $server );
}
}
/**
* Handle legacy v1 REST API requests.
*
* @since 2.2
* @deprecated 2.6.0
*/
private function handle_v1_rest_api_request() {
// Include legacy required files for v1 REST API request.
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php';
// Allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );
// Register available resources for legacy v1 REST API request.
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Customers',
'WC_API_Orders',
'WC_API_Products',
'WC_API_Coupons',
'WC_API_Reports',
)
);
foreach ( $api_classes as $api_class ) {
$this->$api_class = new $api_class( $this->server );
}
// Fire off the request.
$this->server->serve_request();
}
/**
* Handle legacy v2 REST API requests.
*
* @since 2.4
* @deprecated 2.6.0
*/
private function handle_v2_rest_api_request() {
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php';
// allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );
// Register available resources for legacy v2 REST API request.
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Customers',
'WC_API_Orders',
'WC_API_Products',
'WC_API_Coupons',
'WC_API_Reports',
'WC_API_Webhooks',
)
);
foreach ( $api_classes as $api_class ) {
$this->$api_class = new $api_class( $this->server );
}
// Fire off the request.
$this->server->serve_request();
}
/**
* Rest API Init.
*
* @deprecated 3.7.0 - REST API classes autoload.
*/
public function rest_api_init() {}
/**
* Include REST API classes.
*
* @deprecated 3.7.0 - REST API classes autoload.
*/
public function rest_api_includes() {
$this->rest_api_init();
}
/**
* Register REST API routes.
*
* @deprecated 3.7.0
*/
public function register_rest_routes() {
wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' );
$this->register_wp_admin_settings();
}
}

View File

@ -10,7 +10,7 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\RestApiUtil;
use Automattic\WooCommerce\Internal\RestApiParameterUtil;
/**
* REST API Order Refunds controller class.
@ -36,7 +36,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controll
* @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
*/
protected function prepare_object_for_database( $request, $creating = false ) {
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$order = wc_get_order( (int) $request['order_id'] );

View File

@ -1102,20 +1102,21 @@ function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly =
}
/**
* Get the URL to the WooCommerce REST API.
* Get the URL to the WooCommerce Legacy REST API.
*
* Note that as of WooCommerce 9.0 the WooCommerce Legacy REST API has been moved to a dedicated extension,
* and the implementation of its root endpoint in WooCommerce core is now just a stub that will always return an error.
* See the setup_legacy_api_stub method in includes/class-woocommerce.php and:
* https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/
*
* @deprecated 9.0.0 The Legacy REST API has been removed from WooCommerce core.
*
* @since 2.1
* @param string $path an endpoint to include in the URL.
* @return string the URL.
*/
function get_woocommerce_api_url( $path ) {
if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) {
$version = Constants::get_constant( 'WC_API_REQUEST_VERSION' );
} else {
$version = substr( WC_API::VERSION, 0, 1 );
}
$url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' );
$url = get_home_url( null, 'wc-api/v3/', is_ssl() ? 'https' : 'http' );
if ( ! empty( $path ) && is_string( $path ) ) {
$url .= ltrim( $path, '/' );

View File

@ -16,6 +16,7 @@ use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\RestApiUtil;
use Automattic\WooCommerce\Utilities\TimeUtil;
/**
@ -35,6 +36,7 @@ class UtilsClassesServiceProvider extends AbstractInterfaceServiceProvider {
PluginUtil::class,
COTMigrationUtil::class,
WebhookUtil::class,
RestApiUtil::class,
TimeUtil::class,
PluginInstaller::class,
);
@ -51,6 +53,7 @@ class UtilsClassesServiceProvider extends AbstractInterfaceServiceProvider {
$this->share( COTMigrationUtil::class )
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
$this->share( WebhookUtil::class );
$this->share( RestApiUtil::class );
$this->share( TimeUtil::class );
$this->share_with_implements_tags( PluginInstaller::class );
}

View File

@ -6,13 +6,13 @@
namespace Automattic\WooCommerce\Internal;
/**
* Helper methos for the REST API.
* Helper methods for the REST API.
*
* Class ApiUtil
*
* @package Automattic\WooCommerce\Internal
*/
class RestApiUtil {
class RestApiParameterUtil {
/**
* Converts a create refund request from the public API format:
@ -193,10 +193,10 @@ class RestApiUtil {
/**
* Is an array sequential or associative?
*
* @param array $array The array to check.
* @param array $the_array The array to check.
* @return bool True if the array is associative, false if it's sequential.
*/
private static function is_associative( array $array ) {
return array_keys( $array ) !== range( 0, count( $array ) - 1 );
private static function is_associative( array $the_array ) {
return array_keys( $the_array ) !== range( 0, count( $the_array ) - 1 );
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Automattic\WooCommerce\Internal\Utilities;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* The Legacy REST API was removed in WooCommerce 9.0 and is now available as a dedicated extension.
* A stub is kept in WooCommerce core that just returns a "The WooCommerce API is disabled on this site"
* error if the extension is not installed or is inactive. See:
* https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/
*/
class LegacyRestApiStub {
use AccessiblePrivateMethods;
/**
* Set up the Legacy REST API stub.
*/
public static function setup() {
self::add_action( 'init', array( __CLASS__, 'add_rewrite_rules_for_legacy_rest_api_stub' ), 0 );
self::add_action( 'query_vars', array( __CLASS__, 'add_query_vars_for_legacy_rest_api_stub' ), 0 );
self::add_action( 'parse_request', array( __CLASS__, 'parse_legacy_rest_api_request' ), 0 );
}
/**
* Add the necessary rewrite rules for the Legacy REST API
* (either the dedicated extension if it's installed, or the stub otherwise).
*/
private static function add_rewrite_rules_for_legacy_rest_api_stub() {
add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' );
add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' );
add_rewrite_endpoint( 'wc-api', EP_ALL );
}
/**
* Add the necessary request query variables for the Legacy REST API
* (either the dedicated extension if it's installed, or the stub otherwise).
*
* @param array $vars The query variables array to extend.
* @return array The extended query variables array.
*/
private static function add_query_vars_for_legacy_rest_api_stub( $vars ) {
$vars[] = 'wc-api-version';
$vars[] = 'wc-api-route';
$vars[] = 'wc-api';
return $vars;
}
/**
* Process an incoming request for the Legacy REST API.
*
* If the dedicated Legacy REST API extension is installed and active, this method does nothing.
* Otherwise it returns a "The WooCommerce API is disabled on this site" error,
* unless the request contains a "wc-api" variable and the appropriate
* "woocommerce_api_*" hook is set.
*/
private static function parse_legacy_rest_api_request() {
global $wp;
// The WC_API class existing means that the Legacy REST API extension is installed and active.
if ( class_exists( 'WC_API' ) ) {
return;
}
self::maybe_process_wc_api_query_var();
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
if ( ! empty( $_GET['wc-api-version'] ) ) {
$wp->query_vars['wc-api-version'] = $_GET['wc-api-version'];
}
if ( ! empty( $_GET['wc-api-route'] ) ) {
$wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
}
if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) {
header(
sprintf(
'Content-Type: %s; charset=%s',
isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json',
get_option( 'blog_charset' )
)
);
status_header( 404 );
echo wp_json_encode(
array(
'errors' => array(
'code' => 'woocommerce_api_disabled',
'message' => 'The WooCommerce API is disabled on this site',
),
)
);
exit;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
}
/**
* Process a "wc-api" variable if present in the query, by triggering the appropriate hooks.
*/
private static function maybe_process_wc_api_query_var() {
global $wp;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['wc-api'] ) ) {
$wp->query_vars['wc-api'] = sanitize_key( wp_unslash( $_GET['wc-api'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// wc-api endpoint requests.
if ( ! empty( $wp->query_vars['wc-api'] ) ) {
// Buffer, we won't want any output here.
ob_start();
// No cache headers.
wc_nocache_headers();
// Clean the API request.
$api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) );
// Make sure gateways are available for request.
WC()->payment_gateways();
// phpcs:disable WooCommerce.Commenting.CommentHooks.HookCommentWrongStyle
// Trigger generic action before request hook.
do_action( 'woocommerce_api_request', $api_request );
// Is there actually something hooked into this API request? If not trigger 400 - Bad request.
status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 );
// Trigger an action which plugins can hook into to fulfill the request.
do_action( 'woocommerce_api_' . $api_request );
// phpcs:enable WooCommerce.Commenting.CommentHooks.HookCommentWrongStyle
// Done, clear buffer and exit.
ob_end_clean();
die( '-1' );
}
}
}

View File

@ -9,6 +9,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
use Automattic\WooCommerce\Utilities\RestApiUtil;
/**
* Checkout class.
@ -321,7 +322,7 @@ class Checkout extends AbstractCartRoute {
private function add_data_to_error_object( $error, $data, $http_status_code, bool $include_cart = false ) {
$data = array_merge( $data, [ 'status' => $http_status_code ] );
if ( $include_cart ) {
$data = array_merge( $data, [ 'cart' => wc()->api->get_endpoint_data( '/wc/store/v1/cart' ) ] );
$data = array_merge( $data, [ 'cart' => wc_get_container()->get( RestApiUtil::class )->get_endpoint_data( '/wc/store/v1/cart' ) ] );
}
$error->add_data( $data );
return $error;

View File

@ -0,0 +1,30 @@
<?php
namespace Automattic\WooCommerce\Utilities;
/**
* Utility methods related to the REST API.
*/
class RestApiUtil {
/**
* Get data from a WooCommerce API endpoint.
* This method used to be part of the WooCommerce Legacy REST API.
*
* @since 9.0.0
*
* @param string $endpoint Endpoint.
* @param array $params Params to pass with request.
* @return array|\WP_Error
*/
public function get_endpoint_data( $endpoint, $params = array() ) {
$request = new \WP_REST_Request( 'GET', $endpoint );
if ( $params ) {
$request->set_query_params( $params );
}
$response = rest_do_request( $request );
$server = rest_get_server();
$json = wp_json_encode( $server->response_to_data( $response, false ) );
return json_decode( $json, true );
}
}

View File

@ -1,14 +1,10 @@
const { test, expect } = require( '@playwright/test' );
const { API_BASE_URL } = process.env;
const shouldSkip = API_BASE_URL != undefined;
const shouldSkip = API_BASE_URL !== undefined;
const exp = require( 'constants' );
const { keys } = require( 'lodash' );
const {
countries,
currencies,
externalCurrencies,
externalCountries,
stateOptions,
} = require( '../../data/settings' );
@ -677,7 +673,6 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
},
}
);
const responseJSON = await response.json();
expect( response.status() ).toEqual( 200 );
// retrieve the updated settings values
@ -1422,11 +1417,8 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
'Remove personal data from orders on request',
type: 'checkbox',
default: 'no',
tip: expect.stringContaining(
'When handling an <a href='
),
tip: expect.stringContaining(
'wp-admin/erase-personal-data.php">account erasure request</a>, should personal data within orders be retained or removed?'
tip: expect.stringMatching(
'When handling an <a href="[^"]*wp-admin/erase-personal-data.php">account erasure request</a>, should personal data within orders be retained or removed?'
),
value: 'no',
} ),
@ -1440,11 +1432,8 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
description: 'Remove access to downloads on request',
type: 'checkbox',
default: 'no',
tip: expect.stringContaining(
'When handling an <a href='
),
tip: expect.stringContaining(
'wp-admin/erase-personal-data.php">account erasure request</a>, should access to downloadable files be revoked and download logs cleared?'
tip: expect.stringMatching(
'When handling an <a href="[^"]*wp-admin/erase-personal-data.php">account erasure request</a>, should access to downloadable files be revoked and download logs cleared?'
),
value: 'no',
} ),
@ -1865,18 +1854,6 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
} ),
] )
);
expect( responseJSON ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
id: 'woocommerce_api_enabled',
label: 'Legacy API',
description: 'Enable the legacy REST API',
type: 'checkbox',
default: 'no',
value: 'no',
} ),
] )
);
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [

View File

@ -20,8 +20,12 @@ class WC_API_Unit_Test_Case extends WC_Unit_Test_Case {
*
* @since 2.2
* @see WC_Unit_Test_Case::setUp()
* @throws Exception The Legacy REST API plugin is not installed.
*/
public function setUp(): void {
if ( is_null( wc()->api ) ) {
throw new Exception( 'The Legacy REST API plugin is not installed' );
}
parent::setUp();

View File

@ -24,23 +24,4 @@ class WC_Tests_Packages extends WC_Unit_Test_Case {
$this->assertTrue( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) );
$this->assertTrue( class_exists( '\Automattic\WooCommerce\RestApi\Package' ) );
}
/**
* Check API package returns values.
*
* @return void
*/
public function test_api_package() {
$this->assertNotNull( wc()->api->get_rest_api_package_version() );
$this->assertNotNull( wc()->api->get_rest_api_package_path() );
}
/**
* Test that the REST API package is working by hitting some endpoints.
*/
public function test_api_endpoints_exist() {
$response = wc()->api->get_endpoint_data( '/wc/v3' );
$this->assertFalse( is_wp_error( $response ) );
$this->assertEquals( 'wc/v3', $response['namespace'] ); // phpcs:ignore
}
}

View File

@ -19,12 +19,13 @@ class WooCommerce_Test extends \WC_Unit_Test_Case {
}
/**
* Test that the $api property is defined, public and initialized correctly.
* Test that the $api property is defined, public and is null
* (the Legacy REST API was removed in WooCommerce 9.0).
*/
public function test_api_property(): void {
$property = new ReflectionProperty( WooCommerce::class, 'api' );
$this->assertTrue( $property->isPublic() );
$this->assertInstanceOf( WC_API::class, $property->getValue( WC() ) );
$this->assertNull( $property->getValue( WC() ) );
}
}

View File

@ -2,13 +2,13 @@
namespace Automattic\WooCommerce\Tests\Internal;
use Automattic\WooCommerce\Internal\RestApiUtil;
use Automattic\WooCommerce\Internal\RestApiParameterUtil;
/**
* Tests for the RestApiUtil class.
* Tests for the RestApiParameterUtil class.
* @package Automattic\WooCommerce\Tests\Internal
*/
class RestApiUtilTest extends \WC_Unit_Test_Case {
class RestApiParameterUtilTest extends \WC_Unit_Test_Case {
/**
* @testdox 'adjust_create_refund_request_parameters' adjust the 'reason' parameter to null if it's somehow empty.
@ -26,7 +26,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request['reason'] = $input_reason;
}
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$this->assertEquals( $expected_output_reason, $request['reason'] );
}
@ -49,7 +49,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request['api_refund'] = $input_api_refund;
}
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$this->assertEquals( $expected_output_api_refund, $request['api_refund'] );
}
@ -77,7 +77,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request = new \WP_REST_Request();
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$expected = array(
'1' => array(
@ -117,7 +117,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request = new \WP_REST_Request();
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$expected = array(
'1' => array(
@ -148,7 +148,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request = new \WP_REST_Request();
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$this->assertEquals( $line_items, $request['line_items'] );
}
@ -173,7 +173,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request = new \WP_REST_Request();
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$expected = strval( 10 + 20 + 30 + 40 );
$this->assertEquals( $expected, $request['amount'] );
@ -197,7 +197,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request['amount'] = '999';
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$this->assertEquals( '999', $request['amount'] );
}
@ -276,7 +276,7 @@ class RestApiUtilTest extends \WC_Unit_Test_Case {
$request = new \WP_REST_Request();
$request['line_items'] = $line_items;
RestApiUtil::adjust_create_refund_request_parameters( $request );
RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
$this->assertFalse( isset( $request['amount'] ) );
}