PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / includes / rest-api / controller / version2 / class-mainwp-rest-monitors-controller.php

class-mainwp-rest-monitors-controller.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at includes/rest-api/controller/version2/class-mainwp-rest-monitors-controller.php

2,502 lines 92.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP REST Controller
4 *
5 * This class handles the REST API
6 *
7 * @package MainWP\Dashboard
8 */
9
10 use MainWP\Dashboard\MainWP_DB;
11 use MainWP\Dashboard\MainWP_DB_Uptime_Monitoring;
12 use MainWP\Dashboard\MainWP_Uptime_Monitoring_Connect;
13 use MainWP\Dashboard\MainWP_Uptime_Monitoring_Handle;
14 use MainWP\Dashboard\MainWP_Utility;
15 use MainWP\Dashboard\MainWP_Monitoring_Handler;
16 use MainWP\Dashboard\MainWP_Uptime_Monitoring_Edit;
17
18 // Exit if accessed directly.
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Class MainWP_Rest_Monitors_Controller
25 *
26 * @package MainWP\Dashboard
27 */
28 class MainWP_Rest_Monitors_Controller extends MainWP_REST_Controller { //phpcs:ignore -- NOSONAR - multi methods.
29
30 // phpcs:disable Generic.Metrics.CyclomaticComplexity -- complexity.
31
32 /**
33 * Protected static variable to hold the single instance of the class.
34 *
35 * @var mixed Default null
36 */
37 private static $instance = null;
38
39 /**
40 * Route base.
41 *
42 * @var string
43 */
44 protected $rest_base = 'monitors';
45
46 /**
47 * Global settings variable.
48 *
49 * @var null Instance variable.
50 */
51 private $global_settings = null;
52
53 /**
54 * Database instance.
55 *
56 * @var MainWP_DB_Uptime_Monitoring
57 */
58 private $db = null;
59
60 /**
61 * Valid statuses map.
62 *
63 * @var array<string,int>
64 */
65 private const STATUS_MAP = array(
66 'up' => MainWP_Uptime_Monitoring_Connect::UP,
67 'down' => MainWP_Uptime_Monitoring_Connect::DOWN,
68 'pending' => MainWP_Uptime_Monitoring_Connect::PENDING,
69 'first' => MainWP_Uptime_Monitoring_Connect::FIRST,
70 'paused' => -1,
71 );
72
73 /**
74 * Valid types map.
75 *
76 * @var array<string,string>
77 */
78 private const TYPE_MAP = array(
79 'useglobal' => 'useglobal',
80 'http' => 'http',
81 'ping' => 'ping',
82 'keyword' => 'keyword',
83 );
84
85 /**
86 * Valid methods map.
87 *
88 * @var array<string,string>
89 */
90 private const METHOD_MAP = array(
91 'useglobal' => 'useglobal',
92 'head' => 'head',
93 'get' => 'get',
94 'post' => 'post',
95 'push' => 'push',
96 'patch' => 'patch',
97 'delete' => 'delete',
98 );
99
100 /**
101 * Constructor.
102 *
103 * @uses MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings()
104 */
105 public function __construct() {
106 add_filter( 'mainwp_rest_routes_monitors_controller_filter_allowed_fields_by_context', array( $this, 'hook_filter_allowed_fields_by_context' ), 10, 2 );
107 add_filter( 'mainwp_rest_heartbeat_monitor_object_query', array( $this, 'heartbeat_monitor_custom_query_args' ), 10, 2 );
108
109 $this->db = MainWP_DB_Uptime_Monitoring::instance();
110 $this->global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
111 }
112
113 /**
114 * Method instance()
115 *
116 * Create public static instance.
117 *
118 * @static
119 * @return static::$instance
120 */
121 public static function instance() {
122 if ( null === static::$instance ) {
123 static::$instance = new self();
124 }
125 return static::$instance;
126 }
127
128 /**
129 * Get filter client.
130 *
131 * @param object $item item data.
132 * @param string $context context.
133 *
134 * @return object item in context.
135 */
136 public function hook_filter_allowed_fields_by_context( $item, $context = 'view' ) {
137 return $this->filter_response_data_by_allowed_fields( $item, $context );
138 }
139
140 /**
141 * Add custom query args.
142 *
143 * @param array $args Query args.
144 * @param WP_REST_Request $request Request object.
145 *
146 * @return array
147 */
148 public function heartbeat_monitor_custom_query_args( $args, $request ) {
149 // Add custom args for heartbeat endpoint.
150 if ( ! empty( $request['period'] ) ) {
151 $args['period'] = $request['period'];
152 }
153
154 if ( ! empty( $request['since'] ) ) {
155 $args['since'] = $request['since'];
156 }
157
158 return $args;
159 }
160
161 /**
162 * Method register_routes()
163 *
164 * Creates the necessary endpoints for the api.
165 * Note, for a request to be successful the URL query parameters consumer_key and consumer_secret need to be set and correct.
166 */
167 public function register_routes() { // phpcs:ignore -- NOSONAR - complex.
168 register_rest_route(
169 $this->namespace,
170 '/' . $this->rest_base,
171 array(
172 array(
173 'methods' => WP_REST_Server::READABLE,
174 'callback' => array( $this, 'get_monitors' ),
175 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
176 'args' => $this->get_monitors_allowed_fields(),
177 ),
178 )
179 );
180
181 register_rest_route(
182 $this->namespace,
183 '/' . $this->rest_base . '/basic',
184 array(
185 array(
186 'methods' => WP_REST_Server::READABLE,
187 'callback' => array( $this, 'get_basic_monitors' ),
188 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
189 'args' => $this->get_monitors_allowed_fields(),
190 ),
191 )
192 );
193
194 register_rest_route(
195 $this->namespace,
196 '/' . $this->rest_base . '/count',
197 array(
198 array(
199 'methods' => WP_REST_Server::READABLE,
200 'callback' => array( $this, 'count_monitors' ),
201 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
202 'args' => $this->get_count_monitors_allowed_fields(),
203 ),
204 )
205 );
206
207 register_rest_route(
208 $this->namespace,
209 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))',
210 array(
211 array(
212 'methods' => WP_REST_Server::READABLE,
213 'callback' => array( $this, 'get_monitor' ),
214 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
215 'args' => $this->get_monitor_allowed_id_domain_field(),
216 ),
217 )
218 );
219
220 register_rest_route(
221 $this->namespace,
222 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/basic',
223 array(
224 array(
225 'methods' => WP_REST_Server::READABLE,
226 'callback' => array( $this, 'get_basic_monitor' ),
227 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
228 'args' => $this->get_monitor_allowed_id_domain_field(),
229 ),
230 )
231 );
232
233 register_rest_route(
234 $this->namespace,
235 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/heartbeat',
236 array(
237 array(
238 'methods' => WP_REST_Server::READABLE,
239 'callback' => array( $this, 'get_heartbeat_monitor' ),
240 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
241 'args' => $this->get_monitor_heartbeat_allowed_fields(),
242 ),
243 )
244 );
245
246 register_rest_route(
247 $this->namespace,
248 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/incidents',
249 array(
250 array(
251 'methods' => WP_REST_Server::READABLE,
252 'callback' => array( $this, 'get_monitor_incidents' ),
253 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
254 'args' => $this->get_monitor_incidents_allowed_fields(),
255 ),
256 )
257 );
258
259 register_rest_route(
260 $this->namespace,
261 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/incidents/count',
262 array(
263 array(
264 'methods' => WP_REST_Server::READABLE,
265 'callback' => array( $this, 'get_monitor_incidents_count' ),
266 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
267 'args' => $this->get_monitor_incidents_allowed_fields(),
268 ),
269 )
270 );
271
272 register_rest_route(
273 $this->namespace,
274 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/settings',
275 array(
276 array(
277 'methods' => 'PUT, PATCH',
278 'callback' => array( $this, 'update_individual_monitor_settings' ),
279 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
280 'args' => $this->get_individual_monitor_settings_allowed_fields(),
281 ),
282 )
283 );
284
285 register_rest_route(
286 $this->namespace,
287 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/check',
288 array(
289 array(
290 'methods' => WP_REST_Server::CREATABLE,
291 'callback' => array( $this, 'check_monitor' ),
292 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
293 ),
294 )
295 );
296
297 register_rest_route(
298 $this->namespace,
299 '/' . $this->rest_base . '/settings',
300 array(
301 array(
302 'methods' => 'PUT, PATCH',
303 'callback' => array( $this, 'update_global_monitoring_settings' ),
304 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
305 'args' => $this->get_global_monitor_settings_allowed_fields(),
306 ),
307 )
308 );
309 }
310
311 /**
312 * Get all Monitors.
313 *
314 * @param WP_REST_Request $request Full details about the request.
315 *
316 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_monitors()
317 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_uptime_monitor_stat_hourly_by()
318 * @uses MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp()
319 * @uses MainWP_DB_Uptime_Monitoring::instance()->count_site_incidents_stats()
320 *
321 * @return WP_Error|WP_REST_Response
322 */
323 public function get_monitors( $request ) { // phpcs:ignore -- NOSONAR - complex.
324 $args = $this->prepare_objects_query( $request );
325 $params = array(
326 'exclude' => ! empty( $args['exclude'] ) ? $args['exclude'] : '',
327 'include' => ! empty( $args['include'] ) ? $args['include'] : '',
328 'status' => isset( $args['status'] ) ? $args['status'] : '',
329 'search' => ! empty( $args['s'] ) ? $args['s'] : '',
330 'page' => ! empty( $args['paged'] ) ? (int) $args['paged'] : 1,
331 'per_page' => ! empty( $args['items_per_page'] ) ? (int) $args['items_per_page'] : 20,
332 );
333
334 $now = time();
335 $today = gmdate( 'Y-m-d', $now );
336 $one_month_ago = gmdate( 'Y-m-d', $now - 30 * DAY_IN_SECONDS );
337 $one_week_ago = gmdate( 'Y-m-d', $now - 7 * DAY_IN_SECONDS );
338 // Get last 24h hourly key.
339 $last24_key = MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp( $now - DAY_IN_SECONDS );
340
341 // Get data from uptime monitoring DB.
342 $monitors = $this->db->get_monitors( $params ); // get monitors.
343 if ( empty( $monitors ) || ! is_array( $monitors ) ) {
344 return rest_ensure_response(
345 array(
346 'success' => 1,
347 'total' => 0,
348 'data' => array(),
349 )
350 );
351 }
352
353 $data = array();
354 foreach ( $monitors as $monitor ) {
355 $wpid = (int) ( $monitor->wpid ?? 0 );
356 $monitor_id = (int) ( $monitor->monitor_id ?? 0 );
357
358 // Get reports data.
359 $reports_data = apply_filters( 'mainwp_uptime_monitoring_get_reports_data', $wpid, $one_month_ago, $today );
360
361 // Get 24h uptime data as array.
362 $status_24h = $this->db->get_uptime_monitor_stat_hourly_by( $monitor_id, 'last24', $last24_key );
363 $last24_data = $this->get_uptime_24h_data( $status_24h, $last24_key );
364
365 // Get 7d and 30d incidents data.
366 $incidents_7d = (int) $this->db->count_site_incidents_stats( $monitor_id, $one_week_ago, $today );
367 $incidents_30d = (int) $this->db->count_site_incidents_stats( $monitor_id, $one_month_ago, $today );
368
369 // Get monitor type & check frequency.
370 $type = $this->get_apply_setting( 'type', $monitor->type ?? '', 'useglobal', 'http' );
371 $interval_min = (int) $this->get_apply_setting( 'interval', (int) ( $monitor->interval ?? 0 ), -1, 60 );
372 $bypass_cache_opt = (int) $this->get_apply_setting( 'bypass_cache', (int) ( $monitor->bypass_cache ?? -1 ), -1, 0 );
373
374 $record = array(
375 'id' => $monitor_id,
376 'url' => $monitor->url ?? '',
377 'uptime_ratio_7d' => (float) ( $reports_data['uptime_ratios_data']['uptimeratios7'] ?? 0 ),
378 'uptime_ratio_30d' => (float) ( $reports_data['uptime_ratios_data']['uptimeratios30'] ?? 0 ),
379 'uptime_24h' => $last24_data,
380 'incidents_count_7d' => $incidents_7d,
381 'incidents_count_30d' => $incidents_30d,
382 'type' => $type ? $type : 'http',
383 'check_frequency' => $interval_min > 0 ? ( $interval_min . 'm' ) : '',
384 'response_time_avg_ms' => (int) ( $reports_data['avg_time_ms'] ?? 0 ),
385 'response_time_min_ms' => (int) ( $reports_data['min_time_ms'] ?? 0 ),
386 'response_time_max_ms' => (int) ( $reports_data['max_time_ms'] ?? 0 ),
387 'last_check_at' => ! empty( $monitor->lasttime_check ) ? gmdate( 'Y-m-d H:i:s', (int) $monitor->lasttime_check ) : '',
388 'last_status_code' => ! empty( $monitor->last_http_code ) ? (int) $monitor->last_http_code : '',
389 'last_status' => $this->uptime_status( $monitor->last_status ?? null ),
390 'bypass_cache' => $bypass_cache_opt,
391 );
392
393 // Filter data by allowed fields.
394 $data[] = $this->filter_response_data_by_allowed_fields( $record, 'view' );
395 }
396
397 return rest_ensure_response(
398 array(
399 'success' => 1,
400 'total' => count( $data ),
401 'data' => $data,
402 )
403 );
404 }
405
406 /**
407 * Get basic all Monitors.
408 *
409 * @param WP_REST_Request $request Full details about the request.
410 *
411 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_monitors()
412 * @return WP_Error|WP_REST_Response
413 */
414 public function get_basic_monitors( $request ) {
415 $args = $this->prepare_objects_query( $request );
416 $params = array(
417 'exclude' => ! empty( $args['exclude'] ) ? $args['exclude'] : '',
418 'include' => ! empty( $args['include'] ) ? $args['include'] : '',
419 'status' => isset( $args['status'] ) ? $args['status'] : '',
420 'search' => ! empty( $args['s'] ) ? $args['s'] : '',
421 'page' => ! empty( $args['paged'] ) ? (int) $args['paged'] : 1,
422 'per_page' => ! empty( $args['items_per_page'] ) ? (int) $args['items_per_page'] : 20,
423 );
424
425 // Get data from uptime monitoring DB.
426 $monitors = $this->db->get_monitors( $params ); // get monitors.
427 if ( empty( $monitors ) || ! is_array( $monitors ) ) {
428 return rest_ensure_response(
429 array(
430 'success' => 1,
431 'total' => 0,
432 'data' => array(),
433 )
434 );
435 }
436
437 // Filter data by allowed fields.
438 $data = array_map(
439 function ( $monitor ) {
440 $record = array(
441 'id' => $monitor->monitor_id,
442 'url' => $monitor->url ?? '',
443 'last_status' => $this->uptime_status( $monitor->last_status ?? null ),
444 );
445 return $this->filter_response_data_by_allowed_fields( $record, 'simple_view' );
446 },
447 $monitors
448 );
449
450 return rest_ensure_response(
451 array(
452 'success' => 1,
453 'total' => count( $data ),
454 'data' => $data,
455 )
456 );
457 }
458
459 /**
460 * Count all Monitors.
461 *
462 * @param WP_REST_Request $request Full details about the request.
463 *
464 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_monitors()
465 * @return WP_Error|WP_REST_Response
466 */
467 public function count_monitors( $request ) {
468 $args = $this->prepare_objects_query( $request );
469
470 $params = array(
471 'exclude' => ! empty( $args['exclude'] ) ? $args['exclude'] : '',
472 'include' => ! empty( $args['include'] ) ? $args['include'] : '',
473 'status' => isset( $args['status'] ) ? $args['status'] : '',
474 'search' => ! empty( $args['s'] ) ? $args['s'] : '',
475 );
476
477 // Get data from uptime monitoring DB.
478 $monitors = $this->db->get_monitors( $params );
479 $total = is_array( $monitors ) ? count( $monitors ) : 0;
480
481 return rest_ensure_response(
482 array(
483 'success' => 1,
484 'count' => $total,
485 )
486 );
487 }
488
489
490 /**
491 * Get Monitor.
492 *
493 * @param WP_REST_Request $request Full details about the request.
494 *
495 * @uses MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp()
496 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_uptime_monitor_stat_hourly_by()
497 * @uses MainWP_DB_Uptime_Monitoring::instance()->count_site_incidents_stats()
498 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_uptime_monitoring_stats()
499 *
500 * @return WP_Error|WP_REST_Response
501 */
502 public function get_monitor( $request ) { // phpcs:ignore -- NOSONAR - complex.
503 $monitor = $this->get_request_item( $request );
504
505 if ( empty( $monitor ) ) {
506 return rest_ensure_response(
507 array(
508 'success' => 0,
509 'message' => __( 'Monitor not found.', 'mainwp' ),
510 )
511 );
512 }
513
514 // Get reports data.
515 $now = time();
516 $today = gmdate( 'Y-m-d', $now );
517 $one_month_ago = gmdate( 'Y-m-d', $now - 30 * DAY_IN_SECONDS );
518 $one_week_ago = gmdate( 'Y-m-d', $now - 7 * DAY_IN_SECONDS );
519 // Get last 24h hourly key.
520 $last24_key = MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp( $now - DAY_IN_SECONDS );
521 $monitor_id = (int) ( $monitor->monitor_id ?? 0 );
522 $reports_data = apply_filters( 'mainwp_uptime_monitoring_get_reports_data', $monitor_id, $one_month_ago, $today );
523
524 // Get 24h uptime data as array.
525 $status_24h = $this->db->get_uptime_monitor_stat_hourly_by( $monitor_id, 'last24', $last24_key );
526 $last24_data = $this->get_uptime_24h_data( $status_24h, $last24_key );
527
528 // Get 7d and 30d incidents data.
529 $incidents_7d = (int) $this->db->count_site_incidents_stats( $monitor_id, $one_week_ago, $today );
530 $incidents_30d = (int) $this->db->count_site_incidents_stats( $monitor_id, $one_month_ago, $today );
531
532 // Get full heartbeat data.
533 $heartbeat_data = $this->db->get_uptime_monitoring_stats( $monitor_id, $one_month_ago, $today );
534
535 // Get monitor type & check frequency.
536 $type = $this->get_apply_setting( 'type', $monitor->type ?? '', 'useglobal', 'http' );
537 $interval_min = (int) $this->get_apply_setting( 'interval', (int) ( $monitor->interval ?? 0 ), -1, 60 );
538 $bypass_cache_opt = (int) $this->get_apply_setting( 'bypass_cache', (int) ( $monitor->bypass_cache ?? -1 ), -1, 0 );
539
540 $record = array(
541 'id' => $monitor_id,
542 'name' => $monitor->name ?? '',
543 'url' => $monitor->url ?? '',
544 'uptime_ratio_7d' => (float) ( $reports_data['uptime_ratios_data']['uptimeratios7'] ?? 0 ),
545 'uptime_ratio_30d' => (float) ( $reports_data['uptime_ratios_data']['uptimeratios30'] ?? 0 ),
546 'uptime_24h' => $last24_data,
547 'incidents_count_7d' => $incidents_7d,
548 'incidents_count_30d' => $incidents_30d,
549 'type' => $type ? $type : 'http',
550 'check_frequency' => $interval_min > 0 ? ( $interval_min . 'm' ) : '',
551 'response_time_avg_ms' => (int) ( $reports_data['avg_time_ms'] ?? 0 ),
552 'response_time_min_ms' => (int) ( $reports_data['min_time_ms'] ?? 0 ),
553 'response_time_max_ms' => (int) ( $reports_data['max_time_ms'] ?? 0 ),
554 'heartbeats' => $heartbeat_data ?? array(),
555 'last_check_at' => ! empty( $monitor->lasttime_check ) ? gmdate( 'Y-m-d H:i:s', (int) $monitor->lasttime_check ) : '',
556 'last_status_code' => ! empty( $monitor->last_http_code ) ? (int) $monitor->last_http_code : '',
557 'last_status' => $this->uptime_status( $monitor->last_status ?? null ),
558 'bypass_cache' => $bypass_cache_opt,
559 );
560
561 return rest_ensure_response(
562 array(
563 'success' => 1,
564 'data' => $this->filter_response_data_by_allowed_fields( $record, 'monitor_view' ),
565 )
566 );
567 }
568
569 /**
570 * Get Basic Monitor.
571 *
572 * @param mixed $request Full details about the request.
573 * @return WP_Error|WP_REST_Response
574 */
575 public function get_basic_monitor( $request ) { // phpcs:ignore -- NOSONAR - complex.
576 $monitors = $this->get_request_item( $request );
577
578 if ( empty( $monitors ) ) {
579 return rest_ensure_response(
580 array(
581 'success' => 0,
582 'message' => __( 'Monitor not found.', 'mainwp' ),
583 )
584 );
585 }
586
587 // Filter data by allowed fields.
588 $data = array(
589 'id' => $monitors->monitor_id,
590 'url' => $monitors->url ?? '',
591 'status' => $this->uptime_status( $monitors->last_status ?? null ),
592 );
593
594 return rest_ensure_response(
595 array(
596 'success' => 1,
597 'data' => $this->filter_response_data_by_allowed_fields( $data, 'simple_view' ),
598 )
599 );
600 }
601
602 /**
603 * Get Monitor Heartbeat.
604 *
605 * @param WP_REST_Request $request Full details about the request.
606 *
607 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_heartbeat_count()
608 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_heartbeat_data_paginated()
609 *
610 * @return WP_Error|WP_REST_Response
611 */
612 public function get_heartbeat_monitor( $request ) { // phpcs:ignore -- NOSONAR - complex.
613 $monitor = $this->get_request_item( $request );
614
615 if ( empty( $monitor ) ) {
616 return rest_ensure_response(
617 array(
618 'success' => 0,
619 'message' => __( 'Monitor not found.', 'mainwp' ),
620 )
621 );
622 }
623
624 // Get params.
625 $args = $this->prepare_objects_query( $request, 'heartbeat_monitor' );
626 $params = array(
627 'limit' => ! empty( $args['limit'] ) ? (int) $args['limit'] : 0,
628 'period' => ! empty( $args['period'] ) ? $args['period'] : '',
629 'status' => isset( $args['status'] ) ? $args['status'] : '',
630 'since' => ! empty( $args['since'] ) ? $args['since'] : '',
631 'page' => ! empty( $args['page'] ) ? (int) $args['page'] : 1,
632 );
633
634 // Parse period to date range.
635 $date_range = $this->parse_period( $params['period'], $params['since'] );
636 if ( is_wp_error( $date_range ) ) {
637 return rest_ensure_response(
638 array(
639 'success' => 0,
640 'message' => __( 'Invalid period format.', 'mainwp' ),
641 )
642 );
643 }
644
645 $monitor_id = (int) ( $monitor->monitor_id ?? 0 );
646 // Get total count for pagination.
647 $total_count = $this->db->get_heartbeat_count( $monitor_id, $date_range, $params['status'] );
648 if ( empty( $total_count ) ) {
649 return rest_ensure_response(
650 array(
651 'success' => 0,
652 'message' => __( 'No heartbeats found.', 'mainwp' ),
653 )
654 );
655 }
656
657 // Calculate pagination.
658 $per_page = $params['limit'];
659 $page = $params['page'];
660 $total_pages = $per_page > 0 ? ceil( $total_count / $per_page ) : 1;
661 $offset = ( $page - 1 ) * $per_page;
662
663 // Get heartbeats with pagination..
664 $heartbeats = $this->db->get_heartbeat_data_paginated(
665 $monitor->monitor_id,
666 $date_range,
667 $params['status'],
668 $per_page,
669 $offset
670 );
671
672 if ( empty( $heartbeats ) || ! is_array( $heartbeats ) ) {
673 return rest_ensure_response(
674 array(
675 'success' => 0,
676 'message' => __( 'No heartbeats found.', 'mainwp' ),
677 )
678 );
679 }
680
681 $formatted_heartbeats = array();
682 foreach ( $heartbeats as $heartbeat ) {
683 $record = array(
684 'heartbeat_id' => (int) $heartbeat->heartbeat_id,
685 'monitor_id' => (int) $heartbeat->monitor_id,
686 'msg' => ! empty( $heartbeat->msg ) ? $heartbeat->msg : '',
687 'importance' => (int) $heartbeat->importance,
688 'status' => (int) $heartbeat->status,
689 'time' => gmdate( 'Y-m-d\TH:i:s\Z', strtotime( $heartbeat->time ) ),
690 'ping_ms' => ! empty( $heartbeat->ping_ms ) ? (int) $heartbeat->ping_ms : null,
691 'http_code' => ! empty( $heartbeat->http_code ) ? (int) $heartbeat->http_code : null,
692 );
693 $formatted_heartbeats[] = $this->filter_response_data_by_allowed_fields( $record, 'heartbeat_view' );
694 }
695
696 return rest_ensure_response(
697 array(
698 'success' => 1,
699 'data' => $formatted_heartbeats,
700 'meta' => array(
701 'page' => $page,
702 'per_page' => $per_page,
703 'total' => $total_count,
704 'total_pages' => $total_pages,
705 ),
706 )
707 );
708 }
709
710 /**
711 * Get Monitor Incidents.
712 *
713 * @param WP_REST_Request $request Full details about the request.
714 *
715 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_heartbeat_data_for_incidents()
716 *
717 * @return WP_Error|WP_REST_Response
718 */
719 public function get_monitor_incidents( $request ) { // phpcs:ignore -- NOSONAR - complex.
720 $monitor = $this->get_request_item( $request );
721
722 if ( empty( $monitor ) ) {
723 return rest_ensure_response(
724 array(
725 'success' => 0,
726 'message' => __( 'Monitor not found.', 'mainwp' ),
727 )
728 );
729 }
730 // Get Params.
731 $args = $this->prepare_objects_query( $request );
732 $page = ! empty( $args['page'] ) ? (int) $args['page'] : 1;
733 $limit = ! empty( $args['items_per_page'] ) ? (int) $args['items_per_page'] : 20;
734 $offset = ( $page > 1 ? ( $page - 1 ) * $limit : 0 );
735
736 $monitor_id = isset( $monitor->monitor_id ) ? (int) $monitor->monitor_id : 0;
737 if ( $monitor_id <= 0 ) {
738 return rest_ensure_response(
739 array(
740 'success' => 0,
741 'message' => __( 'Monitor not found.', 'mainwp' ),
742 )
743 );
744 }
745
746 // Get all heartbeat data for this monitor to process incidents.
747 $heartbeats = $this->db->get_heartbeat_data_for_incidents( $monitor_id );
748
749 if ( empty( $heartbeats ) || ! is_array( $heartbeats ) ) {
750 return rest_ensure_response(
751 array(
752 'success' => 0,
753 'message' => __( 'No heartbeats found.', 'mainwp' ),
754 )
755 );
756 }
757
758 // Process heartbeats to identify incidents.
759 $incidents = $this->process_heartbeats_to_incidents( $heartbeats );
760
761 if ( empty( $incidents ) ) {
762 return rest_ensure_response(
763 array(
764 'success' => 1,
765 'data' => array(),
766 )
767 );
768 }
769
770 // Paginate incidents.
771 $paginated = array_slice( $incidents, $offset, $limit );
772
773 // Format incidents for response.
774 $formatted = array();
775 foreach ( $paginated as $incident ) {
776 $start_ts = isset( $incident['started_at'] ) ? strtotime( $incident['started_at'] ) : null;
777 $end_ts = ! empty( $incident['ended_at'] ) ? strtotime( $incident['ended_at'] ) : null;
778
779 $record = array(
780 'started_at' => $start_ts ? gmdate( 'Y-m-d\TH:i:s\Z', $start_ts ) : null,
781 'ended_at' => $end_ts ? gmdate( 'Y-m-d\TH:i:s\Z', $end_ts ) : null,
782 'duration_sec' => isset( $incident['duration_sec'] ) ? (int) $incident['duration_sec'] : 0,
783 'resolved' => ! empty( $incident['resolved'] ),
784 'down_count' => isset( $incident['down_count'] ) ? (int) $incident['down_count'] : 0,
785 );
786
787 $formatted[] = $this->filter_response_data_by_allowed_fields( $record, 'incidents_view' );
788 }
789
790 return rest_ensure_response(
791 array(
792 'success' => 1,
793 'data' => $formatted,
794 )
795 );
796 }
797
798 /**
799 * Get Monitor Incidents Count.
800 *
801 * @param WP_REST_Request $request Full details about the request.
802 *
803 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_heartbeat_data_for_incidents()
804 *
805 * @return WP_Error|WP_REST_Response
806 */
807 public function get_monitor_incidents_count( $request ) { // phpcs:ignore -- NOSONAR - complex.
808 $monitor = $this->get_request_item( $request );
809
810 if ( empty( $monitor ) ) {
811 return rest_ensure_response(
812 array(
813 'success' => 0,
814 'message' => __( 'Monitor not found.', 'mainwp' ),
815 )
816 );
817 }
818 // Get Params.
819 $args = $this->prepare_objects_query( $request );
820 $page = ! empty( $args['page'] ) ? (int) $args['page'] : 1;
821 $limit = ! empty( $args['items_per_page'] ) ? (int) $args['items_per_page'] : 20;
822 $offset = ( $page > 1 ? ( $page - 1 ) * $limit : 0 );
823
824 $monitor_id = isset( $monitor->monitor_id ) ? (int) $monitor->monitor_id : 0;
825 if ( $monitor_id <= 0 ) {
826 return rest_ensure_response(
827 array(
828 'success' => 0,
829 'message' => __( 'Monitor not found.', 'mainwp' ),
830 )
831 );
832 }
833
834 // Get all heartbeat data for this monitor to process incidents.
835 $heartbeats = $this->db->get_heartbeat_data_for_incidents( $monitor_id );
836
837 if ( empty( $heartbeats ) || ! is_array( $heartbeats ) ) {
838 return rest_ensure_response(
839 array(
840 'success' => 0,
841 'message' => __( 'No heartbeats found.', 'mainwp' ),
842 )
843 );
844 }
845
846 // Process heartbeats to identify incidents.
847 $incidents = $this->process_heartbeats_to_incidents( $heartbeats );
848
849 if ( empty( $incidents ) ) {
850 return rest_ensure_response(
851 array(
852 'success' => 1,
853 'count' => 0,
854 )
855 );
856 }
857
858 // Paginate incidents.
859 $paginated = array_slice( $incidents, $offset, $limit );
860
861 // Format incidents for response.
862 $down_count = 0;
863 foreach ( $paginated as $incident ) {
864 $down_count += isset( $incident['down_count'] ) ? (int) $incident['down_count'] : 0;
865 }
866
867 return rest_ensure_response(
868 array(
869 'success' => 1,
870 'count' => $down_count,
871 )
872 );
873 }
874
875 /**
876 * Update global monitoring settings.
877 *
878 * @param WP_REST_Request $request Full details about the request.
879 *
880 * @uses MainWP_Uptime_Monitoring_Handle::update_uptime_global_settings()
881 *
882 * @return WP_Error|WP_REST_Response
883 */
884 public function update_global_monitoring_settings( $request ) {
885 // Get request body.
886 $body = $request->get_json_params();
887
888 // A JSON scalar or list body carries none of the settings keys: array_intersect_key() below
889 // fatals on the scalar, and the list would intersect down to nothing and answer as a
890 // successful no-op, so both are treated as no body at all.
891 if ( ! is_array( $body ) || ( array() !== $body && array_keys( $body ) === range( 0, count( $body ) - 1 ) ) ) {
892 $body = array();
893 }
894
895 if ( empty( $body ) ) {
896 return new WP_Error(
897 'empty_body',
898 __( 'Request body is empty.', 'mainwp' ),
899 array( 'status' => 400 )
900 );
901 }
902
903 // Check content type.
904 $content_type = (string) $request->get_header( 'content-type' );
905 if ( 'application/json' !== $content_type ) {
906 return new WP_Error(
907 'invalid_content_type',
908 __( 'Invalid content type. Expected application/json.', 'mainwp' ),
909 );
910 }
911
912 // Reject a key the route does not declare. get_sanitized_settings_params() drops it, so a
913 // misspelled setting would otherwise reach the processor as an empty settings array and
914 // answer as a successful no-op. The individual route reports the same mistake through its
915 // schema, so it gets the same error code here.
916 $unknown_keys = array_diff( array_keys( $body ), array_keys( $this->get_global_monitor_settings_allowed_fields() ) );
917 if ( ! empty( $unknown_keys ) ) {
918 return new WP_Error(
919 'rest_additional_properties_forbidden',
920 sprintf(
921 /* translators: %s: comma separated list of request body keys the route does not accept. */
922 __( 'Unknown settings: %s.', 'mainwp' ),
923 implode( ', ', $unknown_keys )
924 ),
925 array( 'status' => 400 )
926 );
927 }
928
929 // Process global monitoring settings update.
930 $result = $this->process_global_monitoring_settings_update( $this->get_sanitized_settings_params( $request, $body ) );
931 if ( is_wp_error( $result ) ) {
932 return $result;
933 }
934
935 // Get updated settings.
936 $current_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
937 return rest_ensure_response(
938 array(
939 'success' => 1,
940 'data' => $current_settings,
941 )
942 );
943 }
944
945 /**
946 * Update individual monitor settings (per-monitor API).
947 *
948 * @param WP_REST_Request $request Full details about the request.
949 *
950 * @return WP_Error|WP_REST_Response
951 */
952 public function update_individual_monitor_settings( $request ) {
953 $monitor = $this->get_request_item( $request );
954
955 if ( empty( $monitor ) ) {
956 return rest_ensure_response(
957 array(
958 'success' => 0,
959 'message' => __( 'Monitor not found.', 'mainwp' ),
960 )
961 );
962 }
963
964 // Get request body.
965 $body = $request->get_json_params();
966
967 // A JSON scalar or list body carries none of the settings keys: array_intersect_key() below
968 // fatals on the scalar, and the list would intersect down to nothing and answer as a
969 // successful no-op, so both are treated as no body at all.
970 if ( ! is_array( $body ) || ( array() !== $body && array_keys( $body ) === range( 0, count( $body ) - 1 ) ) ) {
971 $body = array();
972 }
973
974 if ( empty( $body ) ) {
975 return new WP_Error(
976 'empty_body',
977 __( 'Request body is empty.', 'mainwp' ),
978 array( 'status' => 400 )
979 );
980 }
981
982 // Check content type.
983 $content_type = (string) $request->get_header( 'content-type' );
984 if ( 'application/json' !== $content_type ) {
985 return new WP_Error(
986 'invalid_content_type',
987 __( 'Invalid content type. Expected application/json.', 'mainwp' ),
988 );
989 }
990
991 // Validate request body against schema. The schema has to see the raw body: the sanitized
992 // params keep only the keys the route registered, so a misspelled key would be filtered out
993 // before additionalProperties could reject it.
994 $schema = $this->get_monitor_settings_schema(); // Get default schema.
995 $valid = rest_validate_value_from_schema( $body, $schema, 'body' );
996 if ( is_wp_error( $valid ) ) {
997 // A schema error carries no status of its own and REST falls back to 500 for one that
998 // has none, which is the wrong answer for a body the caller got wrong.
999 $valid->add_data( array( 'status' => 400 ) );
1000 return $valid;
1001 }
1002
1003 // Sanitize request body.
1004 $data = rest_sanitize_value_from_schema( $this->get_sanitized_settings_params( $request, $body ), $schema );
1005
1006 // Process individual monitor settings update.
1007 $result = $this->process_individual_monitor_settings_update( $monitor, $data );
1008
1009 if ( is_wp_error( $result ) ) {
1010 return $result;
1011 }
1012
1013 return rest_ensure_response(
1014 array(
1015 'success' => 1,
1016 'data' => $result,
1017 )
1018 );
1019 }
1020
1021 /**
1022 * Check monitor uptime.
1023 *
1024 * @param WP_REST_Request $request Full details about the request.
1025 *
1026 * @uses MainWP_DB::instance()->get_website_by_id()
1027 * @uses MainWP_Monitoring_Handler::handle_check_website()
1028 *
1029 * @return WP_Error|WP_REST_Response
1030 */
1031 public function check_monitor( $request ) {
1032 $monitor = $this->get_request_item( $request );
1033
1034 if ( empty( $monitor ) ) {
1035 return rest_ensure_response(
1036 array(
1037 'success' => 0,
1038 'message' => __( 'Monitor not found.', 'mainwp' ),
1039 )
1040 );
1041 }
1042
1043 // Get website.
1044 $website_id = isset( $monitor->wpid ) ? (int) $monitor->wpid : 0;
1045 $website = MainWP_DB::instance()->get_website_by_id( intval( $website_id ) );
1046 if ( empty( $website ) ) {
1047 return rest_ensure_response(
1048 array(
1049 'success' => 0,
1050 'message' => __( 'Website not found.', 'mainwp' ),
1051 )
1052 );
1053 }
1054
1055 // Check website.
1056 $error_msg = '';
1057 $result = '';
1058 try {
1059 if ( ! empty( $monitor->issub ) ) {
1060 $result = MainWP_Uptime_Monitoring_Handle::check_website_uptime_monitoring_status(
1061 $monitor,
1062 array(
1063 'ignore_compatible_save' => 1, // compatible.
1064 )
1065 );
1066 } else {
1067 $result = MainWP_Monitoring_Handler::handle_check_website( $website );
1068 }
1069 } catch ( \Exception $e ) {
1070 $error_msg = $e->getMessage();
1071 }
1072
1073 if ( ! empty( $error_msg ) ) {
1074 return rest_ensure_response(
1075 array(
1076 'success' => 0,
1077 'message' => $error_msg,
1078 )
1079 );
1080 }
1081
1082 $http_code = is_array( $result ) && isset( $result['httpCode'] ) ? $result['httpCode'] : 0;
1083 if ( is_array( $result ) && isset( $result['new_uptime_status'] ) ) {
1084 $check_result = $result['new_uptime_status'];
1085 }
1086
1087 return rest_ensure_response(
1088 array(
1089 'success' => 1,
1090 'data' => array(
1091 'http_code' => $http_code,
1092 'uptime_status' => $check_result ? 'up' : 'down',
1093 ),
1094 )
1095 );
1096 }
1097
1098 /**
1099 * Get the settings the request carries, as the registered callbacks sanitized them.
1100 *
1101 * @param WP_REST_Request $request Full details about the request.
1102 * @param array $body Raw request body.
1103 *
1104 * @return array Sanitized settings, keyed by the keys the body carried.
1105 */
1106 private function get_sanitized_settings_params( $request, $body ) {
1107 // The registered sanitize_callbacks produce the request params, so the persisted value is
1108 // read from there rather than from the raw body; WordPress currently writes the sanitized
1109 // value back into the JSON body too, but that is internal to sanitize_params(), not a
1110 // contract. get_params() also carries auth, query and route params, so keep only the
1111 // settings keys the route declares and only the ones this body actually sent. It merges in
1112 // the reverse of get_parameter_order(), so a JSON body value beats a query parameter of the
1113 // same name rather than the other way round.
1114 $allowed = $this->get_global_monitor_settings_allowed_fields();
1115 $params = array_intersect_key( $request->get_params(), $allowed );
1116
1117 return array_intersect_key( $params, $body );
1118 }
1119
1120 /**
1121 * Process global monitoring settings update with all business logic.
1122 *
1123 * @param array $settings Settings to update.
1124 *
1125 * @uses MainWP_Uptime_Monitoring_Handle::update_uptime_global_settings()
1126 * @uses MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings()
1127 *
1128 * @return array|WP_Error Updated global settings or error.
1129 */
1130 private function process_global_monitoring_settings_update( $settings ) { // phpcs:ignore -- NOSONAR - complex.
1131
1132 // Get current settings.
1133 $current_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
1134 $updated_settings = $current_settings;
1135
1136 // Process enabled setting.
1137 if ( isset( $settings['active'] ) ) {
1138 $updated_settings['active'] = $settings['active'] ? 1 : 0;
1139 }
1140
1141 // Process interval setting.
1142 if ( isset( $settings['interval'] ) ) {
1143 $updated_settings['interval'] = $settings['interval'];
1144 }
1145
1146 // Process timeout setting.
1147 if ( isset( $settings['timeout'] ) ) {
1148 $updated_settings['timeout'] = $settings['timeout']; // Convert to seconds.
1149 }
1150
1151 // Process maxretries setting.
1152 if ( isset( $settings['maxretries'] ) ) {
1153 $validation_result = $this->validate_retries( $settings['maxretries'] );
1154 if ( is_wp_error( $validation_result ) ) {
1155 return $validation_result;
1156 }
1157 $updated_settings['maxretries'] = (int) $settings['maxretries'];
1158 }
1159
1160 // Process type setting.
1161 if ( isset( $settings['type'] ) ) {
1162 if ( ! in_array( $settings['type'], array_keys( self::TYPE_MAP ), true ) ) {
1163 return array();
1164 }
1165 $updated_settings['type'] = $settings['type'];
1166 }
1167
1168 // Process method setting.
1169 if ( isset( $settings['method'] ) ) {
1170 if ( ! in_array( $settings['method'], array_keys( self::METHOD_MAP ), true ) ) {
1171 return array();
1172 }
1173 $updated_settings['method'] = $settings['method'];
1174 }
1175
1176 // Process HTTP-specific settings.
1177 if ( isset( $settings['expected_status'] ) ) {
1178 $updated_settings['up_status_codes'] = $settings['expected_status'];
1179 }
1180
1181 // Process keyword setting.
1182 if ( isset( $settings['keyword'] ) ) {
1183 $updated_settings['keyword'] = $settings['keyword'];
1184 }
1185
1186 if ( isset( $settings['bypass_cache'] ) && in_array( (int) $settings['bypass_cache'], array( 1, 0 ), true ) ) {
1187 $updated_settings['bypass_cache'] = $settings['bypass_cache'];
1188 }
1189
1190 // Apply filters to allow customization.
1191 $updated_settings = apply_filters( 'mainwp_uptime_monitoring_update_global_settings', $updated_settings );
1192
1193 // Save the updated settings.
1194 MainWP_Uptime_Monitoring_Handle::update_uptime_global_settings( $updated_settings );
1195
1196 return $settings;
1197 }
1198
1199 /**
1200 * Process individual monitor settings update with all business logic.
1201 *
1202 * @param object $monitor Monitor object from database.
1203 * @param array $settings Settings to update.
1204 *
1205 * @uses MainWP_DB_Uptime_Monitoring::update_wp_monitor()
1206 *
1207 * @return array|WP_Error Updated monitor settings or error.
1208 */
1209 private function process_individual_monitor_settings_update( $monitor, $settings ) { // phpcs:ignore -- NOSONAR - complexity.
1210 $monitor_id = (int) ( $monitor->monitor_id ?? 0 );
1211
1212 $update_data = array( 'monitor_id' => $monitor_id );
1213
1214 // Process active setting.
1215 if ( isset( $settings['active'] ) ) {
1216 $update_data['active'] = $settings['active'] ? $settings['active'] : 0;
1217 }
1218
1219 // Process check_frequency setting.
1220 if ( isset( $settings['interval'] ) ) {
1221 $update_data['interval'] = $settings['interval'];
1222 }
1223
1224 // Process timeout setting.
1225 if ( isset( $settings['timeout'] ) ) {
1226 $update_data['timeout'] = $settings['timeout'];
1227 }
1228
1229 // Process maxretries setting.
1230 if ( isset( $settings['maxretries'] ) ) {
1231 $validation_result = $this->validate_retries( $settings['maxretries'] );
1232 if ( is_wp_error( $validation_result ) ) {
1233 return $validation_result;
1234 }
1235 $update_data['maxretries'] = (int) $settings['maxretries'];
1236 }
1237
1238 // Process HTTP-specific settings.
1239 if ( isset( $settings['expected_status'] ) ) {
1240 $update_data['up_status_codes'] = $settings['expected_status'];
1241 }
1242
1243 // Process port settings.
1244 if ( isset( $settings['keyword'] ) ) {
1245 $update_data['keyword'] = $settings['keyword'];
1246 }
1247
1248 // Process type setting.
1249 if ( isset( $settings['type'] ) ) {
1250 if ( ! in_array( $settings['type'], array_keys( self::TYPE_MAP ), true ) ) {
1251 return array();
1252 }
1253 $update_data['type'] = $settings['type'];
1254 }
1255
1256 // Process method setting.
1257 if ( isset( $settings['method'] ) ) {
1258 if ( ! in_array( $settings['method'], array_keys( self::METHOD_MAP ), true ) ) {
1259 return array();
1260 }
1261 $update_data['method'] = $settings['method'];
1262 }
1263
1264 // Apply filters to allow customization.
1265 $update_data = apply_filters( 'mainwp_uptime_monitoring_update_monitor_data', $update_data, $monitor_id );
1266
1267 // Update the monitor.
1268 $result = $this->db->update_wp_monitor( $update_data );
1269
1270 if ( isset( $settings['bypass_cache'] ) ) {
1271 $bypass_cache = (int) $settings['bypass_cache'];
1272 if ( in_array( $bypass_cache, array( -1, 1, 0 ), true ) ) {
1273 MainWP_DB_Uptime_Monitoring::instance()->update_website_option( $monitor->wpid, 'bypass_cache', $bypass_cache );
1274 $update_data['bypass_cache'] = $bypass_cache;
1275 }
1276 }
1277
1278 if ( false === $result ) {
1279 return new WP_Error(
1280 'update_failed',
1281 __( 'Failed to update monitor settings.', 'mainwp' ),
1282 );
1283 }
1284
1285 return $update_data;
1286 }
1287
1288 /**
1289 * Process heartbeats to identify incidents.
1290 *
1291 * An incident is a contiguous sequence of heartbeats where status = 0 (down)
1292 * bounded by up statuses.
1293 *
1294 * @param array $heartbeats Array of heartbeat objects with status and time.
1295 * @return array Array of incidents with started_at, ended_at, duration_sec, resolved, down_count.
1296 */
1297 private function process_heartbeats_to_incidents( $heartbeats ) {
1298 $incidents = array();
1299 $current_incident = null;
1300
1301 foreach ( $heartbeats as $heartbeat ) {
1302 $status = (int) $heartbeat->status;
1303 $time = $heartbeat->time;
1304
1305 if ( MainWP_Uptime_Monitoring_Connect::DOWN === $status ) {
1306 // Start a new incident or continue existing one.
1307 if ( null === $current_incident ) {
1308 $current_incident = array(
1309 'started_at' => $time,
1310 'ended_at' => null,
1311 'duration_sec' => null,
1312 'resolved' => false,
1313 'down_count' => 1,
1314 );
1315 } else {
1316 // Continue existing incident.
1317 ++$current_incident['down_count'];
1318 }
1319 } elseif ( MainWP_Uptime_Monitoring_Connect::UP === $status ) {
1320 // End current incident if one exists.
1321 if ( null !== $current_incident ) {
1322 $current_incident['ended_at'] = $time;
1323 $current_incident['resolved'] = true;
1324
1325 // Calculate duration in seconds.
1326 $start_timestamp = strtotime( $current_incident['started_at'] );
1327 $end_timestamp = strtotime( $current_incident['ended_at'] );
1328 $current_incident['duration_sec'] = $end_timestamp - $start_timestamp;
1329
1330 $incidents[] = $current_incident;
1331 $current_incident = null;
1332 }
1333 }
1334 // Note: PENDING and FIRST statuses don't affect incident boundaries.
1335 }
1336
1337 // If there's an ongoing incident (not resolved), add it to the list.
1338 if ( null !== $current_incident ) {
1339 $incidents[] = $current_incident;
1340 }
1341
1342 // Sort incidents by started_at descending (most recent first).
1343 usort(
1344 $incidents,
1345 function ( $a, $b ) {
1346 return strtotime( $b['started_at'] ) - strtotime( $a['started_at'] );
1347 }
1348 );
1349
1350 return $incidents;
1351 }
1352
1353 /**
1354 * Parse period to date range.
1355 *
1356 * @param string $period Period.
1357 * @param string $date Date.
1358 *
1359 * @return array|WP_Error Date range or WP_Error.
1360 */
1361 private function parse_period( $period, $date ) { // phpcs:ignore -- NOSONAR - long method.
1362 $now = time();
1363 $day_start = '';
1364
1365 // Format UTC time.
1366 $format_date = static function ( $day ) {
1367 return gmdate( 'Y-m-d H:i:s', (int) $day );
1368 };
1369
1370 if ( ! empty( $date ) ) {
1371 $day_start = strtotime( $date );
1372 }
1373
1374 switch ( $period ) {
1375 case '24h':
1376 $start = ! empty( $day_start ) ? $day_start : $now - DAY_IN_SECONDS;
1377 return array(
1378 'start' => $format_date( $start ),
1379 'end' => $format_date( $now ),
1380 );
1381 case '7d':
1382 $start = ! empty( $day_start ) ? $day_start : $now - 7 * DAY_IN_SECONDS;
1383 return array(
1384 'start' => $format_date( $start ),
1385 'end' => $format_date( $now ),
1386 );
1387 case '30d':
1388 $start = ! empty( $day_start ) ? $day_start : $now - 30 * DAY_IN_SECONDS;
1389 return array(
1390 'start' => $format_date( $start ),
1391 'end' => $format_date( $now ),
1392 );
1393 default:
1394 // Try to parse as ISO8601 range (start/end).
1395 if ( strpos( $period, '/' ) !== false ) {
1396 $parts = explode( '/', $period );
1397 if ( count( $parts ) === 2 ) {
1398 $start = ! empty( $day_start ) ? $day_start : strtotime( $parts[0] );
1399 $end = strtotime( $parts[1] );
1400 if ( $start && $end && $start < $end ) {
1401 return array(
1402 'start' => $format_date( $start ),
1403 'end' => $format_date( $end ),
1404 );
1405 }
1406 }
1407 }
1408 return new WP_Error( 'invalid_period', __( 'Invalid period format.', 'mainwp' ) );
1409 }
1410 }
1411
1412 /**
1413 * Get uptime status.
1414 *
1415 * @param int $status Status.
1416 *
1417 * @return string Uptime status.
1418 */
1419 private function uptime_status( $status ) {
1420 switch ( $status ) {
1421 case 0:
1422 return 'down';
1423 case 1:
1424 return 'up';
1425 case 2:
1426 return 'pending';
1427 case 3:
1428 return 'first';
1429 default:
1430 return 'unknown';
1431 }
1432 }
1433
1434 /**
1435 * Get monitor by.
1436 *
1437 * @param WP_REST_Request $request Full details about the request.
1438 *
1439 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by()
1440 *
1441 * @return WP_Error|Object Item.
1442 */
1443 private function get_request_item( $request ) {
1444 // Get id or domain raw value.
1445 $raw = (string) $request->get_param( 'id_domain' );
1446 $raw = trim( $raw );
1447
1448 if ( empty( $raw ) ) {
1449 return false;
1450 }
1451
1452 // Get monitor by monitor id.
1453 if ( ctype_digit( $raw ) ) {
1454 $monitor_id = (int) $raw;
1455 return $this->db->get_monitor_by( false, 'monitor_id', $monitor_id );
1456 }
1457
1458 // Get monitor by domain.
1459 $domain = strtolower( rtrim( rawurldecode( $raw ), '/' ) );
1460 $website = $this->get_site_by( 'domain', $domain );
1461 if ( empty( $website ) ) {
1462 return false;
1463 }
1464
1465 $site_id = (int) $website->id;
1466 return $this->db->get_monitor_by( $site_id, 'wpid', $site_id );
1467 }
1468
1469 /**
1470 * Get apply setting.
1471 *
1472 * @param string $name Name.
1473 * @param mixed $indiv_setting Individual setting.
1474 * @param mixed $global_value Global value.
1475 * @param mixed $default_value Default value.
1476 *
1477 * @uses MainWP_Uptime_Monitoring_Connect::get_apply_setting()
1478 *
1479 * @return mixed
1480 */
1481 private function get_apply_setting( $name, $indiv_setting, $global_value, $default_value ) {
1482 return MainWP_Uptime_Monitoring_Connect::get_apply_setting(
1483 $name,
1484 $indiv_setting,
1485 $this->global_settings,
1486 $global_value,
1487 $default_value
1488 );
1489 }
1490
1491 /**
1492 * Get uptime 24h data.
1493 *
1494 * @param array $data data.
1495 * @param string $last24_starttime last24 start time.
1496 *
1497 * @uses MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp()
1498 *
1499 * @return array uptime 24h data.
1500 */
1501 private function get_uptime_24h_data( $data, $last24_starttime ) {
1502 $uptime_24h = array();
1503 $uptime_data = array();
1504 $hourly_key = MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp( $last24_starttime );
1505
1506 foreach ( $data as $value ) {
1507 $uptime_data[ $value['timestamp'] ] = $value;
1508 }
1509 for ( $i = 0; $i < 24; $i++ ) {
1510 if ( isset( $uptime_data[ $hourly_key ] ) ) {
1511 $uptime_24h[ $i ] = array(
1512 'timestamp' => $hourly_key,
1513 'status' => ! empty( $uptime_data[ $hourly_key ]['up'] ) ? 1 : 0,
1514 );
1515 } else {
1516 $uptime_24h[ $i ] = array(
1517 'timestamp' => $hourly_key,
1518 'status' => 0,
1519 );
1520 }
1521
1522 $hourly_key = MainWP_Uptime_Monitoring_Handle::get_hourly_key_by_timestamp( $hourly_key + HOUR_IN_SECONDS );
1523 }
1524 return $uptime_24h;
1525 }
1526
1527 /**
1528 * Get monitor incidents allowed fields.
1529 *
1530 * @return array Allowed fields.
1531 */
1532 public function get_monitor_incidents_allowed_fields() {
1533 $args = array(
1534 'page' => $this->field_page(),
1535 'per_page' => $this->field_per_page(),
1536 );
1537 return array_merge( $args, $this->get_monitor_allowed_id_domain_field() );
1538 }
1539
1540 /**
1541 * Get allowed fields for monitoring settings.
1542 *
1543 * @return array Allowed fields.
1544 */
1545 private function get_global_monitor_settings_allowed_fields() {
1546 return array(
1547 'active' => array(
1548 'required' => false,
1549 'type' => 'boolean',
1550 'description' => __( 'Active or disable monitoring globally.', 'mainwp' ),
1551 'sanitize_callback' => 'rest_sanitize_boolean',
1552 ),
1553 'interval' => array(
1554 'required' => false,
1555 'type' => 'string',
1556 'description' => __( 'Default check frequency (e.g., "5m", "1h").', 'mainwp' ),
1557 'sanitize_callback' => array( $this, 'sanitize_interval_text_field' ),
1558 'validate_callback' => array( $this, 'settings_validate_interval_param' ),
1559 ),
1560 'timeout' => array(
1561 'required' => false,
1562 'type' => 'string',
1563 'description' => __( 'Request timeout in milliseconds.', 'mainwp' ),
1564 'sanitize_callback' => array( $this, 'sanitize_timeout_text_field' ),
1565 'validate_callback' => array( $this, 'settings_validate_timeout_param' ),
1566 ),
1567 'maxretries' => array(
1568 'required' => false,
1569 'type' => 'integer',
1570 'description' => __( 'Number of retries on failure.', 'mainwp' ),
1571 'sanitize_callback' => 'sanitize_text_field',
1572 'validate_callback' => array( $this, 'settings_validate_maxretries_param' ),
1573 ),
1574 'type' => array(
1575 'required' => false,
1576 'type' => 'string',
1577 'description' => __( 'Monitor type.', 'mainwp' ),
1578 'sanitize_callback' => 'sanitize_text_field',
1579 'validate_callback' => array( $this, 'settings_validate_type_param' ),
1580 ),
1581 'method' => array(
1582 'required' => false,
1583 'type' => 'string',
1584 'description' => __( 'Monitor method.', 'mainwp' ),
1585 'sanitize_callback' => 'sanitize_text_field',
1586 'validate_callback' => array( $this, 'settings_validate_method_param' ),
1587 ),
1588 'expected_status' => array(
1589 'required' => false,
1590 'type' => 'string',
1591 'description' => __( 'Expected HTTP status codes.', 'mainwp' ),
1592 'sanitize_callback' => array( $this, 'sanitize_expected_status_text_field' ),
1593 'validate_callback' => array( $this, 'settings_validate_expected_status_param' ),
1594 ),
1595 'keyword' => array(
1596 'required' => false,
1597 'type' => 'string',
1598 'description' => __( 'Keyword to match.', 'mainwp' ),
1599 'sanitize_callback' => 'sanitize_text_field',
1600 ),
1601 );
1602 }
1603
1604 /**
1605 * Get allowed fields for monitor settings.
1606 *
1607 * @return array Allowed fields.
1608 */
1609 private function get_individual_monitor_settings_allowed_fields() {
1610 return array_merge(
1611 $this->get_monitor_allowed_id_domain_field(),
1612 $this->get_global_monitor_settings_allowed_fields(),
1613 array(
1614 'active' => array(
1615 'required' => false,
1616 'type' => 'string',
1617 'description' => __( 'Active or disable monitoring globally.', 'mainwp' ),
1618 'sanitize_callback' => function ( $value ) {
1619 if ( 'useglobal' === $value ) {
1620 return -1;
1621 }
1622 return $value;
1623 },
1624 'validate_callback' => function ( $value ) {
1625 if ( in_array( $value, array( '0', '1', 'useglobal' ), true ) ) {
1626 return true;
1627 }
1628 return new WP_Error( 'invalid_active', __( 'Invalid active value. Must be 0,1 or useglobal', 'mainwp' ) );
1629 },
1630 ),
1631 )
1632 );
1633 }
1634
1635 /**
1636 * Get allowed fields for monitor heartbeat.
1637 *
1638 * @return array
1639 */
1640 private function get_monitor_heartbeat_allowed_fields() {
1641 $args = array(
1642 'limit' => array(
1643 'required' => false,
1644 'type' => 'integer',
1645 'sanitize_callback' => 'absint',
1646 'description' => __( 'Limit number of heartbeats.', 'mainwp' ),
1647 ),
1648 'period' => array(
1649 'required' => false,
1650 'type' => 'string',
1651 'sanitize_callback' => 'sanitize_text_field',
1652 'validate_callback' => array( $this, 'validate_period_param' ),
1653 'default' => '30d',
1654 'description' => __( 'Time period (24h, 7d, 30d, or ISO8601 range like 2024-01-01T00:00:00Z/2024-01-02T00:00:00Z).', 'mainwp' ),
1655 ),
1656 'since' => array(
1657 'required' => false,
1658 'type' => 'string',
1659 'sanitize_callback' => 'sanitize_text_field',
1660 'validate_callback' => array( $this, 'validate_datetime_param' ),
1661 'description' => __( 'ISO8601 datetime, only return newer entries.', 'mainwp' ),
1662 ),
1663 'status' => $this->field_status(),
1664 'page' => $this->field_page(),
1665 );
1666 return array_merge( $args, $this->get_monitor_allowed_id_domain_field() );
1667 }
1668
1669 /**
1670 * Get allowed fields for monitors.
1671 *
1672 * @return array
1673 */
1674 private function get_monitor_allowed_id_domain_field() {
1675 return array(
1676 'id_domain' => array(
1677 'description' => __( 'Site ID (number) or domain (string).', 'mainwp' ),
1678 'type' => 'string',
1679 'sanitize_callback' => 'sanitize_text_field',
1680 ),
1681 );
1682 }
1683
1684 /**
1685 * Get allowed fields for monitors.
1686 *
1687 * @return array
1688 */
1689 private function get_monitors_allowed_fields() {
1690 return array(
1691 'status' => $this->field_status(),
1692 'search' => $this->field_search(),
1693 'include' => $this->field_include(),
1694 'exclude' => $this->field_exclude(),
1695 'page' => $this->field_page(),
1696 'per_page' => $this->field_per_page(),
1697 );
1698 }
1699
1700 /**
1701 * Get allowed fields for count monitors.
1702 *
1703 * @return array
1704 */
1705 private function get_count_monitors_allowed_fields() {
1706 return array(
1707 'status' => $this->field_status(),
1708 'include' => $this->field_include(),
1709 'exclude' => $this->field_exclude(),
1710 'search' => $this->field_search(),
1711 );
1712 }
1713
1714 /**
1715 * Sanitize timeout text field.
1716 *
1717 * @param string $value Timeout value.
1718 * @param WP_REST_Request $request Request object.
1719 *
1720 * @uses MainWP_Uptime_Monitoring_Edit::get_timeout_values()
1721 *
1722 * @return string|WP_Error
1723 */
1724 public function sanitize_timeout_text_field( $value, $request ) {
1725 $value = sanitize_text_field( wp_unslash( $value ) );
1726
1727 // Get is individual monitor.
1728 $individual = $this->is_individual_monitor( $request );
1729 $value = $individual ? $this->is_use_global_field( $value ) : $value;
1730 $all_timeouts = MainWP_Uptime_Monitoring_Edit::get_timeout_values( $individual );
1731
1732 // Check timeout value exists.
1733 $key = array_search( $value, $all_timeouts, true );
1734 if ( false === $key ) {
1735 return new WP_Error(
1736 'invalid_timeout',
1737 __( 'Invalid timeout value.', 'mainwp' ),
1738 );
1739 }
1740 return (string) $key;
1741 }
1742
1743 /**
1744 * Validate timeout parameter.
1745 *
1746 * @param string $value Timeout value.
1747 * @param WP_REST_Request $request Request object.
1748 * @param string $param Parameter name.
1749 *
1750 * @uses MainWP_Uptime_Monitoring_Edit::get_timeout_values()
1751 *
1752 * @return bool|WP_Error
1753 */
1754 public function settings_validate_timeout_param( $value, $request, $param ) {
1755 if ( empty( $value ) ) {
1756 return true; // Allow empty, will use default.
1757 }
1758
1759 $value = sanitize_text_field( wp_unslash( $value ) );
1760 // Get is individual monitor.
1761 $individual = $this->is_individual_monitor( $request );
1762 $value = $individual ? $this->is_use_global_field( $value ) : $value;
1763 $all_timeouts = MainWP_Uptime_Monitoring_Edit::get_timeout_values( $individual );
1764
1765 // Check value exists.
1766 $result = array_search( $value, $all_timeouts ); // check value exists.
1767 if ( false === $result ) {
1768 return new WP_Error(
1769 'invalid_timeout',
1770 __( 'Invalid timeout value.', 'mainwp' ),
1771 );
1772 }
1773
1774 return true;
1775 }
1776
1777 /**
1778 * Sanitize interval text field.
1779 *
1780 * @param string $value Interval value.
1781 * @param WP_REST_Request $request Request object.
1782 *
1783 * @uses MainWP_Uptime_Monitoring_Edit::get_interval_values()
1784 *
1785 * @return string|WP_Error
1786 */
1787 public function sanitize_interval_text_field( $value, $request ) {
1788 $value = sanitize_text_field( wp_unslash( $value ) );
1789
1790 // Get is individual monitor.
1791 $individual = $this->is_individual_monitor( $request );
1792 $value = $individual ? $this->is_use_global_field( $value ) : $value;
1793
1794 // Get interval values.
1795 $interval_values = MainWP_Uptime_Monitoring_Edit::get_interval_values( $individual );
1796
1797 // Check value exists.
1798 $key = array_search( $value, $interval_values, true );
1799 if ( false === $key ) {
1800 return new WP_Error(
1801 'invalid_interval',
1802 __( 'Invalid interval value.', 'mainwp' ),
1803 );
1804 }
1805
1806 return (string) $key;
1807 }
1808
1809 /**
1810 * Validate frequency parameter.
1811 *
1812 * @param string $value Frequency value.
1813 * @param WP_REST_Request $request Request object.
1814 * @param string $param Parameter name.
1815 *
1816 * @uses MainWP_Uptime_Monitoring_Edit::get_interval_values()
1817 *
1818 * @return bool|WP_Error
1819 */
1820 public function settings_validate_interval_param( $value, $request, $param ) {
1821 if ( empty( $value ) ) {
1822 return true; // Allow empty, will use default.
1823 }
1824
1825 $value = sanitize_text_field( wp_unslash( $value ) );
1826
1827 // Get is individual monitor.
1828 $individual = $this->is_individual_monitor( $request );
1829 $value = $individual ? $this->is_use_global_field( $value ) : $value;
1830
1831 // Get interval values.
1832 $interval_values = MainWP_Uptime_Monitoring_Edit::get_interval_values( $individual );
1833
1834 // Check value exists.
1835 $result = array_search( $value, $interval_values ); // check value exists.
1836 if ( false === $result ) {
1837 return new WP_Error(
1838 'invalid_interval',
1839 __( 'Invalid interval value.', 'mainwp' ),
1840 );
1841 }
1842
1843 // Return the interval value.
1844 return true;
1845 }
1846
1847 /**
1848 * Sanitize expected status text field.
1849 *
1850 * @param string $value Expected status value.
1851 * @param WP_REST_Request $request Request object.
1852 *
1853 * @uses MainWP_Utility::get_http_codes()
1854 *
1855 * @return array
1856 */
1857 public function sanitize_expected_status_text_field( $value, $request ) {
1858 // Get value field.
1859 $value = sanitize_text_field( wp_unslash( $value ) );
1860 // Get individual monitor.
1861 $individual = $this->is_individual_monitor( $request );
1862 if ( $individual && 'useglobal' === $value ) {
1863 return $value;
1864 }
1865
1866 // Get http error codes.
1867 $http_error_codes = array_keys( MainWP_Utility::get_http_codes() );
1868 $status_codes = ! empty( $value ) ? wp_parse_id_list( $value ) : array();
1869
1870 foreach ( $status_codes as $code ) {
1871 if ( ! in_array( $code, $http_error_codes ) ) {
1872 return new WP_Error(
1873 'invalid_status_code',
1874 /* translators: %d: HTTP status code */ sprintf( __( 'Invalid HTTP status code: %d', 'mainwp' ), $code ),
1875 array( 'status' => 400 )
1876 );
1877 }
1878 }
1879 return $value;
1880 }
1881
1882 /**
1883 * Validate expected status parameter.
1884 *
1885 * @param array $value Expected status value.
1886 * @param WP_REST_Request $request Request object.
1887 * @param string $param Parameter name.
1888 *
1889 * @uses MainWP_Utility::get_http_codes()
1890 *
1891 * @return bool|WP_Error
1892 */
1893 public function settings_validate_expected_status_param( $value, $request, $param ) {
1894 if ( empty( $value ) ) {
1895 return true; // Allow empty, will use default.
1896 }
1897
1898 $value = sanitize_text_field( wp_unslash( $value ) );
1899 // Get individual monitor.
1900 $individual = $this->is_individual_monitor( $request );
1901 if ( $individual && 'useglobal' === $value ) {
1902 return true;
1903 }
1904
1905 // Get http error codes.
1906 $http_error_codes = array_keys( MainWP_Utility::get_http_codes() );
1907 $status_codes = ! empty( $value ) ? wp_parse_id_list( $value ) : array();
1908 foreach ( $status_codes as $code ) {
1909 if ( ! in_array( $code, $http_error_codes ) ) {
1910 return new WP_Error(
1911 'invalid_status_code',
1912 /* translators: %d: HTTP status code */ sprintf( __( 'Invalid HTTP status code: %d', 'mainwp' ), $code ),
1913 array( 'status' => 400 )
1914 );
1915 }
1916 }
1917
1918 return true;
1919 }
1920
1921 /**
1922 * Validate maxretries parameter.
1923 *
1924 * @param int $value Max retries value.
1925 * @param WP_REST_Request $request Request object.
1926 * @param string $param Parameter name.
1927 * @return bool
1928 */
1929 public function settings_validate_maxretries_param( $value, $request, $param ) {
1930 if ( empty( $value ) ) {
1931 return true; // Allow empty, will use default.
1932 }
1933 if ( $value < -1 || $value > 1 ) {
1934 return new WP_Error(
1935 'invalid_maxretries',
1936 __( 'The maxretries value must be -1, 0, or 1.', 'mainwp' ),
1937 );
1938 }
1939 return true;
1940 }
1941
1942 /**
1943 * Validate type parameter.
1944 *
1945 * @param string $value Type value.
1946 * @param WP_REST_Request $request Request object.
1947 * @param string $param Parameter name.
1948 * @return bool|WP_Error
1949 */
1950 public function settings_validate_type_param( $value, $request, $param ) {
1951 if ( empty( $value ) ) {
1952 return true; // Allow empty, will use default.
1953 }
1954
1955 if ( ! in_array( $value, array_keys( self::TYPE_MAP ), true ) ) {
1956 return new WP_Error(
1957 'invalid_type',
1958 __( 'Invalid type value.', 'mainwp' ),
1959 );
1960 }
1961 return true;
1962 }
1963
1964 /**
1965 * Validate method parameter.
1966 *
1967 * @param string $value Method value.
1968 * @param WP_REST_Request $request Request object.
1969 * @param string $param Parameter name.
1970 * @return bool|WP_Error
1971 */
1972 public function settings_validate_method_param( $value, $request, $param ) {
1973 if ( empty( $value ) ) {
1974 return true; // Allow empty, will use default.
1975 }
1976
1977 if ( ! in_array( $value, array_keys( self::METHOD_MAP ), true ) ) {
1978 return new WP_Error(
1979 'invalid_method',
1980 __( 'Invalid method value.', 'mainwp' ),
1981 );
1982 }
1983 return true;
1984 }
1985
1986 /**
1987 * Validate period parameter.
1988 *
1989 * @param string $value Period value.
1990 * @param WP_REST_Request $request Request object.
1991 * @param string $param Parameter name.
1992 * @return bool|WP_Error
1993 */
1994 public function validate_period_param( $value, $request, $param ) {
1995 if ( empty( $value ) ) {
1996 return true; // Allow empty, will use default.
1997 }
1998
1999 // Check predefined periods.
2000 $allowed_periods = array( '24h', '7d', '30d' );
2001 if ( in_array( $value, $allowed_periods, true ) ) {
2002 return true;
2003 }
2004
2005 // Check ISO8601 range format (start/end).
2006 if ( strpos( $value, '/' ) !== false ) {
2007 $parts = explode( '/', $value );
2008 if ( count( $parts ) !== 2 ) {
2009 return new WP_Error(
2010 'invalid_period_format',
2011 __( 'ISO8601 range must be in format: start/end (e.g., 2024-01-01T00:00:00Z/2024-01-02T00:00:00Z)', 'mainwp' )
2012 );
2013 }
2014
2015 $start_time = $this->validate_iso8601_datetime( $parts[0] );
2016 $end_time = $this->validate_iso8601_datetime( $parts[1] );
2017
2018 if ( ! $start_time ) {
2019 return new WP_Error(
2020 'invalid_start_datetime',
2021 __( 'Invalid start datetime format. Use ISO8601 format (e.g., 2024-01-01T00:00:00Z)', 'mainwp' )
2022 );
2023 }
2024
2025 if ( ! $end_time ) {
2026 return new WP_Error(
2027 'invalid_end_datetime',
2028 __( 'Invalid end datetime format. Use ISO8601 format (e.g., 2024-01-01T00:00:00Z)', 'mainwp' )
2029 );
2030 }
2031
2032 if ( $start_time >= $end_time ) {
2033 return new WP_Error(
2034 'invalid_date_range',
2035 __( 'Start datetime must be earlier than end datetime.', 'mainwp' )
2036 );
2037 }
2038
2039 return true;
2040 }
2041
2042 return new WP_Error(
2043 'invalid_period',
2044 sprintf(
2045 /* translators: %s: Comma-separated list of allowed period values */
2046 __( 'Invalid period format. Allowed values: %s or ISO8601 range (start/end)', 'mainwp' ),
2047 implode( ', ', $allowed_periods )
2048 )
2049 );
2050 }
2051
2052 /**
2053 * Validate datetime parameter.
2054 *
2055 * @param string $value Datetime value.
2056 * @param WP_REST_Request $request Request object.
2057 * @param string $param Parameter name.
2058 * @return bool|WP_Error
2059 */
2060 public function validate_datetime_param( $value, $request, $param ) {
2061 if ( empty( $value ) ) {
2062 return true; // Allow empty.
2063 }
2064
2065 if ( ! $this->validate_iso8601_datetime( $value ) ) {
2066 return new WP_Error(
2067 'invalid_datetime',
2068 __( 'Invalid datetime format. Use ISO8601 format (e.g., 2024-01-01T00:00:00Z)', 'mainwp' )
2069 );
2070 }
2071
2072 return true;
2073 }
2074
2075 /**
2076 * Validate ISO8601 datetime format.
2077 *
2078 * @param string $datetime Datetime string.
2079 * @return int|false Unix timestamp or false if invalid.
2080 */
2081 private function validate_iso8601_datetime( $datetime ) {
2082 // Try to parse various ISO8601 formats.
2083 $formats = array(
2084 'Y-m-d\TH:i:s\Z',
2085 'Y-m-d\TH:i:sP',
2086 'Y-m-d\TH:i:s',
2087 'Y-m-d H:i:s',
2088 'Y-m-d',
2089 );
2090
2091 foreach ( $formats as $format ) {
2092 $date = DateTime::createFromFormat( $format, $datetime );
2093 if ( $date && $date->format( $format ) === $datetime ) {
2094 return $date->getTimestamp();
2095 }
2096 }
2097
2098 // Try strtotime as fallback.
2099 $timestamp = strtotime( $datetime );
2100 return false !== $timestamp ? $timestamp : false;
2101 }
2102
2103 /**
2104 * Validate status parameter.
2105 *
2106 * @param mixed $param The parameter value.
2107 * @return bool|WP_Error
2108 */
2109 public function validate_status_param( $param ) {
2110 if ( is_string( $param ) ) {
2111 return array_key_exists( $param, self::STATUS_MAP );
2112 }
2113
2114 if ( is_numeric( $param ) ) {
2115 $int = (int) $param;
2116 return in_array( $int, array_values( self::STATUS_MAP ), true );
2117 }
2118
2119 return new WP_Error(
2120 'invalid_status',
2121 __( 'Invalid status value.', 'mainwp' ),
2122 );
2123 }
2124
2125 /**
2126 * Validate retries value.
2127 *
2128 * @param mixed $retries Number of retries.
2129 * @return bool|WP_Error
2130 */
2131 private function validate_retries( $retries ) {
2132 $retries = (int) $retries;
2133 if ( $retries < -1 || $retries > 10 ) {
2134 return new WP_Error(
2135 'invalid_retries',
2136 __( 'Retries must be between 0 and 10.', 'mainwp' ),
2137 );
2138 }
2139 return true;
2140 }
2141
2142 /**
2143 * Sanitize status parameter - convert string to numeric.
2144 *
2145 * @param mixed $param The parameter value.
2146 * @return int
2147 */
2148 public function sanitize_status_param( $param ) {
2149 if ( is_string( $param ) && isset( self::STATUS_MAP[ $param ] ) ) {
2150 return (int) self::STATUS_MAP[ $param ];
2151 }
2152 return (int) $param;
2153 }
2154
2155 /**
2156 * Check if monitor is individual.
2157 *
2158 * @param WP_REST_Request $request Request object.
2159 * @return bool
2160 */
2161 private function is_individual_monitor( $request ) {
2162 $is_domain = $request->get_param( 'id_domain' );
2163 return ! empty( $is_domain ) ? true : false;
2164 }
2165
2166 /**
2167 * Check if value is use global.
2168 *
2169 * @param mixed $value Value to check.
2170 * @return mixed
2171 */
2172 private function is_use_global_field( $value ) {
2173 if ( 'useglobal' === $value ) {
2174 return 'Use global setting';
2175 }
2176 return $value;
2177 }
2178
2179 /**
2180 * Get status field.
2181 *
2182 * @return array
2183 */
2184 private function field_status() {
2185 return array(
2186 'required' => false,
2187 'type' => 'string',
2188 'description' => __( 'Monitor status.', 'mainwp' ),
2189 'validate_callback' => array( $this, 'validate_status_param' ),
2190 'sanitize_callback' => array( $this, 'sanitize_status_param' ),
2191 );
2192 }
2193
2194 /**
2195 * Get search field.
2196 *
2197 * @return array
2198 */
2199 private function field_search() {
2200 return array(
2201 'required' => false,
2202 'type' => 'string',
2203 'sanitize_callback' => 'sanitize_text_field',
2204 'description' => __( 'Search string.', 'mainwp' ),
2205 );
2206 }
2207
2208 /**
2209 * Get include field.
2210 *
2211 * @return array
2212 */
2213 private function field_include() {
2214 return array(
2215 'required' => false,
2216 'type' => 'string',
2217 'sanitize_callback' => 'sanitize_text_field',
2218 'description' => __( 'Include monitor IDs.', 'mainwp' ),
2219 );
2220 }
2221
2222 /**
2223 * Get exclude field.
2224 *
2225 * @return array
2226 */
2227 private function field_exclude() {
2228 return array(
2229 'required' => false,
2230 'type' => 'string',
2231 'sanitize_callback' => 'sanitize_text_field',
2232 'description' => __( 'Exclude monitor IDs.', 'mainwp' ),
2233 );
2234 }
2235
2236 /**
2237 * Get page field.
2238 *
2239 * @return array
2240 */
2241 private function field_page() {
2242 return array(
2243 'required' => false,
2244 'type' => 'integer',
2245 'default' => 1,
2246 'minimum' => 1,
2247 'sanitize_callback' => 'absint',
2248 'description' => __( 'Page number.', 'mainwp' ),
2249 );
2250 }
2251
2252 /**
2253 * Get per_page field.
2254 *
2255 * @return array
2256 */
2257 private function field_per_page() {
2258 return array(
2259 'required' => false,
2260 'type' => 'integer',
2261 'sanitize_callback' => 'absint',
2262 'minimum' => 1,
2263 'default' => 20,
2264 'maximum' => 200,
2265 'description' => __( 'Number of monitors per page.', 'mainwp' ),
2266 );
2267 }
2268
2269 /**
2270 * Get the monitor schema, conforming to JSON Schema.
2271 *
2272 * @since 5.2
2273 * @return array
2274 */
2275 private function get_monitor_settings_schema() {
2276 return array(
2277 '$schema' => 'http://json-schema.org/draft-04/schema#',
2278 'title' => 'monitor settings',
2279 'type' => 'object',
2280 'additionalProperties' => false,
2281 'properties' => array(
2282 'active' => array( 'type' => 'integer' ),
2283 'type' => array(
2284 'type' => 'string',
2285 'enum' => array_keys( self::TYPE_MAP ),
2286 ),
2287 'method' => array(
2288 'type' => 'string',
2289 'enum' => array_keys( self::METHOD_MAP ),
2290 ),
2291 'maxretries' => array( 'type' => 'integer' ),
2292 'timeout' => array( 'type' => 'integer' ),
2293 'interval' => array( 'type' => 'string' ),
2294 'expected_status' => array( 'type' => array( 'string' ) ),
2295 'keyword' => array( 'type' => 'string' ),
2296 'bypass_cache' => array(
2297 'type' => 'integer',
2298 'enum' => array( -1, 0, 1 ),
2299 'description' => __( 'Cache bypass setting. -1 uses the global setting, 0 disables it, and 1 enables it.', 'mainwp' ),
2300 ),
2301 ),
2302 );
2303 }
2304
2305 /**
2306 * Get the Tags schema, conforming to JSON Schema.
2307 *
2308 * @since 5.2
2309 * @return array
2310 */
2311 public function get_item_schema() { // phpcs:ignore -- NOSONAR - long schema.
2312 return array(
2313 '$schema' => 'http://json-schema.org/draft-04/schema#',
2314 'title' => 'monitors',
2315 'type' => 'object',
2316 'properties' => array(
2317 'id' => array(
2318 'type' => 'integer',
2319 'description' => __( 'Monitor ID.', 'mainwp' ),
2320 'context' => array( 'view', 'monitor_view', 'simple_view' ),
2321 'readonly' => true,
2322 ),
2323 'monitor_id' => array(
2324 'type' => 'integer',
2325 'readonly' => true,
2326 'description' => __( 'Monitor ID', 'mainwp' ),
2327 'context' => array( 'heartbeat_view' ),
2328 ),
2329 'heartbeat_id' => array(
2330 'type' => 'integer',
2331 'readonly' => true,
2332 'description' => __( 'Heartbeat ID', 'mainwp' ),
2333 'context' => array( 'heartbeat_view' ),
2334 ),
2335 'name' => array(
2336 'type' => 'string',
2337 'description' => __( 'Website name.', 'mainwp' ),
2338 'context' => array( 'monitor_view' ),
2339 ),
2340 'url' => array(
2341 'type' => 'string',
2342 'description' => __( 'Website URL.', 'mainwp' ),
2343 'format' => 'uri',
2344 'context' => array( 'view', 'monitor_view', 'simple_view' ),
2345 ),
2346 'type' => array(
2347 'type' => 'string',
2348 'description' => __( 'Monitor type.', 'mainwp' ),
2349 'context' => array( 'view', 'monitor_view' ),
2350 ),
2351 'bypass_cache' => array(
2352 'type' => 'integer',
2353 'description' => __( 'Attempt to bypass page cache.', 'mainwp' ),
2354 'context' => array( 'view', 'monitor_view' ),
2355 ),
2356 'last_status' => array(
2357 'type' => 'string',
2358 'description' => __( 'Monitor Status.', 'mainwp' ),
2359 'enum' => array( 'up', 'down', 'pending', 'first' ),
2360 'context' => array( 'view', 'simple_view', 'monitor_view' ),
2361 ),
2362 'status' => array(
2363 'type' => 'string',
2364 'description' => __( 'Monitor Status.', 'mainwp' ),
2365 'enum' => array( 'up', 'down', 'pending', 'first' ),
2366 'context' => array( 'heartbeat_view' ),
2367 ),
2368 'check_frequency' => array(
2369 'type' => 'string',
2370 'description' => __( 'Check Frequency.', 'mainwp' ),
2371 'context' => array( 'view', 'monitor_view' ),
2372 ),
2373 'last_check_at' => array(
2374 'type' => array( 'string', 'null' ),
2375 'format' => 'date-time',
2376 'description' => __( 'Check interval (e.g. 1m, 5m)', 'mainwp' ),
2377 'context' => array( 'view', 'monitor_view' ),
2378 ),
2379 'last_status_code' => array(
2380 'description' => 'Last HTTP status code if applicable',
2381 'type' => array( 'integer', 'null' ),
2382 'context' => array( 'view', 'monitor_view' ),
2383 ),
2384 'uptime_24h' => array(
2385 'type' => 'array',
2386 'description' => __( 'Uptime % for last 24h', 'mainwp' ),
2387 'context' => array( 'view', 'monitor_view' ),
2388 'items' => array(
2389 'type' => 'object',
2390 'properties' => array(
2391 'timestamp' => array(
2392 'type' => 'integer',
2393 'description' => __( 'Unix timestamp', 'mainwp' ),
2394 ),
2395 'status' => array(
2396 'type' => 'integer',
2397 'description' => __( 'Status code (1=up, 0=down)', 'mainwp' ),
2398 ),
2399 ),
2400 ),
2401 ),
2402 'uptime_ratio_7d' => array(
2403 'type' => 'float',
2404 'description' => __( 'Uptime % over last 7 days', 'mainwp' ),
2405 'context' => array( 'view', 'monitor_view' ),
2406 ),
2407 'uptime_ratio_30d' => array(
2408 'type' => 'float',
2409 'description' => __( 'Uptime % over last 30 days', 'mainwp' ),
2410 'context' => array( 'view', 'monitor_view' ),
2411 ),
2412 'incidents_count_7d' => array(
2413 'type' => 'integer',
2414 'description' => __( 'Incidents count last 7 days', 'mainwp' ),
2415 'context' => array( 'view', 'monitor_view' ),
2416 ),
2417 'incidents_count_30d' => array(
2418 'type' => 'integer',
2419 'description' => __( 'Incidents count last 30 days', 'mainwp' ),
2420 'context' => array( 'view', 'monitor_view' ),
2421 ),
2422 'response_time_avg_ms' => array(
2423 'type' => 'integer',
2424 'description' => __( 'Average response time (ms)', 'mainwp' ),
2425 'context' => array( 'view', 'monitor_view' ),
2426 ),
2427 'response_time_min_ms' => array(
2428 'type' => 'integer',
2429 'description' => __( 'Min response time (ms)', 'mainwp' ),
2430 'context' => array( 'view', 'monitor_view' ),
2431 ),
2432 'response_time_max_ms' => array(
2433 'type' => 'integer',
2434 'description' => __( 'Max response time (ms)', 'mainwp' ),
2435 'context' => array( 'view', 'monitor_view' ),
2436 ),
2437 'heartbeats' => array(
2438 'type' => 'array',
2439 'description' => __( 'Uptime % for last 24h', 'mainwp' ),
2440 'context' => array( 'monitor_view' ),
2441 'items' => array(
2442 'type' => 'object',
2443 ),
2444 ),
2445 'msg' => array(
2446 'type' => 'string',
2447 'description' => __( 'Heartbeat message', 'mainwp' ),
2448 'context' => array( 'heartbeat_view' ),
2449 ),
2450 'importance' => array(
2451 'type' => 'integer',
2452 'description' => __( 'Importance status', 'mainwp' ),
2453 'context' => array( 'heartbeat_view' ),
2454 ),
2455 'time' => array(
2456 'type' => 'string',
2457 'format' => 'date-time',
2458 'description' => __( 'Heartbeat time', 'mainwp' ),
2459 'context' => array( 'heartbeat_view' ),
2460 ),
2461 'ping_ms' => array(
2462 'type' => 'integer',
2463 'description' => __( 'Ping time (ms)', 'mainwp' ),
2464 'context' => array( 'heartbeat_view' ),
2465 ),
2466 'http_code' => array(
2467 'type' => 'integer',
2468 'description' => __( 'HTTP status code', 'mainwp' ),
2469 'context' => array( 'heartbeat_view' ),
2470 ),
2471 'started_at' => array(
2472 'type' => 'string',
2473 'format' => 'date-time',
2474 'description' => __( 'Timestamp of the first down in the run', 'mainwp' ),
2475 'context' => array( 'incidents_view' ),
2476 ),
2477 'ended_at' => array(
2478 'type' => array( 'string', 'null' ),
2479 'format' => 'date-time',
2480 'description' => __( 'Timestamp of the first subsequent up; null if ongoing', 'mainwp' ),
2481 'context' => array( 'incidents_view' ),
2482 ),
2483 'duration_sec' => array(
2484 'type' => array( 'integer', 'null' ),
2485 'description' => __( 'Duration in seconds; null if ongoing', 'mainwp' ),
2486 'context' => array( 'incidents_view' ),
2487 ),
2488 'resolved' => array(
2489 'type' => 'boolean',
2490 'description' => __( 'True if ended_at is present', 'mainwp' ),
2491 'context' => array( 'incidents_view' ),
2492 ),
2493 'down_count' => array(
2494 'type' => 'integer',
2495 'description' => __( 'Number of down heartbeats in the run', 'mainwp' ),
2496 'context' => array( 'incidents_view' ),
2497 ),
2498 ),
2499 );
2500 }
2501 }
2502