PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / admin / reports / class-wc-admin-report.php
woocommerce / includes / admin / reports Last commit date
class-wc-admin-report.php 2 weeks ago class-wc-report-coupon-usage.php 5 years ago class-wc-report-customer-list.php 10 months ago class-wc-report-customers.php 10 months ago class-wc-report-downloads.php 6 years ago class-wc-report-low-in-stock.php 6 years ago class-wc-report-most-stocked.php 6 years ago class-wc-report-out-of-stock.php 6 years ago class-wc-report-sales-by-category.php 2 years ago class-wc-report-sales-by-date.php 1 month ago class-wc-report-sales-by-product.php 5 months ago class-wc-report-stock.php 1 year ago class-wc-report-taxes-by-code.php 1 year ago class-wc-report-taxes-by-date.php 1 year ago
class-wc-admin-report.php
852 lines
1 <?php
2 /**
3 * Admin report functionality.
4 *
5 * @package WooCommerce\Admin\Reports
6 */
7
8 use Automattic\WooCommerce\Enums\OrderStatus;
9 use Automattic\WooCommerce\Internal\Admin\Reports\HposLegacyOrderReportQueryBuilder;
10 use Automattic\WooCommerce\Utilities\OrderUtil;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 /**
17 * Admin Report.
18 *
19 * Extended by reports to show charts and stats in admin.
20 *
21 * @package WooCommerce\Admin\Reports
22 * @version 2.1.0
23 */
24 class WC_Admin_Report {
25
26 /**
27 * List of transients name that have been updated and need persisting.
28 *
29 * @var array
30 */
31 protected static $transients_to_update = array();
32
33 /**
34 * The list of transients.
35 *
36 * @var array
37 */
38 protected static $cached_results = array();
39
40 /**
41 * The chart interval.
42 *
43 * @var int
44 */
45 public $chart_interval;
46
47 /**
48 * Group by SQL query.
49 *
50 * @var string
51 */
52 public $group_by_query;
53
54 /**
55 * The bar width.
56 *
57 * @var int
58 */
59 public $barwidth;
60
61 /**
62 * Group chart item by day or month.
63 *
64 * @var string
65 */
66 public $chart_groupby;
67
68 /**
69 * The start date of the report.
70 *
71 * @var int timestamp
72 */
73 public $start_date;
74
75 /**
76 * The end date of the report.
77 *
78 * @var int timestamp
79 */
80 public $end_date;
81
82 /**
83 * Get report totals such as order totals and discount amounts.
84 *
85 * Data example:
86 *
87 * '_order_total' => array(
88 * 'type' => 'meta',
89 * 'function' => 'SUM',
90 * 'name' => 'total_sales'
91 * )
92 *
93 * @param array $args arguments for the report.
94 * @return mixed depending on query_type
95 */
96 public function get_order_report_data( $args = array() ) {
97 global $wpdb;
98
99 $default_args = array(
100 'data' => array(),
101 'where' => array(),
102 'where_meta' => array(),
103 'query_type' => 'get_row',
104 'group_by' => '',
105 'order_by' => '',
106 'limit' => '',
107 'filter_range' => false,
108 'nocache' => false,
109 'debug' => false,
110 'order_types' => wc_get_order_types( 'reports' ),
111 'order_status' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING, OrderStatus::ON_HOLD ),
112 'parent_order_status' => false,
113 );
114 $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args );
115 $args = wp_parse_args( $args, $default_args );
116
117 // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
118 extract( $args );
119
120 if ( empty( $data ) ) {
121 return '';
122 }
123
124 $order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status );
125
126 $args['order_status'] = $order_status;
127
128 if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
129 $query = wc_get_container()->get( HposLegacyOrderReportQueryBuilder::class )->build_query( $args, (int) $this->start_date, (int) $this->end_date );
130 } else {
131 $query = array();
132 $select = array();
133
134 foreach ( $data as $raw_key => $value ) {
135 $key = sanitize_key( $raw_key );
136 $distinct = '';
137
138 if ( isset( $value['distinct'] ) ) {
139 $distinct = 'DISTINCT';
140 }
141
142 switch ( $value['type'] ) {
143 case 'meta':
144 $get_key = "meta_{$key}.meta_value";
145 break;
146 case 'parent_meta':
147 $get_key = "parent_meta_{$key}.meta_value";
148 break;
149 case 'post_data':
150 $get_key = "posts.{$key}";
151 break;
152 case 'order_item_meta':
153 $get_key = "order_item_meta_{$key}.meta_value";
154 break;
155 case 'order_item':
156 $get_key = "order_items.{$key}";
157 break;
158 }
159
160 if ( empty( $get_key ) ) {
161 // Skip to the next foreach iteration else the query will be invalid.
162 continue;
163 }
164
165 if ( $value['function'] ) {
166 $get = "{$value['function']}({$distinct} {$get_key})";
167 } else {
168 $get = "{$distinct} {$get_key}";
169 }
170
171 $select[] = "{$get} as {$value['name']}";
172 }
173
174 $query['select'] = 'SELECT ' . implode( ',', $select );
175 $query['from'] = "FROM {$wpdb->posts} AS posts";
176
177 // Joins.
178 $joins = array();
179
180 foreach ( ( $data + $where ) as $raw_key => $value ) {
181 $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
182 $type = isset( $value['type'] ) ? $value['type'] : false;
183 $key = sanitize_key( $raw_key );
184
185 switch ( $type ) {
186 case 'meta':
187 $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON ( posts.ID = meta_{$key}.post_id AND meta_{$key}.meta_key = '{$raw_key}' )";
188 break;
189 case 'parent_meta':
190 $joins[ "parent_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS parent_meta_{$key} ON (posts.post_parent = parent_meta_{$key}.post_id) AND (parent_meta_{$key}.meta_key = '{$raw_key}')";
191 break;
192 case 'order_item_meta':
193 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)";
194
195 if ( ! empty( $value['order_item_type'] ) ) {
196 $joins['order_items'] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')";
197 }
198
199 $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " .
200 "(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " .
201 " AND (order_item_meta_{$key}.meta_key = '{$raw_key}')";
202 break;
203 case 'order_item':
204 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
205 break;
206 }
207 }
208
209 if ( ! empty( $where_meta ) ) {
210 foreach ( $where_meta as $value ) {
211 if ( ! is_array( $value ) ) {
212 continue;
213 }
214 $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
215 $type = isset( $value['type'] ) ? $value['type'] : false;
216 $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );
217
218 if ( 'order_item_meta' === $type ) {
219
220 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
221 $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id";
222
223 } else {
224 // If we have a where clause for meta, join the postmeta table.
225 $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id";
226 }
227 }
228 }
229
230 if ( ! empty( $parent_order_status ) ) {
231 $joins['parent'] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID";
232 }
233
234 $query['join'] = implode( ' ', $joins );
235
236 $query['where'] = "
237 WHERE posts.post_type IN ( '" . implode( "','", $order_types ) . "' )
238 ";
239
240 if ( ! empty( $order_status ) ) {
241 $query['where'] .= "
242 AND posts.post_status IN ( 'wc-" . implode( "','wc-", $order_status ) . "')
243 ";
244 }
245
246 if ( ! empty( $parent_order_status ) ) {
247 if ( ! empty( $order_status ) ) {
248 $query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) ";
249 } else {
250 $query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') ";
251 }
252 }
253
254 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date
255 if ( $filter_range ) {
256 $query['where'] .= "
257 AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "'
258 AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "'
259 ";
260 }
261 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
262
263 if ( ! empty( $where_meta ) ) {
264
265 $relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND';
266
267 $query['where'] .= ' AND (';
268
269 foreach ( $where_meta as $index => $value ) {
270
271 if ( ! is_array( $value ) ) {
272 continue;
273 }
274
275 $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );
276
277 if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) {
278
279 if ( ! empty( $value['meta_value'] ) && ! is_array( $value['meta_value'] ) ) {
280 $value['meta_value'] = (array) $value['meta_value']; // @phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
281 }
282
283 if ( ! empty( $value['meta_value'] ) ) {
284 $formats = implode( ', ', array_fill( 0, count( $value['meta_value'] ), '%s' ) );
285 $where_value = $value['operator'] . ' (' . $wpdb->prepare( $formats, $value['meta_value'] ) . ')'; // @phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
286 }
287 } else {
288 $where_value = $value['operator'] . ' ' . $wpdb->prepare( '%s', $value['meta_value'] );
289 }
290
291 if ( ! empty( $where_value ) ) {
292 if ( $index > 0 ) {
293 $query['where'] .= ' ' . $relation;
294 }
295
296 if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) {
297
298 if ( is_array( $value['meta_key'] ) ) {
299 $query['where'] .= " ( order_item_meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')";
300 } else {
301 $query['where'] .= " ( order_item_meta_{$key}.meta_key = '{$value['meta_key']}'";
302 }
303
304 $query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )";
305 } else {
306
307 if ( is_array( $value['meta_key'] ) ) {
308 $query['where'] .= " ( meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')";
309 } else {
310 $query['where'] .= " ( meta_{$key}.meta_key = '{$value['meta_key']}'";
311 }
312
313 $query['where'] .= " AND meta_{$key}.meta_value {$where_value} )";
314 }
315 }
316 }
317
318 $query['where'] .= ')';
319 }
320
321 if ( ! empty( $where ) ) {
322
323 foreach ( $where as $value ) {
324
325 if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) {
326
327 if ( ! empty( $value['value'] ) && ! is_array( $value['value'] ) ) {
328 $value['value'] = (array) $value['value'];
329 }
330 if ( ! empty( $value['value'] ) ) {
331 $formats = implode( ', ', array_fill( 0, count( $value['value'] ), '%s' ) );
332 $where_value = $value['operator'] . ' (' . $wpdb->prepare( $formats, $value['value'] ) . ')'; // @phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
333 }
334 } else {
335 $where_value = $value['operator'] . ' ' . $wpdb->prepare( '%s', $value['value'] );
336 }
337
338 if ( ! empty( $where_value ) ) {
339 $query['where'] .= " AND {$value['key']} {$where_value}";
340 }
341 }
342 }
343
344 if ( $group_by ) {
345 $query['group_by'] = "GROUP BY {$group_by}";
346 }
347
348 if ( $order_by ) {
349 $query['order_by'] = "ORDER BY {$order_by}";
350 }
351
352 if ( $limit ) {
353 $query['limit'] = "LIMIT {$limit}";
354 }
355 }
356
357 /**
358 * Filters the legacy order report SQL clauses before they are combined into a SQL string.
359 *
360 * @param array $query SQL clauses keyed by select/from/join/where/group_by/order_by/limit.
361 *
362 * @since 2.1.0
363 */
364 $query = apply_filters( 'woocommerce_reports_get_order_report_query', $query );
365 $query = implode( ' ', $query );
366
367 if ( $debug ) {
368 echo '<pre>';
369 wc_print_r( $query );
370 echo '</pre>';
371 }
372
373 if ( $debug || $nocache ) {
374 self::enable_big_selects();
375
376 /**
377 * Filters the legacy order report query result.
378 *
379 * @param mixed $result Query result returned by wpdb.
380 * @param array $data Report data arguments.
381 *
382 * @since 2.1.0
383 */
384 $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
385 } else {
386 $query_hash = md5( $query_type . $query );
387 $result = $this->get_cached_query( $query_hash );
388 if ( null === $result ) {
389 self::enable_big_selects();
390
391 /**
392 * Filters the legacy order report query result.
393 *
394 * @param mixed $result Query result returned by wpdb.
395 * @param array $data Report data arguments.
396 *
397 * @since 2.1.0
398 */
399 $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
400 }
401 $this->set_cached_query( $query_hash, $result );
402 }
403
404 return $result;
405 }
406
407 /**
408 * Init the static hooks of the class.
409 */
410 protected static function add_update_transients_hook() {
411 if ( ! has_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ) ) {
412 add_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) );
413 }
414 }
415
416 /**
417 * Enables big mysql selects for reports, just once for this session.
418 */
419 protected static function enable_big_selects() {
420 static $big_selects = false;
421
422 global $wpdb;
423
424 if ( ! $big_selects ) {
425 $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
426 $big_selects = true;
427 }
428 }
429
430 /**
431 * Get the cached query result or null if it's not in the cache.
432 *
433 * @param string $query_hash The query hash.
434 *
435 * @return mixed
436 */
437 protected function get_cached_query( $query_hash ) {
438 $class = strtolower( get_class( $this ) );
439
440 if ( ! isset( self::$cached_results[ $class ] ) ) {
441 self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) );
442 }
443
444 if ( isset( self::$cached_results[ $class ][ $query_hash ] ) ) {
445 return self::$cached_results[ $class ][ $query_hash ];
446 }
447
448 return null;
449 }
450
451 /**
452 * Set the cached query result.
453 *
454 * @param string $query_hash The query hash.
455 * @param mixed $data The data to cache.
456 */
457 protected function set_cached_query( $query_hash, $data ) {
458 $class = strtolower( get_class( $this ) );
459
460 if ( ! isset( self::$cached_results[ $class ] ) ) {
461 self::$cached_results[ $class ] = get_transient( $class );
462 }
463
464 if ( false === self::$cached_results[ $class ] ) {
465 self::$cached_results[ $class ] = array();
466 }
467
468 self::add_update_transients_hook();
469
470 self::$transients_to_update[ $class ] = $class;
471 self::$cached_results[ $class ][ $query_hash ] = $data;
472 }
473
474 /**
475 * Function to update the modified transients at the end of the request.
476 */
477 public static function maybe_update_transients() {
478 foreach ( self::$transients_to_update as $key => $transient_name ) {
479 set_transient( $transient_name, self::$cached_results[ $transient_name ], DAY_IN_SECONDS );
480 }
481 // Transients have been updated reset the list.
482 self::$transients_to_update = array();
483 }
484
485 /**
486 * Put data with post_date's into an array of times.
487 *
488 * @param array $data array of your data.
489 * @param string $date_key key for the 'date' field. e.g. 'post_date'.
490 * @param string $data_key key for the data you are charting.
491 * @param int $interval interval to use.
492 * @param string $start_date start date.
493 * @param string $group_by group by.
494 * @return array
495 */
496 public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) {
497 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date
498
499 $prepared_data = array();
500
501 // Ensure all days (or months) have values in this range.
502 if ( 'day' === $group_by ) {
503 for ( $i = 0; $i <= $interval; $i ++ ) {
504 $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000';
505
506 if ( ! isset( $prepared_data[ $time ] ) ) {
507 $prepared_data[ $time ] = array( esc_js( $time ), 0 );
508 }
509 }
510 } else {
511 $current_yearnum = date( 'Y', $start_date );
512 $current_monthnum = date( 'm', $start_date );
513
514 for ( $i = 0; $i <= $interval; $i ++ ) {
515 $time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000';
516
517 if ( ! isset( $prepared_data[ $time ] ) ) {
518 $prepared_data[ $time ] = array( esc_js( $time ), 0 );
519 }
520
521 $current_monthnum ++;
522
523 if ( $current_monthnum > 12 ) {
524 $current_monthnum = 1;
525 $current_yearnum ++;
526 }
527 }
528 }
529
530 foreach ( $data as $d ) {
531 switch ( $group_by ) {
532 case 'day':
533 $time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000';
534 break;
535 case 'month':
536 default:
537 $time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000';
538 break;
539 }
540
541 if ( ! isset( $prepared_data[ $time ] ) ) {
542 continue;
543 }
544
545 if ( $data_key ) {
546 $prepared_data[ $time ][1] += is_numeric( $d->$data_key ) ? $d->$data_key : 0;
547 } else {
548 $prepared_data[ $time ][1] ++;
549 }
550 }
551
552 return $prepared_data;
553
554 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
555 }
556
557 /**
558 * Prepares the data for a sparkline to show sales in the last X days.
559 *
560 * @param int $id ID of the product to show. Blank to get all orders.
561 * @param int $days Days of stats to get. Default to 7 days.
562 * @param string $type Type of sparkline to get. Ignored if ID is not set.
563 * @return array
564 */
565 public function get_sales_sparkline( $id = '', $days = 7, $type = 'sales' ) {
566
567 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
568
569 if ( $id ) {
570 $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty';
571
572 $data = $this->get_order_report_data(
573 array(
574 'data' => array(
575 '_product_id' => array(
576 'type' => 'order_item_meta',
577 'order_item_type' => 'line_item',
578 'function' => '',
579 'name' => 'product_id',
580 ),
581 $meta_key => array(
582 'type' => 'order_item_meta',
583 'order_item_type' => 'line_item',
584 'function' => 'SUM',
585 'name' => 'sparkline_value',
586 ),
587 'post_date' => array(
588 'type' => 'post_data',
589 'function' => '',
590 'name' => 'post_date',
591 ),
592 ),
593 'where' => array(
594 array(
595 'key' => 'post_date',
596 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
597 'operator' => '>',
598 ),
599 array(
600 'key' => 'order_item_meta__product_id.meta_value',
601 'value' => $id,
602 'operator' => '=',
603 ),
604 ),
605 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
606 'query_type' => 'get_results',
607 'filter_range' => false,
608 )
609 );
610 } else {
611
612 $data = $this->get_order_report_data(
613 array(
614 'data' => array(
615 '_order_total' => array(
616 'type' => 'meta',
617 'function' => 'SUM',
618 'name' => 'sparkline_value',
619 ),
620 'post_date' => array(
621 'type' => 'post_data',
622 'function' => '',
623 'name' => 'post_date',
624 ),
625 ),
626 'where' => array(
627 array(
628 'key' => 'post_date',
629 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
630 'operator' => '>',
631 ),
632 ),
633 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
634 'query_type' => 'get_results',
635 'filter_range' => false,
636 )
637 );
638 }
639
640 $total = 0;
641 foreach ( $data as $d ) {
642 $total += $d->sparkline_value;
643 }
644
645 $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) );
646
647 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
648
649 return array(
650 'total' => $total,
651 'data' => $sparkline_data,
652 );
653 }
654
655 /**
656 * Prepares the markup for a sparkline to show sales in the last X days.
657 *
658 * @param int $id ID of the product to show. Blank to get all orders.
659 * @param int $days Days of stats to get. Default to 7 days.
660 * @param string $type Type of sparkline to get.
661 * @return string
662 */
663 public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) {
664 $sparkline = $this->get_sales_sparkline( $id, $days, $type );
665 $total = $sparkline['total'];
666
667 if ( 'sales' === $type ) {
668 /* translators: 1: total income 2: days */
669 $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), wp_strip_all_tags( wc_price( $total ) ), $days );
670 } else {
671 /* translators: 1: total items sold 2: days */
672 $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days );
673 }
674
675 $sparkline_data = $sparkline['data'];
676
677 return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>';
678 }
679
680 /**
681 * Get the current range and calculate the start and end dates.
682 *
683 * @param string $current_range Type of range.
684 */
685 public function calculate_current_range( $current_range ) {
686
687 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
688 // phpcs:disable WordPress.Security.NonceVerification.Recommended
689
690 switch ( $current_range ) {
691
692 case 'custom':
693 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
694 $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) ) );
695
696 if ( empty( $_GET['end_date'] ) ) {
697 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
698 } else {
699 $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) ) );
700 }
701
702 $interval = 0;
703 $min_date = $this->start_date;
704
705 // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
706 while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
707 $interval ++;
708 }
709
710 // 3 months max for day view
711 if ( $interval > 3 ) {
712 $this->chart_groupby = 'month';
713 } else {
714 $this->chart_groupby = 'day';
715 }
716 break;
717
718 case 'year':
719 $this->start_date = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) );
720 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
721 $this->chart_groupby = 'month';
722 break;
723
724 case 'last_month':
725 $first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
726 $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) );
727 $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) );
728 $this->chart_groupby = 'day';
729 break;
730
731 case 'month':
732 $this->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
733 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
734 $this->chart_groupby = 'day';
735 break;
736
737 case '7day':
738 $this->start_date = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) );
739 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
740 $this->chart_groupby = 'day';
741 break;
742 }
743
744 // Group by.
745 switch ( $this->chart_groupby ) {
746
747 case 'day':
748 $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)';
749 $this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) );
750 $this->barwidth = 60 * 60 * 24 * 1000;
751 break;
752
753 case 'month':
754 $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)';
755 $this->chart_interval = 0;
756 $min_date = strtotime( date( 'Y-m-01', $this->start_date ) );
757
758 // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
759 while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
760 $this->chart_interval ++;
761 }
762
763 $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000;
764 break;
765 }
766
767 // phpcs:enable WordPress.Security.NonceVerification.Recommended
768 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
769 }
770
771 /**
772 * Return currency tooltip JS based on WooCommerce currency position settings.
773 *
774 * @return string
775 */
776 public function get_currency_tooltip() {
777 switch ( get_option( 'woocommerce_currency_pos' ) ) {
778 case 'right':
779 $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"';
780 break;
781 case 'right_space':
782 $currency_tooltip = 'append_tooltip: "&nbsp;' . get_woocommerce_currency_symbol() . '"';
783 break;
784 case 'left':
785 $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"';
786 break;
787 case 'left_space':
788 default:
789 $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '&nbsp;"';
790 break;
791 }
792
793 return $currency_tooltip;
794 }
795
796 /**
797 * Get the main chart.
798 */
799 public function get_main_chart() {}
800
801 /**
802 * Get the legend for the main chart sidebar.
803 *
804 * @return array
805 */
806 public function get_chart_legend() {
807 return array();
808 }
809
810 /**
811 * Get chart widgets.
812 *
813 * @return array
814 */
815 public function get_chart_widgets() {
816 return array();
817 }
818
819 /**
820 * Get an export link if needed.
821 */
822 public function get_export_button() {}
823
824 /**
825 * Output the report.
826 */
827 public function output_report() {}
828
829 /**
830 * Check nonce for current range.
831 *
832 * @since 3.0.4
833 * @param string $current_range Current range.
834 */
835 public function check_current_range_nonce( $current_range ) {
836 if ( 'custom' !== $current_range ) {
837 return;
838 }
839
840 if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) {
841 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
842 wp_die(
843 /* translators: %1$s: open link, %2$s: close link */
844 sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '<a href="' . esc_url( wp_nonce_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'custom_range', 'wc_reports_nonce' ) ) . '">', '</a>' ),
845 esc_attr__( 'Confirm navigation', 'woocommerce' )
846 );
847 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
848 exit;
849 }
850 }
851 }
852