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
← All changes | classes/class-cron.php +489 -7 115158 View file →
@@ -6,10 +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;
11 15 use LassoLite\Classes\Processes\Revert_All;
16 +use LassoLite\Classes\Setting;
17 +use LassoLite\Classes\Enum;
18 +use LassoLite\Admin\Constant;
12 19
13 20 /**
14 21 * Config
15 22 */
@@ -15,22 +22,37 @@
15 22 */
16 23 class Cron {
17 24
18 25 const CRONS = array(
19 - 'lasso_lite_tracking_support_status' => 'daily',
26 + 'lasso_lite_amazon_shortlink' => 'lasso_lite_15_minutes',
27 + 'lasso_lite_update_amazon' => 'lasso_lite_15_minutes',
20 28 'lasso_lite_import_all' => 'lasso_lite_15_minutes',
21 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',
22 37 );
23 38
24 39 /**
25 40 * Cron constructor.
26 41 */
27 - public function __construct() {
42 + public function register_hooks() {
28 43 add_filter( 'cron_schedules', array( $this, 'add_lasso_cron' ) );
29 44 add_action( 'lasso_lite_tracking_support_status', array( $this, 'lasso_lite_tracking_support_status' ) );
30 45 add_action( 'lasso_lite_import_all', array( $this, 'lasso_import_all' ) );
31 46 add_action( 'lasso_lite_revert_all', array( $this, 'lasso_revert_all' ) );
32 -
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' ) );
33 55 $this->lasso_create_schedule_hook();
34 56 }
35 57
36 58 /**
@@ -43,18 +65,76 @@
43 65 'interval' => 15 * MINUTE_IN_SECONDS, // ? 15 minutes in seconds
44 66 'display' => __( '15 minutes' ),
45 67 );
46 68
69 + if ( ! isset( $schedules['weekly'] ) ) {
70 + $schedules['weekly'] = array(
71 + 'interval' => WEEK_IN_SECONDS,
72 + 'display' => __( 'Once Weekly' ),
73 + );
74 + }
75 +
47 76 return $schedules;
48 77 }
49 78
50 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 + /**
51 131 * Create hook for the new cron
52 132 */
53 133 public function lasso_create_schedule_hook() {
54 134 $crons = self::CRONS;
55 135 $events = array();
56 - $crons_array = _get_cron_array();
136 + $crons_array = $this->lasso_get_stored_cron_array();
57 137
58 138 if ( ! is_array( $crons_array ) ) {
59 139 return;
60 140 }
@@ -59,8 +139,11 @@
59 139 return;
60 140 }
61 141
62 142 foreach ( $crons_array as $time => $cron ) {
143 + if ( ! is_array( $cron ) ) {
144 + continue;
145 + }
63 146 foreach ( $cron as $hook => $dings ) {
64 147 if ( strpos( $hook, 'lasso_lite_' ) === false ) {
65 148 continue;
66 149 }
@@ -78,21 +161,290 @@
78 161 'time' => $time, // ? UTC
79 162 'schedule' => $data['schedule'],
80 163 'interval' => $interval,
81 164 );
82 -
83 165 }
84 166 }
85 167 }
86 168
169 + $load_slot = self::site_load_slot( self::site_schedule_key() );
170 +
87 171 foreach ( $crons as $cron_name => $interval ) {
88 - if ( ! wp_next_scheduled( $cron_name ) ) {
89 - 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;
90 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 + }
91 198 }
92 199 }
93 200
94 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 + /**
95 447 * Tracking support status
96 448 */
97 449 public function lasso_lite_tracking_support_status() {
98 450 $settings = Setting::get_settings();
@@ -120,6 +472,136 @@
120 472 if ( 1 === intval( $allow_revert_all ) ) {
121 473 $lasso_import_all = new Revert_All();
122 474 $lasso_import_all->revert();
123 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();
124 606 }
125 607 }