• File: StepLogRestController.php
  • Full Path: /home/slvgwrnz/public_html/wp-content/plugins/woocommerce/src/Internal/PushNotifications/Controllers/StepLogRestController.php
  • Date Modified: 10/26/2026 10:38 AM
  • File size: 9.79 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php

declare( strict_types = 1 );

namespace Automattic\WooCommerce\Internal\PushNotifications\Controllers;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
use Automattic\WooCommerce\Internal\PushNotifications\Services\StepLogQuery;
use Automattic\WooCommerce\Internal\PushNotifications\Traits\AuthorizesPushNotificationRequests;
use Automattic\WooCommerce\Internal\RestApiControllerBase;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Read-only REST endpoints through which Mission Control reads the push
 * notification step log: everything recorded on the store, the journey of one
 * notification, and the attempts made against one device or one user's devices.
 *
 * Only WPCOM can call these. Every response carries the store's logging state
 * and the earliest time the read covered, so a screen with no rows can say why.
 *
 * @since 11.3.0
 */
class StepLogRestController extends RestApiControllerBase {
	use AuthorizesPushNotificationRequests;

	/**
	 * How far back a request reads when it gives no `from`.
	 */
	const DEFAULT_RANGE_SECONDS = 2 * DAY_IN_SECONDS;

	/**
	 * The longest range a request may ask for.
	 *
	 * The file reader opens one path per day per rotation per source, so an
	 * unbounded range is a stat call per day of however long the caller asked
	 * for. Set past the 30 day default log retention, since nothing older than
	 * that survives to be read anyway.
	 */
	const MAX_RANGE_SECONDS = 90 * DAY_IN_SECONDS;

	/**
	 * The root namespace for the JSON REST API endpoints.
	 *
	 * @var string
	 */
	protected string $route_namespace = 'wc-push-notifications';

	/**
	 * The REST base for the endpoints URL.
	 *
	 * @var string
	 */
	protected string $rest_base = 'step-logs';

	/**
	 * Rows returned when the caller asks for no particular number.
	 */
	const DEFAULT_LIMIT = 100;

	/**
	 * Most rows a caller may ask for in one page.
	 */
	const MAX_LIMIT = 500;

	/**
	 * The step log query service.
	 *
	 * @var StepLogQuery
	 */
	private StepLogQuery $query;

	/**
	 * Initialize injected dependencies.
	 *
	 * @internal
	 *
	 * @param StepLogQuery $query The step log query service.
	 *
	 * @since 11.3.0
	 */
	final public function init( StepLogQuery $query ): void {
		$this->query = $query;
	}

	/**
	 * Class identifier used by `woocommerce_rest_api_get_rest_namespaces`.
	 *
	 * Distinct from the URL namespace, because the filter keys one class per
	 * value and sibling controllers share the URL namespace.
	 *
	 * @return string
	 *
	 * @since 11.3.0
	 */
	protected function get_rest_api_namespace(): string {
		return 'wc-push-notifications-step-logs';
	}

