PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 158
Lasso Lite – Affiliate Link Manager & Product Displays v158
158 157 155 156 154 153 152 151 150 149 148 trunk 0.9.9 104 105 106 107 108 109 110 111 112 113 114 115 All 57 releases
simple-urls / classes / class-cron.php

class-cron.php in Lasso Lite – Affiliate Link Manager & Product Displays 158, at classes/class-cron.php

608 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Declare class Config
4 *
5 * @package Config
6 */
7
8 namespace LassoLite\Classes;
9
10 use LassoLite\Classes\Estimate_Earning;
11 use LassoLite\Classes\License;
12 use LassoLite\Classes\Processes\Amazon;
13 use LassoLite\Classes\Processes\Amazon_Shortlink;
14 use LassoLite\Classes\Processes\Import_All;
15 use LassoLite\Classes\Processes\Revert_All;
16 use LassoLite\Classes\Setting;
17 use LassoLite\Classes\Enum;
18 use LassoLite\Admin\Constant;
19
20 /**
21 * Config
22 */
23 class Cron {
24
25 const CRONS = array(
26 'lasso_lite_amazon_shortlink' => 'lasso_lite_15_minutes',
27 'lasso_lite_update_amazon' => 'lasso_lite_15_minutes',
28 'lasso_lite_import_all' => 'lasso_lite_15_minutes',
29 'lasso_lite_revert_all' => 'lasso_lite_15_minutes',
30 'lasso_lite_tracking_support_status' => 'daily',
31 'lasso_lite_update_license_status' => 'daily',
32 'lasso_lite_cron_get_snippet' => 'daily',
33 'lasso_lite_cron_get_js_domain' => 'daily',
34 'lasso_lite_cron_get_info' => 'daily',
35 'lasso_lite_check_lite_user' => 'daily',
36 Estimate_Earning::CRON_HOOK => 'weekly',
37 );
38
39 /**
40 * Cron constructor.
41 */
42 public function register_hooks() {
43 add_filter( 'cron_schedules', array( $this, 'add_lasso_cron' ) );
44 add_action( 'lasso_lite_tracking_support_status', array( $this, 'lasso_lite_tracking_support_status' ) );
45 add_action( 'lasso_lite_import_all', array( $this, 'lasso_import_all' ) );
46 add_action( 'lasso_lite_revert_all', array( $this, 'lasso_revert_all' ) );
47 add_action( 'lasso_lite_update_amazon', array( $this, 'lasso_lite_update_amazon' ) );
48 add_action( 'lasso_lite_amazon_shortlink', array( $this, 'lasso_lite_amazon_shortlink' ) );
49 add_action( 'lasso_lite_update_license_status', array( $this, 'lasso_lite_update_license_status' ) );
50 add_action( 'lasso_lite_cron_get_snippet', array( $this, 'lasso_lite_cron_get_snippet' ) );
51 add_action( 'lasso_lite_cron_get_js_domain', array( $this, 'lasso_lite_cron_get_js_domain' ) );
52 add_action( 'lasso_lite_cron_get_info', array( $this, 'lasso_lite_cron_get_info' ) );
53 add_action( 'lasso_lite_check_lite_user', array( $this, 'lasso_lite_check_lite_user' ) );
54 add_action( Estimate_Earning::CRON_HOOK, array( $this, 'lasso_lite_weekly_estimate_earning' ) );
55 $this->lasso_create_schedule_hook();
56 }
57
58 /**
59 * Add a custom cron to WordPress
60 *
61 * @param array $schedules An array of non-default cron schedules. Default empty.
62 */
63 public function add_lasso_cron( $schedules ) {
64 $schedules['lasso_lite_15_minutes'] = array(
65 'interval' => 15 * MINUTE_IN_SECONDS, // ? 15 minutes in seconds
66 'display' => __( '15 minutes' ),
67 );
68
69 if ( ! isset( $schedules['weekly'] ) ) {
70 $schedules['weekly'] = array(
71 'interval' => WEEK_IN_SECONDS,
72 'display' => __( 'Once Weekly' ),
73 );
74 }
75
76 return $schedules;
77 }
78
79 /**
80 * Upgrade the stored cron option structure to version 2 (hash-keyed args).
81 *
82 * Mirrors WordPress core cron array shape so iteration in this class matches
83 * `wp_next_scheduled` / `wp_schedule_event` expectations.
84 *
85 * @param array $cron Cron info array from lasso_get_stored_cron_array().
86 * @return array Upgraded cron info array.
87 */
88 private function lasso_upgrade_stored_cron_array( $cron ) {
89 if ( isset( $cron['version'] ) && 2 === $cron['version'] ) {
90 return $cron;
91 }
92
93 $new_cron = array();
94
95 foreach ( (array) $cron as $timestamp => $hooks ) {
96 foreach ( (array) $hooks as $hook => $args ) {
97 $key = md5( serialize( $args['args'] ) );
98
99 $new_cron[ $timestamp ][ $hook ][ $key ] = $args;
100 }
101 }
102
103 $new_cron['version'] = 2;
104
105 update_option( 'cron', $new_cron );
106
107 return $new_cron;
108 }
109
110 /**
111 * Load the `cron` option as an array, upgrading legacy shape when needed.
112 *
113 * @return array Cron events (no `version` key in the returned array).
114 */
115 private function lasso_get_stored_cron_array() {
116 $cron = get_option( 'cron' );
117 if ( ! is_array( $cron ) ) {
118 return array();
119 }
120
121 if ( ! isset( $cron['version'] ) ) {
122 $cron = $this->lasso_upgrade_stored_cron_array( $cron );
123 }
124
125 unset( $cron['version'] );
126
127 return $cron;
128 }
129
130 /**
131 * Create hook for the new cron
132 */
133 public function lasso_create_schedule_hook() {
134 $crons = self::CRONS;
135 $events = array();
136 $crons_array = $this->lasso_get_stored_cron_array();
137
138 if ( ! is_array( $crons_array ) ) {
139 return;
140 }
141
142 foreach ( $crons_array as $time => $cron ) {
143 if ( ! is_array( $cron ) ) {
144 continue;
145 }
146 foreach ( $cron as $hook => $dings ) {
147 if ( strpos( $hook, 'lasso_lite_' ) === false ) {
148 continue;
149 }
150
151 foreach ( $dings as $sig => $data ) {
152 $interval = $data['interval'] ?? HOUR_IN_SECONDS;
153
154 // ? get the cron that is less than the existing one
155 if ( isset( $events[ $hook ] ) && $interval >= $events[ $hook ]->interval ) {
156 continue;
157 }
158
159 $events[ $hook ] = (object) array(
160 'hook' => $hook,
161 'time' => $time, // ? UTC
162 'schedule' => $data['schedule'],
163 'interval' => $interval,
164 );
165 }
166 }
167 }
168
169 $load_slot = self::site_load_slot( self::site_schedule_key() );
170
171 foreach ( $crons as $cron_name => $interval ) {
172 $next_scheduled = wp_next_scheduled( $cron_name );
173 $is_daily = ( 'daily' === $interval );
174 $is_15m = ( 'lasso_lite_15_minutes' === $interval );
175 $is_weekly = ( 'weekly' === $interval );
176
177 if ( $next_scheduled && $is_daily && ! self::timestamp_matches_daily_slot( $next_scheduled, $load_slot ) ) {
178 wp_clear_scheduled_hook( $cron_name );
179 $next_scheduled = false;
180 } elseif ( $next_scheduled && $is_15m && ! self::timestamp_matches_15m_slot( $next_scheduled, $load_slot ) ) {
181 wp_clear_scheduled_hook( $cron_name );
182 $next_scheduled = false;
183 } elseif ( $next_scheduled && $is_weekly && ! self::timestamp_matches_daily_slot( $next_scheduled, $load_slot ) ) {
184 wp_clear_scheduled_hook( $cron_name );
185 $next_scheduled = false;
186 }
187
188 if ( ! $next_scheduled ) {
189 $current_time = time();
190 $next_run_time = $current_time;
191 if ( $is_daily || $is_weekly ) {
192 $next_run_time = self::next_daily_run_ts( $load_slot, $current_time );
193 } elseif ( $is_15m ) {
194 $next_run_time = self::next_15m_run_ts( $load_slot, $current_time );
195 }
196 wp_schedule_event( $next_run_time, $interval, $cron_name );
197 }
198 }
199 }
200
201 /**
202 * Stable key for load-slot hashing. Prefer site URL.
203 *
204 * @return string
205 */
206 public static function site_schedule_key() {
207 $url = '';
208 if ( function_exists( 'home_url' ) ) {
209 $url = (string) home_url();
210 }
211 if ( function_exists( 'untrailingslashit' ) ) {
212 $url = untrailingslashit( strtolower( trim( $url ) ) );
213 } else {
214 $url = strtolower( trim( $url ) );
215 }
216 if ( '' !== $url ) {
217 return $url;
218 }
219
220 return 'lasso-missing-site-key';
221 }
222
223 /**
224 * Deterministic minute-of-day slot from a unique site key.
225 *
226 * @param string $site_key home_url.
227 * @return array
228 */
229 public static function site_load_slot( $site_key ) {
230 $key = (string) $site_key;
231 if ( '' === $key ) {
232 $key = 'lasso-missing-site-key';
233 }
234
235 $hash = abs( crc32( $key ) );
236 // Minutes 60–1439 (hour 1–23). UTC hour 0 stays for sites that never
237 // upgrade so 00Z is not re-occupied by hashed slots.
238 $minute_of_day = 60 + ( $hash % 1380 );
239
240 return array(
241 'minute_of_day' => $minute_of_day,
242 'hour' => (int) floor( $minute_of_day / 60 ),
243 'minute' => $minute_of_day % 60,
244 );
245 }
246
247 /**
248 * Next daily UTC timestamp at the hashed hour:minute.
249 *
250 * @param array $slot site_load_slot().
251 * @param int $current_time Unix timestamp.
252 * @return int
253 */
254 public static function next_daily_run_ts( $slot, $current_time ) {
255 $hour = isset( $slot['hour'] ) ? max( 1, min( 23, (int) $slot['hour'] ) ) : 1;
256 $minute = isset( $slot['minute'] ) ? max( 0, min( 59, (int) $slot['minute'] ) ) : 0;
257 $today = gmmktime( $hour, $minute, 0 );
258 if ( $today > $current_time ) {
259 return $today;
260 }
261
262 return $today + DAY_IN_SECONDS;
263 }
264
265 /**
266 * Next 15-minute UTC timestamp at hashed offset inside the window.
267 *
268 * @param array $slot site_load_slot().
269 * @param int $current_time Unix timestamp.
270 * @return int
271 */
272 public static function next_15m_run_ts( $slot, $current_time ) {
273 $offset = isset( $slot['minute'] ) ? ( (int) $slot['minute'] % 15 ) : 0;
274 $cur_min = (int) gmdate( 'i', $current_time );
275 $hour = (int) gmdate( 'G', $current_time );
276 $window = $cur_min - ( $cur_min % 15 );
277 $candidate = gmmktime( $hour, $window + $offset, 0 );
278 if ( $candidate <= $current_time ) {
279 $candidate += 15 * MINUTE_IN_SECONDS;
280 }
281 // Do not land 15-minute events in UTC hour 0.
282 while ( 0 === (int) gmdate( 'G', $candidate ) ) {
283 $candidate += 15 * MINUTE_IN_SECONDS;
284 }
285
286 return $candidate;
287 }
288
289 /**
290 * Whether a scheduled timestamp sits on the daily slot.
291 *
292 * @param int $timestamp Unix timestamp.
293 * @param array $slot site_load_slot().
294 * @return bool
295 */
296 public static function timestamp_matches_daily_slot( $timestamp, $slot ) {
297 if ( 0 === (int) gmdate( 'G', $timestamp ) ) {
298 return false;
299 }
300
301 return (int) gmdate( 'G', $timestamp ) === (int) $slot['hour']
302 && (int) gmdate( 'i', $timestamp ) === (int) $slot['minute'];
303 }
304
305 /**
306 * Whether a 15-minute event sits on the hashed offset.
307 *
308 * @param int $timestamp Unix timestamp.
309 * @param array $slot site_load_slot().
310 * @return bool
311 */
312 public static function timestamp_matches_15m_slot( $timestamp, $slot ) {
313 if ( 0 === (int) gmdate( 'G', $timestamp ) ) {
314 return false;
315 }
316
317 $offset = isset( $slot['minute'] ) ? ( (int) $slot['minute'] % 15 ) : 0;
318 return ( (int) gmdate( 'i', $timestamp ) % 15 ) === $offset;
319 }
320
321 /**
322 * 0–499ms delay so one site's URL list does not hit lasso.link as one burst.
323 *
324 * @param string $site_key Site URL.
325 * @param string $item_key Request URL.
326 * @return int
327 */
328 public static function request_spread_delay_ms( $site_key, $item_key ) {
329 return abs( crc32( (string) $site_key . "\0" . (string) $item_key ) ) % 500;
330 }
331
332 /**
333 * Whether the current request should spread lasso.link API calls.
334 *
335 * Cron handlers enqueue background-process work that runs via admin-ajax.php
336 * where DOING_CRON is false; LASSO_LITE_BACKGROUND_PROCESS marks those workers.
337 *
338 * @return bool
339 */
340 public static function is_paced_background_context() {
341 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
342 return true;
343 }
344
345 return defined( 'LASSO_LITE_BACKGROUND_PROCESS' ) && LASSO_LITE_BACKGROUND_PROCESS;
346 }
347
348 /**
349 * Sleep only on cron/background API calls. Admin save stays instant.
350 *
351 * @param string $item_key Request URL.
352 * @param bool $is_lasso_save Interactive save.
353 * @return int Milliseconds slept (0 when skipped).
354 */
355 public static function maybe_pace_background_request( $item_key, $is_lasso_save = false ) {
356 if ( $is_lasso_save ) {
357 return 0;
358 }
359 if ( ! self::is_paced_background_context() ) {
360 return 0;
361 }
362
363 $ms = self::request_spread_delay_ms( self::site_schedule_key(), $item_key );
364 $ms = (int) apply_filters( 'lasso_lite_request_spread_delay_ms', $ms, $item_key );
365 if ( $ms > 0 && $ms < 500 ) {
366 usleep( $ms * 1000 );
367 }
368
369 return $ms;
370 }
371
372 const OPTION_BLS_LAST_TICK = 'lasso_lite_bls_last_tick';
373
374 /**
375 * Per-URL minute-of-day due slot (site + url).
376 *
377 * @param string $site_key Site URL.
378 * @param string $item_key Request URL.
379 * @return int
380 */
381 public static function item_due_minute( $site_key, $item_key ) {
382 return abs( crc32( (string) $site_key . "\0" . (string) $item_key ) ) % 1440;
383 }
384
385 /**
386 * Late = due more than an hour ago (skip the line vs current-hour items).
387 *
388 * @param int $due_minute Minute of day.
389 * @param int $now Unix timestamp.
390 * @return bool
391 */
392 public static function is_late_data_request( $due_minute, $now ) {
393 $now_minute = ( (int) gmdate( 'G', $now ) * 60 ) + (int) gmdate( 'i', $now );
394 $age = ( $now_minute - (int) $due_minute + 1440 ) % 1440;
395 return $age > 60;
396 }
397
398 /**
399 * Hourly due window: current hour plus late catch-up (2h, or 3h in UTC hour 1).
400 *
401 * 15-minute crons skip UTC hour 0, so the first tick after midnight is ~01:xx.
402 * A 2h lookback from 01:xx starts ~23:xx and drops the 22:xx band that a 00:xx
403 * tick used to catch (WP-Cron often misses the last evening window).
404 *
405 * @param string $item_key Request URL.
406 * @param int|null $now Unix timestamp.
407 * @param int|null $last_tick Previous tick (0 = first run).
408 * @param int|null $due_minute Injected slot for tests.
409 * @return bool
410 */
411 public static function should_send_scheduled_data_request( $item_key, $now = null, $last_tick = null, $due_minute = null ) {
412 $now = null === $now ? time() : (int) $now;
413 if ( null === $last_tick ) {
414 $last_tick = (int) get_option( self::OPTION_BLS_LAST_TICK, 0 );
415 }
416 if ( $last_tick <= 0 ) {
417 $last_tick = $now - HOUR_IN_SECONDS;
418 }
419
420 $lookback = ( 1 === (int) gmdate( 'G', $now ) ) ? ( 3 * HOUR_IN_SECONDS ) : ( 2 * HOUR_IN_SECONDS );
421 $window_start = max( $last_tick - 300, $now - $lookback );
422 if ( null === $due_minute ) {
423 $due_minute = self::item_due_minute( self::site_schedule_key(), $item_key );
424 }
425
426 $midnight = gmmktime( 0, 0, 0, (int) gmdate( 'n', $now ), (int) gmdate( 'j', $now ), (int) gmdate( 'Y', $now ) );
427 $due_today = $midnight + ( (int) $due_minute * 60 );
428 if ( $due_today <= $now ) {
429 return $due_today > $window_start;
430 }
431
432 // Due later today: still catch yesterday's occurrence inside the lookback
433 // window (WP-Cron often fires after UTC midnight and would otherwise skip 22:00–23:59).
434 return ( $due_today - DAY_IN_SECONDS ) > $window_start;
435 }
436
437 /**
438 * Close this hour's window so the next tick only opens new + late-capped work.
439 *
440 * @param int|null $now Unix timestamp.
441 */
442 public static function advance_data_request_tick( $now = null ) {
443 update_option( self::OPTION_BLS_LAST_TICK, null === $now ? time() : (int) $now, false );
444 }
445
446 /**
447 * Tracking support status
448 */
449 public function lasso_lite_tracking_support_status() {
450 $settings = Setting::get_settings();
451 if ( boolval( $settings[ Enum::SUPPORT_ENABLED ] ) ) {
452 Setting::save_support( false );
453 }
454 }
455
456 /**
457 * Import all
458 */
459 public function lasso_import_all() {
460 $allow_import_all = get_option( Import_All::OPTION, '0' );
461 if ( 1 === intval( $allow_import_all ) ) {
462 $lasso_import_all = new Import_All();
463 $lasso_import_all->import();
464 }
465 }
466
467 /**
468 * Revert all
469 */
470 public function lasso_revert_all() {
471 $allow_revert_all = get_option( Revert_All::OPTION, '0' );
472 if ( 1 === intval( $allow_revert_all ) ) {
473 $lasso_import_all = new Revert_All();
474 $lasso_import_all->revert();
475 }
476 }
477
478 /**
479 * Revert all
480 */
481 public function lasso_lite_update_amazon() {
482 $settings = Setting::get_settings();
483 if ( boolval( $settings['amazon_pricing_daily'] ) ) {
484 $lasso_amazon = new Amazon();
485 $lasso_amazon->run();
486 }
487 }
488
489 /**
490 * Revert all
491 */
492 public function lasso_lite_amazon_shortlink() {
493 $settings = Setting::get_settings();
494 if ( boolval( $settings['amazon_pricing_daily'] ) ) {
495 $lasso_amazon = new Amazon_Shortlink();
496 $lasso_amazon->run();
497 }
498 }
499
500 /**
501 * Update license status.
502 */
503 public function lasso_lite_update_license_status() {
504 License::check_user_license();
505 }
506
507 /**
508 * Daily update snippet: Fetch snippet performance and write to connect-snippet.min.js
509 */
510 public function lasso_lite_cron_get_snippet() {
511 try {
512 $url = Constant::LASSO_LINK . '/api/snippet/performance?ver=' . time();
513 $res = Helper::send_request( 'get', $url );
514
515 $status_code = isset( $res['status_code'] ) ? intval( $res['status_code'] ) : 0;
516 $body = isset( $res['response'] ) ? ( $res['response']->content ?? '' ) : '';
517 $body_str = is_string( $body ) ? $body : wp_json_encode( $body );
518
519 if ( 200 === $status_code && ! empty( $body_str ) && strpos( $body_str, 'LASSO_REDIRECT_AMAZON_URL,' ) !== false ) {
520 $file_path = LASSO_CONNECT_SNIPPET_FILE_LITE;
521 $result = file_put_contents( $file_path, (string) $body_str );
522 if ( false === $result ) {
523 return false;
524 }
525 }
526 return true;
527 } catch ( \Exception $e ) {
528 return false;
529 }
530 }
531
532 /**
533 * Fetches the JS domain URL from the remote API and updates the 'js_domain' option if valid.
534 *
535 * @return string Returns an empty string. Returns early on exception or after processing.
536 * @throws \Exception This method catches all exceptions internally and does not throw.
537 */
538 public function lasso_lite_cron_get_js_domain() {
539 try {
540 $url = Constant::LASSO_LINK . '/api/js-domain?ver=' . time();
541 $res = Helper::send_request( 'get', $url );
542 $status_code = intval( $res['status_code'] ?? 500 );
543 $response = $res['response'] ?? '';
544
545 if ( 200 === $status_code && $response ) {
546 // API returns JSON object that contains a URL to the JS file
547 $file_url = $response->url ?? '';
548 // Only return the URL. Do not fetch or write files here.
549 if ( ! empty( $file_url ) && Helper::validate_url( $file_url ) ) {
550 Helper::update_option( 'js_domain', $file_url );
551 }
552
553 $full_file_url = $response->full_url ?? '';
554 if ( ! empty( $full_file_url ) && Helper::validate_url( $full_file_url ) ) {
555 Helper::update_option( 'full_js_domain', $full_file_url );
556 }
557 }
558 } catch ( \Exception $e ) {
559 return '';
560 }
561
562 return '';
563 }
564
565 /**
566 * Get info
567 */
568 public function lasso_lite_cron_get_info() {
569 try {
570 License::lasso_getinfo(['license_key']);
571 } catch ( \Exception $e ) {
572 return false;
573 }
574
575 return true;
576 }
577
578 /**
579 * Daily: request lite user record for this site's admin email.
580 *
581 * @return bool True when the HTTP request completes with 200.
582 */
583 public function lasso_lite_check_lite_user() {
584 try {
585 $admin_email = get_option( 'admin_email' );
586 if ( empty( $admin_email ) || ! is_email( $admin_email ) ) {
587 return false;
588 }
589
590 $url = Constant::LASSO_LINK . '/plugin/lite/users/' . rawurlencode( $admin_email );
591 $headers = Helper::get_headers();
592 $res = Helper::send_request( 'get', $url, array(), $headers );
593
594 $status_code = isset( $res['status_code'] ) ? intval( $res['status_code'] ) : 0;
595 return 200 === $status_code;
596 } catch ( \Exception $e ) {
597 return false;
598 }
599 }
600
601 /**
602 * Weekly: refresh cached orphan Affiliate+ payout estimate for the weekly banner.
603 */
604 public function lasso_lite_weekly_estimate_earning() {
605 Estimate_Earning::fetch_and_cache();
606 }
607 }
608