PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 All 162 releases
woocommerce-pos / includes / Services / Landing_Profile.php

Landing_Profile.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.16, at includes/Services/Landing_Profile.php

296 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Landing page profile service.
4 *
5 * Gathers store metrics and configuration for the landing page React app.
6 * Data is split into two tiers:
7 * - Functional data (locale, version): always available, no consent needed
8 * - Profile data (store metrics, UUIDs): requires explicit user consent
9 *
10 * @package WCPOS\WooCommercePOS\Services
11 */
12
13 namespace WCPOS\WooCommercePOS\Services;
14
15 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
16 use Automattic\WooCommerce\Utilities\OrderUtil;
17 use const HOUR_IN_SECONDS;
18 use const WCPOS\WooCommercePOS\VERSION as PLUGIN_VERSION;
19
20 /**
21 * Landing Profile service class.
22 */
23 class Landing_Profile {
24
25 /**
26 * Transient key for cached metrics.
27 *
28 * @var string
29 */
30 const TRANSIENT_KEY = 'wcpos_landing_profile';
31
32 /**
33 * Cache TTL in seconds (1 hour).
34 *
35 * @var int
36 */
37 const CACHE_TTL = HOUR_IN_SECONDS;
38
39 /**
40 * Default updates server base URL.
41 *
42 * @var string
43 */
44 const UPDATES_SERVER_URL = 'https://updates.wcpos.com';
45
46 /**
47 * Get functional data that is always available (no consent required).
48 *
49 * Used for translations, feature gating, and schema versioning. Includes the
50 * anonymous identity and the server-resolved experiment flags so the landing
51 * bundle can bootstrap its A/B variant at first paint without a network flag
52 * fetch (landing-experiments spec §5.1).
53 *
54 * @return array
55 */
56 public function get_functional_data(): array {
57 $anon_id = ( new Anon_ID() )->get();
58
59 return array(
60 'schema_version' => 2, // bumped: anon_id added (landing-experiments spec §5.1).
61 'locale' => get_locale(),
62 'plugin_version' => PLUGIN_VERSION,
63 'pro_active' => wcpos_is_pro_active(),
64 'anon_id' => $anon_id,
65 'bootstrap_flags' => ( new Feature_Flags() )->get_landing_bootstrap_flags( $anon_id ),
66 );
67 }
68
69 /**
70 * Get consent-gated data (store profile + service config).
71 *
72 * Only sent when the user has explicitly allowed tracking.
73 *
74 * @return array
75 */
76 public function get_consented_data(): array {
77 return array(
78 'profile' => $this->get_profile(),
79 'updates_server' => $this->get_updates_server_config(),
80 );
81 }
82
83 /**
84 * Get the store profile (consent-gated fields only).
85 *
86 * Merges cached expensive metrics with cheap/user-specific fields
87 * that are computed fresh on every call.
88 *
89 * @return array
90 */
91 public function get_profile(): array {
92 $cached = $this->get_metrics();
93 $user = wp_get_current_user();
94
95 return array_merge(
96 $cached,
97 array(
98 'wc_version' => WC()->version,
99 'php_version' => PHP_VERSION,
100 'site_uuid' => wcpos_get_site_uuid(),
101 'user_uuid' => get_user_meta( $user->ID, '_woocommerce_pos_uuid', true ),
102 'user_role' => ! empty( $user->roles ) ? $user->roles[0] : '',
103 'site_domain' => $this->get_url_host( home_url() ),
104 'admin_domain' => $this->get_url_host( admin_url() ),
105 'wc_currency' => get_woocommerce_currency(),
106 'wc_country' => WC()->countries->get_base_country(),
107 )
108 );
109 }
110
111 /**
112 * Extract a hostname from a WordPress URL without retaining paths or query strings.
113 *
114 * @param string $url URL to parse.
115 *
116 * @return string
117 */
118 private function get_url_host( string $url ): string {
119 $host = wp_parse_url( $url, PHP_URL_HOST );
120
121 if ( ! is_string( $host ) ) {
122 return '';
123 }
124
125 return strtolower( $host );
126 }
127
128 /**
129 * Get updates server configuration.
130 *
131 * @return array
132 */
133 public function get_updates_server_config(): array {
134 /**
135 * Filters the updates server profile endpoint URL.
136 *
137 * @since 1.9.0
138 *
139 * @param string $profile_url The profile endpoint URL.
140 */
141 $profile_url = apply_filters(
142 'woocommerce_pos_updates_server_profile_url',
143 self::UPDATES_SERVER_URL . '/v1/profile'
144 );
145
146 return array(
147 'profile_url' => $profile_url,
148 );
149 }
150
151 /**
152 * Get cached expensive metrics, computing them if the cache is stale.
153 *
154 * Public because the analytics site profile reports the same store-size
155 * numbers; sharing the cache keeps the count queries to one per hour
156 * instead of one per consumer.
157 *
158 * @return array
159 */
160 public function get_metrics(): array {
161 $cached = get_transient( self::TRANSIENT_KEY );
162
163 if ( false !== $cached ) {
164 return $cached;
165 }
166
167 $metrics = $this->compute_metrics();
168 set_transient( self::TRANSIENT_KEY, $metrics, self::CACHE_TTL );
169
170 return $metrics;
171 }
172
173 /**
174 * Compute expensive store metrics.
175 *
176 * @return array
177 */
178 private function compute_metrics(): array {
179 $installed_at = get_option( 'woocommerce_pos_installed_at' );
180 if ( false === $installed_at ) {
181 $installed_at = time();
182 add_option( 'woocommerce_pos_installed_at', $installed_at );
183 }
184 $installed_at = (int) $installed_at;
185
186 return array(
187 'days_since_install' => max( 0, (int) floor( ( time() - $installed_at ) / DAY_IN_SECONDS ) ),
188 'product_count' => (int) wp_count_posts( 'product' )->publish,
189 'order_count' => $this->get_pos_order_count(),
190 'pos_user_count' => $this->get_pos_user_count(),
191 'active_gateways' => $this->get_active_gateway_ids(),
192 'active_extensions' => $this->get_active_extension_slugs(),
193 );
194 }
195
196 /**
197 * Count POS orders using direct SQL for performance.
198 *
199 * Supports both HPOS (custom orders table) and legacy post-based storage.
200 *
201 * @return int
202 */
203 private function get_pos_order_count(): int {
204 global $wpdb;
205
206 if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
207 /* @phpstan-ignore-next-line */
208 $orders_table = method_exists( OrdersTableDataStore::class, 'get_orders_table_name' )
209 ? OrdersTableDataStore::get_orders_table_name()
210 : $wpdb->prefix . 'wc_orders';
211 /* @phpstan-ignore-next-line */
212 $op_table = method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' )
213 ? OrdersTableDataStore::get_operational_data_table_name()
214 : $wpdb->prefix . 'wc_order_operational_data';
215
216 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
217 return (int) $wpdb->get_var(
218 $wpdb->prepare(
219 "SELECT COUNT(*) FROM {$orders_table} orders
220 INNER JOIN {$op_table} op ON op.order_id = orders.id
221 WHERE orders.type = 'shop_order'
222 AND op.created_via = %s
223 AND orders.status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')",
224 'woocommerce-pos'
225 )
226 );
227 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
228 }
229
230 return (int) $wpdb->get_var(
231 $wpdb->prepare(
232 "SELECT COUNT(*) FROM {$wpdb->posts} p
233 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
234 WHERE p.post_type = 'shop_order'
235 AND pm.meta_key = '_created_via'
236 AND pm.meta_value = %s
237 AND p.post_status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')",
238 'woocommerce-pos'
239 )
240 );
241 }
242
243 /**
244 * Count users with POS access capability.
245 *
246 * @return int
247 */
248 private function get_pos_user_count(): int {
249 $users = get_users(
250 array(
251 'capability' => 'access_woocommerce_pos',
252 'fields' => 'ID',
253 )
254 );
255
256 return \count( $users );
257 }
258
259 /**
260 * Get IDs of enabled POS payment gateways.
261 *
262 * @return array
263 */
264 private function get_active_gateway_ids(): array {
265 $settings = Settings::instance()->get_payment_gateways_settings();
266 $gateways = $settings['gateways'] ?? array();
267 $active = array();
268
269 foreach ( $gateways as $id => $gw ) {
270 if ( ! empty( $gw['enabled'] ) ) {
271 $active[] = $id;
272 }
273 }
274
275 return $active;
276 }
277
278 /**
279 * Get slugs of active WCPOS extensions.
280 *
281 * @return array
282 */
283 private function get_active_extension_slugs(): array {
284 $extensions = Extensions::instance()->get_extensions();
285 $active = array();
286
287 foreach ( $extensions as $ext ) {
288 if ( 'active' === ( $ext['status'] ?? '' ) || 'update_available' === ( $ext['status'] ?? '' ) ) {
289 $active[] = $ext['slug'] ?? '';
290 }
291 }
292
293 return array_values( array_filter( $active ) );
294 }
295 }
296