| 1 |
<?php |
| 2 |
|
| 3 |
namespace gVectors\News\Services; |
| 4 |
|
| 5 |
use gVectors\News\Config; |
| 6 |
|
| 7 |
// Exit if accessed directly |
| 8 |
if( ! defined( 'ABSPATH' ) ) exit; |
| 9 |
|
| 10 |
/** |
| 11 |
* Daily sync with the proxy server: |
| 12 |
* 1. bail unless the service is enabled (consent gate) |
| 13 |
* 2. fetch news → cache in a transient for the admin notices |
| 14 |
* 3. build the installed-versions map of licensed addons |
| 15 |
* 4. fetch at-risk licenses (proxy computes phases + builds the email) |
| 16 |
* 5. dispatch news digest emails |
| 17 |
* 6. dispatch expiry reminder emails |
| 18 |
*/ |
| 19 |
class CronService { |
| 20 |
private $config; |
| 21 |
private $api; |
| 22 |
private $consent; |
| 23 |
private $email; |
| 24 |
|
| 25 |
public function __construct( Config $config, ApiService $api, ConsentService $consent, EmailService $email ) { |
| 26 |
$this->config = $config; |
| 27 |
$this->api = $api; |
| 28 |
$this->consent = $consent; |
| 29 |
$this->email = $email; |
| 30 |
|
| 31 |
add_action( $this->config->get_cron_hook(), [ $this, 'sync' ] ); |
| 32 |
add_action( 'init', [ $this, 'maybe_schedule' ] ); |
| 33 |
} |
| 34 |
|
| 35 |
/** |
| 36 |
* Self-healing scheduler: keep the daily event scheduled while the service |
| 37 |
* is enabled, clear it when consent is withdrawn. |
| 38 |
*/ |
| 39 |
public function maybe_schedule(): void { |
| 40 |
$scheduled = wp_next_scheduled( $this->config->get_cron_hook() ); |
| 41 |
if( $this->consent->is_enabled() ) { |
| 42 |
if( ! $scheduled ) { |
| 43 |
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', $this->config->get_cron_hook() ); |
| 44 |
} |
| 45 |
} elseif( $scheduled ) { |
| 46 |
wp_clear_scheduled_hook( $this->config->get_cron_hook() ); |
| 47 |
} |
| 48 |
} |
| 49 |
|
| 50 |
/** |
| 51 |
* Daily sync handler. |
| 52 |
*/ |
| 53 |
public function sync(): void { |
| 54 |
if( ! $this->consent->is_enabled() ) return; |
| 55 |
|
| 56 |
// 1. News → transient (admin notices read from here; no request per pageload) |
| 57 |
$news_payload = $this->api->get_news(); |
| 58 |
if( $news_payload !== null ) { |
| 59 |
$news_payload['fetched_at'] = time(); |
| 60 |
set_transient( $this->config->get_news_transient(), $news_payload, $this->config->get_news_cache_ttl() ); |
| 61 |
} |
| 62 |
|
| 63 |
// 2. At-risk licenses (only when there are licensed addons to report on) |
| 64 |
$at_risk = null; |
| 65 |
$versions = $this->get_installed_versions_map(); |
| 66 |
if( ! empty( $versions ) ) { |
| 67 |
$at_risk = $this->api->get_at_risk_licenses( $versions ); |
| 68 |
} |
| 69 |
|
| 70 |
// 3. Emails — EmailService filters through the site + per-admin |
| 71 |
// channel/category matrix (PrefsService), so no gating needed here. |
| 72 |
if( $news_payload !== null ) { |
| 73 |
$this->email->send_news_digest( $news_payload ); |
| 74 |
} |
| 75 |
if( $at_risk !== null ) { |
| 76 |
$this->email->send_expiry_reminders( $at_risk ); // upcoming expirations (phase > 0) |
| 77 |
$this->email->send_winback_reminders( $at_risk ); // already expired (phase < 0) |
| 78 |
$this->email->send_dunning_notices( $at_risk ); // failed payments (past_due) |
| 79 |
$this->email->send_offer_reminders( $at_risk ); // unused personal discounts |
| 80 |
} |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Cached news payload for the notices renderer. |
| 85 |
*/ |
| 86 |
public function get_cached_news(): array { |
| 87 |
$payload = get_transient( $this->config->get_news_transient() ); |
| 88 |
|
| 89 |
return is_array( $payload ) && ! empty( $payload['news'] ) && is_array( $payload['news'] ) ? $payload['news'] : []; |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* plugin_slug => installed version map for every addon ANY registered |
| 94 |
* gVectors plugin has a local license for. Licenses are gVectors-wide, so |
| 95 |
* the map merges the license storage of every plugin context (wpForo |
| 96 |
* addon licenses + wpDiscuz addon licenses + ...), and the proxy answers |
| 97 |
* with at-risk licenses for all of them in one request. |
| 98 |
*/ |
| 99 |
private function get_installed_versions_map(): array { |
| 100 |
$slugs = []; |
| 101 |
foreach( \gVectors\News\NewsModule::get_contexts() as $config ) { |
| 102 |
$licenses = get_option( $config->get_licenses_option(), [] ); |
| 103 |
if( ! is_array( $licenses ) ) continue; |
| 104 |
foreach( $licenses as $license ) { |
| 105 |
if( is_array( $license ) && ! empty( $license['plugin_slug'] ) && is_string( $license['plugin_slug'] ) ) { |
| 106 |
$slugs[ $license['plugin_slug'] ] = true; |
| 107 |
} |
| 108 |
} |
| 109 |
} |
| 110 |
if( empty( $slugs ) ) return []; |
| 111 |
|
| 112 |
if( ! function_exists( 'get_plugins' ) ) { |
| 113 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 114 |
} |
| 115 |
|
| 116 |
$versions = []; |
| 117 |
foreach( get_plugins() as $plugin_file => $plugin_data ) { |
| 118 |
$dir = dirname( $plugin_file ); |
| 119 |
if( isset( $slugs[ $dir ] ) ) { |
| 120 |
$versions[ $dir ] = (string) ( $plugin_data['Version'] ?? '' ); |
| 121 |
} |
| 122 |
} |
| 123 |
|
| 124 |
// Licensed but not installed addons are still reported (version unknown) so |
| 125 |
// the proxy can include them in expiry reminders. |
| 126 |
foreach( array_keys( $slugs ) as $slug ) { |
| 127 |
if( ! isset( $versions[ $slug ] ) ) { |
| 128 |
$versions[ $slug ] = ''; |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
return $versions; |
| 133 |
} |
| 134 |
} |
| 135 |
|