	/**
	 * Registers the routes.
	 *
	 * @return void
	 *
	 * @since 11.3.0
	 */
	public function register_routes(): void {
		register_rest_route(
			$this->route_namespace,
			$this->rest_base . '/site',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'get_site' ),
					'permission_callback' => array( $this, 'authorize_as_from_wpcom' ),
					'args'                => $this->get_range_args(),
				),
			)
		);

		register_rest_route(
			$this->route_namespace,
			$this->rest_base . '/notifications/(?P<type>[a-z_]+)/(?P<resource_id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'get_notification' ),
					'permission_callback' => array( $this, 'authorize_as_from_wpcom' ),
					'args'                => array_merge(
						array(
							'type'        => array(
								'type'              => 'string',
								'required'          => true,
								'enum'              => array_keys( Notification::NOTIFICATION_CLASSES ),
								'sanitize_callback' => 'sanitize_key',
								'validate_callback' => 'rest_validate_request_arg',
							),
							'resource_id' => array(
								'type'              => 'integer',
								'required'          => true,
								'minimum'           => 1,
								'sanitize_callback' => 'absint',
								'validate_callback' => 'rest_validate_request_arg',
							),
						),
						$this->get_range_args()
					),
				),
			)
		);

		register_rest_route(
			$this->route_namespace,
			$this->rest_base . '/tokens/(?P<token_id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'get_token' ),
					'permission_callback' => array( $this, 'authorize_as_from_wpcom' ),
					'args'                => array_merge(
						array(
							'token_id' => array(
								'type'              => 'integer',
								'required'          => true,
								'minimum'           => 1,
								'sanitize_callback' => 'absint',
								'validate_callback' => 'rest_validate_request_arg',
							),
						),
						$this->get_range_args()
					),
				),
			)
		);

		register_rest_route(
			$this->route_namespace,
			$this->rest_base . '/users/(?P<user_id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'get_user' ),
					'permission_callback' => array( $this, 'authorize_as_from_wpcom' ),
					'args'                => array_merge(
						array(
							'user_id' => array(
								'type'              => 'integer',
								'required'          => true,
								'minimum'           => 1,
								'sanitize_callback' => 'absint',
								'validate_callback' => 'rest_validate_request_arg',
							),
						),
						$this->get_range_args()
					),
				),
			)
		);
	}

	/**
	 * Handles the notification journey request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return WP_REST_Response
	 *
	 * @since 11.3.0
	 */
	protected function get_notification( WP_REST_Request $request ): WP_REST_Response {
		list( $from, $to ) = $this->get_range( $request );

		return new WP_REST_Response(
			$this->query->for_notification(
				(string) $request->get_param( 'type' ),
				(int) $request->get_param( 'resource_id' ),
				$from,
				$to,
				$this->get_limit( $request )
			)
		);
	}

	/**
	 * Handles the whole-store request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return WP_REST_Response
	 *
	 * @since 11.3.0
	 */
	protected function get_site( WP_REST_Request $request ): WP_REST_Response {
		list( $from, $to ) = $this->get_range( $request );

		return new WP_REST_Response( $this->query->for_site( $from, $to, $this->get_limit( $request ) ) );
	}

	/**
	 * Handles the device request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return WP_REST_Response
	 *
	 * @since 11.3.0
	 */
	protected function get_token( WP_REST_Request $request ): WP_REST_Response {
		list( $from, $to ) = $this->get_range( $request );

		return new WP_REST_Response( $this->query->for_token( (int) $request->get_param( 'token_id' ), $from, $to, $this->get_limit( $request ) ) );
	}

	/**
	 * Handles the user request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return WP_REST_Response
	 *
	 * @since 11.3.0
	 */
	protected function get_user( WP_REST_Request $request ): WP_REST_Response {
		list( $from, $to ) = $this->get_range( $request );

		return new WP_REST_Response( $this->query->for_user( (int) $request->get_param( 'user_id' ), $from, $to, $this->get_limit( $request ) ) );
	}

	/**
	 * The `from` and `to` arguments every route accepts, as Unix timestamps.
	 *
	 * @return array
	 */
	private function get_range_args(): array {
		return array(
			'limit' => array(
				'description'       => __( 'Rows to return, newest first. A page carries every row of its last second, so it can exceed this.', 'woocommerce' ),
				'type'              => 'integer',
				'default'           => self::DEFAULT_LIMIT,
				'minimum'           => 1,
				'maximum'           => self::MAX_LIMIT,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'from'  => array(
				'description'       => __( 'Earliest Unix timestamp to include. Defaults to 48 hours before `to`, and is clamped to 90 days before it.', 'woocommerce' ),
				'type'              => 'integer',
				'minimum'           => 0,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'to'    => array(
				'description'       => __( 'Latest Unix timestamp to include. Defaults to now.', 'woocommerce' ),
				'type'              => 'integer',
				'minimum'           => 0,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			),
		);
	}

	/**
	 * The page size the request asked for, or the default.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return int
	 */
	private function get_limit( WP_REST_Request $request ): int {
		$limit = (int) $request->get_param( 'limit' );

		return $limit > 0 ? $limit : self::DEFAULT_LIMIT;
	}

	/**
	 * Resolves the requested time range, applying the defaults.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
	 * @return array{0: int, 1: int}
	 */
	private function get_range( WP_REST_Request $request ): array {
		$to   = (int) $request->get_param( 'to' );
		$from = (int) $request->get_param( 'from' );

		if ( $to <= 0 ) {
			$to = time();
		}

		if ( $from <= 0 ) {
			$from = $to - self::DEFAULT_RANGE_SECONDS;
		}

		$from = min( $from, $to );

		return array( max( $from, $to - self::MAX_RANGE_SECONDS ), $to );
	}
}