PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
1.10.19 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 All 163 releases
woocommerce-pos / includes / Services / Landing_Profile.php

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

292 lines 7.7 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' => class_exists( '\WCPOS\WooCommercePOSPro\WooCommercePOSPro' ),
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_cached_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' => get_option( 'woocommerce_pos_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 * @return array
155 */
156 private function get_cached_metrics(): array {
157 $cached = get_transient( self::TRANSIENT_KEY );
158
159 if ( false !== $cached ) {
160 return $cached;
161 }
162
163 $metrics = $this->compute_metrics();
164 set_transient( self::TRANSIENT_KEY, $metrics, self::CACHE_TTL );
165
166 return $metrics;
167 }
168
169 /**
170 * Compute expensive store metrics.
171 *
172 * @return array
173 */
174 private function compute_metrics(): array {
175 $installed_at = get_option( 'woocommerce_pos_installed_at' );
176 if ( false === $installed_at ) {
177 $installed_at = time();
178 add_option( 'woocommerce_pos_installed_at', $installed_at );
179 }
180 $installed_at = (int) $installed_at;
181
182 return array(
183 'days_since_install' => max( 0, (int) floor( ( time() - $installed_at ) / DAY_IN_SECONDS ) ),
184 'product_count' => (int) wp_count_posts( 'product' )->publish,
185 'order_count' => $this->get_pos_order_count(),
186 'pos_user_count' => $this->get_pos_user_count(),
187 'active_gateways' => $this->get_active_gateway_ids(),
188 'active_extensions' => $this->get_active_extension_slugs(),
189 );
190 }
191
192 /**
193 * Count POS orders using direct SQL for performance.
194 *
195 * Supports both HPOS (custom orders table) and legacy post-based storage.
196 *
197 * @return int
198 */
199 private function get_pos_order_count(): int {
200 global $wpdb;
201
202 if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
203 /* @phpstan-ignore-next-line */
204 $orders_table = method_exists( OrdersTableDataStore::class, 'get_orders_table_name' )
205 ? OrdersTableDataStore::get_orders_table_name()
206 : $wpdb->prefix . 'wc_orders';
207 /* @phpstan-ignore-next-line */
208 $op_table = method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' )
209 ? OrdersTableDataStore::get_operational_data_table_name()
210 : $wpdb->prefix . 'wc_order_operational_data';
211
212 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
213 return (int) $wpdb->get_var(
214 $wpdb->prepare(
215 "SELECT COUNT(*) FROM {$orders_table} orders
216 INNER JOIN {$op_table} op ON op.order_id = orders.id
217 WHERE orders.type = 'shop_order'
218 AND op.created_via = %s
219 AND orders.status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')",
220 'woocommerce-pos'
221 )
222 );
223 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
224 }
225
226 return (int) $wpdb->get_var(
227 $wpdb->prepare(
228 "SELECT COUNT(*) FROM {$wpdb->posts} p
229 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
230 WHERE p.post_type = 'shop_order'
231 AND pm.meta_key = '_created_via'
232 AND pm.meta_value = %s
233 AND p.post_status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')",
234 'woocommerce-pos'
235 )
236 );
237 }
238
239 /**
240 * Count users with POS access capability.
241 *
242 * @return int
243 */
244 private function get_pos_user_count(): int {
245 $users = get_users(
246 array(
247 'capability' => 'access_woocommerce_pos',
248 'fields' => 'ID',
249 )
250 );
251
252 return \count( $users );
253 }
254
255 /**
256 * Get IDs of enabled POS payment gateways.
257 *
258 * @return array
259 */
260 private function get_active_gateway_ids(): array {
261 $settings = Settings::instance()->get_payment_gateways_settings();
262 $gateways = $settings['gateways'] ?? array();
263 $active = array();
264
265 foreach ( $gateways as $id => $gw ) {
266 if ( ! empty( $gw['enabled'] ) ) {
267 $active[] = $id;
268 }
269 }
270
271 return $active;
272 }
273
274 /**
275 * Get slugs of active WCPOS extensions.
276 *
277 * @return array
278 */
279 private function get_active_extension_slugs(): array {
280 $extensions = Extensions::instance()->get_extensions();
281 $active = array();
282
283 foreach ( $extensions as $ext ) {
284 if ( 'active' === ( $ext['status'] ?? '' ) || 'update_available' === ( $ext['status'] ?? '' ) ) {
285 $active[] = $ext['slug'] ?? '';
286 }
287 }
288
289 return array_values( array_filter( $active ) );
290 }
291 }
292