set_id( (int) $data['id'] ); } if ( array_key_exists( 'user_id', $data ) ) { $this->set_user_id( (int) $data['user_id'] ); } if ( array_key_exists( 'token', $data ) ) { $this->set_token( (string) $data['token'] ); } if ( array_key_exists( 'device_uuid', $data ) ) { $this->set_device_uuid( (string) $data['device_uuid'] ); } if ( array_key_exists( 'platform', $data ) ) { $this->set_platform( (string) $data['platform'] ); } if ( array_key_exists( 'origin', $data ) ) { $this->set_origin( (string) $data['origin'] ); } if ( array_key_exists( 'device_locale', $data ) ) { $this->set_device_locale( (string) $data['device_locale'] ); } if ( array_key_exists( 'metadata', $data ) ) { $this->set_metadata( (array) $data['metadata'] ); } if ( array_key_exists( 'created_at_gmt', $data ) ) { $this->set_created_at_gmt( null === $data['created_at_gmt'] ? null : (string) $data['created_at_gmt'] ); } if ( array_key_exists( 'last_confirmed_at_gmt', $data ) ) { $this->set_last_confirmed_at_gmt( null === $data['last_confirmed_at_gmt'] ? null : (string) $data['last_confirmed_at_gmt'] ); } if ( array_key_exists( 'last_sent_at_gmt', $data ) ) { $this->set_last_sent_at_gmt( null === $data['last_sent_at_gmt'] ? null : (string) $data['last_sent_at_gmt'] ); } } /** * Validates and sets the ID. * * @param int $id The ID of the token post. * @throws PushTokenInvalidDataException If ID is not valid. * @return void * * @since 10.4.0 */ public function set_id( int $id ): void { $result = PushTokenValidator::validate( compact( 'id' ), array( 'id' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->id = $id; } /** * Validates and sets the user ID. * * @param int $user_id The ID of the user who owns the token. * @throws PushTokenInvalidDataException If user ID is not valid. * @return void * * @since 10.4.0 */ public function set_user_id( int $user_id ): void { $result = PushTokenValidator::validate( compact( 'user_id' ), array( 'user_id' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->user_id = $user_id; } /** * Validates and sets the token. * * @param string $token The token representing a device we can send a push notification to. * @throws PushTokenInvalidDataException If token is not valid. * @return void * * @since 10.4.0 */ public function set_token( string $token ): void { $result = PushTokenValidator::validate( compact( 'token' ), array( 'token' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->token = trim( $token ); } /** * Validates and sets the device UUID, normalize empty (non-null) values to null. * * @param string|null $device_uuid The identifier the app generated for its own install. * @throws PushTokenInvalidDataException If device UUID is not valid. * @return void * * @since 10.4.0 */ public function set_device_uuid( ?string $device_uuid ): void { $result = PushTokenValidator::validate( compact( 'device_uuid' ), array( 'device_uuid' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } if ( null !== $device_uuid ) { $device_uuid = trim( $device_uuid ); } $this->device_uuid = $device_uuid ? $device_uuid : null; } /** * Validates and sets the device locale. * * @param string $device_locale The locale of the device the token belongs to. * @throws PushTokenInvalidDataException If device locale is not valid. * @return void * * @since 10.6.0 */ public function set_device_locale( string $device_locale ): void { $result = PushTokenValidator::validate( compact( 'device_locale' ), array( 'device_locale' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->device_locale = trim( $device_locale ); } /** * Validates and sets the platform. * * @param string $platform The platform the token was generated by. * @throws PushTokenInvalidDataException If platform is not valid. * @return void * * @since 10.4.0 */ public function set_platform( string $platform ): void { $result = PushTokenValidator::validate( compact( 'platform' ), array( 'platform' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->platform = trim( $platform ); } /** * Validates and sets the origin. * * @param string $origin The origin of the token, e.g. the app it came from. * @throws PushTokenInvalidDataException If origin is not valid. * @return void * * @since 10.4.0 */ public function set_origin( string $origin ): void { $result = PushTokenValidator::validate( compact( 'origin' ), array( 'origin' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } $this->origin = trim( $origin ); } /** * Validates and sets the metadata. * * @param array $metadata An array of metadata for the token, e.g. the app version, device OS etc. * @throws PushTokenInvalidDataException If metadata is not valid. * @return void * * @since 10.6.0 */ public function set_metadata( array $metadata ): void { $result = PushTokenValidator::validate( compact( 'metadata' ), array( 'metadata' ) ); if ( is_wp_error( $result ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new PushTokenInvalidDataException( $result->get_error_message() ); } if ( ! empty( $metadata ) ) { $keys = array_map( 'sanitize_key', array_keys( $metadata ) ); $values = array_map( 'sanitize_text_field', array_values( $metadata ) ); /** * Typehint for PHPStan, as it can't infer the $keys and $values are * the same length therefore array_combine won't return false. * * @var array $metadata */ $metadata = array_combine( $keys, $values ); } $this->metadata = $metadata; } /** * Sets the date the token was registered. * * Unlike the other setters this does not run through * {@see PushTokenValidator}. Timestamps are derived from the underlying * post record rather than supplied by an API client, so there is no * untrusted input to guard against. * * @param string|null $created_at_gmt A GMT `Y-m-d H:i:s` datetime, or null if unknown. * @return void * * @since 11.2.0 */ public function set_created_at_gmt( ?string $created_at_gmt ): void { $this->created_at_gmt = $this->validate_gmt_datetime( $created_at_gmt ); } /** * Sets the date the app last confirmed this token. * * See {@see self::set_created_at_gmt()} for why this bypasses validation. * * @param string|null $last_confirmed_at_gmt A GMT `Y-m-d H:i:s` datetime, or null if unknown. * @return void * * @since 11.2.0 */ public function set_last_confirmed_at_gmt( ?string $last_confirmed_at_gmt ): void { $this->last_confirmed_at_gmt = $this->validate_gmt_datetime( $last_confirmed_at_gmt ); } /** * Sets the date this token was last sent to WPCOM. * * See {@see self::set_created_at_gmt()} for why this bypasses validation. * * @param string|null $last_sent_at_gmt A GMT `Y-m-d H:i:s` datetime, or null if never sent. * @return void * * @since 11.2.0 */ public function set_last_sent_at_gmt( ?string $last_sent_at_gmt ): void { $this->last_sent_at_gmt = $this->validate_gmt_datetime( $last_sent_at_gmt ); } /** * Returns a GMT datetime as `Y-m-d H:i:s`, or null if it is not one. * * @param string|null $datetime The GMT datetime string. * @return string|null */ private function validate_gmt_datetime( ?string $datetime ): ?string { $datetime = null === $datetime ? '' : trim( $datetime ); if ( '' === $datetime || self::ZERO_DATETIME === $datetime ) { return null; } $normalized = str_replace( 'T', ' ', $datetime ); if ( ! TimeUtil::is_valid_date( $normalized ) ) { wc_get_logger()->warning( 'Unparseable push token timestamp.', array( 'source' => PushNotifications::FEATURE_NAME, 'token_id' => $this->id, 'value' => $datetime, ) ); return null; } return $normalized; } /** * Converts a GMT `Y-m-d H:i:s` datetime to `Y-m-d\TH:i:s`, the format * `wc_rest_prepare_date_response()` gives every other `_gmt` field in the * Woo REST API. * * @param string|null $datetime A datetime already through {@see self::validate_gmt_datetime()}. * @return string|null */ private function to_rest_datetime( ?string $datetime ): ?string { return null === $datetime ? null : str_replace( ' ', 'T', $datetime ); } /** * Gets the ID. * * @return int|null * * @since 10.4.0 */ public function get_id(): ?int { return $this->id; } /** * Gets the user ID. * * @return int|null * * @since 10.4.0 */ public function get_user_id(): ?int { return $this->user_id; } /** * Gets the token. * * @return string|null * * @since 10.4.0 */ public function get_token(): ?string { return $this->token; } /** * Gets the device UUID. * * @return string|null * * @since 10.4.0 */ public function get_device_uuid(): ?string { return $this->device_uuid; } /** * Gets the platform. * * @return string|null * * @since 10.4.0 */ public function get_platform(): ?string { return $this->platform; } /** * Gets the origin. * * @return string|null * * @since 10.4.0 */ public function get_origin(): ?string { return $this->origin; } /** * Gets the device locale. * * @return string|null * * @since 10.6.0 */ public function get_device_locale(): ?string { return $this->device_locale; } /** * Gets the metadata. * * @return array|null * * @since 10.6.0 */ public function get_metadata(): ?array { return $this->metadata; } /** * Gets the date the token was registered, as a GMT `Y-m-d H:i:s` string. * * @return string|null * * @since 11.2.0 */ public function get_created_at_gmt(): ?string { return $this->created_at_gmt; } /** * Gets the date the app last confirmed this token, as a GMT `Y-m-d H:i:s` string. * * @return string|null * * @since 11.2.0 */ public function get_last_confirmed_at_gmt(): ?string { return $this->last_confirmed_at_gmt; } /** * Gets the date this token was last sent to WPCOM, as a GMT `Y-m-d H:i:s` * string, or null if it has never been sent. * * This records that WPCOM accepted a payload containing the token. It is * not a delivery receipt: what happens between WPCOM and the device is * downstream of anything this plugin can observe. * * @return string|null * * @since 11.2.0 */ public function get_last_sent_at_gmt(): ?string { return $this->last_sent_at_gmt; } /** * Returns this token formatted for the WPCOM push notifications endpoint. * * @return array{user_id: int|null, token: string|null, origin: string|null, device_locale: string|null} * * @since 10.7.0 */ public function to_wpcom_format(): array { return array( 'user_id' => $this->user_id, 'token' => $this->token, 'origin' => $this->origin, 'device_locale' => $this->device_locale ?? self::DEFAULT_DEVICE_LOCALE, ); } /** * Returns this token formatted for the push tokens REST index response. * * Deliberately separate from {@see self::to_wpcom_format()}. That method is * also the per-token payload the dispatcher POSTs to the WPCOM send * endpoint, so adding fields to it would change every notification request. * * Metadata is cast to an object so that an empty value encodes as `{}` rather * than `[]`, matching the `object` type the schema declares for it. * * @return array{user_id: int|null, token: string|null, origin: string|null, device_locale: string|null, id: int|null, device_uuid: string|null, platform: string|null, metadata: stdClass, created_at_gmt: string|null, last_confirmed_at_gmt: string|null, last_sent_at_gmt: string|null} * * @since 11.2.0 */ public function to_rest_format(): array { return array_merge( $this->to_wpcom_format(), array( 'id' => $this->id, 'device_uuid' => $this->device_uuid, 'platform' => $this->platform, 'metadata' => (object) ( $this->metadata ?? array() ), 'created_at_gmt' => $this->to_rest_datetime( $this->created_at_gmt ), 'last_confirmed_at_gmt' => $this->to_rest_datetime( $this->last_confirmed_at_gmt ), 'last_sent_at_gmt' => $this->to_rest_datetime( $this->last_sent_at_gmt ), ) ); } /** * Determines whether this token can be created. * * @return bool * * @since 10.4.0 */ public function can_be_created(): bool { return ! $this->get_id() && $this->has_required_parameters(); } /** * Determines whether this token can be updated. * * @return bool * * @since 10.4.0 */ public function can_be_updated(): bool { return $this->get_id() && $this->has_required_parameters(); } /** * Determines whether this token can be read. * * @return bool * * @since 10.4.0 */ public function can_be_read(): bool { return (bool) $this->get_id(); } /** * Determines whether this token can be deleted. * * @return bool * * @since 10.4.0 */ public function can_be_deleted(): bool { return (bool) $this->get_id(); } /** * Determines whether all the required non-ID parameters are filled. * * @return bool * * @since 10.4.0 */ private function has_required_parameters(): bool { return $this->get_user_id() && $this->get_token() && $this->get_platform() && $this->get_origin() && $this->get_device_locale() && ( $this->get_device_uuid() || $this->get_platform() === self::PLATFORM_BROWSER ); } }