PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 156
Lasso Lite – Affiliate Link Manager & Product Displays v156
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
← All changes | classes/class-cron.php +483 -7 112156 View file →
@@ -6,9 +6,17 @@
6 6 */
7 7
8 8 namespace LassoLite\Classes;
9 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;
10 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;
11 19
12 20 /**
13 21 * Config
14 22 */
@@ -14,20 +22,37 @@
14 22 */
15 23 class Cron {
16 24
17 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',
18 30 'lasso_lite_tracking_support_status' => 'daily',
19 - 'lasso_lite_import_all' => 'lasso_lite_15_minutes',
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',
20 37 );
21 38
22 39 /**
23 40 * Cron constructor.
24 41 */
25 - public function __construct() {
42 + public function register_hooks() {
26 43 add_filter( 'cron_schedules', array( $this, 'add_lasso_cron' ) );
27 44 add_action( 'lasso_lite_tracking_support_status', array( $this, 'lasso_lite_tracking_support_status' ) );
28 45 add_action( 'lasso_lite_import_all', array( $this, 'lasso_import_all' ) );
29 -
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' ) );
30 55 $this->lasso_create_schedule_hook();
31 56 }
32 57
33 58 /**
@@ -40,18 +65,76 @@
40 65 'interval' => 15 * MINUTE_IN_SECONDS, // ? 15 minutes in seconds
41 66 'display' => __( '15 minutes' ),
42 67 );
43 68
69 + if ( ! isset( $schedules['weekly'] ) ) {
70 + $schedules['weekly'] = array(
71 + 'interval' => WEEK_IN_SECONDS,
72 + 'display' => __( 'Once Weekly' ),
73 + );
74 + }
75 +
44 76 return $schedules;
45 77 }
46 78
47 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 + /**
48 131 * Create hook for the new cron
49 132 */
50 133 public function lasso_create_schedule_hook() {
51 134 $crons = self::CRONS;
52 135 $events = array();
53 - $crons_array = _get_cron_array();
136 + $crons_array = $this->lasso_get_stored_cron_array();
54 137
55 138 if ( ! is_array( $crons_array ) ) {
56 139 return;
57 140 }
@@ -56,8 +139,11 @@
56 139 return;
57 140 }
58 141
59 142 foreach ( $crons_array as $time => $cron ) {
143 + if ( ! is_array( $cron ) ) {
144 + continue;
145 + }
60 146 foreach ( $cron as $hook => $dings ) {
61 147 if ( strpos( $hook, 'lasso_lite_' ) === false ) {
62 148 continue;
63 149 }
@@ -75,21 +161,270 @@
75 161 'time' => $time, // ? UTC
76 162 'schedule' => $data['schedule'],
77 163 'interval' => $interval,
78 164 );
79 -
80 165 }
81 166 }
82 167 }
83 168
169 + $load_slot = self::site_load_slot( self::site_schedule_key() );
170 +
84 171 foreach ( $crons as $cron_name => $interval ) {
85 - if ( ! wp_next_scheduled( $cron_name ) ) {
86 - wp_schedule_event( time(), $interval, $cron_name );
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;
87 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 + }
88 198 }
89 199 }
90 200
91 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 + $minute_of_day = abs( crc32( $key ) ) % 1440;
236 +
237 + return array(
238 + 'minute_of_day' => $minute_of_day,
239 + 'hour' => (int) floor( $minute_of_day / 60 ),
240 + 'minute' => $minute_of_day % 60,
241 + );
242 + }
243 +
244 + /**
245 + * Next daily UTC timestamp at the hashed hour:minute.
246 + *
247 + * @param array $slot site_load_slot().
248 + * @param int $current_time Unix timestamp.
249 + * @return int
250 + */
251 + public static function next_daily_run_ts( $slot, $current_time ) {
252 + $hour = isset( $slot['hour'] ) ? max( 0, min( 23, (int) $slot['hour'] ) ) : 0;
253 + $minute = isset( $slot['minute'] ) ? max( 0, min( 59, (int) $slot['minute'] ) ) : 0;
254 + $today = gmmktime( $hour, $minute, 0 );
255 + if ( $today > $current_time ) {
256 + return $today;
257 + }
258 +
259 + return $today + DAY_IN_SECONDS;
260 + }
261 +
262 + /**
263 + * Next 15-minute UTC timestamp at hashed offset inside the window.
264 + *
265 + * @param array $slot site_load_slot().
266 + * @param int $current_time Unix timestamp.
267 + * @return int
268 + */
269 + public static function next_15m_run_ts( $slot, $current_time ) {
270 + $offset = isset( $slot['minute'] ) ? ( (int) $slot['minute'] % 15 ) : 0;
271 + $cur_min = (int) gmdate( 'i', $current_time );
272 + $hour = (int) gmdate( 'G', $current_time );
273 + $window = $cur_min - ( $cur_min % 15 );
274 + $candidate = gmmktime( $hour, $window + $offset, 0 );
275 + if ( $candidate > $current_time ) {
276 + return $candidate;
277 + }
278 +
279 + return $candidate + ( 15 * MINUTE_IN_SECONDS );
280 + }
281 +
282 + /**
283 + * Whether a scheduled timestamp sits on the daily slot.
284 + *
285 + * @param int $timestamp Unix timestamp.
286 + * @param array $slot site_load_slot().
287 + * @return bool
288 + */
289 + public static function timestamp_matches_daily_slot( $timestamp, $slot ) {
290 + return (int) gmdate( 'G', $timestamp ) === (int) $slot['hour']
291 + && (int) gmdate( 'i', $timestamp ) === (int) $slot['minute'];
292 + }
293 +
294 + /**
295 + * Whether a 15-minute event sits on the hashed offset.
296 + *
297 + * @param int $timestamp Unix timestamp.
298 + * @param array $slot site_load_slot().
299 + * @return bool
300 + */
301 + public static function timestamp_matches_15m_slot( $timestamp, $slot ) {
302 + $offset = isset( $slot['minute'] ) ? ( (int) $slot['minute'] % 15 ) : 0;
303 + return ( (int) gmdate( 'i', $timestamp ) % 15 ) === $offset;
304 + }
305 +
306 + /**
307 + * 0–499ms delay so one site's URL list does not hit lasso.link as one burst.
308 + *
309 + * @param string $site_key Site URL.
310 + * @param string $item_key Request URL.
311 + * @return int
312 + */
313 + public static function request_spread_delay_ms( $site_key, $item_key ) {
314 + return abs( crc32( (string) $site_key . "\0" . (string) $item_key ) ) % 500;
315 + }
316 +
317 + /**
318 + * Whether the current request should spread lasso.link API calls.
319 + *
320 + * Cron handlers enqueue background-process work that runs via admin-ajax.php
321 + * where DOING_CRON is false; LASSO_LITE_BACKGROUND_PROCESS marks those workers.
322 + *
323 + * @return bool
324 + */
325 + public static function is_paced_background_context() {
326 + if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
327 + return true;
328 + }
329 +
330 + return defined( 'LASSO_LITE_BACKGROUND_PROCESS' ) && LASSO_LITE_BACKGROUND_PROCESS;
331 + }
332 +
333 + /**
334 + * Sleep only on cron/background API calls. Admin save stays instant.
335 + *
336 + * @param string $item_key Request URL.
337 + * @param bool $is_lasso_save Interactive save.
338 + * @return int Milliseconds slept (0 when skipped).
339 + */
340 + public static function maybe_pace_background_request( $item_key, $is_lasso_save = false ) {
341 + if ( $is_lasso_save ) {
342 + return 0;
343 + }
344 + if ( ! self::is_paced_background_context() ) {
345 + return 0;
346 + }
347 +
348 + $ms = self::request_spread_delay_ms( self::site_schedule_key(), $item_key );
349 + $ms = (int) apply_filters( 'lasso_lite_request_spread_delay_ms', $ms, $item_key );
350 + if ( $ms > 0 && $ms < 500 ) {
351 + usleep( $ms * 1000 );
352 + }
353 +
354 + return $ms;
355 + }
356 +
357 + const OPTION_BLS_LAST_TICK = 'lasso_lite_bls_last_tick';
358 +
359 + /**
360 + * Per-URL minute-of-day due slot (site + url).
361 + *
362 + * @param string $site_key Site URL.
363 + * @param string $item_key Request URL.
364 + * @return int
365 + */
366 + public static function item_due_minute( $site_key, $item_key ) {
367 + return abs( crc32( (string) $site_key . "\0" . (string) $item_key ) ) % 1440;
368 + }
369 +
370 + /**
371 + * Late = due more than an hour ago (skip the line vs current-hour items).
372 + *
373 + * @param int $due_minute Minute of day.
374 + * @param int $now Unix timestamp.
375 + * @return bool
376 + */
377 + public static function is_late_data_request( $due_minute, $now ) {
378 + $now_minute = ( (int) gmdate( 'G', $now ) * 60 ) + (int) gmdate( 'i', $now );
379 + $age = ( $now_minute - (int) $due_minute + 1440 ) % 1440;
380 + return $age > 60;
381 + }
382 +
383 + /**
384 + * Hourly due window: current hour plus up to 2h of late catch-up.
385 + *
386 + * @param string $item_key Request URL.
387 + * @param int|null $now Unix timestamp.
388 + * @param int|null $last_tick Previous tick (0 = first run).
389 + * @param int|null $due_minute Injected slot for tests.
390 + * @return bool
391 + */
392 + public static function should_send_scheduled_data_request( $item_key, $now = null, $last_tick = null, $due_minute = null ) {
393 + $now = null === $now ? time() : (int) $now;
394 + if ( null === $last_tick ) {
395 + $last_tick = (int) get_option( self::OPTION_BLS_LAST_TICK, 0 );
396 + }
397 + if ( $last_tick <= 0 ) {
398 + $last_tick = $now - HOUR_IN_SECONDS;
399 + }
400 +
401 + $window_start = max( $last_tick - 300, $now - ( 2 * HOUR_IN_SECONDS ) );
402 + if ( null === $due_minute ) {
403 + $due_minute = self::item_due_minute( self::site_schedule_key(), $item_key );
404 + }
405 +
406 + $midnight = gmmktime( 0, 0, 0, (int) gmdate( 'n', $now ), (int) gmdate( 'j', $now ), (int) gmdate( 'Y', $now ) );
407 + $due_today = $midnight + ( (int) $due_minute * 60 );
408 + if ( $due_today <= $now ) {
409 + return $due_today > $window_start;
410 + }
411 +
412 + // Due later today: still catch yesterday's occurrence inside the 2h window
413 + // (WP-Cron often fires after UTC midnight and would otherwise skip 22:00–23:59).
414 + return ( $due_today - DAY_IN_SECONDS ) > $window_start;
415 + }
416 +
417 + /**
418 + * Close this hour's window so the next tick only opens new + late-capped work.
419 + *
420 + * @param int|null $now Unix timestamp.
421 + */
422 + public static function advance_data_request_tick( $now = null ) {
423 + update_option( self::OPTION_BLS_LAST_TICK, null === $now ? time() : (int) $now, false );
424 + }
425 +
426 + /**
92 427 * Tracking support status
93 428 */
94 429 public function lasso_lite_tracking_support_status() {
95 430 $settings = Setting::get_settings();
@@ -106,6 +441,147 @@
106 441 if ( 1 === intval( $allow_import_all ) ) {
107 442 $lasso_import_all = new Import_All();
108 443 $lasso_import_all->import();
109 444 }
445 + }
446 +
447 + /**
448 + * Revert all
449 + */
450 + public function lasso_revert_all() {
451 + $allow_revert_all = get_option( Revert_All::OPTION, '0' );
452 + if ( 1 === intval( $allow_revert_all ) ) {
453 + $lasso_import_all = new Revert_All();
454 + $lasso_import_all->revert();
455 + }
456 + }
457 +
458 + /**
459 + * Revert all
460 + */
461 + public function lasso_lite_update_amazon() {
462 + $settings = Setting::get_settings();
463 + if ( boolval( $settings['amazon_pricing_daily'] ) ) {
464 + $lasso_amazon = new Amazon();
465 + $lasso_amazon->run();
466 + }
467 + }
468 +
469 + /**
470 + * Revert all
471 + */
472 + public function lasso_lite_amazon_shortlink() {
473 + $settings = Setting::get_settings();
474 + if ( boolval( $settings['amazon_pricing_daily'] ) ) {
475 + $lasso_amazon = new Amazon_Shortlink();
476 + $lasso_amazon->run();
477 + }
478 + }
479 +
480 + /**
481 + * Update license status.
482 + */
483 + public function lasso_lite_update_license_status() {
484 + License::check_user_license();
485 + }
486 +
487 + /**
488 + * Daily update snippet: Fetch snippet performance and write to connect-snippet.min.js
489 + */
490 + public function lasso_lite_cron_get_snippet() {
491 + try {
492 + $url = Constant::LASSO_LINK . '/api/snippet/performance?ver=' . time();
493 + $res = Helper::send_request( 'get', $url );
494 +
495 + $status_code = isset( $res['status_code'] ) ? intval( $res['status_code'] ) : 0;
496 + $body = isset( $res['response'] ) ? ( $res['response']->content ?? '' ) : '';
497 + $body_str = is_string( $body ) ? $body : wp_json_encode( $body );
498 +
499 + if ( 200 === $status_code && ! empty( $body_str ) && strpos( $body_str, 'LASSO_REDIRECT_AMAZON_URL,' ) !== false ) {
500 + $file_path = LASSO_CONNECT_SNIPPET_FILE_LITE;
501 + $result = file_put_contents( $file_path, (string) $body_str );
502 + if ( false === $result ) {
503 + return false;
504 + }
505 + }
506 + return true;
507 + } catch ( \Exception $e ) {
508 + return false;
509 + }
510 + }
511 +
512 + /**
513 + * Fetches the JS domain URL from the remote API and updates the 'js_domain' option if valid.
514 + *
515 + * @return string Returns an empty string. Returns early on exception or after processing.
516 + * @throws \Exception This method catches all exceptions internally and does not throw.
517 + */
518 + public function lasso_lite_cron_get_js_domain() {
519 + try {
520 + $url = Constant::LASSO_LINK . '/api/js-domain?ver=' . time();
521 + $res = Helper::send_request( 'get', $url );
522 + $status_code = intval( $res['status_code'] ?? 500 );
523 + $response = $res['response'] ?? '';
524 +
525 + if ( 200 === $status_code && $response ) {
526 + // API returns JSON object that contains a URL to the JS file
527 + $file_url = $response->url ?? '';
528 + // Only return the URL. Do not fetch or write files here.
529 + if ( ! empty( $file_url ) && Helper::validate_url( $file_url ) ) {
530 + Helper::update_option( 'js_domain', $file_url );
531 + }
532 +
533 + $full_file_url = $response->full_url ?? '';
534 + if ( ! empty( $full_file_url ) && Helper::validate_url( $full_file_url ) ) {
535 + Helper::update_option( 'full_js_domain', $full_file_url );
536 + }
537 + }
538 + } catch ( \Exception $e ) {
539 + return '';
540 + }
541 +
542 + return '';
543 + }
544 +
545 + /**
546 + * Get info
547 + */
548 + public function lasso_lite_cron_get_info() {
549 + try {
550 + License::lasso_getinfo(['license_key']);
551 + } catch ( \Exception $e ) {
552 + return false;
553 + }
554 +
555 + return true;
556 + }
557 +
558 + /**
559 + * Daily: request lite user record for this site's admin email.
560 + *
561 + * @return bool True when the HTTP request completes with 200.
562 + */
563 + public function lasso_lite_check_lite_user() {
564 + try {
565 + $admin_email = get_option( 'admin_email' );
566 + if ( empty( $admin_email ) || ! is_email( $admin_email ) ) {
567 + return false;
568 + }
569 +
570 + $url = Constant::LASSO_LINK . '/plugin/lite/users/' . rawurlencode( $admin_email );
571 + $headers = Helper::get_headers();
572 + $res = Helper::send_request( 'get', $url, array(), $headers );
573 +
574 + $status_code = isset( $res['status_code'] ) ? intval( $res['status_code'] ) : 0;
575 + return 200 === $status_code;
576 + } catch ( \Exception $e ) {
577 + return false;
578 + }
579 + }
580 +
581 + /**
582 + * Weekly: refresh cached orphan Affiliate+ payout estimate for the weekly banner.
583 + */
584 + public function lasso_lite_weekly_estimate_earning() {
585 + Estimate_Earning::fetch_and_cache();
110 586 }
111 587 }