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