PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / includes / mlsimport-alerts.php

mlsimport-alerts.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.2, at includes/mlsimport-alerts.php

134 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Internal incident alerts (issue #208).
4 *
5 * File role: gives the plugin one small way to tell the MLSImport SaaS that
6 * something on a customer site broke (a stalled import, permanently invalid
7 * credentials) and one way to say it recovered — so support sees the broken
8 * site before the customer cancels, instead of relying on the once-a-day
9 * telemetry heartbeat.
10 *
11 * How it works, step by step:
12 * 1. A caller opens an incident with mlsimport_alert_open( key, class, ctx ).
13 * 2. Open incidents are remembered in the mlsimport_open_alerts option as
14 * incident-key => opened-at; a key that is already open is deduplicated,
15 * so each incident produces exactly one alert no matter how often the
16 * detecting code runs (hourly cron, polled progress screens).
17 * 3. mlsimport_alert_resolve( key ) sends a matching resolution event and
18 * forgets the key, re-arming the alert for a future incident.
19 * 4. Payloads travel over ThemeImport's fire-and-forget SaaS POST to the
20 * 'alert' endpoint: non-blocking, response ignored, so alerting can never
21 * slow down or break a customer request.
22 * 5. Credentials never leave the site: context keys that look like secrets
23 * are stripped from every payload before dispatch.
24 *
25 * @package MLSImport
26 */
27
28 if ( ! defined( 'ABSPATH' ) ) {
29 exit;
30 }
31
32 /**
33 * Open an incident and send its alert, once.
34 *
35 * The incident key is the dedup unit: the first open for a key dispatches an
36 * 'alert_opened' event and records the key; every later open for the same key
37 * is silent until mlsimport_alert_resolve() forgets it. Keys older than 30
38 * days are pruned on the way in, so a resolution that never happened (site
39 * deactivated mid-incident, lost option) cannot suppress alerts forever.
40 *
41 * @param string $key Unique incident identifier (e.g. "import_stalled:12").
42 * @param string $class Incident class keyword for grouping on the SaaS side.
43 * @param array $context Sanitized diagnostic context (no credentials).
44 * @return bool True when a new alert was dispatched, false when deduplicated.
45 */
46 function mlsimport_alert_open( $key, $class, array $context = array() ) {
47 $open = get_option( 'mlsimport_open_alerts', array() );
48 if ( ! is_array( $open ) ) {
49 $open = array();
50 }
51
52 // Prune forgotten incidents so one lost resolution cannot mute a key forever.
53 foreach ( $open as $open_key => $opened_at ) {
54 if ( time() - intval( $opened_at ) > 30 * 86400 ) {
55 unset( $open[ $open_key ] );
56 }
57 }
58
59 // Already open → this exact incident was alerted before. Stay silent.
60 if ( isset( $open[ $key ] ) ) {
61 update_option( 'mlsimport_open_alerts', $open, false );
62 return false;
63 }
64
65 // New incident: remember it, then dispatch the single alert event.
66 $open[ $key ] = time();
67 update_option( 'mlsimport_open_alerts', $open, false );
68 mlsimport_alert_dispatch( 'alert_opened', $key, $class, $context );
69 return true;
70 }
71
72 /**
73 * Resolve an open incident: send the recovery event and re-arm the key.
74 *
75 * Only a key that is currently open sends anything — resolution of an unknown
76 * or already-resolved incident is silent, so recovery checks can also run on
77 * every cron pass without spamming. After resolution the key is forgotten and
78 * a future mlsimport_alert_open() for it alerts again (a new incident).
79 *
80 * @param string $key Incident identifier used at open time.
81 * @param array $context Sanitized recovery context (e.g. final counts).
82 * @return bool True when a resolution event was dispatched.
83 */
84 function mlsimport_alert_resolve( $key, array $context = array() ) {
85 $open = get_option( 'mlsimport_open_alerts', array() );
86 if ( ! is_array( $open ) || ! isset( $open[ $key ] ) ) {
87 return false;
88 }
89
90 // Forget the incident first, then announce the recovery.
91 $context['open_seconds'] = time() - intval( $open[ $key ] );
92 unset( $open[ $key ] );
93 update_option( 'mlsimport_open_alerts', $open, false );
94 mlsimport_alert_dispatch( 'alert_resolved', $key, '', $context );
95 return true;
96 }
97
98 /**
99 * Build one alert payload and hand it to the fire-and-forget SaaS sender.
100 *
101 * Shared by open and resolve so both events have the same shape. Strips any
102 * secret-looking context keys (#208: token and credential values must never
103 * appear in alerts), stamps site and time, and lets integrations adjust the
104 * payload through the mlsimport_alert_payload filter before sending.
105 *
106 * @param string $event 'alert_opened' or 'alert_resolved'.
107 * @param string $key Incident identifier.
108 * @param string $class Incident class keyword.
109 * @param array $context Diagnostic context.
110 * @return void
111 */
112 function mlsimport_alert_dispatch( $event, $key, $class, array $context ) {
113 // Hard rule: no credential material in any alert payload.
114 foreach ( array( 'token', 'password', 'secret', 'username', 'authorization', 'api_key' ) as $secret_key ) {
115 unset( $context[ $secret_key ] );
116 }
117
118 $payload = array(
119 'event' => $event,
120 'incident' => (string) $key,
121 'class' => (string) $class,
122 'context' => $context,
123 'time' => time(),
124 );
125
126 /** Filter an outgoing incident-alert payload. @since 7.2 */
127 $payload = apply_filters( 'mlsimport_alert_payload', $payload );
128
129 // Fire-and-forget: never blocks, response never inspected.
130 if ( class_exists( 'ThemeImport' ) ) {
131 ThemeImport::globalApiRequestSaasFireAndForget( 'alert', $payload );
132 }
133 }
134