PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
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 1 year ago class-wc-report-coupon-usage.php 5 years ago class-wc-report-customer-list.php 9 months ago class-wc-report-customers.php 9 months ago class-wc-report-downloads.php 5 years ago class-wc-report-low-in-stock.php 5 years ago class-wc-report-most-stocked.php 5 years ago class-wc-report-out-of-stock.php 5 years ago class-wc-report-sales-by-category.php 2 years ago class-wc-report-sales-by-date.php 1 year ago class-wc-report-sales-by-product.php 3 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
822 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\Utilities\ArrayUtil;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 /**
16 * Admin Report.
17 *
18 * Extended by reports to show charts and stats in admin.
19 *
20 * @package WooCommerce\Admin\Reports
21 * @version 2.1.0
22 */
23 class WC_Admin_Report {
24
25 /**
26 * List of transients name that have been updated and need persisting.
27 *
28 * @var array
29 */
30 protected static $transients_to_update = array();
31
32 /**
33 * The list of transients.
34 *
35 * @var array
36 */
37 protected static $cached_results = array();
38
39 /**
40 * The chart interval.
41 *
42 * @var int
43 */
44 public $chart_interval;
45
46 /**
47 * Group by SQL query.
48 *
49 * @var string
50 */
51 public $group_by_query;
52
53 /**
54 * The bar width.
55 *
56 * @var int
57 */
58 public $barwidth;
59
60 /**
61 * Group chart item by day or month.
62 *
63 * @var string
64 */
65 public $chart_groupby;
66
67 /**
68 * The start date of the report.
69 *
70 * @var int timestamp
71 */
72 public $start_date;
73
74 /**
75 * The end date of the report.
76 *
77 * @var int timestamp
78 */
79 public $end_date;
80
81 /**
82 * Get report totals such as order totals and discount amounts.
83 *
84 * Data example:
85 *
86 * '_order_total' => array(
87 * 'type' => 'meta',
88 * 'function' => 'SUM',
89 * 'name' => 'total_sales'
90 * )
91 *
92 * @param array $args arguments for the report.
93 * @return mixed depending on query_type
94 */
95 public function get_order_report_data( $args = array() ) {
96 global $wpdb;
97
98 $default_args = array(
99 'data' => array(),
100 'where' => array(),
101 'where_meta' => array(),
102 'query_type' => 'get_row',
103 'group_by' => '',
104 'order_by' => '',
105 'limit' => '',
106 'filter_range' => false,
107 'nocache' => false,
108 'debug' => false,
109 'order_types' => wc_get_order_types( 'reports' ),
110 'order_status' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING, OrderStatus::ON_HOLD ),
111 'parent_order_status' => false,
112 );
113 $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args );
114 $args = wp_parse_args( $args, $default_args );
115
116 // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
117 extract( $args );
118
119 if ( empty( $data ) ) {
120 return '';
121 }
122
123 $order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status );
124
125 $query = array();
126 $select = array();
127
128 foreach ( $data as $raw_key => $value ) {
129 $key = sanitize_key( $raw_key );
130 $distinct = '';
131
132 if ( isset( $value['distinct'] ) ) {
133 $distinct = 'DISTINCT';
134 }
135
136 switch ( $value['type'] ) {
137 case 'meta':
138 $get_key = "meta_{$key}.meta_value";
139 break;
140 case 'parent_meta':
141 $get_key = "parent_meta_{$key}.meta_value";
142 break;
143 case 'post_data':
144 $get_key = "posts.{$key}";
145 break;
146 case 'order_item_meta':
147 $get_key = "order_item_meta_{$key}.meta_value";
148 break;
149 case 'order_item':
150 $get_key = "order_items.{$key}";
151 break;
152 }
153
154 if ( empty( $get_key ) ) {
155 // Skip to the next foreach iteration else the query will be invalid.
156 continue;
157 }
158
159 if ( $value['function'] ) {
160 $get = "{$value['function']}({$distinct} {$get_key})";
161 } else {
162 $get = "{$distinct} {$get_key}";
163 }
164
165 $select[] = "{$get} as {$value['name']}";
166 }
167
168 $query['select'] = 'SELECT ' . implode( ',', $select );
169 $query['from'] = "FROM {$wpdb->posts} AS posts";
170
171 // Joins.
172 $joins = array();
173
174 foreach ( ( $data + $where ) as $raw_key => $value ) {
175 $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
176 $type = isset( $value['type'] ) ? $value['type'] : false;
177 $key = sanitize_key( $raw_key );
178
179 switch ( $type ) {
180 case 'meta':
181 $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}' )";
182 break;
183 case 'parent_meta':
184 $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}')";
185 break;
186 case 'order_item_meta':
187 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)";
188
189 if ( ! empty( $value['order_item_type'] ) ) {
190 $joins['order_items'] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')";
191 }
192
193 $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " .
194 "(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " .
195 " AND (order_item_meta_{$key}.meta_key = '{$raw_key}')";
196 break;
197 case 'order_item':
198 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
199 break;
200 }
201 }
202
203 if ( ! empty( $where_meta ) ) {
204 foreach ( $where_meta as $value ) {
205 if ( ! is_array( $value ) ) {
206 continue;
207 }
208 $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
209 $type = isset( $value['type'] ) ? $value['type'] : false;
210 $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );
211
212 if ( 'order_item_meta' === $type ) {
213
214 $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
215 $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";
216
217 } else {
218 // If we have a where clause for meta, join the postmeta table.
219 $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id";
220 }
221 }
222 }
223
224 if ( ! empty( $parent_order_status ) ) {
225 $joins['parent'] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID";
226 }
227
228 $query['join'] = implode( ' ', $joins );
229
230 $query['where'] = "
231 WHERE posts.post_type IN ( '" . implode( "','", $order_types ) . "' )
232 ";
233
234 if ( ! empty( $order_status ) ) {
235 $query['where'] .= "
236 AND posts.post_status IN ( 'wc-" . implode( "','wc-", $order_status ) . "')
237 ";
238 }
239
240 if ( ! empty( $parent_order_status ) ) {
241 if ( ! empty( $order_status ) ) {
242 $query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) ";
243 } else {
244 $query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') ";
245 }
246 }
247
248 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date
249 if ( $filter_range ) {
250 $query['where'] .= "
251 AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "'
252 AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "'
253 ";
254 }
255 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
256
257 if ( ! empty( $where_meta ) ) {
258
259 $relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND';
260
261 $query['where'] .= ' AND (';
262
263 foreach ( $where_meta as $index => $value ) {
264
265 if ( ! is_array( $value ) ) {
266 continue;
267 }
268
269 $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );
270
271 if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) {
272
273 if ( ! empty( $value['meta_value'] ) && ! is_array( $value['meta_value'] ) ) {
274 $value['meta_value'] = (array) $value['meta_value']; // @phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
275 }
276
277 if ( ! empty( $value['meta_value'] ) ) {
278 $formats = implode( ', ', array_fill( 0, count( $value['meta_value'] ), '%s' ) );
279 $where_value = $value['operator'] . ' (' . $wpdb->prepare( $formats, $value['meta_value'] ) . ')'; // @phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
280 }
281 } else {
282 $where_value = $value['operator'] . ' ' . $wpdb->prepare( '%s', $value['meta_value'] );
283 }
284
285 if ( ! empty( $where_value ) ) {
286 if ( $index > 0 ) {
287 $query['where'] .= ' ' . $relation;
288 }
289
290 if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) {
291
292 if ( is_array( $value['meta_key'] ) ) {
293 $query['where'] .= " ( order_item_meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')";
294 } else {
295 $query['where'] .= " ( order_item_meta_{$key}.meta_key = '{$value['meta_key']}'";
296 }
297
298 $query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )";
299 } else {
300
301 if ( is_array( $value['meta_key'] ) ) {
302 $query['where'] .= " ( meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')";
303 } else {
304 $query['where'] .= " ( meta_{$key}.meta_key = '{$value['meta_key']}'";
305 }
306
307 $query['where'] .= " AND meta_{$key}.meta_value {$where_value} )";
308 }
309 }
310 }
311
312 $query['where'] .= ')';
313 }
314
315 if ( ! empty( $where ) ) {
316
317 foreach ( $where as $value ) {
318
319 if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) {
320
321 if ( ! empty( $value['value'] ) && ! is_array( $value['value'] ) ) {
322 $value['value'] = (array) $value['value'];
323 }
324 if ( ! empty( $value['value'] ) ) {
325 $formats = implode( ', ', array_fill( 0, count( $value['value'] ), '%s' ) );
326 $where_value = $value['operator'] . ' (' . $wpdb->prepare( $formats, $value['value'] ) . ')'; // @phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
327 }
328 } else {
329 $where_value = $value['operator'] . ' ' . $wpdb->prepare( '%s', $value['value'] );
330 }
331
332 if ( ! empty( $where_value ) ) {
333 $query['where'] .= " AND {$value['key']} {$where_value}";
334 }
335 }
336 }
337
338 if ( $group_by ) {
339 $query['group_by'] = "GROUP BY {$group_by}";
340 }
341
342 if ( $order_by ) {
343 $query['order_by'] = "ORDER BY {$order_by}";
344 }
345
346 if ( $limit ) {
347 $query['limit'] = "LIMIT {$limit}";
348 }
349
350 $query = apply_filters( 'woocommerce_reports_get_order_report_query', $query );
351 $query = implode( ' ', $query );
352
353 if ( $debug ) {
354 echo '<pre>';
355 wc_print_r( $query );
356 echo '</pre>';
357 }
358
359 if ( $debug || $nocache ) {
360 self::enable_big_selects();
361
362 $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
363 } else {
364 $query_hash = md5( $query_type . $query );
365 $result = $this->get_cached_query( $query_hash );
366 if ( null === $result ) {
367 self::enable_big_selects();
368
369 $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
370 }
371 $this->set_cached_query( $query_hash, $result );
372 }
373
374 return $result;
375 }
376
377 /**
378 * Init the static hooks of the class.
379 */
380 protected static function add_update_transients_hook() {
381 if ( ! has_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ) ) {
382 add_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) );
383 }
384 }
385
386 /**
387 * Enables big mysql selects for reports, just once for this session.
388 */
389 protected static function enable_big_selects() {
390 static $big_selects = false;
391
392 global $wpdb;
393
394 if ( ! $big_selects ) {
395 $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
396 $big_selects = true;
397 }
398 }
399
400 /**
401 * Get the cached query result or null if it's not in the cache.
402 *
403 * @param string $query_hash The query hash.
404 *
405 * @return mixed
406 */
407 protected function get_cached_query( $query_hash ) {
408 $class = strtolower( get_class( $this ) );
409
410 if ( ! isset( self::$cached_results[ $class ] ) ) {
411 self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) );
412 }
413
414 if ( isset( self::$cached_results[ $class ][ $query_hash ] ) ) {
415 return self::$cached_results[ $class ][ $query_hash ];
416 }
417
418 return null;
419 }
420
421 /**
422 * Set the cached query result.
423 *
424 * @param string $query_hash The query hash.
425 * @param mixed $data The data to cache.
426 */
427 protected function set_cached_query( $query_hash, $data ) {
428 $class = strtolower( get_class( $this ) );
429
430 if ( ! isset( self::$cached_results[ $class ] ) ) {
431 self::$cached_results[ $class ] = get_transient( $class );
432 }
433
434 if ( false === self::$cached_results[ $class ] ) {
435 self::$cached_results[ $class ] = array();
436 }
437
438 self::add_update_transients_hook();
439
440 self::$transients_to_update[ $class ] = $class;
441 self::$cached_results[ $class ][ $query_hash ] = $data;
442 }
443
444 /**
445 * Function to update the modified transients at the end of the request.
446 */
447 public static function maybe_update_transients() {
448 foreach ( self::$transients_to_update as $key => $transient_name ) {
449 set_transient( $transient_name, self::$cached_results[ $transient_name ], DAY_IN_SECONDS );
450 }
451 // Transients have been updated reset the list.
452 self::$transients_to_update = array();
453 }
454
455 /**
456 * Put data with post_date's into an array of times.
457 *
458 * @param array $data array of your data.
459 * @param string $date_key key for the 'date' field. e.g. 'post_date'.
460 * @param string $data_key key for the data you are charting.
461 * @param int $interval interval to use.
462 * @param string $start_date start date.
463 * @param string $group_by group by.
464 * @return array
465 */
466 public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) {
467 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date
468
469 $prepared_data = array();
470
471 // Ensure all days (or months) have values in this range.
472 if ( 'day' === $group_by ) {
473 for ( $i = 0; $i <= $interval; $i ++ ) {
474 $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000';
475
476 if ( ! isset( $prepared_data[ $time ] ) ) {
477 $prepared_data[ $time ] = array( esc_js( $time ), 0 );
478 }
479 }
480 } else {
481 $current_yearnum = date( 'Y', $start_date );
482 $current_monthnum = date( 'm', $start_date );
483
484 for ( $i = 0; $i <= $interval; $i ++ ) {
485 $time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000';
486
487 if ( ! isset( $prepared_data[ $time ] ) ) {
488 $prepared_data[ $time ] = array( esc_js( $time ), 0 );
489 }
490
491 $current_monthnum ++;
492
493 if ( $current_monthnum > 12 ) {
494 $current_monthnum = 1;
495 $current_yearnum ++;
496 }
497 }
498 }
499
500 foreach ( $data as $d ) {
501 switch ( $group_by ) {
502 case 'day':
503 $time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000';
504 break;
505 case 'month':
506 default:
507 $time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000';
508 break;
509 }
510
511 if ( ! isset( $prepared_data[ $time ] ) ) {
512 continue;
513 }
514
515 if ( $data_key ) {
516 $prepared_data[ $time ][1] += is_numeric( $d->$data_key ) ? $d->$data_key : 0;
517 } else {
518 $prepared_data[ $time ][1] ++;
519 }
520 }
521
522 return $prepared_data;
523
524 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
525 }
526
527 /**
528 * Prepares the data for a sparkline to show sales in the last X days.
529 *
530 * @param int $id ID of the product to show. Blank to get all orders.
531 * @param int $days Days of stats to get. Default to 7 days.
532 * @param string $type Type of sparkline to get. Ignored if ID is not set.
533 * @return array
534 */
535 public function get_sales_sparkline( $id = '', $days = 7, $type = 'sales' ) {
536
537 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
538
539 if ( $id ) {
540 $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty';
541
542 $data = $this->get_order_report_data(
543 array(
544 'data' => array(
545 '_product_id' => array(
546 'type' => 'order_item_meta',
547 'order_item_type' => 'line_item',
548 'function' => '',
549 'name' => 'product_id',
550 ),
551 $meta_key => array(
552 'type' => 'order_item_meta',
553 'order_item_type' => 'line_item',
554 'function' => 'SUM',
555 'name' => 'sparkline_value',
556 ),
557 'post_date' => array(
558 'type' => 'post_data',
559 'function' => '',
560 'name' => 'post_date',
561 ),
562 ),
563 'where' => array(
564 array(
565 'key' => 'post_date',
566 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
567 'operator' => '>',
568 ),
569 array(
570 'key' => 'order_item_meta__product_id.meta_value',
571 'value' => $id,
572 'operator' => '=',
573 ),
574 ),
575 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
576 'query_type' => 'get_results',
577 'filter_range' => false,
578 )
579 );
580 } else {
581
582 $data = $this->get_order_report_data(
583 array(
584 'data' => array(
585 '_order_total' => array(
586 'type' => 'meta',
587 'function' => 'SUM',
588 'name' => 'sparkline_value',
589 ),
590 'post_date' => array(
591 'type' => 'post_data',
592 'function' => '',
593 'name' => 'post_date',
594 ),
595 ),
596 'where' => array(
597 array(
598 'key' => 'post_date',
599 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
600 'operator' => '>',
601 ),
602 ),
603 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
604 'query_type' => 'get_results',
605 'filter_range' => false,
606 )
607 );
608 }
609
610 $total = 0;
611 foreach ( $data as $d ) {
612 $total += $d->sparkline_value;
613 }
614
615 $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) );
616
617 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
618
619 return array(
620 'total' => $total,
621 'data' => $sparkline_data,
622 );
623 }
624
625 /**
626 * Prepares the markup for a sparkline to show sales in the last X days.
627 *
628 * @param int $id ID of the product to show. Blank to get all orders.
629 * @param int $days Days of stats to get. Default to 7 days.
630 * @param string $type Type of sparkline to get.
631 * @return string
632 */
633 public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) {
634 $sparkline = $this->get_sales_sparkline( $id, $days, $type );
635 $total = $sparkline['total'];
636
637 if ( 'sales' === $type ) {
638 /* translators: 1: total income 2: days */
639 $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), wp_strip_all_tags( wc_price( $total ) ), $days );
640 } else {
641 /* translators: 1: total items sold 2: days */
642 $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 );
643 }
644
645 $sparkline_data = $sparkline['data'];
646
647 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>';
648 }
649
650 /**
651 * Get the current range and calculate the start and end dates.
652 *
653 * @param string $current_range Type of range.
654 */
655 public function calculate_current_range( $current_range ) {
656
657 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
658 // phpcs:disable WordPress.Security.NonceVerification.Recommended
659
660 switch ( $current_range ) {
661
662 case 'custom':
663 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
664 $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) ) );
665
666 if ( empty( $_GET['end_date'] ) ) {
667 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
668 } else {
669 $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) ) );
670 }
671
672 $interval = 0;
673 $min_date = $this->start_date;
674
675 // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
676 while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
677 $interval ++;
678 }
679
680 // 3 months max for day view
681 if ( $interval > 3 ) {
682 $this->chart_groupby = 'month';
683 } else {
684 $this->chart_groupby = 'day';
685 }
686 break;
687
688 case 'year':
689 $this->start_date = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) );
690 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
691 $this->chart_groupby = 'month';
692 break;
693
694 case 'last_month':
695 $first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
696 $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) );
697 $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) );
698 $this->chart_groupby = 'day';
699 break;
700
701 case 'month':
702 $this->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
703 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
704 $this->chart_groupby = 'day';
705 break;
706
707 case '7day':
708 $this->start_date = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) );
709 $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
710 $this->chart_groupby = 'day';
711 break;
712 }
713
714 // Group by.
715 switch ( $this->chart_groupby ) {
716
717 case 'day':
718 $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)';
719 $this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) );
720 $this->barwidth = 60 * 60 * 24 * 1000;
721 break;
722
723 case 'month':
724 $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)';
725 $this->chart_interval = 0;
726 $min_date = strtotime( date( 'Y-m-01', $this->start_date ) );
727
728 // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
729 while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
730 $this->chart_interval ++;
731 }
732
733 $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000;
734 break;
735 }
736
737 // phpcs:enable WordPress.Security.NonceVerification.Recommended
738 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested
739 }
740
741 /**
742 * Return currency tooltip JS based on WooCommerce currency position settings.
743 *
744 * @return string
745 */
746 public function get_currency_tooltip() {
747 switch ( get_option( 'woocommerce_currency_pos' ) ) {
748 case 'right':
749 $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"';
750 break;
751 case 'right_space':
752 $currency_tooltip = 'append_tooltip: "&nbsp;' . get_woocommerce_currency_symbol() . '"';
753 break;
754 case 'left':
755 $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"';
756 break;
757 case 'left_space':
758 default:
759 $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '&nbsp;"';
760 break;
761 }
762
763 return $currency_tooltip;
764 }
765
766 /**
767 * Get the main chart.
768 */
769 public function get_main_chart() {}
770
771 /**
772 * Get the legend for the main chart sidebar.
773 *
774 * @return array
775 */
776 public function get_chart_legend() {
777 return array();
778 }
779
780 /**
781 * Get chart widgets.
782 *
783 * @return array
784 */
785 public function get_chart_widgets() {
786 return array();
787 }
788
789 /**
790 * Get an export link if needed.
791 */
792 public function get_export_button() {}
793
794 /**
795 * Output the report.
796 */
797 public function output_report() {}
798
799 /**
800 * Check nonce for current range.
801 *
802 * @since 3.0.4
803 * @param string $current_range Current range.
804 */
805 public function check_current_range_nonce( $current_range ) {
806 if ( 'custom' !== $current_range ) {
807 return;
808 }
809
810 if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) {
811 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
812 wp_die(
813 /* translators: %1$s: open link, %2$s: close link */
814 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>' ),
815 esc_attr__( 'Confirm navigation', 'woocommerce' )
816 );
817 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
818 exit;
819 }
820 }
821 }
822