\WP_REST_Server::READABLE, 'callback' => array( $this, 'get_scheduled_refreshes_site' ), 'permission_callback' => $this->get_admin_permission_callback(), ), array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'schedule_refresh_site' ), 'permission_callback' => $this->get_admin_permission_callback(), ), ), ); self::register_rest_endpoint( self::ENDPOINT_PATH_CRON_STATUS, self::ENDPOINT_VERSION, array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_cron_status' ), 'permission_callback' => $this->get_admin_permission_callback(), ), ); // Register DELETE endpoint with ID path parameter. self::register_rest_endpoint( self::ENDPOINT_PATH . '/(?P[a-f0-9\-]+)', self::ENDPOINT_VERSION, array( array( 'methods' => \WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_schedule_refresh_site' ), 'permission_callback' => $this->get_admin_permission_callback(), 'args' => array( 'id' => array( 'description' => 'The unique identifier of the scheduled refresh.', 'type' => 'string', 'required' => true, ), ), ), ), ); } /** * Method for registering all of our actions. * * @return void */ public function register_actions(): void { add_action( 'init', array( $this, 'track_cron_run' ) ); add_action( self::ACTION_NAME_SCHEDULE_REFRESH_SITE, array( $this, 'executeSiteRefresh' ) ); } /** * Record the most recent WordPress cron request timestamp. * * @return void */ public function track_cron_run(): void { if ( ! wp_doing_cron() ) { return; } self::save_last_cron_run( time() ); } /** * Method for executing a scheduled site refresh. * * @param string $uuid The UUID for the scheduled refresh. * * @return void */ public function executeSiteRefresh( string $uuid ): void { Refresh_Service::update_version_site(); } /** * Method for getting the timestamp of the last cron execution. * * @return int|null Unix timestamp of the last run, or null if never run. */ public static function get_last_cron_run(): ?int { $value = get_option( self::OPTION_NAME_LAST_CRON_RUN, null ); return null !== $value ? (int) $value : null; } /** * Persist the latest tracked cron run timestamp. * * @param int $timestamp The Unix timestamp for the cron run. * * @return void */ private static function save_last_cron_run( int $timestamp ): void { update_option( self::OPTION_NAME_LAST_CRON_RUN, $timestamp ); } /** * Method to get all scheduled refreshes from a cron event. * * @param array $cron_event The cron event. * @param int $timestamp The Unix timestamp for the event. * * @return array The scheduled refreshes */ public static function get_scheduled_refreshes_from_cron_event( array $cron_event, int $timestamp ) { $scheduled_refreshes = array(); foreach ( $cron_event as $event_name => $event_data ) { if ( self::ACTION_NAME_SCHEDULE_REFRESH_SITE === $event_name ) { foreach ( $event_data as $event ) { array_push( $scheduled_refreshes, array( 'timestamp' => $timestamp, 'id' => $event['args'][0], ) ); } } } return $scheduled_refreshes; } /** * Method for getting all scheduled refresh. * * @return mixed The scheduled events. */ public static function get_scheduled_refreshes() { $scheduled_refreshes = array(); $cron = get_option( 'cron' ); if ( ! is_array( $cron ) ) { return $scheduled_refreshes; } foreach ( $cron as $timestamp => $cron_event ) { if ( ! is_array( $cron_event ) ) { continue; } $scheduled_refreshes = array_merge( $scheduled_refreshes, self::get_scheduled_refreshes_from_cron_event( $cron_event, (int) $timestamp ), ); } return self::sort_scheduled_refreshes_by_timestamp_desc( $scheduled_refreshes ); } /** * Sort scheduled refreshes by timestamp in descending order (newest first). * * @param array $scheduled_refreshes Array of scheduled refresh items. * * @return array Sorted scheduled refreshes. */ public static function sort_scheduled_refreshes_by_timestamp_desc( array $scheduled_refreshes ): array { usort( $scheduled_refreshes, function ( $a, $b ) { return $b['timestamp'] - $a['timestamp']; } ); return $scheduled_refreshes; } /** * Method for getting all scheduled refreshes. * * @return \WP_REST_Response */ public function get_scheduled_refreshes_site(): \WP_REST_Response { return $this->return_api_response( \WP_Http::OK, 'Successfully retrieved scheduled refreshes.', array( 'scheduled_refreshes' => self::get_scheduled_refreshes(), ) ); } /** * Method for deleting a scheduled refresh. * * @param \WP_REST_Request $request The request object. * * @return \WP_REST_Response */ public function delete_schedule_refresh_site( \WP_REST_Request $request ): \WP_REST_Response { $id = $request->get_param( 'id' ) ?? null; // Validate ID is provided. if ( empty( $id ) ) { return $this->return_api_response( \WP_Http::BAD_REQUEST, 'Missing schedule ID.', array() ); } // Check if the scheduled event exists before attempting to delete. $cron = get_option( 'cron' ); $event_exists = false; if ( is_array( $cron ) ) { foreach ( $cron as $cron_event ) { if ( ! is_array( $cron_event ) ) { continue; } if ( isset( $cron_event[ self::ACTION_NAME_SCHEDULE_REFRESH_SITE ] ) ) { foreach ( $cron_event[ self::ACTION_NAME_SCHEDULE_REFRESH_SITE ] as $event ) { if ( isset( $event['args'][0] ) && $event['args'][0] === $id ) { $event_exists = true; break 2; } } } } } // Return 404 if event doesn't exist. if ( ! $event_exists ) { return $this->return_api_response( \WP_Http::NOT_FOUND, 'Scheduled refresh not found.', array( 'id' => $id ) ); } // Attempt to delete the scheduled hook. $deleted = wp_clear_scheduled_hook( self::ACTION_NAME_SCHEDULE_REFRESH_SITE, array( $id ) ); // Check if deletion was successful. if ( ! $deleted ) { return $this->return_api_response( \WP_Http::INTERNAL_SERVER_ERROR, 'Failed to delete scheduled refresh.', array( 'id' => $id ) ); } // Successfully deleted. return $this->return_api_response( \WP_Http::ACCEPTED, 'You\'ve successfully deleted a site refresh.', array( 'id' => $id, ) ); } /** * Method for scheduling a site refresh. * * @param \WP_REST_Request $request The request object. * * @return \WP_REST_Response */ public function schedule_refresh_site( \WP_REST_Request $request ): \WP_REST_Response { $scheduled_refresh = $request->get_param( 'schedule_refresh_timestamp' ) ?? null; // Validate timestamp is provided. if ( empty( $scheduled_refresh ) ) { return $this->return_api_response( \WP_Http::BAD_REQUEST, 'Missing required parameter: schedule_refresh_timestamp', array() ); } // Parse the timestamp. $scheduled_refresh_time = strtotime( $scheduled_refresh ); // Validate timestamp is valid. if ( false === $scheduled_refresh_time ) { return $this->return_api_response( \WP_Http::BAD_REQUEST, 'Invalid timestamp format.', array() ); } // Validate timestamp is in the future. if ( $scheduled_refresh_time <= time() ) { return $this->return_api_response( \WP_Http::BAD_REQUEST, 'Scheduled time must be in the future.', array() ); } $uuid = wp_generate_uuid4(); // Attempt to schedule the event. $scheduled = wp_schedule_single_event( $scheduled_refresh_time, self::ACTION_NAME_SCHEDULE_REFRESH_SITE, array( $uuid ) ); // Check if scheduling was successful. if ( false === $scheduled ) { return $this->return_api_response( \WP_Http::INTERNAL_SERVER_ERROR, 'Failed to schedule refresh.', array() ); } return $this->return_api_response( \WP_Http::CREATED, 'You\'ve successfully scheduled a site refresh.', array( 'scheduled_refresh_time' => $scheduled_refresh_time, 'id' => $uuid, ) ); } /** * Returns the cron status for this site. * * @return \WP_REST_Response */ public function get_cron_status(): \WP_REST_Response { return $this->return_api_response( \WP_Http::OK, 'Successfully retrieved cron status.', array( 'last_cron_run' => self::get_last_cron_run(), ) ); } /** * Method for getting the endpoint for this service. * * @return string The service endpoint. */ public static function get_rest_endpoint(): string { return self::get_formatted_rest_endpoint( self::ENDPOINT_PATH, self::ENDPOINT_VERSION ); } /** * Method for getting the cron status endpoint. * * @return string The cron status endpoint. */ public static function get_rest_endpoint_cron_status(): string { return self::get_formatted_rest_endpoint( self::ENDPOINT_PATH_CRON_STATUS, self::ENDPOINT_VERSION ); } }