PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 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-beta, at jetpack_vendor/automattic/jetpack-connection/src/class-partner-coupon.php

465 lines 10.8 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 where the redemption screen can show.
172 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
173 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
174 if ( ! in_array( $page, array( 'jetpack', 'my-jetpack' ), true ) ) {
175 return;
176 }
177
178 if ( ( new Status() )->is_offline_mode() ) {
179 return;
180 }
181
182 $connection = new Connection_Manager( $plugin_slug );
183 if ( ! $connection->is_connected() ) {
184 return;
185 }
186
187 if ( $this->maybe_purge_coupon_by_added_date() ) {
188 return;
189 }
190
191 // Limit checks to happen once a minute at most.
192 if ( get_transient( self::$last_check_transient ) ) {
193 return;
194 }
195
196 set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS );
197
198 $this->maybe_purge_coupon_by_availability_check();
199 }
200
201 /**
202 * Purge coupon based on local added date.
203 *
204 * We automatically remove the coupon after a month to "self-heal" if
205 * something in the claim process has broken with the site.
206 *
207 * @return bool Return whether we should skip further purge checks.
208 */
209 protected function maybe_purge_coupon_by_added_date() {
210 $date = Jetpack_Options::get_option( self::$added_option, '' );
211
212 if ( empty( $date ) ) {
213 return true;
214 }
215
216 $expire_date = strtotime( '+30 days', $date );
217 $today = time();
218
219 if ( $today >= $expire_date ) {
220 $this->delete_coupon_data();
221
222 return true;
223 }
224
225 return false;
226 }
227
228 /**
229 * Purge coupon based on availability check.
230 *
231 * @return bool Return whether we deleted coupon data.
232 */
233 protected function maybe_purge_coupon_by_availability_check() {
234 $blog_id = Jetpack_Options::get_option( 'id', false );
235
236 if ( ! $blog_id ) {
237 return false;
238 }
239
240 $coupon = self::get_coupon();
241
242 if ( ! $coupon ) {
243 return false;
244 }
245
246 $response = call_user_func_array(
247 $this->request_as_blog,
248 array(
249 add_query_arg(
250 array( 'coupon_code' => $coupon['coupon_code'] ),
251 sprintf(
252 '/sites/%d/jetpack-partner/coupon/v1/site/coupon',
253 $blog_id
254 )
255 ),
256 2,
257 array( 'method' => 'GET' ),
258 null,
259 'wpcom',
260 )
261 );
262
263 $body = json_decode( wp_remote_retrieve_body( $response ), true );
264
265 if (
266 200 === wp_remote_retrieve_response_code( $response ) &&
267 is_array( $body ) &&
268 isset( $body['available'] ) &&
269 false === $body['available']
270 ) {
271 $this->delete_coupon_data();
272
273 return true;
274 }
275
276 return false;
277 }
278
279 /**
280 * Delete all coupon data.
281 */
282 protected function delete_coupon_data() {
283 Jetpack_Options::delete_option(
284 array(
285 self::$coupon_option,
286 self::$added_option,
287 )
288 );
289 }
290
291 /**
292 * Get partner coupon data.
293 *
294 * @return array|bool
295 */
296 public static function get_coupon() {
297 $coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' );
298
299 if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) {
300 return false;
301 }
302
303 $instance = self::get_instance();
304 $partner = $instance->get_coupon_partner( $coupon_code );
305
306 if ( ! $partner ) {
307 return false;
308 }
309
310 $preset = $instance->get_coupon_preset( $coupon_code );
311
312 if ( ! $preset ) {
313 return false;
314 }
315
316 $product = $instance->get_coupon_product( $preset );
317
318 if ( ! $product ) {
319 return false;
320 }
321
322 return array(
323 'coupon_code' => $coupon_code,
324 'partner' => $partner,
325 'preset' => $preset,
326 'product' => $product,
327 );
328 }
329
330 /**
331 * Get coupon partner.
332 *
333 * @param string $coupon_code Coupon code to go through.
334 * @return array|bool
335 */
336 private function get_coupon_partner( $coupon_code ) {
337 if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) {
338 return false;
339 }
340
341 $prefix = strtok( $coupon_code, '_' );
342 $supported_partners = $this->get_supported_partners();
343
344 if ( ! isset( $supported_partners[ $prefix ] ) ) {
345 return false;
346 }
347
348 return array(
349 'name' => $supported_partners[ $prefix ]['name'],
350 'prefix' => $prefix,
351 'logo' => $supported_partners[ $prefix ]['logo'] ?? null,
352 );
353 }
354
355 /**
356 * Get coupon product.
357 *
358 * @param string $coupon_preset The preset we wish to find a product for.
359 * @return array|bool
360 */
361 private function get_coupon_product( $coupon_preset ) {
362 if ( ! is_string( $coupon_preset ) ) {
363 return false;
364 }
365
366 /**
367 * Allow for plugins to register supported products.
368 *
369 * @since 1.6.0
370 *
371 * @param array A list of product details.
372 * @return array
373 */
374 $product_details = apply_filters( 'jetpack_partner_coupon_products', array() );
375 $product_slug = $this->get_supported_presets()[ $coupon_preset ];
376
377 foreach ( $product_details as $product ) {
378 if ( ! $this->array_keys_exist( array( 'title', 'slug', 'description', 'features' ), $product ) ) {
379 continue;
380 }
381
382 if ( $product_slug === $product['slug'] ) {
383 return $product;
384 }
385 }
386
387 return false;
388 }
389
390 /**
391 * Checks if multiple keys are present in an array.
392 *
393 * @param array $needles The keys we wish to check for.
394 * @param array $haystack The array we want to compare keys against.
395 *
396 * @return bool
397 */
398 private function array_keys_exist( $needles, $haystack ) {
399 foreach ( $needles as $needle ) {
400 if ( ! isset( $haystack[ $needle ] ) ) {
401 return false;
402 }
403 }
404
405 return true;
406 }
407
408 /**
409 * Get coupon preset.
410 *
411 * @param string $coupon_code Coupon code to go through.
412 * @return string|bool
413 */
414 private function get_coupon_preset( $coupon_code ) {
415 if ( ! is_string( $coupon_code ) ) {
416 return false;
417 }
418
419 $regex = '/^.*?_(?P<slug>.*?)_.+$/';
420 $matches = array();
421
422 if ( ! preg_match( $regex, $coupon_code, $matches ) ) {
423 return false;
424 }
425
426 return isset( $this->get_supported_presets()[ $matches['slug'] ] ) ? $matches['slug'] : false;
427 }
428
429 /**
430 * Get supported partners.
431 *
432 * @return array
433 */
434 private function get_supported_partners() {
435 /**
436 * Allow external code to add additional supported partners.
437 *
438 * @since partner-1.6.0
439 * @since 2.0.0
440 *
441 * @param array $supported_partners A list of supported partners.
442 * @return array
443 */
444 return apply_filters( 'jetpack_partner_coupon_supported_partners', self::$supported_partners );
445 }
446
447 /**
448 * Get supported presets.
449 *
450 * @return array
451 */
452 private function get_supported_presets() {
453 /**
454 * Allow external code to add additional supported presets.
455 *
456 * @since partner-1.6.0
457 * @since 2.0.0
458 *
459 * @param array $supported_presets A list of supported presets.
460 * @return array
461 */
462 return apply_filters( 'jetpack_partner_coupon_supported_presets', self::$supported_presets );
463 }
464 }
465