rebase on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:23:21 -04:00
parent d37566ffb6
commit d096058d7d
4789 changed files with 254611 additions and 307223 deletions

View File

@@ -11,15 +11,20 @@
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\BlocksUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
defined( 'ABSPATH' ) || exit;
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Backwards compatibility.
/**
* WooCommerce Tracker Class
*/
class WC_Tracker {
// phpcs:enable
/**
* URL to the WooThemes Tracker API endpoint.
*
@@ -30,7 +35,7 @@ class WC_Tracker {
/**
* Hook into cron event.
*/
public static function init() {
public static function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingFinal, WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag -- Not an injection.
add_action( 'woocommerce_tracker_send_event', array( __CLASS__, 'send_tracking_data' ) );
}
@@ -45,10 +50,15 @@ class WC_Tracker {
return;
}
/**
* Filter whether to send tracking data or not.
*
* @since 2.3.0
*/
if ( ! apply_filters( 'woocommerce_tracker_send_override', $override ) ) {
// Send a maximum of once per week by default.
$last_send = self::get_last_send_time();
if ( $last_send && $last_send > apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) {
if ( $last_send && $last_send > apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { // phpcs:ignore
return;
}
} else {
@@ -84,6 +94,11 @@ class WC_Tracker {
* @return int|bool
*/
private static function get_last_send_time() {
/**
* Filter the last time tracking data was sent.
*
* @since 2.3.0
*/
return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) );
}
@@ -118,7 +133,12 @@ class WC_Tracker {
$data = array();
// General site info.
$data['url'] = home_url();
$data['url'] = home_url();
/**
* Filter the admin email that's sent with data.
*
* @since 2.3.0
*/
$data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) );
$data['theme'] = self::get_theme_info();
@@ -134,7 +154,6 @@ class WC_Tracker {
$data['inactive_plugins'] = $all_plugins['inactive_plugins'];
// Jetpack & WooCommerce Connect.
$data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none';
$data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no';
$data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no';
@@ -158,6 +177,9 @@ class WC_Tracker {
// Shipping method info.
$data['shipping_methods'] = self::get_active_shipping_methods();
// Features.
$data['enabled_features'] = self::get_enabled_features();
// Get all WooCommerce options info.
$data['settings'] = self::get_all_woocommerce_options_values();
@@ -172,12 +194,21 @@ class WC_Tracker {
$data['mini_cart_block'] = self::get_mini_cart_info();
}
// WooCommerce Admin info.
/**
* Filter whether to disable admin tracking.
*
* @since 5.2.0
*/
$data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no';
// Mobile info.
$data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage();
/**
* Filter the data that's sent with the tracker.
*
* @since 2.3.0
*/
return apply_filters( 'woocommerce_tracker_data', $data );
}
@@ -212,6 +243,7 @@ class WC_Tracker {
$memory = wc_let_to_num( WP_MEMORY_LIMIT );
if ( function_exists( 'memory_get_usage' ) ) {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- False positive.
$system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) );
$memory = max( $memory, $system_memory );
}
@@ -280,27 +312,31 @@ class WC_Tracker {
include ABSPATH . '/wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
$plugins = wc_get_container()->get( LegacyProxy::class )->call_function( 'get_plugins' );
$active_plugins_keys = get_option( 'active_plugins', array() );
$active_plugins = array();
foreach ( $plugins as $k => $v ) {
// Take care of formatting the data how we want it.
$formatted = array();
$formatted['name'] = strip_tags( $v['Name'] );
$formatted['name'] = wp_strip_all_tags( $v['Name'] );
if ( isset( $v['Version'] ) ) {
$formatted['version'] = strip_tags( $v['Version'] );
$formatted['version'] = wp_strip_all_tags( $v['Version'] );
}
if ( isset( $v['Author'] ) ) {
$formatted['author'] = strip_tags( $v['Author'] );
$formatted['author'] = wp_strip_all_tags( $v['Author'] );
}
if ( isset( $v['Network'] ) ) {
$formatted['network'] = strip_tags( $v['Network'] );
$formatted['network'] = wp_strip_all_tags( $v['Network'] );
}
if ( isset( $v['PluginURI'] ) ) {
$formatted['plugin_uri'] = strip_tags( $v['PluginURI'] );
$formatted['plugin_uri'] = wp_strip_all_tags( $v['PluginURI'] );
}
if ( in_array( $k, $active_plugins_keys ) ) {
$formatted['feature_compatibility'] = array();
if ( wc_get_container()->get( PluginUtil::class )->is_woocommerce_aware_plugin( $k ) ) {
$formatted['feature_compatibility'] = array_filter( FeaturesUtil::get_compatible_features_for_plugin( $k ) );
}
if ( in_array( $k, $active_plugins_keys, true ) ) {
// Remove active plugins from list so we can show active and inactive separately.
unset( $plugins[ $k ] );
$active_plugins[ $k ] = $formatted;
@@ -381,10 +417,9 @@ class WC_Tracker {
* @return array
*/
private static function get_order_counts() {
$order_count = array();
$order_count_data = wp_count_posts( 'shop_order' );
$order_count = array();
foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
$order_count[ $status_slug ] = $order_count_data->{ $status_slug };
$order_count[ $status_slug ] = wc_orders_count( $status_slug );
}
return $order_count;
}
@@ -413,33 +448,59 @@ class WC_Tracker {
private static function get_order_totals() {
global $wpdb;
$gross_total = $wpdb->get_var(
$orders_table = OrdersTableDataStore::get_orders_table_name();
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$gross_total = $wpdb->get_var(
"
SELECT SUM(total_amount) AS 'gross_total'
FROM $orders_table
WHERE status in ('wc-completed', 'wc-refunded');
"
SELECT
SUM( order_meta.meta_value ) AS 'gross_total'
FROM {$wpdb->prefix}posts AS orders
LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
WHERE order_meta.meta_key = '_order_total'
AND orders.post_status in ( 'wc-completed', 'wc-refunded' )
GROUP BY order_meta.meta_key
"
);
);
// phpcs:enable
} else {
$gross_total = $wpdb->get_var(
"
SELECT
SUM( order_meta.meta_value ) AS 'gross_total'
FROM {$wpdb->prefix}posts AS orders
LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
WHERE order_meta.meta_key = '_order_total'
AND orders.post_status in ( 'wc-completed', 'wc-refunded' )
GROUP BY order_meta.meta_key
"
);
}
if ( is_null( $gross_total ) ) {
$gross_total = 0;
}
$processing_gross_total = $wpdb->get_var(
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$processing_gross_total = $wpdb->get_var(
"
SELECT SUM(total_amount) AS 'gross_total'
FROM $orders_table
WHERE status = 'wc-processing';
"
SELECT
SUM( order_meta.meta_value ) AS 'gross_total'
FROM {$wpdb->prefix}posts AS orders
LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
WHERE order_meta.meta_key = '_order_total'
AND orders.post_status = 'wc-processing'
GROUP BY order_meta.meta_key
"
);
);
// phpcs:enable
} else {
$processing_gross_total = $wpdb->get_var(
"
SELECT
SUM( order_meta.meta_value ) AS 'gross_total'
FROM {$wpdb->prefix}posts AS orders
LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
WHERE order_meta.meta_key = '_order_total'
AND orders.post_status = 'wc-processing'
GROUP BY order_meta.meta_key
"
);
}
if ( is_null( $processing_gross_total ) ) {
$processing_gross_total = 0;
@@ -459,16 +520,31 @@ class WC_Tracker {
private static function get_order_dates() {
global $wpdb;
$min_max = $wpdb->get_row(
"
SELECT
MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last'
FROM {$wpdb->prefix}posts
WHERE post_type = 'shop_order'
AND post_status = 'wc-completed'
",
ARRAY_A
);
$orders_table = OrdersTableDataStore::get_orders_table_name();
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$min_max = $wpdb->get_row(
"
SELECT
MIN( date_created_gmt ) as 'first', MAX( date_created_gmt ) as 'last'
FROM $orders_table
WHERE status = 'wc-completed';
",
ARRAY_A
);
// phpcs:enable
} else {
$min_max = $wpdb->get_row(
"
SELECT
MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last'
FROM {$wpdb->prefix}posts
WHERE post_type = 'shop_order'
AND post_status = 'wc-completed'
",
ARRAY_A
);
}
if ( is_null( $min_max ) ) {
$min_max = array(
@@ -477,16 +553,30 @@ class WC_Tracker {
);
}
$processing_min_max = $wpdb->get_row(
"
SELECT
MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last'
FROM {$wpdb->prefix}posts
WHERE post_type = 'shop_order'
AND post_status = 'wc-processing'
",
ARRAY_A
);
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$processing_min_max = $wpdb->get_row(
"
SELECT
MIN( date_created_gmt ) as 'processing_first', MAX( date_created_gmt ) as 'processing_last'
FROM $orders_table
WHERE status = 'wc-processing';
",
ARRAY_A
);
// phpcs:enable
} else {
$processing_min_max = $wpdb->get_row(
"
SELECT
MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last'
FROM {$wpdb->prefix}posts
WHERE post_type = 'shop_order'
AND post_status = 'wc-processing'
",
ARRAY_A
);
}
if ( is_null( $processing_min_max ) ) {
$processing_min_max = array(
@@ -498,6 +588,69 @@ class WC_Tracker {
return array_merge( $min_max, $processing_min_max );
}
/**
* Extract the group key for an associative array of objects which have unique ids in the key.
* A 'group_key' property is introduced in the object.
* For example, two objects with keys like 'WooDataPay ** #123' and 'WooDataPay ** #78' would
* both have a group_key of 'WooDataPay **' after this function call.
*
* @param array $objects The array of objects that need to be grouped.
* @param string $default_key The property that will be the default group_key.
* @return array Contains the objects with a group_key property.
*/
private static function extract_group_key( $objects, $default_key ) {
$keys = array_keys( $objects );
// Sort keys by length and then by characters within the same length keys.
usort(
$keys,
function( $a, $b ) {
if ( strlen( $a ) === strlen( $b ) ) {
return strcmp( $a, $b );
}
return ( strlen( $a ) < strlen( $b ) ) ? -1 : 1;
}
);
// Look for common tokens in every pair of adjacent keys.
$prev = '';
foreach ( $keys as $key ) {
if ( $prev ) {
$comm_tokens = array();
// Tokenize the current and previous gateway names.
$curr_tokens = preg_split( '/[ :,\-_]+/', $key );
$prev_tokens = preg_split( '/[ :,\-_]+/', $prev );
$len_curr = count( $curr_tokens );
$len_prev = count( $prev_tokens );
$index_unique = -1;
// Gather the common tokens.
// Let us allow for the unique reference id to be anywhere in the name.
for ( $i = 0; $i < $len_curr && $i < $len_prev; $i++ ) {
if ( $curr_tokens[ $i ] === $prev_tokens[ $i ] ) {
$comm_tokens[] = $curr_tokens[ $i ];
} elseif ( preg_match( '/\d/', $curr_tokens[ $i ] ) && preg_match( '/\d/', $prev_tokens[ $i ] ) ) {
$index_unique = $i;
}
}
// If only one token is different, and those tokens contain digits, then that could be the unique id.
if ( count( $curr_tokens ) - count( $comm_tokens ) <= 1 && count( $comm_tokens ) > 0 && $index_unique > -1 ) {
$objects[ $key ]->group_key = implode( ' ', $comm_tokens );
$objects[ $prev ]->group_key = implode( ' ', $comm_tokens );
} else {
$objects[ $key ]->group_key = $objects[ $key ]->$default_key;
}
} else {
$objects[ $key ]->group_key = $objects[ $key ]->$default_key;
}
$prev = $key;
}
return $objects;
}
/**
* Get order details by gateway.
*
@@ -506,38 +659,94 @@ class WC_Tracker {
private static function get_orders_by_gateway() {
global $wpdb;
$orders_by_gateway = $wpdb->get_results(
"
SELECT
gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts
FROM (
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
$orders_table = OrdersTableDataStore::get_orders_table_name();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$orders_and_gateway_details = $wpdb->get_results(
"
SELECT payment_method AS gateway, currency AS currency, SUM( total_amount ) AS totals, count( id ) AS counts
FROM $orders_table
WHERE status IN ( 'wc-completed', 'wc-processing', 'wc-refunded' )
GROUP BY gateway, currency;
"
);
// phpcs:enable
} else {
$orders_and_gateway_details = $wpdb->get_results(
"
SELECT
orders.id AS order_id,
MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway,
MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total,
MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency
FROM
{$wpdb->prefix}posts orders
LEFT JOIN
{$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id
WHERE orders.post_type = 'shop_order'
AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' )
AND meta_key in( '_payment_method','_order_total','_order_currency')
GROUP BY orders.id
) order_gateways
GROUP BY gateway, currency
"
);
gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts
FROM (
SELECT
orders.id AS order_id,
MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway,
MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total,
MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency
FROM
{$wpdb->prefix}posts orders
LEFT JOIN
{$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id
WHERE orders.post_type = 'shop_order'
AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' )
AND meta_key in( '_payment_method','_order_total','_order_currency')
GROUP BY orders.id
) order_gateways
GROUP BY gateway, currency
"
);
}
$orders_by_gateway_currency = array();
foreach ( $orders_by_gateway as $orders_details ) {
$gateway = 'gateway_' . $orders_details->gateway;
$currency = $orders_details->currency;
$count = $gateway . '_' . $currency . '_count';
$total = $gateway . '_' . $currency . '_total';
$orders_by_gateway_currency[ $count ] = $orders_details->counts;
$orders_by_gateway_currency[ $total ] = $orders_details->totals;
// The associative array that is created as the result of array_reduce is passed to extract_group_key()
// This function has the logic that will remove specific transaction identifiers that may sometimes be part of a
// payment method. For example, two payments methods like 'WooDataPay ** #123' and 'WooDataPay ** #78' would
// both have the same group_key 'WooDataPay **'.
$orders_by_gateway = self::extract_group_key(
// Convert into an associative array with a combination of currency and gateway as key.
array_reduce(
$orders_and_gateway_details,
function( $result, $item ) {
$item->gateway = preg_replace( '/\s+/', ' ', $item->gateway );
// Introduce currency as a prefix for the key.
$key = $item->currency . '==' . $item->gateway;
$result[ $key ] = $item;
return $result;
},
array()
),
'gateway'
);
// Aggregate using group_key.
foreach ( $orders_by_gateway as $orders_details ) {
$gkey = $orders_details->group_key;
// Remove currency as prefix of key for backward compatibility.
if ( str_contains( $gkey, '==' ) ) {
$tokens = preg_split( '/==/', $gkey );
$key = $tokens[1];
} else {
$key = $gkey;
}
$key = str_replace( array( 'payment method', 'payment gateway', 'gateway' ), '', strtolower( $key ) );
$key = trim( preg_replace( '/[: ,#*\-_]+/', ' ', $key ) );
// Add currency as postfix of gateway for backward compatibility.
$key = 'gateway_' . $key . '_' . $orders_details->currency;
$count_key = $key . '_count';
$total_key = $key . '_total';
if ( array_key_exists( $count_key, $orders_by_gateway_currency ) || array_key_exists( $total_key, $orders_by_gateway_currency ) ) {
$orders_by_gateway_currency[ $count_key ] = $orders_by_gateway_currency[ $count_key ] + $orders_details->counts;
$orders_by_gateway_currency[ $total_key ] = $orders_by_gateway_currency[ $total_key ] + $orders_details->totals;
} else {
$orders_by_gateway_currency[ $count_key ] = $orders_details->counts;
$orders_by_gateway_currency[ $total_key ] = $orders_details->totals;
}
}
return $orders_by_gateway_currency;
@@ -551,24 +760,63 @@ class WC_Tracker {
private static function get_orders_origins() {
global $wpdb;
$orders_origin = $wpdb->get_results(
"
SELECT
meta_value as origin, COUNT( DISTINCT ( orders.id ) ) as count
FROM
$wpdb->posts orders
LEFT JOIN
$wpdb->postmeta order_meta ON order_meta.post_id = orders.id
WHERE
meta_key = '_created_via'
GROUP BY
meta_value;
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
$op_table_name = OrdersTableDataStore::get_operational_data_table_name();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$orders_origin = $wpdb->get_results(
"
SELECT created_via as origin, COUNT( order_id ) as count
FROM $op_table_name
GROUP BY created_via;
"
);
// phpcs:enable
} else {
$orders_origin = $wpdb->get_results(
"
SELECT
meta_value as origin, COUNT( DISTINCT ( orders.id ) ) as count
FROM
$wpdb->posts orders
LEFT JOIN
$wpdb->postmeta order_meta ON order_meta.post_id = orders.id
WHERE
meta_key = '_created_via'
GROUP BY
meta_value;
"
);
}
// The associative array that is created as the result of array_reduce is passed to extract_group_key()
// This function has the logic that will remove specific identifiers that may sometimes be part of an origin.
// For example, two origins like 'Import #123' and 'Import ** #78' would both have a group_key 'Import **'.
$orders_and_origins = self::extract_group_key(
// Convert into an associative array with the origin as key.
array_reduce(
$orders_origin,
function( $result, $item ) {
$key = $item->origin;
$result[ $key ] = $item;
return $result;
},
array()
),
'origin'
);
$orders_by_origin = array();
foreach ( $orders_origin as $origin ) {
$orders_by_origin[ $origin->origin ] = (int) $origin->count;
// Aggregate using group_key.
foreach ( $orders_and_origins as $origin ) {
$key = strtolower( $origin->group_key );
if ( array_key_exists( $key, $orders_by_origin ) ) {
$orders_by_origin[ $key ] = $orders_by_origin[ $key ] + (int) $origin->count;
} else {
$orders_by_origin[ $key ] = (int) $origin->count;
}
}
return array( 'created_via' => $orders_by_origin );
@@ -663,6 +911,23 @@ class WC_Tracker {
return $active_methods;
}
/**
* Get an array of slugs for WC features that are enabled on the site.
*
* @return string[]
*/
private static function get_enabled_features() {
$all_features = FeaturesUtil::get_features( true, true );
$enabled_features = array_filter(
$all_features,
function( $feature ) {
return $feature['is_enabled'];
}
);
return array_keys( $enabled_features );
}
/**
* Get all options starting with woocommerce_ prefix.
*
@@ -705,7 +970,12 @@ class WC_Tracker {
* @return array
*/
private static function get_all_template_overrides() {
$override_data = array();
$override_data = array();
/**
* Filter the paths to scan for template overrides.
*
* @since 2.3.0
*/
$template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) );
$scanned_files = array();