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 / helper / class-wc-helper.php
woocommerce / includes / admin / helper Last commit date
views 7 months ago class-wc-helper-admin.php 6 months ago class-wc-helper-api.php 1 year ago class-wc-helper-compat.php 5 years ago class-wc-helper-options.php 3 years ago class-wc-helper-orders-api.php 2 years ago class-wc-helper-sanitization.php 1 year ago class-wc-helper-subscriptions-api.php 3 weeks ago class-wc-helper-updater.php 2 weeks ago class-wc-helper.php 2 weeks ago class-wc-plugin-api-updater.php 1 year ago class-wc-product-usage-notice.php 1 year ago class-wc-woo-helper-connection.php 6 months ago class-wc-woo-update-manager-plugin.php 2 years ago
class-wc-helper.php
2988 lines
1 <?php
2 /**
3 * WooCommerce Admin Helper
4 *
5 * @package WooCommerce\Admin\Helper
6 */
7
8 use Automattic\Jetpack\Constants;
9 use Automattic\WooCommerce\Admin\PluginsHelper;
10 use Automattic\WooCommerce\Admin\Notes\Note;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * WC_Helper Class
18 *
19 * The main entry-point for all things related to the Helper.
20 */
21 class WC_Helper {
22 const NOTE_NAME = 'wccom-api-failed';
23
24 /**
25 * A log object returned by wc_get_logger().
26 *
27 * @var $log
28 */
29 public static $log;
30
31 private const CACHE_KEY_CONNECTION_DATA = '_woocommerce_helper_connection_data';
32
33 /**
34 * Get an absolute path to the requested helper view.
35 *
36 * @param string $view The requested view file.
37 *
38 * @return string The absolute path to the view file.
39 */
40 public static function get_view_filename( $view ) {
41 return __DIR__ . "/views/$view";
42 }
43
44 /**
45 * Loads the helper class, runs on init.
46 *
47 * @return void
48 */
49 public static function load() {
50 self::includes();
51
52 add_action( 'current_screen', array( __CLASS__, 'current_screen' ) );
53 add_action( 'woocommerce_helper_output', array( __CLASS__, 'render_helper_output' ) );
54 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) );
55 add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
56
57 do_action( 'woocommerce_helper_loaded' );
58 }
59
60 /**
61 * Remove all notes signaling an error with the WCCOM API, when the request was successful.
62 *
63 * @return void
64 */
65 protected static function remove_api_error_notice() {
66 try {
67 $data_store = \WC_Data_Store::load( 'admin-note' );
68 } catch ( Exception $e ) {
69 return;
70 }
71
72 $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
73
74 if ( ! empty( $note_ids ) ) {
75 foreach ( $note_ids as $note_id ) {
76 $note = new Note( $note_id );
77 if ( $note->get_id() ) {
78 $data_store->delete( $note );
79 }
80 }
81 }
82 }
83
84 /**
85 * Adds at most one note signaling that there was an error with the WCCOM API.
86 *
87 * @return void
88 */
89 protected static function add_api_error_notice() {
90 try {
91 $data_store = \WC_Data_Store::load( 'admin-note' );
92 } catch ( Exception $e ) {
93 return;
94 }
95
96 $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
97
98 if ( ! empty( $note_ids ) ) {
99 $current_notice_id = array_shift( $note_ids );
100
101 foreach ( $note_ids as $note_id ) {
102 $note = new Note( $note_id );
103 if ( $note->get_id() ) {
104 $data_store->delete( $note );
105 }
106 }
107
108 $note = new Note( $current_notice_id );
109 } else {
110 $note = new Note();
111 }
112
113 $note->set_props(
114 array(
115 'title' => __( 'We’re having trouble connecting to WooCommerce.com', 'woocommerce' ),
116 'content' => __( 'Some subscription data may be temporarily unavailable. Please refresh the page in a few minutes to try again.', 'woocommerce' ),
117 'type' => Note::E_WC_ADMIN_NOTE_UPDATE,
118 'name' => self::NOTE_NAME,
119 'content_data' => (object) array(),
120 'source' => 'woocommerce-admin',
121 'status' => Note::E_WC_ADMIN_NOTE_UNACTIONED,
122 'is_deleted' => false,
123 )
124 );
125
126 $note->save();
127 }
128
129 /**
130 * Get the source page for the connect URL (wc-admin or wc-addons/extensions)
131 *
132 * @return string
133 */
134 private static function get_source_page() {
135 $page = wc_clean( wp_unslash( $_GET['page'] ?? 'wc-admin' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
136 return in_array( $page, array( 'wc-admin', 'wc-addons' ), true ) ? $page : 'wc-admin';
137 }
138
139 /**
140 * Get the URL of the My Subscriptions screen used in admin notices.
141 *
142 * @return string
143 */
144 private static function get_subscriptions_url() {
145 return add_query_arg(
146 array(
147 'page' => 'wc-admin',
148 'tab' => 'my-subscriptions',
149 'path' => rawurlencode( '/extensions' ),
150 ),
151 admin_url( 'admin.php' )
152 );
153 }
154
155 /**
156 * Include supporting helper classes.
157 *
158 * @return void
159 */
160 protected static function includes() {
161 include_once __DIR__ . '/class-wc-helper-options.php';
162 include_once __DIR__ . '/class-wc-helper-api.php';
163 include_once __DIR__ . '/class-wc-woo-update-manager-plugin.php';
164 include_once __DIR__ . '/class-wc-woo-helper-connection.php';
165 include_once __DIR__ . '/class-wc-helper-updater.php';
166 include_once __DIR__ . '/class-wc-plugin-api-updater.php';
167 include_once __DIR__ . '/class-wc-helper-compat.php';
168 include_once __DIR__ . '/class-wc-helper-sanitization.php';
169 include_once __DIR__ . '/class-wc-helper-admin.php';
170 include_once __DIR__ . '/class-wc-helper-subscriptions-api.php';
171 include_once __DIR__ . '/class-wc-helper-orders-api.php';
172 include_once __DIR__ . '/class-wc-product-usage-notice.php';
173 }
174
175 /**
176 * Render the helper section content based on context.
177 *
178 * @return void
179 */
180 public static function render_helper_output() {
181 $auth = WC_Helper_Options::get( 'auth' );
182 $auth_user_data = WC_Helper_Options::get( 'auth_user_data' );
183
184 // Return success/error notices.
185 $notices = self::_get_return_notices();
186
187 // No active connection.
188 if ( ! self::is_site_connected() ) {
189 $connect_url = add_query_arg(
190 array(
191 'page' => self::get_source_page(),
192 'section' => 'helper',
193 'wc-helper-connect' => 1,
194 'wc-helper-nonce' => wp_create_nonce( 'connect' ),
195 ),
196 admin_url( 'admin.php' )
197 );
198
199 include self::get_view_filename( 'html-oauth-start.php' );
200 return;
201 }
202 $disconnect_url = add_query_arg(
203 array(
204 'page' => self::get_source_page(),
205 'section' => 'helper',
206 'wc-helper-disconnect' => 1,
207 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ),
208 ),
209 admin_url( 'admin.php' )
210 );
211
212 $current_filter = self::get_current_filter();
213 $refresh_url = add_query_arg(
214 array(
215 'page' => self::get_source_page(),
216 'section' => 'helper',
217 'filter' => $current_filter,
218 'wc-helper-refresh' => 1,
219 'wc-helper-nonce' => wp_create_nonce( 'refresh' ),
220 ),
221 admin_url( 'admin.php' )
222 );
223
224 // Installed plugins and themes, with or without an active subscription.
225 $woo_plugins = self::get_local_woo_plugins();
226 $woo_themes = self::get_local_woo_themes();
227
228 $subscriptions_list_data = self::get_subscription_list_data();
229 $subscriptions = array_filter(
230 $subscriptions_list_data,
231 function ( $subscription ) {
232 return ! empty( $subscription['product_key'] );
233 }
234 );
235 $updates = WC_Helper_Updater::get_update_data();
236 $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );
237
238 foreach ( $subscriptions as &$subscription ) {
239 $subscription['activate_url'] = add_query_arg(
240 array(
241 'page' => self::get_source_page(),
242 'section' => 'helper',
243 'filter' => $current_filter,
244 'wc-helper-activate' => 1,
245 'wc-helper-product-key' => $subscription['product_key'],
246 'wc-helper-product-id' => $subscription['product_id'],
247 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ),
248 ),
249 admin_url( 'admin.php' )
250 );
251
252 $subscription['deactivate_url'] = add_query_arg(
253 array(
254 'page' => self::get_source_page(),
255 'section' => 'helper',
256 'filter' => $current_filter,
257 'wc-helper-deactivate' => 1,
258 'wc-helper-product-key' => $subscription['product_key'],
259 'wc-helper-product-id' => $subscription['product_id'],
260 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ),
261 ),
262 admin_url( 'admin.php' )
263 );
264
265 $subscription['update_url'] = admin_url( 'update-core.php' );
266
267 $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) );
268
269 if ( ! empty( $local ) ) {
270 $local = array_shift( $local );
271 if ( 'plugin' === $local['_type'] ) {
272 // A magic update_url.
273 $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] );
274
275 } elseif ( 'theme' === $local['_type'] ) {
276 // Another magic update_url.
277 $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] );
278 }
279 }
280
281 $subscription['download_primary'] = true;
282 $subscription['download_url'] = 'https://woocommerce.com/my-account/downloads/';
283 if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
284 $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package'];
285 }
286
287 $subscription['actions'] = array();
288
289 if ( $subscription['has_update'] && ! $subscription['expired'] ) {
290 $action = array(
291 /* translators: %s: version number */
292 'message' => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ),
293 'button_label' => __( 'Update', 'woocommerce' ),
294 'button_url' => $subscription['update_url'],
295 'status' => 'update-available',
296 'icon' => 'dashicons-update',
297 );
298
299 // Subscription is not active on this site.
300 if ( ! $subscription['active'] ) {
301 $action['message'] .= ' ' . __( 'To enable this update you need to <strong>activate</strong> this subscription.', 'woocommerce' );
302 $action['button_label'] = null;
303 $action['button_url'] = null;
304 }
305
306 $subscription['actions'][] = $action;
307 }
308
309 if ( $subscription['has_update'] && $subscription['expired'] ) {
310 $action = array(
311 /* translators: %s: version number */
312 'message' => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ),
313 'status' => 'expired',
314 'icon' => 'dashicons-info',
315 );
316
317 $action['message'] .= ' ' . __( 'To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' );
318 $action['button_label'] = __( 'Purchase', 'woocommerce' );
319 $action['button_url'] = self::add_utm_params_to_url_for_subscription_link(
320 $subscription['product_url'],
321 'purchase'
322 );
323
324 $subscription['actions'][] = $action;
325 } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) {
326 $action = array(
327 'message' => sprintf( __( 'This subscription has expired. Contact the owner to <strong>renew</strong> the subscription to receive updates and support.', 'woocommerce' ) ),
328 'status' => 'expired',
329 'icon' => 'dashicons-info',
330 );
331
332 $subscription['actions'][] = $action;
333 } elseif ( $subscription['expired'] ) {
334 $action = array(
335 'message' => sprintf( __( 'This subscription has expired. Please <strong>renew</strong> to receive updates and support.', 'woocommerce' ) ),
336 'button_label' => __( 'Renew', 'woocommerce' ),
337 'button_url' => self::add_utm_params_to_url_for_subscription_link(
338 'https://woocommerce.com/my-account/my-subscriptions/',
339 'renew'
340 ),
341 'status' => 'expired',
342 'icon' => 'dashicons-info',
343 );
344
345 $subscription['actions'][] = $action;
346 }
347
348 if ( $subscription['expiring'] && ! $subscription['autorenew'] ) {
349 $action = array(
350 'message' => __( 'Subscription is <strong>expiring</strong> soon.', 'woocommerce' ),
351 'button_label' => __( 'Enable auto-renew', 'woocommerce' ),
352 'button_url' => self::add_utm_params_to_url_for_subscription_link(
353 'https://woocommerce.com/my-account/my-subscriptions/',
354 'auto-renew'
355 ),
356 'status' => 'expired',
357 'icon' => 'dashicons-info',
358 );
359
360 $subscription['download_primary'] = false;
361 $subscription['actions'][] = $action;
362 } elseif ( $subscription['expiring'] ) {
363 $action = array(
364 'message' => sprintf( __( 'This subscription is expiring soon. Please <strong>renew</strong> to continue receiving updates and support.', 'woocommerce' ) ),
365 'button_label' => __( 'Renew', 'woocommerce' ),
366 'button_url' => self::add_utm_params_to_url_for_subscription_link(
367 'https://woocommerce.com/my-account/my-subscriptions/',
368 'renew'
369 ),
370 'status' => 'expired',
371 'icon' => 'dashicons-info',
372 );
373
374 $subscription['download_primary'] = false;
375 $subscription['actions'][] = $action;
376 }
377
378 // Mark the first action primary.
379 foreach ( $subscription['actions'] as $key => $action ) {
380 if ( ! empty( $action['button_label'] ) ) {
381 $subscription['actions'][ $key ]['primary'] = true;
382 break;
383 }
384 }
385 }
386
387 // Break the by-ref.
388 unset( $subscription );
389
390 // Installed products without a subscription.
391 $no_subscriptions = array();
392 foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) {
393 if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) {
394 continue;
395 }
396
397 $data['_product_url'] = '#';
398 $data['_has_update'] = false;
399
400 if ( ! empty( $updates[ $data['_product_id'] ] ) ) {
401 $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' );
402
403 if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) {
404 $data['_product_url'] = $updates[ $data['_product_id'] ]['url'];
405 } elseif ( ! empty( $data['PluginURI'] ) ) {
406 $data['_product_url'] = $data['PluginURI'];
407 }
408 }
409
410 $data['_actions'] = array();
411
412 if ( $data['_has_update'] ) {
413 $action = array(
414 /* translators: %s: version number */
415 'message' => sprintf( __( 'Version %s is <strong>available</strong>. To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ),
416 'button_label' => __( 'Purchase', 'woocommerce' ),
417 'button_url' => self::add_utm_params_to_url_for_subscription_link(
418 $data['_product_url'],
419 'purchase'
420 ),
421 'status' => 'expired',
422 'icon' => 'dashicons-info',
423 );
424
425 $data['_actions'][] = $action;
426 } else {
427 $action = array(
428 /* translators: 1: subscriptions docs 2: subscriptions docs */
429 'message' => sprintf( __( 'To receive updates and support for this extension, you need to <strong>purchase</strong> a new subscription or consolidate your extensions to one connected account by <strong><a href="%1$s" title="Sharing Docs">sharing</a> or <a href="%2$s" title="Transferring Docs">transferring</a></strong> this extension to this connected account.', 'woocommerce' ), 'https://woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ),
430 'button_label' => __( 'Purchase', 'woocommerce' ),
431 'button_url' => self::add_utm_params_to_url_for_subscription_link(
432 $data['_product_url'],
433 'purchase'
434 ),
435 'status' => 'expired',
436 'icon' => 'dashicons-info',
437 );
438
439 $data['_actions'][] = $action;
440 }
441
442 $no_subscriptions[ $filename ] = $data;
443 }
444
445 // Update the user id if it came from a migrated connection.
446 if ( empty( $auth['user_id'] ) ) {
447 $auth['user_id'] = get_current_user_id();
448 WC_Helper_Options::update( 'auth', $auth );
449 }
450
451 // Sort alphabetically.
452 uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) );
453 uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) );
454
455 // Filters.
456 self::get_filters_counts( $subscriptions ); // Warm it up.
457 self::_filter( $subscriptions, self::get_current_filter() );
458
459 // We have an active connection.
460 include self::get_view_filename( 'html-main.php' );
461 return;
462 }
463
464 /**
465 * Add tracking parameters to buttons (Renew, Purchase, etc.) on subscriptions page
466 *
467 * @param string $url URL to product page or to https://woocommerce.com/my-account/my-subscriptions/.
468 * @param string $utm_content value of utm_content query parameter used for tracking
469 *
470 * @return string URL including utm parameters for tracking
471 */
472 public static function add_utm_params_to_url_for_subscription_link( $url, $utm_content ) {
473 $utm_params = 'utm_source=subscriptionsscreen&' .
474 'utm_medium=product&' .
475 'utm_campaign=wcaddons&' .
476 'utm_content=' . $utm_content;
477
478 // there are already some URL parameters
479 if ( strpos( $url, '?' ) ) {
480 return $url . '&' . $utm_params;
481 }
482
483 return $url . '?' . $utm_params;
484 }
485
486 /**
487 * Get available subscriptions filters.
488 *
489 * @return array An array of filter keys and labels.
490 */
491 public static function get_filters() {
492 $filters = array(
493 'all' => __( 'All', 'woocommerce' ),
494 'active' => __( 'Active', 'woocommerce' ),
495 'inactive' => __( 'Inactive', 'woocommerce' ),
496 'installed' => __( 'Installed', 'woocommerce' ),
497 'update-available' => __( 'Update Available', 'woocommerce' ),
498 'expiring' => __( 'Expiring Soon', 'woocommerce' ),
499 'expired' => __( 'Expired', 'woocommerce' ),
500 'download' => __( 'Download', 'woocommerce' ),
501 );
502
503 return $filters;
504 }
505
506 /**
507 * Get counts data for the filters array.
508 *
509 * @param array $subscriptions The array of all available subscriptions.
510 *
511 * @return array Filter counts (filter => count).
512 */
513 public static function get_filters_counts( $subscriptions = null ) {
514 static $filters;
515
516 if ( isset( $filters ) ) {
517 return $filters;
518 }
519
520 $filters = array_fill_keys( array_keys( self::get_filters() ), 0 );
521 if ( ! is_array( $subscriptions ) || empty( $subscriptions ) ) {
522 return array();
523 }
524
525 foreach ( $filters as $key => $count ) {
526 $_subs = $subscriptions;
527 self::_filter( $_subs, $key );
528 $filters[ $key ] = count( $_subs );
529 }
530
531 return $filters;
532 }
533
534 /**
535 * Get current filter.
536 *
537 * @return string The current filter.
538 */
539 public static function get_current_filter() {
540 $current_filter = 'all';
541 $valid_filters = array_keys( self::get_filters() );
542
543 if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) {
544 $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) );
545 }
546
547 return $current_filter;
548 }
549
550 /**
551 * Filter an array of subscriptions by $filter.
552 *
553 * @param array $subscriptions The subscriptions array, passed by ref.
554 * @param string $filter The filter.
555 *
556 * @return void
557 */
558 private static function _filter( &$subscriptions, $filter ) {
559 switch ( $filter ) {
560 case 'active':
561 $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) );
562 break;
563
564 case 'inactive':
565 $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) );
566 break;
567
568 case 'installed':
569 foreach ( $subscriptions as $key => $subscription ) {
570 if ( empty( $subscription['local']['installed'] ) ) {
571 unset( $subscriptions[ $key ] );
572 }
573 }
574 break;
575
576 case 'update-available':
577 $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) );
578 break;
579
580 case 'expiring':
581 $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) );
582 break;
583
584 case 'expired':
585 $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) );
586 break;
587
588 case 'download':
589 foreach ( $subscriptions as $key => $subscription ) {
590 if ( $subscription['local']['installed'] || $subscription['expired'] ) {
591 unset( $subscriptions[ $key ] );
592 }
593 }
594 break;
595 }
596 }
597
598 /**
599 * Enqueue admin scripts and styles.
600 *
601 * @return void
602 */
603 public static function admin_enqueue_scripts() {
604 $screen = get_current_screen();
605 $screen_id = $screen ? $screen->id : '';
606 $wc_screen_id = 'woocommerce';
607
608 if ( ( $wc_screen_id . '_page_wc-addons' === $screen_id || $wc_screen_id . '_page_wc-admin' === $screen_id ) && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
609 wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) );
610 wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' );
611 }
612 }
613
614 /**
615 * Various success/error notices.
616 *
617 * Runs during admin page render, so no headers/redirects here.
618 *
619 * @return array Array pairs of message/type strings with notices.
620 */
621 private static function _get_return_notices() {
622 $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null;
623 $notices = array();
624
625 switch ( $return_status ) {
626 case 'activate-success':
627 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
628 $subscription = self::_get_subscriptions_from_product_id( $product_id );
629 $notices[] = array(
630 'type' => 'updated',
631 'message' => sprintf(
632 /* translators: %s: product name */
633 __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ),
634 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
635 ),
636 );
637 break;
638
639 case 'activate-error':
640 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
641 $subscription = self::_get_subscriptions_from_product_id( $product_id );
642 $notices[] = array(
643 'type' => 'error',
644 'message' => sprintf(
645 /* translators: %s: product name */
646 __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ),
647 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
648 ),
649 );
650 break;
651
652 case 'deactivate-success':
653 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
654 $subscription = self::_get_subscriptions_from_product_id( $product_id );
655 $local = self::_get_local_from_product_id( $product_id );
656
657 $message = sprintf(
658 /* translators: %s: product name */
659 __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ),
660 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
661 );
662
663 if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) {
664 $deactivate_plugin_url = add_query_arg(
665 array(
666 'page' => self::get_source_page(),
667 'section' => 'helper',
668 'filter' => self::get_current_filter(),
669 'wc-helper-deactivate-plugin' => 1,
670 'wc-helper-product-id' => $subscription['product_id'],
671 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ),
672 ),
673 admin_url( 'admin.php' )
674 );
675
676 $message = sprintf(
677 /* translators: %1$s: product name, %2$s: deactivate url */
678 __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. <a href="%2$s">Click here</a> if you wish to deactivate the plugin as well.', 'woocommerce' ),
679 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>',
680 esc_url( $deactivate_plugin_url )
681 );
682 }
683
684 $notices[] = array(
685 'message' => $message,
686 'type' => 'updated',
687 );
688 break;
689
690 case 'deactivate-error':
691 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
692 $subscription = self::_get_subscriptions_from_product_id( $product_id );
693 $notices[] = array(
694 'type' => 'error',
695 'message' => sprintf(
696 /* translators: %s: product name */
697 __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ),
698 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
699 ),
700 );
701 break;
702
703 case 'deactivate-plugin-success':
704 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
705 $subscription = self::_get_subscriptions_from_product_id( $product_id );
706 $notices[] = array(
707 'type' => 'updated',
708 'message' => sprintf(
709 /* translators: %s: product name */
710 __( 'The extension %s has been deactivated successfully.', 'woocommerce' ),
711 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
712 ),
713 );
714 break;
715
716 case 'deactivate-plugin-error':
717 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
718 $subscription = self::_get_subscriptions_from_product_id( $product_id );
719 $notices[] = array(
720 'type' => 'error',
721 'message' => sprintf(
722 /* translators: %1$s: product name, %2$s: plugins screen url */
723 __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the <a href="%2$s">Plugins screen</a> to deactivate it manually.', 'woocommerce' ),
724 '<strong>' . esc_html( $subscription['product_name'] ) . '</strong>',
725 admin_url( 'plugins.php' )
726 ),
727 );
728 break;
729
730 case 'helper-connected':
731 $notices[] = array(
732 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ),
733 'type' => 'updated',
734 );
735 break;
736
737 case 'helper-disconnected':
738 $notices[] = array(
739 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ),
740 'type' => 'updated',
741 );
742 break;
743
744 case 'helper-refreshed':
745 $notices[] = array(
746 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ),
747 'type' => 'updated',
748 );
749 break;
750 }
751
752 return $notices;
753 }
754
755 /**
756 * Various early-phase actions with possible redirects.
757 *
758 * @param object $screen WP screen object.
759 */
760 public static function current_screen( $screen ) {
761 $wc_screen_id = 'woocommerce';
762
763 if ( $wc_screen_id . '_page_wc-addons' !== $screen->id && $wc_screen_id . '_page_wc-admin' !== $screen->id ) {
764 return;
765 }
766
767 self::maybe_redirect_to_new_marketplace_installer();
768
769 if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) {
770 return;
771 }
772
773 if ( ! empty( $_GET['wc-helper-connect'] ) ) {
774 return self::_helper_auth_connect();
775 }
776
777 if ( ! empty( $_GET['wc-helper-return'] ) ) {
778 return self::_helper_auth_return();
779 }
780
781 if ( ! empty( $_GET['wc-helper-disconnect'] ) ) {
782 return self::_helper_auth_disconnect();
783 }
784
785 if ( ! empty( $_GET['wc-helper-refresh'] ) ) {
786 return self::_helper_auth_refresh();
787 }
788
789 if ( ! empty( $_GET['wc-helper-activate'] ) ) {
790 return self::_helper_subscription_activate();
791 }
792
793 if ( ! empty( $_GET['wc-helper-deactivate'] ) ) {
794 return self::helper_subscription_deactivate();
795 }
796
797 if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) {
798 return self::_helper_plugin_deactivate();
799 }
800 }
801
802 /**
803 * Maybe redirect to the new Marketplace installer.
804 *
805 * @return void
806 */
807 private static function maybe_redirect_to_new_marketplace_installer() {
808 // phpcs:disable WordPress.Security.NonceVerification.Recommended
809 // Redirect requires the "install" URL parameter to be passed.
810 if ( empty( $_GET['install'] ) ) {
811 return;
812 }
813
814 wp_safe_redirect(
815 self::get_helper_redirect_url(
816 array(
817 'page' => self::get_source_page(),
818 'section' => 'helper',
819 )
820 )
821 );
822 }
823
824 /**
825 * Get helper redirect URL.
826 *
827 * @param array $args Query args.
828 * @return string
829 */
830 private static function get_helper_redirect_url( $args = array() ) {
831 global $current_screen;
832
833 // phpcs:disable WordPress.Security.NonceVerification.Recommended
834 $redirect_admin_url = isset( $_GET['redirect_admin_url'] )
835 ? esc_url_raw(
836 urldecode(
837 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
838 wp_unslash( $_GET['redirect_admin_url'] )
839 )
840 )
841 : '';
842 $install_product_key = isset( $_GET['install'] ) ? sanitize_text_field( wp_unslash( $_GET['install'] ) ) : '';
843 // phpcs:enable WordPress.Security.NonceVerification.Recommended
844
845 if (
846 ( 'woocommerce_page_wc-addons' === $current_screen->id ||
847 'woocommerce_page_wc-admin' === $current_screen->id ) &&
848 (
849 false === empty( $redirect_admin_url ) ||
850 false === empty( $install_product_key )
851 )
852 ) {
853 if ( strpos( $redirect_admin_url, admin_url( 'admin.php' ) ) === 0 ) {
854 $new_url = $redirect_admin_url;
855 } else {
856 $new_url = add_query_arg(
857 array(
858 'page' => 'wc-admin',
859 'tab' => 'my-subscriptions',
860 'path' => rawurlencode( '/extensions' ),
861 ),
862 admin_url( 'admin.php' )
863 );
864 }
865
866 if ( ! empty( $install_product_key ) ) {
867 $new_url = add_query_arg(
868 array(
869 'install' => $install_product_key,
870 ),
871 $new_url
872 );
873 }
874 return $new_url;
875 }
876
877 return add_query_arg(
878 $args,
879 admin_url( 'admin.php' )
880 );
881 }
882
883 /**
884 * Initiate a new OAuth connection.
885 *
886 * @return never
887 */
888 private static function _helper_auth_connect() {
889 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
890 self::log( 'Could not verify nonce in _helper_auth_connect' );
891 wp_die( 'Could not verify nonce' );
892 }
893
894 $redirect_url_args = array(
895 'page' => self::get_source_page(),
896 'section' => 'helper',
897 'wc-helper-return' => 1,
898 'wc-helper-nonce' => wp_create_nonce( 'connect' ),
899 );
900
901 if ( isset( $_GET['install'] ) ) {
902 $redirect_url_args['install'] = sanitize_text_field( wp_unslash( $_GET['install'] ) );
903 }
904
905 if ( isset( $_GET['utm_source'] ) ) {
906 $redirect_url_args['utm_source'] = wc_clean( wp_unslash( $_GET['utm_source'] ) );
907 }
908
909 if ( isset( $_GET['utm_campaign'] ) ) {
910 $redirect_url_args['utm_campaign'] = wc_clean( wp_unslash( $_GET['utm_campaign'] ) );
911 }
912
913 $redirect_uri = add_query_arg(
914 $redirect_url_args,
915 admin_url( 'admin.php' )
916 );
917
918 $request = WC_Helper_API::post(
919 'oauth/request_token',
920 array(
921 'body' => array(
922 'home_url' => home_url(),
923 'redirect_uri' => $redirect_uri,
924 ),
925 )
926 );
927
928 $code = wp_remote_retrieve_response_code( $request );
929
930 if ( 200 !== $code ) {
931 self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) );
932 wp_die( 'Something went wrong' );
933 }
934
935 $secret = json_decode( wp_remote_retrieve_body( $request ) );
936 if ( empty( $secret ) ) {
937 self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) );
938 wp_die( 'Something went wrong' );
939 }
940
941 /**
942 * Fires when the Helper connection process is initiated.
943 */
944 do_action( 'woocommerce_helper_connect_start' );
945
946 // Ignore all previously dismissed connected notices.
947 delete_metadata( 'user', 0, \Automattic\WooCommerce\Admin\PluginsHelper::DISMISS_CONNECT_NOTICE, '', true );
948
949 $connect_url = add_query_arg(
950 array(
951 'home_url' => rawurlencode( home_url() ),
952 'redirect_uri' => rawurlencode( $redirect_uri ),
953 'secret' => rawurlencode( $secret ),
954 'redirect_admin_url' => isset( $_GET['redirect_admin_url'] )
955 ? rawurlencode(
956 esc_url_raw(
957 urldecode(
958 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
959 wp_unslash( $_GET['redirect_admin_url'] )
960 )
961 )
962 )
963 : '',
964 'wum-installed' => WC_Woo_Update_Manager_Plugin::is_plugin_installed() ? '1' : '0',
965 ),
966 WC_Helper_API::url( 'oauth/authorize' )
967 );
968
969 wp_redirect( esc_url_raw( $connect_url ) );
970 die();
971 }
972
973 /**
974 * Return from WooCommerce.com OAuth flow.
975 *
976 * @return never
977 */
978 private static function _helper_auth_return() {
979 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
980 self::log( 'Could not verify nonce in _helper_auth_return' );
981 wp_die( 'Something went wrong' );
982 }
983
984 // Bail if the user clicked deny.
985 if ( ! empty( $_GET['deny'] ) ) {
986 /**
987 * Fires when the Helper connection process is denied/cancelled.
988 */
989 do_action( 'woocommerce_helper_denied' );
990
991 wp_safe_redirect(
992 self::get_helper_redirect_url(
993 array(
994 'page' => self::get_source_page(),
995 'section' => 'helper',
996 )
997 )
998 );
999 die();
1000 }
1001
1002 // We do need a request token...
1003 if ( empty( $_GET['request_token'] ) ) {
1004 self::log( 'Request token not found in _helper_auth_return' );
1005 wp_die( 'Something went wrong' );
1006 }
1007
1008 // Obtain an access token.
1009 $request = WC_Helper_API::post(
1010 'oauth/access_token',
1011 array(
1012 'timeout' => 30,
1013 'body' => array(
1014 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1015 'home_url' => home_url(),
1016 ),
1017 )
1018 );
1019
1020 $code = wp_remote_retrieve_response_code( $request );
1021
1022 if ( 200 !== $code ) {
1023 self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) );
1024 wp_die( 'Something went wrong' );
1025 }
1026
1027 $access_token = json_decode( wp_remote_retrieve_body( $request ), true );
1028 if ( ! $access_token ) {
1029 self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) );
1030 wp_die( 'Something went wrong' );
1031 }
1032
1033 self::update_auth_option( $access_token['access_token'], $access_token['access_token_secret'], $access_token['site_id'], home_url() );
1034
1035 /**
1036 * Fires when the Helper connection process has completed successfully.
1037 */
1038 do_action( 'woocommerce_helper_connected' );
1039
1040 // Enable tracking when connected.
1041 if ( class_exists( 'WC_Tracker' ) ) {
1042 $prev_value = get_option( 'woocommerce_allow_tracking', 'no' );
1043 update_option( 'woocommerce_allow_tracking', 'yes' );
1044 WC_Tracker::send_tracking_data( true );
1045
1046 // Track woocommerce_allow_tracking_toggled in case was set as 'no' before.
1047 if ( class_exists( 'WC_Tracks' ) && 'no' === $prev_value ) {
1048 WC_Tracks::track_woocommerce_allow_tracking_toggled( $prev_value, 'yes', 'wccom_connect' );
1049 }
1050 }
1051
1052 // If connecting through in-app purchase, redirects back to WooCommerce.com
1053 // for product installation.
1054 if ( ! empty( $_GET['wccom-install-url'] ) ) {
1055 wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) );
1056 exit;
1057 }
1058
1059 wp_safe_redirect(
1060 self::get_helper_redirect_url(
1061 array(
1062 'page' => self::get_source_page(),
1063 'section' => 'helper',
1064 'wc-helper-status' => 'helper-connected',
1065 )
1066 )
1067 );
1068 die();
1069 }
1070
1071 /**
1072 * Disconnect from WooCommerce.com, clear OAuth tokens.
1073 *
1074 * @return never
1075 */
1076 private static function _helper_auth_disconnect() {
1077 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1078 self::log( 'Could not verify nonce in _helper_auth_disconnect' );
1079 wp_die( 'Could not verify nonce' );
1080 }
1081
1082 /**
1083 * Fires when the Helper has been disconnected.
1084 */
1085 do_action( 'woocommerce_helper_disconnected' );
1086
1087 $redirect_uri = self::get_helper_redirect_url(
1088 array(
1089 'page' => self::get_source_page(),
1090 'section' => 'helper',
1091 'wc-helper-status' => 'helper-disconnected',
1092 )
1093 );
1094
1095 self::disconnect();
1096
1097 wp_safe_redirect( $redirect_uri );
1098 die();
1099 }
1100
1101 /**
1102 * User hit the Refresh button, clear all caches.
1103 *
1104 * @return never
1105 */
1106 private static function _helper_auth_refresh() {
1107 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1108 self::log( 'Could not verify nonce in _helper_auth_refresh' );
1109 wp_die( 'Could not verify nonce' );
1110 }
1111
1112 self::refresh_helper_subscriptions();
1113
1114 $redirect_uri = self::get_helper_redirect_url(
1115 array(
1116 'page' => self::get_source_page(),
1117 'section' => 'helper',
1118 'filter' => self::get_current_filter(),
1119 'wc-helper-status' => 'helper-refreshed',
1120 )
1121 );
1122
1123 wp_safe_redirect( $redirect_uri );
1124 die();
1125 }
1126
1127 /**
1128 * Flush helper authentication cache.
1129 *
1130 * @throws Exception If there is an error refreshing subscriptions.
1131 *
1132 * @return void
1133 */
1134 public static function refresh_helper_subscriptions() {
1135 /**
1136 * Fires when Helper subscriptions are refreshed.
1137 *
1138 * @since 8.3.0
1139 */
1140 do_action( 'woocommerce_helper_subscriptions_refresh' );
1141 self::_flush_authentication_cache();
1142 self::_flush_subscriptions_cache();
1143 self::_flush_updates_cache();
1144 self::flush_product_usage_notice_rules_cache();
1145 }
1146
1147 /**
1148 * Active a product subscription.
1149 *
1150 * @return never
1151 */
1152 private static function _helper_subscription_activate() {
1153 $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
1154 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
1155
1156 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1157 self::log( 'Could not verify nonce in _helper_subscription_activate' );
1158 wp_die( 'Could not verify nonce' );
1159 }
1160
1161 try {
1162 $activated = self::activate_helper_subscription( $product_key, $product_id );
1163 } catch ( Exception $e ) {
1164 $activated = false;
1165 }
1166
1167 $redirect_uri = add_query_arg(
1168 array(
1169 'page' => self::get_source_page(),
1170 'section' => 'helper',
1171 'filter' => self::get_current_filter(),
1172 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error',
1173 'wc-helper-product-id' => $product_id,
1174 ),
1175 admin_url( 'admin.php' )
1176 );
1177
1178 wp_safe_redirect( $redirect_uri );
1179 die();
1180 }
1181
1182 /**
1183 * Activate helper subscription.
1184 *
1185 * @throws Exception If the subscription could not be activated or found.
1186 * @throws WC_Data_Exception If the activation fails with error details.
1187 * @param string $product_key Subscription product key.
1188 * @return bool True if activated, false otherwise.
1189 */
1190 public static function activate_helper_subscription( $product_key ) {
1191 $subscription = self::get_subscription( $product_key );
1192 if ( ! $subscription ) {
1193 throw new Exception( __( 'Subscription not found', 'woocommerce' ) );
1194 }
1195 $product_id = $subscription['product_id'];
1196
1197 // Activate subscription.
1198 list( $activation_response, $activated, $body ) = self::wccom_activate( $product_key );
1199
1200 if ( $activated ) {
1201 /**
1202 * Fires when the Helper activates a product successfully.
1203 *
1204 * @param int $product_id Product ID being activated.
1205 * @param string $product_key Subscription product key.
1206 * @param array $activation_response The response object from wp_safe_remote_request().
1207 */
1208 do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response );
1209 } else {
1210 /**
1211 * Fires when the Helper fails to activate a product.
1212 *
1213 * @param int $product_id Product ID being activated.
1214 * @param string $product_key Subscription product key.
1215 * @param array $activation_response The response object from wp_safe_remote_request().
1216 */
1217 do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
1218
1219 // Include HTTP status code and any extra data from the API response in the exception so callers can surface it.
1220 $status_code = function_exists( 'wp_remote_retrieve_response_code' ) ? (int) wp_remote_retrieve_response_code( $activation_response ) : (int) ( $body['data']['status'] ?? 400 );
1221 $error_data = isset( $body['data'] ) && is_array( $body['data'] ) ? $body['data'] : array();
1222 throw new WC_Data_Exception(
1223 esc_html( $body['code'] ?? 'unknown_error' ),
1224 isset( $body['message'] ) ? esc_html( $body['message'] ) : esc_html__( 'Unknown error', 'woocommerce' ),
1225 (int) $status_code,
1226 function_exists( 'map_deep' ) ? map_deep( $error_data, 'esc_html' ) : array_map( 'esc_html', $error_data ),
1227 );
1228 }
1229
1230 self::_flush_subscriptions_cache();
1231 self::_flush_updates_cache();
1232 self::flush_product_usage_notice_rules_cache();
1233
1234 return $activated;
1235 }
1236
1237 /**
1238 * Activate a plugin for a product key.
1239 *
1240 * @throws Exception When the subscription is not found.
1241 * @param string $product_key Subscription product key.
1242 * @return bool True if activated, false otherwise.
1243 */
1244 public static function activate_plugin( $product_key ) {
1245 $subscription = self::get_subscription( $product_key );
1246 if ( ! $subscription ) {
1247 throw new Exception( esc_html( __( 'Subscription not found', 'woocommerce' ) ) );
1248 }
1249 $product_id = $subscription['product_id'];
1250 $local = self::_get_local_from_product_id( $product_id );
1251
1252 if ( is_plugin_active( $local['_filename'] ) ) {
1253 return true;
1254 }
1255
1256 $response = false;
1257 if ( $local && 'plugin' === $local['_type'] && current_user_can( 'activate_plugins' ) ) {
1258 $response = activate_plugin( $local['_filename'] );
1259 if ( is_wp_error( $response ) ) {
1260 self::log( sprintf( 'Error activating plugin (%s)', $response->get_error_message() ) );
1261 }
1262 $response = null === $response;
1263 }
1264
1265 return $response;
1266 }
1267
1268 /**
1269 * Deactivate a product subscription.
1270 *
1271 * @return never
1272 */
1273 private static function helper_subscription_deactivate() {
1274 $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
1275 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
1276
1277 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1278 self::log( 'Could not verify nonce in helper_subscription_deactivate' );
1279 wp_die( 'Could not verify nonce' );
1280 }
1281
1282 try {
1283 $deactivated = self::deactivate_helper_subscription( $product_key );
1284 } catch ( Exception $e ) {
1285 $deactivated = false;
1286 }
1287
1288 $redirect_uri = add_query_arg(
1289 array(
1290 'page' => self::get_source_page(),
1291 'section' => 'helper',
1292 'filter' => self::get_current_filter(),
1293 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error',
1294 'wc-helper-product-id' => $product_id,
1295 ),
1296 admin_url( 'admin.php' )
1297 );
1298
1299 wp_safe_redirect( $redirect_uri );
1300 die();
1301 }
1302
1303 /**
1304 * Deactivate a product subscription.
1305 *
1306 * @throws Exception If the subscription could not be deactivated or found.
1307 * @param string $product_key Subscription product key.
1308 * @return bool True if deactivated, false otherwise.
1309 */
1310 public static function deactivate_helper_subscription( $product_key ) {
1311 $subscription = self::get_subscription( $product_key );
1312 if ( ! $subscription ) {
1313 throw new Exception( __( 'Subscription not found', 'woocommerce' ) );
1314 }
1315 $product_id = $subscription['product_id'];
1316
1317 $deactivation_response = WC_Helper_API::post(
1318 'deactivate',
1319 array(
1320 'authenticated' => true,
1321 'body' => wp_json_encode(
1322 array(
1323 'product_key' => $product_key,
1324 )
1325 ),
1326 )
1327 );
1328
1329 $code = wp_remote_retrieve_response_code( $deactivation_response );
1330 $deactivated = 200 === $code;
1331
1332 if ( $deactivated ) {
1333 /**
1334 * Fires when the Helper activates a product successfully.
1335 *
1336 * @param int $product_id Product ID being deactivated.
1337 * @param string $product_key Subscription product key.
1338 * @param array $deactivation_response The response object from wp_safe_remote_request().
1339 */
1340 do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response );
1341 } else {
1342 self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) );
1343
1344 /**
1345 * Fires when the Helper fails to activate a product.
1346 *
1347 * @param int $product_id Product ID being deactivated.
1348 * @param string $product_key Subscription product key.
1349 * @param array $deactivation_response The response object from wp_safe_remote_request().
1350 */
1351 do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response );
1352
1353 $body = json_decode( wp_remote_retrieve_body( $deactivation_response ), true );
1354 throw new Exception( $body['message'] ?? __( 'Unknown error', 'woocommerce' ) );
1355 }
1356
1357 self::_flush_subscriptions_cache();
1358 self::_flush_updates_cache();
1359 self::flush_product_usage_notice_rules_cache();
1360
1361 return $deactivated;
1362 }
1363
1364 /**
1365 * Get a subscriptions install URL.
1366 *
1367 * @param string $product_key Subscription product key.
1368 * @param string $product_slug Subscription product slug.
1369 * @return string
1370 */
1371 public static function get_subscription_install_url( $product_key, $product_slug ) {
1372 $install_url = add_query_arg(
1373 array(
1374 'product-key' => rawurlencode( $product_key ),
1375 ),
1376 self::get_install_base_url() . "{$product_slug}/"
1377 );
1378
1379 return WC_Helper_API::add_auth_parameters( $install_url );
1380 }
1381
1382 /**
1383 * Deactivate a plugin.
1384 *
1385 * @return never
1386 */
1387 private static function _helper_plugin_deactivate() {
1388 $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
1389 $deactivated = false;
1390
1391 if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1392 self::log( 'Could not verify nonce in _helper_plugin_deactivate' );
1393 wp_die( 'Could not verify nonce' );
1394 }
1395
1396 if ( ! current_user_can( 'activate_plugins' ) ) {
1397 wp_die( 'You are not allowed to manage plugins on this site.' );
1398 }
1399
1400 $local = wp_list_filter(
1401 array_merge(
1402 self::get_local_woo_plugins(),
1403 self::get_local_woo_themes()
1404 ),
1405 array( '_product_id' => $product_id )
1406 );
1407
1408 // Attempt to deactivate this plugin or theme.
1409 if ( ! empty( $local ) ) {
1410 $local = array_shift( $local );
1411 if ( is_plugin_active( $local['_filename'] ) ) {
1412 deactivate_plugins( $local['_filename'] );
1413 }
1414
1415 $deactivated = ! is_plugin_active( $local['_filename'] );
1416 }
1417
1418 $redirect_uri = add_query_arg(
1419 array(
1420 'page' => self::get_source_page(),
1421 'section' => 'helper',
1422 'filter' => self::get_current_filter(),
1423 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error',
1424 'wc-helper-product-id' => $product_id,
1425 ),
1426 admin_url( 'admin.php' )
1427 );
1428
1429 wp_safe_redirect( $redirect_uri );
1430 die();
1431 }
1432
1433 /**
1434 * Get a local plugin/theme entry from product_id.
1435 *
1436 * @param int $product_id The product id.
1437 *
1438 * @return array|bool The array containing the local plugin/theme data or false.
1439 */
1440 private static function _get_local_from_product_id( $product_id ) {
1441 $local = wp_list_filter(
1442 array_merge(
1443 self::get_local_woo_plugins(),
1444 self::get_local_woo_themes()
1445 ),
1446 array( '_product_id' => $product_id )
1447 );
1448
1449 if ( ! empty( $local ) ) {
1450 return array_shift( $local );
1451 }
1452
1453 return false;
1454 }
1455
1456 /**
1457 * Checks whether current site has product subscription of a given ID.
1458 *
1459 * @since 3.7.0
1460 *
1461 * @param int $product_id The product id.
1462 *
1463 * @return bool Returns true if product subscription exists, false otherwise.
1464 */
1465 public static function has_product_subscription( $product_id ) {
1466 $subscription = self::_get_subscriptions_from_product_id( $product_id, true );
1467 return ! empty( $subscription );
1468 }
1469
1470 /**
1471 * Get the user's connected subscriptions that are installed on the current
1472 * site.
1473 *
1474 * @return array
1475 */
1476 public static function get_installed_subscriptions() {
1477 static $installed_subscriptions = null;
1478
1479 // Cache installed_subscriptions in the current request.
1480 if ( is_null( $installed_subscriptions ) ) {
1481 $auth = WC_Helper_Options::get( 'auth' );
1482 $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
1483 if ( 0 === $site_id ) {
1484 $installed_subscriptions = array();
1485 return $installed_subscriptions;
1486 }
1487
1488 $installed_subscriptions = array_filter(
1489 self::get_subscriptions(),
1490 function ( $subscription ) use ( $site_id ) {
1491 return in_array( $site_id, $subscription['connections'], true );
1492 }
1493 );
1494 }
1495
1496 return $installed_subscriptions;
1497 }
1498
1499 /**
1500 * Get the user's unconnected subscriptions.
1501 *
1502 * @return array
1503 */
1504 public static function get_unconnected_subscriptions() {
1505 static $unconnected_subscriptions = null;
1506
1507 // Cache unconnected_subscriptions in the current request.
1508 if ( is_null( $unconnected_subscriptions ) ) {
1509 $auth = WC_Helper_Options::get( 'auth' );
1510 $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
1511 if ( 0 === $site_id ) {
1512 $unconnected_subscriptions = array();
1513 return $unconnected_subscriptions;
1514 }
1515
1516 $unconnected_subscriptions = array_filter(
1517 self::get_subscriptions(),
1518 function ( $subscription ) use ( $site_id ) {
1519 return empty( $subscription['connections'] );
1520 }
1521 );
1522 }
1523
1524 return $unconnected_subscriptions;
1525 }
1526
1527 /**
1528 * Get subscription state of a given product ID.
1529 *
1530 * @since TBD
1531 *
1532 * @param int $product_id The product id.
1533 *
1534 * @return array Array of state_name => (bool) state
1535 */
1536 public static function get_product_subscription_state( $product_id ) {
1537 $product_subscriptions = wp_list_filter( self::get_installed_subscriptions(), array( 'product_id' => $product_id ) );
1538
1539 $subscription = ! empty( $product_subscriptions )
1540 ? array_shift( $product_subscriptions )
1541 : array();
1542
1543 return array(
1544 'unregistered' => empty( $subscription ),
1545 'expired' => ( isset( $subscription['expired'] ) && $subscription['expired'] ),
1546 'expiring' => ( isset( $subscription['expiring'] ) && $subscription['expiring'] ),
1547 'key' => $subscription['product_key'] ?? '',
1548 'order_id' => $subscription['order_id'] ?? '',
1549 );
1550 }
1551
1552 /**
1553 * Get a subscription entry from product_id. If multiple subscriptions are
1554 * found with the same product id and $single is set to true, will return the
1555 * first one in the list, so you can use this method to get things like extension
1556 * name, version, etc.
1557 *
1558 * @param int $product_id The product id.
1559 * @param bool $single Whether to return a single subscription or all matching a product id.
1560 *
1561 * @return array|bool The array containing sub data or false.
1562 */
1563 private static function _get_subscriptions_from_product_id( $product_id, $single = true ) {
1564 $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) );
1565
1566 if ( ! empty( $subscriptions ) ) {
1567 return $single ? array_shift( $subscriptions ) : $subscriptions;
1568 }
1569
1570 return false;
1571 }
1572
1573 /**
1574 * Get locally installed plugins
1575 *
1576 * @return array
1577 */
1578 public static function get_local_plugins() {
1579 if ( ! function_exists( 'get_plugins' ) ) {
1580 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1581 }
1582 $plugins = get_plugins();
1583
1584 $output_plugins = array();
1585 foreach ( $plugins as $filename => $data ) {
1586 array_push(
1587 $output_plugins,
1588 array(
1589 '_filename' => $filename,
1590 '_type' => 'plugin',
1591 'slug' => dirname( $filename ),
1592 'Version' => $data['Version'],
1593 )
1594 );
1595 }
1596
1597 return $output_plugins;
1598 }
1599
1600 /**
1601 * Get locally installed themes.
1602 *
1603 * @return array
1604 */
1605 public static function get_local_themes() {
1606 if ( ! function_exists( 'wp_get_themes' ) ) {
1607 require_once ABSPATH . 'wp-admin/includes/theme.php';
1608 }
1609 $themes = wp_get_themes();
1610
1611 $output_themes = array();
1612 foreach ( $themes as $theme ) {
1613 array_push(
1614 $output_themes,
1615 array(
1616 '_filename' => $theme->get_stylesheet() . '/style.css',
1617 '_stylesheet' => $theme->get_stylesheet(),
1618 '_type' => 'theme',
1619 'slug' => $theme->get_stylesheet(),
1620 'Version' => $theme->get( 'Version' ),
1621 )
1622 );
1623 }
1624 return $output_themes;
1625 }
1626
1627 /**
1628 * Obtain a list of data about locally installed Woo extensions.
1629 *
1630 * @return array
1631 */
1632 public static function get_local_woo_plugins() {
1633 if ( ! function_exists( 'get_plugins' ) ) {
1634 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1635 }
1636
1637 $plugins = get_plugins();
1638
1639 /**
1640 * Check if plugins have WC headers, if not then clear cache and fetch again.
1641 * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated.
1642 * Also, get_plugins call is expensive, so we should clear this cache very conservatively.
1643 */
1644 if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) {
1645 wp_clean_plugins_cache( false );
1646 $plugins = get_plugins();
1647 }
1648
1649 $woo_plugins = array();
1650
1651 // Backwards compatibility for woothemes_queue_update().
1652 $_compat = array();
1653 if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) {
1654 foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) {
1655 $_compat[ $_compat_plugin->file ] = array(
1656 'product_id' => $_compat_plugin->product_id,
1657 'file_id' => $_compat_plugin->file_id,
1658 );
1659 }
1660 }
1661
1662 foreach ( $plugins as $filename => $data ) {
1663 if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) {
1664 $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] );
1665 }
1666
1667 if ( empty( $data['Woo'] ) ) {
1668 continue;
1669 }
1670
1671 list( $product_id, $file_id ) = explode( ':', $data['Woo'] );
1672 if ( empty( $product_id ) || empty( $file_id ) ) {
1673 continue;
1674 }
1675
1676 // Omit the WooCommerce plugin used on Woo Express sites.
1677 if ( 'WooCommerce' === $data['Name'] ) {
1678 continue;
1679 }
1680
1681 $data['_filename'] = $filename;
1682 $data['_product_id'] = absint( $product_id );
1683 $data['_file_id'] = $file_id;
1684 $data['_type'] = 'plugin';
1685 $data['slug'] = dirname( $filename );
1686 $woo_plugins[ $filename ] = $data;
1687 }
1688
1689 return $woo_plugins;
1690 }
1691
1692 /**
1693 * Get locally installed Woo themes.
1694 *
1695 * @return array
1696 */
1697 public static function get_local_woo_themes() {
1698 $themes = wp_get_themes();
1699 $woo_themes = array();
1700
1701 foreach ( $themes as $theme ) {
1702 $header = $theme->get( 'Woo' );
1703
1704 // Backwards compatibility for theme_info.txt.
1705 if ( ! $header ) {
1706 $txt = $theme->get_stylesheet_directory() . '/theme_info.txt';
1707 if ( is_readable( $txt ) ) {
1708 $txt = file_get_contents( $txt );
1709 $txt = preg_split( '#\s#', $txt );
1710 if ( is_array( $txt ) && count( $txt ) >= 2 ) {
1711 $header = sprintf( '%d:%s', $txt[0], $txt[1] );
1712 }
1713 }
1714 }
1715
1716 if ( empty( $header ) ) {
1717 continue;
1718 }
1719
1720 list( $product_id, $file_id ) = explode( ':', $header );
1721 if ( empty( $product_id ) || empty( $file_id ) ) {
1722 continue;
1723 }
1724
1725 $data = array(
1726 'Name' => $theme->get( 'Name' ),
1727 'Version' => $theme->get( 'Version' ),
1728 'Woo' => $header,
1729
1730 '_filename' => $theme->get_stylesheet() . '/style.css',
1731 '_stylesheet' => $theme->get_stylesheet(),
1732 '_product_id' => absint( $product_id ),
1733 '_file_id' => $file_id,
1734 '_type' => 'theme',
1735 'slug' => dirname( $theme->get_stylesheet() ),
1736 );
1737
1738 $woo_themes[ $data['_filename'] ] = $data;
1739 }
1740
1741 return $woo_themes;
1742 }
1743
1744 /**
1745 * Get rules for displaying notice regarding marketplace product usage.
1746 *
1747 * @return array
1748 *
1749 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- As we wrap the throw in a try/catch.
1750 */
1751 public static function get_product_usage_notice_rules() {
1752 $cache_key = '_woocommerce_helper_product_usage_notice_rules';
1753 $data = get_transient( $cache_key );
1754 if ( false !== $data ) {
1755 if ( is_array( $data ) ) {
1756 return $data;
1757 }
1758 // Cached data is corrupted, delete and fetch fresh.
1759 delete_transient( $cache_key );
1760 }
1761
1762 try {
1763 $request = WC_Helper_API::get(
1764 'product-usage-notice-rules',
1765 array(
1766 'authenticated' => false,
1767 'timeout' => 2,
1768 )
1769 );
1770
1771 if ( is_wp_error( $request ) ) {
1772 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1773
1774 throw new Exception( $request->get_error_message(), (int) $request->get_error_data() );
1775 }
1776
1777 $code = wp_remote_retrieve_response_code( $request );
1778 if ( 200 !== $code ) {
1779 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1780
1781 throw new Exception( self::get_message_for_response_code( $code ), $code );
1782 }
1783
1784 $data = json_decode( wp_remote_retrieve_body( $request ), true );
1785 if ( ! is_array( $data ) ) {
1786 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1787
1788 throw new Exception( __( 'WooCommerce.com API returned an invalid response.', 'woocommerce' ), 422 );
1789 }
1790
1791 set_transient( $cache_key, $data, DAY_IN_SECONDS );
1792
1793 // Remove notice after successful API call as it's no longer applicable.
1794 self::remove_api_error_notice();
1795 return $data;
1796 } catch ( Exception $e ) {
1797 if ( $e->getCode() < 404 ) {
1798 self::remove_api_error_notice();
1799 } else {
1800 // Only show error notice in case there is no proper communication with WCCOM.
1801 self::log( 'Error getting product usage notice rules: ' . $e->getMessage(), 'error' );
1802 self::add_api_error_notice();
1803 }
1804 }
1805
1806 return array();
1807 }
1808
1809 /**
1810 * Verify request hash created by WooCommerce.com.
1811 *
1812 * @param string $request_hash request hash to be verified.
1813 * @return bool
1814 */
1815 public static function verify_request_hash( string $request_hash ): bool {
1816 $request = WC_Helper_API::get(
1817 'verify-request-hash',
1818 array(
1819 'authenticated' => true,
1820 'query_string' => '?request_hash=' . $request_hash,
1821 )
1822 );
1823
1824 if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
1825 return false;
1826 }
1827
1828 $data = json_decode( wp_remote_retrieve_body( $request ), true );
1829
1830 return isset( $data['success'] ) && true === $data['success'];
1831 }
1832
1833 /**
1834 * Get cached connection data
1835 *
1836 * @return array|bool cached connection data or false connection data is not cached.
1837 */
1838 public static function get_cached_connection_data() {
1839 $data = get_transient( self::CACHE_KEY_CONNECTION_DATA );
1840 if ( false !== $data && ! is_array( $data ) ) {
1841 // Cached data is corrupted, delete and return false to trigger fresh fetch.
1842 delete_transient( self::CACHE_KEY_CONNECTION_DATA );
1843 return false;
1844 }
1845 return $data;
1846 }
1847
1848 /**
1849 * Get details of the current WooCommerce.com connection.
1850 *
1851 * @return array|WP_Error
1852 */
1853 public static function fetch_helper_connection_info() {
1854 $data = self::get_cached_connection_data();
1855 if ( false !== $data ) {
1856 if ( ! empty( $data['maybe_deleted_connection'] ) ) {
1857 return new WP_Error( 'deleted_connection', 'Connection may have been deleted' );
1858 }
1859 return $data;
1860 }
1861
1862 $request = WC_Helper_API::get(
1863 'connection-info',
1864 array(
1865 'authenticated' => true,
1866 'query_string' => '?url=' . rawurlencode( home_url() ),
1867 )
1868 );
1869
1870 $status = wp_remote_retrieve_response_code( $request );
1871 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1872 $connection_data = is_array( $body ) ? $body : array();
1873 $message = $connection_data['message'] ?? '';
1874
1875 if ( 200 !== $status ) {
1876 if ( 'Connected site not found.' === $message || 'Invalid access token' === $message ) {
1877 set_transient( self::CACHE_KEY_CONNECTION_DATA, array( 'maybe_deleted_connection' => true ), 1 * HOUR_IN_SECONDS );
1878 }
1879 return new WP_Error(
1880 'invalid_response',
1881 'Invalid response from WooCommerce.com',
1882 array( 'status' => $status )
1883 );
1884 }
1885
1886 $url = $connection_data['url'] ?? '';
1887
1888 if ( ! empty( $url ) ) {
1889 $auth = WC_Helper_Options::get( 'auth' );
1890 $auth['url'] = $url;
1891 WC_Helper_Options::update( 'auth', $auth );
1892 set_transient( self::CACHE_KEY_CONNECTION_DATA, $connection_data, 1 * HOUR_IN_SECONDS );
1893 }
1894
1895 return $connection_data;
1896 }
1897
1898 /**
1899 * Filter malformed entries from subscription data.
1900 *
1901 * @param array $subscriptions Subscription entries.
1902 * @return array
1903 */
1904 private static function filter_valid_subscriptions( $subscriptions ) {
1905 return array_filter(
1906 $subscriptions,
1907 static function ( $subscription ) {
1908 if ( ! is_array( $subscription ) ) {
1909 return false;
1910 }
1911
1912 $product_id = $subscription['product_id'] ?? null;
1913 return (
1914 is_int( $product_id )
1915 || ( is_string( $product_id ) && ctype_digit( $product_id ) )
1916 ) && 0 < (int) $product_id
1917 && is_array( $subscription['connections'] ?? null );
1918 }
1919 );
1920 }
1921
1922 /**
1923 * Get the connected user's subscriptions.
1924 *
1925 * @return array
1926 *
1927 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- As we wrap the throw in a try/catch.
1928 */
1929 public static function get_subscriptions() {
1930 $cache_key = '_woocommerce_helper_subscriptions';
1931 $data = get_transient( $cache_key );
1932 if ( false !== $data ) {
1933 if ( is_array( $data ) ) {
1934 return self::filter_valid_subscriptions( $data );
1935 }
1936 // Cached data is corrupted, delete and fetch fresh.
1937 delete_transient( $cache_key );
1938 }
1939
1940 try {
1941 $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1942 $source = '';
1943 if ( false !== stripos( $request_uri, 'wc/v3/marketplace/refresh' ) ) :
1944 $source = 'refresh-button';
1945 elseif ( false !== stripos( $request_uri, 'my-subscriptions' ) ) :
1946 $source = 'my-subscriptions';
1947 elseif ( false !== stripos( $request_uri, 'plugins.php' ) ) :
1948 $source = 'plugins';
1949 elseif ( false !== stripos( $request_uri, 'wc-admin' ) ) :
1950 $source = 'inbox-notes';
1951 elseif ( false !== stripos( $request_uri, 'admin-ajax.php' ) ) :
1952 $source = 'heartbeat-api';
1953 elseif ( false !== stripos( $request_uri, 'installer' ) ) :
1954 $source = 'wccom-site-installer';
1955 elseif ( defined( 'WP_CLI' ) && WP_CLI ) :
1956 $source = 'wc-cli';
1957 endif;
1958
1959 // Obtain the connected user info.
1960 $request = WC_Helper_API::get(
1961 'subscriptions',
1962 array(
1963 'authenticated' => true,
1964 'query_string' => '' !== $source ? esc_url( '?source=' . $source ) : '',
1965 )
1966 );
1967
1968 if ( is_wp_error( $request ) ) {
1969 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1970
1971 throw new Exception( $request->get_error_message(), (int) $request->get_error_data() );
1972 }
1973
1974 $code = wp_remote_retrieve_response_code( $request );
1975 if ( 200 !== $code ) {
1976 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1977
1978 throw new Exception( self::get_message_for_response_code( $code ), $code );
1979 }
1980
1981 $data = json_decode( wp_remote_retrieve_body( $request ), true );
1982 if ( ! is_array( $data ) ) {
1983 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
1984
1985 throw new Exception( __( 'WooCommerce.com API returned an invalid response.', 'woocommerce' ), 422 );
1986 }
1987
1988 $subscription_count = count( $data );
1989 $data = self::filter_valid_subscriptions( $data );
1990 $invalid_count = $subscription_count - count( $data );
1991 if ( 0 < $invalid_count ) {
1992 self::log(
1993 sprintf(
1994 'Filtered %d malformed subscription entries from the WooCommerce.com API response.',
1995 $invalid_count
1996 ),
1997 'warning'
1998 );
1999 }
2000 set_transient( $cache_key, $data, 3 * HOUR_IN_SECONDS );
2001
2002 // Remove notice after successful API call as it's no longer applicable.
2003 self::remove_api_error_notice();
2004 return $data;
2005 } catch ( Exception $e ) {
2006 if ( $e->getCode() < 404 ) {
2007 self::remove_api_error_notice();
2008 } else {
2009 // Only show error notice in case there is no proper communication with WCCOM.
2010 self::log( 'Error getting subscriptions: ' . $e->getMessage(), 'error' );
2011 self::add_api_error_notice();
2012 }
2013 }
2014
2015 return array();
2016 }
2017
2018 /**
2019 * Get subscription data for a given product key.
2020 *
2021 * @param string $product_key Subscription product key.
2022 * @return array|bool The array containing sub data or false.
2023 */
2024 public static function get_subscription( $product_key ) {
2025 $subscriptions = wp_list_filter(
2026 self::get_subscriptions(),
2027 array( 'product_key' => $product_key )
2028 );
2029
2030 if ( empty( $subscriptions ) ) {
2031 return false;
2032 }
2033
2034 $subscription = array_shift( $subscriptions );
2035 $subscription['local'] = self::get_subscription_local_data( $subscription );
2036 return $subscription;
2037 }
2038
2039 /**
2040 * Get the connected user's subscription list data. Here, we merge connected
2041 * subscriptions with locally installed Woo plugins and themes. We also
2042 * add in information about available updates.
2043 *
2044 * Used by the My Subscriptions page.
2045 *
2046 * @return array
2047 */
2048 public static function get_subscription_list_data() {
2049 // First, connected subscriptions.
2050 $subscriptions = self::get_subscriptions();
2051
2052 // Then, installed plugins and themes, with or without an active subscription.
2053 $woo_plugins = self::get_local_woo_plugins();
2054 $woo_themes = self::get_local_woo_themes();
2055
2056 // Get the product IDs of the subscriptions.
2057 $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );
2058
2059 // Get the site ID.
2060 $auth = WC_Helper_Options::get( 'auth' );
2061 $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
2062
2063 // Now, merge installed products without a subscription.
2064 foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) {
2065 if ( in_array( $data['_product_id'], $subscriptions_product_ids, true ) ) {
2066 continue;
2067 }
2068
2069 // We add these as subscriptions to the previous connected subscriptions list.
2070 $subscriptions[] = array(
2071 'product_key' => '',
2072 'product_id' => $data['_product_id'],
2073 'product_name' => $data['Name'],
2074 'product_url' => $data['PluginURI'] ?? '',
2075 'zip_slug' => $data['slug'],
2076 'documentation_url' => '',
2077 'key_type' => '',
2078 'key_type_label' => '',
2079 'lifetime' => false,
2080 'product_status' => 'publish',
2081 // Connections is empty because this is not a connected subscription.
2082 'connections' => array(),
2083 'expires' => 0,
2084 'expired' => true,
2085 'expiring' => false,
2086 'sites_max' => 0,
2087 'sites_active' => 0,
2088 'autorenew' => false,
2089 'maxed' => false,
2090 );
2091 }
2092
2093 // Fetch updates so we can refine subscriptions with information about updates.
2094 $updates = WC_Helper_Updater::get_update_data();
2095
2096 // Add local data to merged subscriptions list (both locally installed and purchased).
2097 foreach ( $subscriptions as &$subscription ) {
2098 $subscription['active'] = in_array( $site_id, $subscription['connections'], true );
2099
2100 $subscription['local'] = self::get_subscription_local_data( $subscription );
2101
2102 $subscription['has_update'] = false;
2103 if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
2104 $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' );
2105 }
2106
2107 if ( ! empty( $updates[ $subscription['product_id'] ] ) ) {
2108 $subscription['version'] = $updates[ $subscription['product_id'] ]['version'];
2109 }
2110
2111 // If the update endpoint returns a URL, we prefer it over the default PluginURI.
2112 if ( ! empty( $updates[ $subscription['product_id'] ]['url'] ) ) {
2113 $subscription['product_url'] = $updates[ $subscription['product_id'] ]['url'];
2114 }
2115 }
2116
2117 // Sort subscriptions by name and expiration date.
2118 usort(
2119 $subscriptions,
2120 function ( $a, $b ) {
2121 $compare_value = strcasecmp( $a['product_name'], $b['product_name'] );
2122 if ( 0 === $compare_value ) {
2123 return strcasecmp( $a['expires'], $b['expires'] );
2124 }
2125 return $compare_value;
2126 }
2127 );
2128
2129 // Add subscription install flags after the active and local data is set.
2130 foreach ( $subscriptions as &$subscription ) {
2131 $subscription['subscription_available'] = self::is_subscription_available( $subscription, $subscriptions );
2132 $subscription['subscription_installed'] = self::is_subscription_installed( $subscription, $subscriptions );
2133 }
2134
2135 // Break the by-ref.
2136 unset( $subscription );
2137
2138 return $subscriptions;
2139 }
2140
2141 /**
2142 * Check if a subscription is available to use.
2143 * That is, is not already active and hasn't expired, and there are no other subscriptions
2144 * for this product already active on this site.
2145 *
2146 * @param array $subscription The subscription we're checking.
2147 * @param array $subscriptions The list of all the user's subscriptions.
2148 * @return bool True if multiple licenses exist, false otherwise.
2149 */
2150 public static function is_subscription_available( $subscription, $subscriptions ) {
2151 if ( true === $subscription['active'] ) {
2152 return false;
2153 }
2154
2155 if ( true === $subscription['expired'] ) {
2156 return false;
2157 }
2158
2159 $product_subscriptions = wp_list_filter(
2160 $subscriptions,
2161 array(
2162 'product_id' => $subscription['product_id'],
2163 'active' => true,
2164 )
2165 );
2166
2167 // If there are no subscriptions for this product already active on this site, then it's available.
2168 if ( empty( $product_subscriptions ) ) {
2169 return true;
2170 }
2171
2172 return false;
2173 }
2174
2175 /**
2176 * Check if product relating to a subscription is installed.
2177 * This method will return true if the product is installed, but will exclude subscriptions for the same product that are not in use.
2178 * If a product is installed and inactive, this will ensure that one subscription is marked as installed.
2179 *
2180 * @param array $subscription The subscription we're checking.
2181 * @param array $subscriptions The list of all the user's subscriptions.
2182 * @return bool True if installed, false otherwise.
2183 */
2184 public static function is_subscription_installed( $subscription, $subscriptions ) {
2185 if ( false === $subscription['local']['installed'] ) {
2186 return false;
2187 }
2188
2189 // If the subscription is active, then it's installed.
2190 if ( true === $subscription['active'] ) {
2191 return true;
2192 }
2193
2194 $product_subscriptions = wp_list_filter(
2195 $subscriptions,
2196 array(
2197 'product_id' => $subscription['product_id'],
2198 )
2199 );
2200 if ( empty( $product_subscriptions ) ) {
2201 return false;
2202 }
2203
2204 // If there are no other subscriptions for this product, then it's installed.
2205 if ( 1 === count( $product_subscriptions ) ) {
2206 return true;
2207 }
2208
2209 $active_subscription = wp_list_filter(
2210 $product_subscriptions,
2211 array(
2212 'active' => true,
2213 )
2214 );
2215
2216 // If there is another active subscription, this subscription is not installed.
2217 // If the current subscription is active, it would already return true above.
2218 if ( ! empty( $active_subscription ) ) {
2219 return false;
2220 }
2221
2222 // Find subscriptions that can be activated.
2223 $product_subscriptions_without_maxed_connections = wp_list_filter(
2224 $product_subscriptions,
2225 array(
2226 'maxed' => false,
2227 )
2228 );
2229
2230 if ( 0 < count( $product_subscriptions_without_maxed_connections ) ) {
2231 // Pick the first subscription available for activation.
2232 $product_subscription = array_shift( $product_subscriptions_without_maxed_connections );
2233 } else {
2234 // If there are multiple subscriptions, but no active subscriptions, then mark the first one as installed.
2235 $product_subscription = array_shift( $product_subscriptions );
2236 }
2237
2238 if ( $product_subscription['product_key'] === $subscription['product_key'] ) {
2239 return true;
2240 }
2241
2242 return false;
2243 }
2244
2245 /**
2246 * Add local data to a subscription.
2247 *
2248 * @param array $subscription The subscription data.
2249 * @return array The subscription data with local data added.
2250 */
2251 public static function get_subscription_local_data( array $subscription ) {
2252 $local_plugins = self::get_local_plugins();
2253 $local_themes = self::get_local_themes();
2254
2255 $installed_product = wp_list_filter(
2256 array_merge( $local_plugins, $local_themes ),
2257 array( 'slug' => $subscription['zip_slug'] )
2258 );
2259 $installed_product = array_shift( $installed_product );
2260
2261 if ( empty( $installed_product ) ) {
2262 return array(
2263 'installed' => false,
2264 'active' => false,
2265 'version' => null,
2266 'type' => null,
2267 'slug' => null,
2268 'path' => null,
2269 );
2270 }
2271
2272 $local_data = array(
2273 'installed' => true,
2274 'active' => false,
2275 'version' => $installed_product['Version'],
2276 'type' => $installed_product['_type'],
2277 'slug' => null,
2278 'path' => $installed_product['_filename'],
2279 );
2280
2281 if ( 'plugin' === $installed_product['_type'] ) {
2282 $local_data['slug'] = $installed_product['slug'];
2283 if ( is_plugin_active( $installed_product['_filename'] ) ) {
2284 $local_data['active'] = true;
2285 } elseif ( is_multisite() && is_plugin_active_for_network( $installed_product['_filename'] ) ) {
2286 $local_data['active'] = true;
2287 }
2288 } elseif ( 'theme' === $installed_product['_type'] ) {
2289 $local_data['slug'] = $installed_product['_stylesheet'];
2290 if ( in_array( $installed_product['_stylesheet'], array( get_stylesheet(), get_template() ), true ) ) {
2291 $local_data['active'] = true;
2292 }
2293 }
2294
2295 return $local_data;
2296 }
2297
2298 /**
2299 * Runs when any plugin is activated.
2300 *
2301 * Depending on the activated plugin attempts to look through available
2302 * subscriptions and auto-activate one if possible, so the user does not
2303 * need to visit the Helper UI at all after installing a new extension.
2304 *
2305 * @param string $filename The filename of the activated plugin.
2306 *
2307 * @return void
2308 */
2309 public static function activated_plugin( $filename ) {
2310 $plugins = self::get_local_woo_plugins();
2311
2312 // Not a local woo plugin.
2313 if ( empty( $plugins[ $filename ] ) ) {
2314 return;
2315 }
2316
2317 // Make sure we have a connection.
2318 $auth = WC_Helper_Options::get( 'auth' );
2319 if ( empty( $auth ) ) {
2320 return;
2321 }
2322
2323 $plugin = $plugins[ $filename ];
2324 $product_id = $plugin['_product_id'];
2325 $subscription = self::get_available_subscription( $product_id );
2326
2327 self::_flush_subscriptions_cache();
2328 self::_flush_updates_cache();
2329
2330 // No valid subscription found.
2331 if ( ! $subscription ) {
2332 return;
2333 }
2334
2335 $product_key = $subscription['product_key'];
2336 list( $activation_response, $activated, $body ) = self::wccom_activate( $product_key );
2337
2338 if ( $activated ) {
2339 self::log( 'Auto-activated a subscription for ' . $filename );
2340 /**
2341 * Fires when the Helper activates a product successfully.
2342 *
2343 * @param int $product_id Product ID being activated.
2344 * @param string $product_key Subscription product key.
2345 * @param array $activation_response The response object from wp_safe_remote_request().
2346 * @since 9.7
2347 */
2348 do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response );
2349 } else {
2350 self::log( 'Could not activate a subscription upon plugin activation: ' . $filename );
2351
2352 /**
2353 * Fires when the Helper fails to activate a product.
2354 *
2355 * @param int $product_id Product ID being activated.
2356 * @param string $product_key Subscription product key.
2357 * @param array $activation_response The response object from wp_safe_remote_request().
2358 * @since 9.7
2359 */
2360 do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
2361 }
2362 }
2363
2364 /**
2365 * Connect theme to the WCCOM.
2366 *
2367 * Depending on the activated theme attempts to look through available
2368 * subscriptions and auto-activate one if possible, so the user does not
2369 * need to visit the Helper UI at all after installing a new extension.
2370 *
2371 * @param string $product_id The product id of the activated theme.
2372 *
2373 * @return void
2374 */
2375 public static function connect_theme( $product_id ) {
2376 // Make sure we have a connection.
2377 $auth = WC_Helper_Options::get( 'auth' );
2378 if ( empty( $auth ) ) {
2379 return;
2380 }
2381
2382 wp_clean_themes_cache( false );
2383 $themes = self::get_local_woo_themes();
2384
2385 $themes = array_filter(
2386 $themes,
2387 function ( $theme ) use ( $product_id ) {
2388 return $theme['_product_id'] === $product_id;
2389 }
2390 );
2391
2392 if ( empty( $themes ) ) {
2393 return;
2394 }
2395
2396 $theme = reset( $themes );
2397
2398 $subscription = self::get_available_subscription( $product_id );
2399
2400 // No valid subscription found.
2401 if ( ! $subscription ) {
2402 return;
2403 }
2404
2405 $product_key = $subscription['product_key'];
2406 list( $activation_response, $activated, $body ) = self::wccom_activate( $product_key );
2407
2408 if ( $activated ) {
2409 self::log( 'Auto-activated a subscription for ' . $theme['Name'] );
2410 /**
2411 * Fires when the Helper activates a product successfully.
2412 *
2413 * @param int $product_id Product ID being activated.
2414 * @param string $product_key Subscription product key.
2415 * @param array $activation_response The response object from wp_safe_remote_request().
2416 */
2417 do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response );
2418 } else {
2419 self::log( 'Could not activate a subscription for theme: ' . $theme['Name'] );
2420
2421 /**
2422 * Fires when the Helper fails to activate a product.
2423 *
2424 * @param int $product_id Product ID being activated.
2425 * @param string $product_key Subscription product key.
2426 * @param array $activation_response The response object from wp_safe_remote_request().
2427 */
2428 do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
2429 }
2430
2431 self::_flush_subscriptions_cache();
2432 self::_flush_updates_cache();
2433 }
2434
2435 /**
2436 * Runs when any plugin is deactivated.
2437 *
2438 * When a user deactivates a plugin, attempt to deactivate any subscriptions
2439 * associated with the extension.
2440 *
2441 * @param string $filename The filename of the deactivated plugin.
2442 *
2443 * @return void
2444 */
2445 public static function deactivated_plugin( $filename ) {
2446 $plugins = self::get_local_woo_plugins();
2447
2448 // Not a local woo plugin.
2449 if ( empty( $plugins[ $filename ] ) ) {
2450 return;
2451 }
2452
2453 // Make sure we have a connection.
2454 $auth = WC_Helper_Options::get( 'auth' );
2455 if ( empty( $auth ) ) {
2456 return;
2457 }
2458
2459 $plugin = $plugins[ $filename ];
2460 $product_id = $plugin['_product_id'];
2461 $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false );
2462 $site_id = absint( $auth['site_id'] );
2463
2464 // No valid subscriptions for this product.
2465 if ( empty( $subscriptions ) ) {
2466 return;
2467 }
2468
2469 $deactivated = 0;
2470
2471 foreach ( $subscriptions as $subscription ) {
2472 // Don't touch subscriptions that aren't activated on this site.
2473 if ( ! in_array( $site_id, $subscription['connections'], true ) ) {
2474 continue;
2475 }
2476
2477 $product_key = $subscription['product_key'];
2478 $deactivation_response = WC_Helper_API::post(
2479 'deactivate',
2480 array(
2481 'authenticated' => true,
2482 'body' => wp_json_encode(
2483 array(
2484 'product_key' => $product_key,
2485 )
2486 ),
2487 )
2488 );
2489
2490 if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) {
2491 ++$deactivated;
2492
2493 /**
2494 * Fires when the Helper activates a product successfully.
2495 *
2496 * @param int $product_id Product ID being deactivated.
2497 * @param string $product_key Subscription product key.
2498 * @param array $deactivation_response The response object from wp_safe_remote_request().
2499 */
2500 do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response );
2501 } else {
2502 /**
2503 * Fires when the Helper fails to activate a product.
2504 *
2505 * @param int $product_id Product ID being deactivated.
2506 * @param string $product_key Subscription product key.
2507 * @param array $deactivation_response The response object from wp_safe_remote_request().
2508 */
2509 do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response );
2510 }
2511 }
2512
2513 self::_flush_subscriptions_cache();
2514 self::_flush_updates_cache();
2515
2516 if ( $deactivated ) {
2517 self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) );
2518 }
2519 }
2520
2521 /**
2522 * Various Helper-related admin notices.
2523 *
2524 * @return void
2525 */
2526 public static function admin_notices() {
2527 if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) {
2528 return;
2529 }
2530
2531 $screen = get_current_screen();
2532 $screen_id = $screen ? $screen->id : '';
2533
2534 if ( 'update-core' !== $screen_id ) {
2535 return;
2536 }
2537
2538 // Don't nag if Woo doesn't have an update available.
2539 if ( ! self::_woo_core_update_available() ) {
2540 return;
2541 }
2542
2543 // Add a note about available extension updates if Woo core has an update available.
2544 $notice = self::_get_extensions_update_notice();
2545 if ( ! empty( $notice ) ) {
2546 echo '<div class="updated woocommerce-message"><p>' . $notice . '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2547 }
2548 }
2549
2550 /**
2551 * Get an update notice if one or more Woo extensions has an update available.
2552 *
2553 * @return string|null The update notice or null if everything is up to date.
2554 */
2555 private static function _get_extensions_update_notice() {
2556 $plugins = self::get_local_woo_plugins();
2557 $updates = WC_Helper_Updater::get_update_data();
2558 $available = 0;
2559
2560 foreach ( $plugins as $data ) {
2561 if ( empty( $updates[ $data['_product_id'] ] ) ) {
2562 continue;
2563 }
2564
2565 $product_id = $data['_product_id'];
2566 if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) {
2567 ++$available;
2568 }
2569 }
2570
2571 if ( ! $available ) {
2572 return;
2573 }
2574
2575 return sprintf(
2576 /* translators: %1$s: helper url, %2$d: number of extensions */
2577 _n( 'Note: You currently have <a href="%1$s">%2$d paid extension</a> which should be updated first before updating WooCommerce.', 'Note: You currently have <a href="%1$s">%2$d paid extensions</a> which should be updated first before updating WooCommerce.', $available, 'woocommerce' ),
2578 esc_url( self::get_subscriptions_url() ),
2579 $available
2580 );
2581 }
2582
2583 /**
2584 * Whether WooCommerce has an update available.
2585 *
2586 * @return bool True if a Woo core update is available.
2587 */
2588 private static function _woo_core_update_available() {
2589 $updates = get_site_transient( 'update_plugins' );
2590 if ( empty( $updates->response ) ) {
2591 return false;
2592 }
2593
2594 if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) {
2595 return false;
2596 }
2597
2598 $data = $updates->response['woocommerce/woocommerce.php'];
2599 if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) {
2600 return false;
2601 }
2602
2603 return true;
2604 }
2605
2606 /**
2607 * Flush subscriptions cache.
2608 *
2609 * @return void
2610 */
2611 public static function _flush_subscriptions_cache() {
2612 delete_transient( '_woocommerce_helper_subscriptions' );
2613 }
2614
2615 /**
2616 * Flush product-usage-notice-rules cache.
2617 *
2618 * @return void
2619 */
2620 public static function flush_product_usage_notice_rules_cache() {
2621 delete_transient( '_woocommerce_helper_product_usage_notice_rules' );
2622 }
2623
2624 /**
2625 * Flush connection data cache.
2626 *
2627 * @return void
2628 */
2629 public static function flush_connection_data_cache() {
2630 delete_transient( self::CACHE_KEY_CONNECTION_DATA );
2631 }
2632
2633 /**
2634 * Flush auth cache.
2635 *
2636 * @return bool
2637 */
2638 public static function _flush_authentication_cache() {
2639 $request = WC_Helper_API::get(
2640 'oauth/me',
2641 array(
2642 'authenticated' => true,
2643 'timeout' => 30,
2644 )
2645 );
2646
2647 if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
2648 return false;
2649 }
2650
2651 $user_data = json_decode( wp_remote_retrieve_body( $request ), true );
2652 if ( ! $user_data ) {
2653 return false;
2654 }
2655
2656 WC_Helper_Options::update(
2657 'auth_user_data',
2658 array(
2659 'name' => $user_data['name'],
2660 'email' => $user_data['email'],
2661 )
2662 );
2663
2664 return true;
2665 }
2666
2667 /**
2668 * Flush updates cache.
2669 *
2670 * @return void
2671 */
2672 private static function _flush_updates_cache() {
2673 WC_Helper_Updater::flush_updates_cache();
2674 }
2675
2676 /**
2677 * Sort subscriptions by the product_name.
2678 *
2679 * @param array $a Subscription array.
2680 * @param array $b Subscription array.
2681 *
2682 * @return int
2683 */
2684 public static function _sort_by_product_name( $a, $b ) {
2685 return strcmp( $a['product_name'], $b['product_name'] );
2686 }
2687
2688 /**
2689 * Sort subscriptions by the Name.
2690 *
2691 * @param array $a Product array.
2692 * @param array $b Product array.
2693 *
2694 * @return int
2695 */
2696 public static function _sort_by_name( $a, $b ) {
2697 return strcmp( $a['Name'], $b['Name'] );
2698 }
2699
2700 /**
2701 * Log a helper event.
2702 *
2703 * @param string $message Log message.
2704 * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug.
2705 *
2706 * @return void
2707 */
2708 public static function log( $message, $level = 'info' ) {
2709 if ( ! Constants::is_true( 'WP_DEBUG' ) ) {
2710 return;
2711 }
2712
2713 if ( ! isset( self::$log ) ) {
2714 self::$log = wc_get_logger();
2715 }
2716
2717 self::$log->log( $level, $message, array( 'source' => 'helper' ) );
2718 }
2719
2720 /**
2721 * Handles WC Helper disconnect tasks.
2722 *
2723 * @return void
2724 */
2725 public static function disconnect() {
2726 WC_Helper_API::post(
2727 'oauth/invalidate_token',
2728 array(
2729 'authenticated' => true,
2730 )
2731 );
2732
2733 $data = WC_Helper_Options::get( 'auth_user_data' );
2734 WC_Helper_Options::update( 'last_disconnected_user_data', $data );
2735 // Ignore all previously dismissed disconnect notices.
2736 delete_metadata( 'user', 0, PluginsHelper::DISMISS_DISCONNECT_NOTICE, '', true );
2737
2738 WC_Helper_Options::update( 'auth', array() );
2739 WC_Helper_Options::update( 'auth_user_data', array() );
2740
2741 self::_flush_subscriptions_cache();
2742 self::_flush_updates_cache();
2743 self::flush_product_usage_notice_rules_cache();
2744 self::flush_connection_data_cache();
2745 }
2746
2747 /**
2748 * Checks if `access_token` exists in `auth` option.
2749 *
2750 * @return bool
2751 */
2752 public static function is_site_connected(): bool {
2753 $auth = WC_Helper_Options::get( 'auth' );
2754
2755 // If `access_token` is empty, there's no active connection.
2756 return ! empty( $auth['access_token'] );
2757 }
2758
2759 /**
2760 * Allows to connect with WCCOM using application password. used it to connect via CLI
2761 *
2762 * @param string $password The application password.
2763 *
2764 * @return void|WP_Error
2765 */
2766 public static function connect_with_password( string $password ) {
2767 $request = WC_Helper_API::post(
2768 'connect',
2769 array(
2770 'headers' => array(
2771 'X-API-Key' => $password,
2772 'Content-Type' => 'application/json',
2773 ),
2774 'body' => wp_json_encode( array( 'home_url' => home_url() ) ),
2775 'authenticated' => false,
2776 )
2777 );
2778
2779 $code = wp_remote_retrieve_response_code( $request );
2780
2781 if ( $code === 403 ) {
2782 $message = 'Invalid password';
2783 self::log( $message );
2784
2785 return new WP_Error( 'connect-with-password-invalid-password', $message );
2786 } elseif ( $code !== 200 ) {
2787 $message = sprintf( 'Call to /connect returned a non-200 response code (%d)', $code );
2788 self::log( $message );
2789
2790 return new WP_Error( 'connect-with-password-' . $code, $message );
2791 }
2792
2793 $access_data = json_decode( wp_remote_retrieve_body( $request ), true );
2794 if ( empty( $access_data['access_token'] ) || empty( $access_data['access_token_secret'] ) ) {
2795 $message = sprintf( 'Call to /connect returned an invalid body: %s', wp_remote_retrieve_body( $request ) );
2796 self::log( $message );
2797
2798 return new WP_Error( 'connect-with-password-invalid-response', $message );
2799 }
2800
2801 self::update_auth_option( $access_data['access_token'], $access_data['access_token_secret'], $access_data['site_id'], home_url() );
2802 }
2803
2804 /**
2805 * Updates auth options and flushes cache
2806 *
2807 * @param string $access_token The access token.
2808 * @param string $access_token_secret The secret access token.
2809 * @param int $site_id The site id returned by the API.
2810 * @param string $home_url Home url of the site.
2811 *
2812 * @return void
2813 */
2814 public static function update_auth_option( string $access_token, string $access_token_secret, int $site_id, string $home_url ): void {
2815 WC_Helper_Options::update(
2816 'auth',
2817 array(
2818 'access_token' => $access_token,
2819 'access_token_secret' => $access_token_secret,
2820 'site_id' => $site_id,
2821 'url' => $home_url,
2822 'user_id' => get_current_user_id(),
2823 'updated' => time(),
2824 )
2825 );
2826
2827 // Obtain the connected user info.
2828 if ( ! self::_flush_authentication_cache() ) {
2829 self::log( 'Could not obtain connected user info in _helper_auth_return.' );
2830 WC_Helper_Options::update( 'auth', array() );
2831 wp_die( 'Something went wrong. Could not obtain connected user info in _helper_auth_return.' );
2832 }
2833
2834 self::_flush_subscriptions_cache();
2835 self::_flush_updates_cache();
2836 self::flush_product_usage_notice_rules_cache();
2837 }
2838
2839 /**
2840 * Get WooCommerce.com base URL.
2841 *
2842 * @return string
2843 */
2844 public static function get_woocommerce_com_base_url() {
2845 /**
2846 * Filter the base URL used to install the Woo hosted plugins.
2847 *
2848 * @since 8.7.0
2849 */
2850 return trailingslashit( apply_filters( 'woo_com_base_url', 'https://woocommerce.com/' ) );
2851 }
2852
2853
2854 /**
2855 * Get base URL for plugin auto installer.
2856 *
2857 * @return string
2858 */
2859 public static function get_install_base_url() {
2860 return self::get_woocommerce_com_base_url() . 'auto-install-init/';
2861 }
2862
2863 /**
2864 * Retrieve notice for connected store.
2865 *
2866 * @return array An array containing notice data.
2867 */
2868 public static function get_notices() {
2869 $cache_key = '_woocommerce_helper_notices';
2870 $cached_data = get_transient( $cache_key );
2871
2872 if ( false !== $cached_data ) {
2873 if ( is_array( $cached_data ) ) {
2874 return $cached_data;
2875 }
2876 // Cached data is corrupted, delete and fetch fresh.
2877 delete_transient( $cache_key );
2878 }
2879
2880 // Fetch notice data for connected store.
2881 $request = WC_Helper_API::get(
2882 'notices',
2883 array(
2884 'authenticated' => true,
2885 )
2886 );
2887
2888 if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
2889 set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
2890 return array();
2891 }
2892
2893 $data = json_decode( wp_remote_retrieve_body( $request ), true );
2894
2895 if ( empty( $data ) || ! is_array( $data ) ) {
2896 $data = array();
2897 }
2898
2899 set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS );
2900 return $data;
2901 }
2902
2903 /**
2904 * Activate the product subscription to WCCOM
2905 *
2906 * @param string $product_key the product key to be activated.
2907 *
2908 * @return array
2909 */
2910 protected static function wccom_activate( $product_key ): array {
2911 $activation_response = WC_Helper_API::post(
2912 'activate',
2913 array(
2914 'authenticated' => true,
2915 'body' => wp_json_encode(
2916 array(
2917 'product_key' => $product_key,
2918 )
2919 ),
2920 )
2921 );
2922
2923 $activated = wp_remote_retrieve_response_code( $activation_response ) === 200;
2924 $body = json_decode( wp_remote_retrieve_body( $activation_response ), true );
2925
2926 if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) {
2927 $activated = true;
2928 }
2929
2930 return array( $activation_response, $activated, $body );
2931 }
2932
2933 /**
2934 * Get subscriptions for a product if it is available
2935 *
2936 * @param string|int $product_id the product id to get subscriptions for.
2937 *
2938 * @return mixed|null
2939 */
2940 protected static function get_available_subscription( $product_id ) {
2941 $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false );
2942
2943 // No valid subscriptions for this product.
2944 if ( empty( $subscriptions ) ) {
2945 return null;
2946 }
2947
2948 $subscription = null;
2949 foreach ( $subscriptions as $_sub ) {
2950
2951 // Don't attempt to activate expired subscriptions.
2952 if ( $_sub['expired'] ) {
2953 continue;
2954 }
2955
2956 // No more sites available in this subscription.
2957 if ( isset( $_sub['maxed'] ) && $_sub['maxed'] ) {
2958 continue;
2959 }
2960
2961 // Looks good.
2962 $subscription = $_sub;
2963 break;
2964 }
2965
2966 return $subscription;
2967 }
2968
2969 /**
2970 * Gets a user-friendly error message based on the HTTP response code.
2971 *
2972 * @param int $code The HTTP response code.
2973 * @return string The user-friendly error message.
2974 */
2975 protected static function get_message_for_response_code( int $code ): string {
2976 if ( 429 === $code ) {
2977 return __( 'You have exceeded the request limit. Please try again after a few minutes.', 'woocommerce' );
2978 } elseif ( 403 === $code ) {
2979 return __( 'Authentication failed. Please try again after a few minutes. If the issue persists, disconnect your store from WooCommerce.com and reconnect.', 'woocommerce' );
2980 }
2981
2982 // translators: %d: HTTP status code.
2983 return sprintf( __( 'WooCommerce.com API returned HTTP status code %d.', 'woocommerce' ), $code );
2984 }
2985 }
2986
2987 WC_Helper::load();
2988