PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-partner-coupon.php

class-partner-coupon.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.3, at jetpack_vendor/automattic/jetpack-connection/src/class-partner-coupon.php

467 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class for the Jetpack partner coupon logic.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack;
9
10 use Automattic\Jetpack\Connection\Client as Connection_Client;
11 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 use Jetpack_Options;
13
14 /**
15 * Disable direct access.
16 */
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit( 0 );
19 }
20
21 /**
22 * Class Jetpack_Partner_Coupon
23 *
24 * @since partner-1.6.0
25 * @since 2.0.0
26 */
27 class Partner_Coupon {
28
29 /**
30 * Name of the Jetpack_Option coupon option.
31 *
32 * @var string
33 */
34 public static $coupon_option = 'partner_coupon';
35
36 /**
37 * Name of the Jetpack_Option added option.
38 *
39 * @var string
40 */
41 public static $added_option = 'partner_coupon_added';
42
43 /**
44 * Name of "last availability check" transient.
45 *
46 * @var string
47 */
48 public static $last_check_transient = 'jetpack_partner_coupon_last_check';
49
50 /**
51 * Callable that executes a blog-authenticated request.
52 *
53 * @var callable
54 */
55 protected $request_as_blog;
56
57 /**
58 * Jetpack_Partner_Coupon
59 *
60 * @var Partner_Coupon|null
61 **/
62 private static $instance = null;
63
64 /**
65 * A list of supported partners.
66 *
67 * @var array
68 */
69 private static $supported_partners = array(
70 'IONOS' => array(
71 'name' => 'IONOS',
72 'logo' => array(
73 'src' => '/images/ionos-logo.jpg',
74 'width' => 119,
75 'height' => 32,
76 ),
77 ),
78 );
79
80 /**
81 * A list of supported presets.
82 *
83 * @var array
84 */
85 private static $supported_presets = array(
86 'IONA' => 'jetpack_backup_daily',
87 );
88
89 /**
90 * Get singleton instance of class.
91 *
92 * @return Partner_Coupon
93 */
94 public static function get_instance() {
95 if ( self::$instance === null ) {
96 self::$instance = new Partner_Coupon( array( Connection_Client::class, 'wpcom_json_api_request_as_blog' ) );
97 }
98
99 return self::$instance;
100 }
101
102 /**
103 * Constructor.
104 *
105 * @param callable $request_as_blog Callable that executes a blog-authenticated request.
106 */
107 public function __construct( $request_as_blog ) {
108 $this->request_as_blog = $request_as_blog;
109 }
110
111 /**
112 * Register hooks to catch and purge coupon.
113 *
114 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
115 * @param string $redirect_location The location we should redirect to after catching the coupon.
116 */
117 public static function register_coupon_admin_hooks( $plugin_slug, $redirect_location ) {
118 $instance = self::get_instance();
119
120 // We have to use an anonymous function, so we can pass along relevant information
121 // and not have to hardcode values for a single plugin.
122 // This open up the opportunity for e.g. the "all-in-one" and backup plugins
123 // to both implement partner coupon logic.
124 add_action(
125 'admin_init',
126 function () use ( $plugin_slug, $redirect_location, $instance ) {
127 $instance->catch_coupon( $plugin_slug, $redirect_location );
128 $instance->maybe_purge_coupon( $plugin_slug );
129 }
130 );
131 }
132
133 /**
134 * Catch partner coupon and redirect to claim component.
135 *
136 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
137 * @param string $redirect_location The location we should redirect to after catching the coupon.
138 */
139 public function catch_coupon( $plugin_slug, $redirect_location ) {
140 // Accept and store a partner coupon if present, and redirect to Jetpack connection screen.
141 $partner_coupon = isset( $_GET['jetpack-partner-coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack-partner-coupon'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
142 if ( $partner_coupon ) {
143 Jetpack_Options::update_options(
144 array(
145 self::$coupon_option => $partner_coupon,
146 self::$added_option => time(),
147 )
148 );
149
150 $connection = new Connection_Manager( $plugin_slug );
151 if ( $connection->is_connected() ) {
152 $redirect_location = add_query_arg( array( 'showCouponRedemption' => 1 ), $redirect_location );
153 wp_safe_redirect( $redirect_location );
154 } else {
155 wp_safe_redirect( $redirect_location );
156 }
157 }
158 }
159
160 /**
161 * Purge partner coupon.
162 *
163 * We try to remotely check if a coupon looks valid. We also automatically purge
164 * partner coupons after a certain amount of time to prevent unnecessary look-ups
165 * and/or promoting a product for months or years in the future due to unknown
166 * errors.
167 *
168 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
169 */
170 public function maybe_purge_coupon( $plugin_slug ) {
171 // Only run coupon checks on Jetpack admin pages.
172 // The "admin-ui" package is responsible for registering the Jetpack admin
173 // page for all Jetpack plugins and has hardcoded the settings page to be
174 // "jetpack", so we shouldn't need to allow for dynamic/custom values.
175 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
176 if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) {
177 return;
178 }
179
180 if ( ( new Status() )->is_offline_mode() ) {
181 return;
182 }
183
184 $connection = new Connection_Manager( $plugin_slug );
185 if ( ! $connection->is_connected() ) {
186 return;
187 }
188
189 if ( $this->maybe_purge_coupon_by_added_date() ) {
190 return;
191 }
192
193 // Limit checks to happen once a minute at most.
194 if ( get_transient( self::$last_check_transient ) ) {
195 return;
196 }
197
198 set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS );
199
200 $this->maybe_purge_coupon_by_availability_check();
201 }
202
203 /**
204 * Purge coupon based on local added date.
205 *
206 * We automatically remove the coupon after a month to "self-heal" if
207 * something in the claim process has broken with the site.
208 *
209 * @return bool Return whether we should skip further purge checks.
210 */
211 protected function maybe_purge_coupon_by_added_date() {
212 $date = Jetpack_Options::get_option( self::$added_option, '' );
213
214 if ( empty( $date ) ) {
215 return true;
216 }
217
218 $expire_date = strtotime( '+30 days', $date );
219 $today = time();
220
221 if ( $today >= $expire_date ) {
222 $this->delete_coupon_data();
223
224 return true;
225 }
226
227 return false;
228 }
229
230 /**
231 * Purge coupon based on availability check.
232 *
233 * @return bool Return whether we deleted coupon data.
234 */
235 protected function maybe_purge_coupon_by_availability_check() {
236 $blog_id = Jetpack_Options::get_option( 'id', false );
237
238 if ( ! $blog_id ) {
239 return false;
240 }
241
242 $coupon = self::get_coupon();
243
244 if ( ! $coupon ) {
245 return false;
246 }
247
248 $response = call_user_func_array(
249 $this->request_as_blog,
250 array(
251 add_query_arg(
252 array( 'coupon_code' => $coupon['coupon_code'] ),
253 sprintf(
254 '/sites/%d/jetpack-partner/coupon/v1/site/coupon',
255 $blog_id
256 )
257 ),
258 2,
259 array( 'method' => 'GET' ),
260 null,
261 'wpcom',
262 )
263 );
264
265 $body = json_decode( wp_remote_retrieve_body( $response ), true );
266
267 if (
268 200 === wp_remote_retrieve_response_code( $response ) &&
269 is_array( $body ) &&
270 isset( $body['available'] ) &&
271 false === $body['available']
272 ) {
273 $this->delete_coupon_data();
274
275 return true;
276 }
277
278 return false;
279 }
280
281 /**
282 * Delete all coupon data.
283 */
284 protected function delete_coupon_data() {
285 Jetpack_Options::delete_option(
286 array(
287 self::$coupon_option,
288 self::$added_option,
289 )
290 );
291 }
292
293 /**
294 * Get partner coupon data.
295 *
296 * @return array|bool
297 */
298 public static function get_coupon() {
299 $coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' );
300
301 if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) {
302 return false;
303 }
304
305 $instance = self::get_instance();
306 $partner = $instance->get_coupon_partner( $coupon_code );
307
308 if ( ! $partner ) {
309 return false;
310 }
311
312 $preset = $instance->get_coupon_preset( $coupon_code );
313
314 if ( ! $preset ) {
315 return false;
316 }
317
318 $product = $instance->get_coupon_product( $preset );
319
320 if ( ! $product ) {
321 return false;
322 }
323
324 return array(
325 'coupon_code' => $coupon_code,
326 'partner' => $partner,
327 'preset' => $preset,
328 'product' => $product,
329 );
330 }
331
332 /**
333 * Get coupon partner.
334 *
335 * @param string $coupon_code Coupon code to go through.
336 * @return array|bool
337 */
338 private function get_coupon_partner( $coupon_code ) {
339 if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) {
340 return false;
341 }
342
343 $prefix = strtok( $coupon_code, '_' );
344 $supported_partners = $this->get_supported_partners();
345
346 if ( ! isset( $supported_partners[ $prefix ] ) ) {
347 return false;
348 }
349
350 return array(
351 'name' => $supported_partners[ $prefix ]['name'],
352 'prefix' => $prefix,
353 'logo' => $supported_partners[ $prefix ]['logo'] ?? null,
354 );
355 }
356
357 /**
358 * Get coupon product.
359 *
360 * @param string $coupon_preset The preset we wish to find a product for.
361 * @return array|bool
362 */
363 private function get_coupon_product( $coupon_preset ) {
364 if ( ! is_string( $coupon_preset ) ) {
365 return false;
366 }
367
368 /**
369 * Allow for plugins to register supported products.
370 *
371 * @since 1.6.0
372 *
373 * @param array A list of product details.
374 * @return array
375 */
376 $product_details = apply_filters( 'jetpack_partner_coupon_products', array() );
377 $product_slug = $this->get_supported_presets()[ $coupon_preset ];
378
379 foreach ( $product_details as $product ) {
380 if ( ! $this->array_keys_exist( array( 'title', 'slug', 'description', 'features' ), $product ) ) {
381 continue;
382 }
383
384 if ( $product_slug === $product['slug'] ) {
385 return $product;
386 }
387 }
388
389 return false;
390 }
391
392 /**
393 * Checks if multiple keys are present in an array.
394 *
395 * @param array $needles The keys we wish to check for.
396 * @param array $haystack The array we want to compare keys against.
397 *
398 * @return bool
399 */
400 private function array_keys_exist( $needles, $haystack ) {
401 foreach ( $needles as $needle ) {
402 if ( ! isset( $haystack[ $needle ] ) ) {
403 return false;
404 }
405 }
406
407 return true;
408 }
409
410 /**
411 * Get coupon preset.
412 *
413 * @param string $coupon_code Coupon code to go through.
414 * @return string|bool
415 */
416 private function get_coupon_preset( $coupon_code ) {
417 if ( ! is_string( $coupon_code ) ) {
418 return false;
419 }
420
421 $regex = '/^.*?_(?P<slug>.*?)_.+$/';
422 $matches = array();
423
424 if ( ! preg_match( $regex, $coupon_code, $matches ) ) {
425 return false;
426 }
427
428 return isset( $this->get_supported_presets()[ $matches['slug'] ] ) ? $matches['slug'] : false;
429 }
430
431 /**
432 * Get supported partners.
433 *
434 * @return array
435 */
436 private function get_supported_partners() {
437 /**
438 * Allow external code to add additional supported partners.
439 *
440 * @since partner-1.6.0
441 * @since 2.0.0
442 *
443 * @param array $supported_partners A list of supported partners.
444 * @return array
445 */
446 return apply_filters( 'jetpack_partner_coupon_supported_partners', self::$supported_partners );
447 }
448
449 /**
450 * Get supported presets.
451 *
452 * @return array
453 */
454 private function get_supported_presets() {
455 /**
456 * Allow external code to add additional supported presets.
457 *
458 * @since partner-1.6.0
459 * @since 2.0.0
460 *
461 * @param array $supported_presets A list of supported presets.
462 * @return array
463 */
464 return apply_filters( 'jetpack_partner_coupon_supported_presets', self::$supported_presets );
465 }
466 }
467