get_current_user_id(), 't' => $table, 'x' => self::context_key( $context ), 's' => self::scope_key( $context ), 'h' => $hash, 'e' => Clock::timestamp() + self::TTL, 'c' => $counts, ) ); $body = rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- an opaque URL-safe token, not obfuscation. return $body . '.' . self::sign( $body ); } public static function read( string $token, string $table, array $context ): ?array { if ( '' === $token || strlen( $token ) > self::MAX_LENGTH || 1 !== preg_match( '/^([A-Za-z0-9_-]+)\.([a-f0-9]{32})$/', $token, $m ) ) { return null; } if ( ! hash_equals( self::sign( $m[1] ), $m[2] ) ) { return null; } $json = base64_decode( strtr( $m[1], '-_', '+/' ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- decodes the URL-safe token issue() made (not obfuscation). $data = is_string( $json ) ? json_decode( $json, true ) : null; if ( ! is_array( $data ) || ! isset( $data['u'], $data['t'], $data['x'], $data['s'], $data['h'], $data['e'], $data['c'] ) || ! is_array( $data['c'] ) ) { return null; } $user = get_current_user_id(); if ( $user <= 0 || (int) $data['u'] !== $user || $table !== $data['t'] || self::context_key( $context ) !== $data['x'] || (int) $data['e'] < Clock::timestamp() ) { return null; } if ( ! hash_equals( self::scope_key( $context ), (string) $data['s'] ) ) { return null; } return array( 'hash' => (string) $data['h'], 'counts' => $data['c'], ); } public static function counts( string $token, string $table, array $context, string $hash ): ?array { $data = self::read( $token, $table, $context ); return null !== $data && '' !== $hash && hash_equals( $data['hash'], $hash ) ? $data['counts'] : null; } private static function sign( string $body ): string { return substr( hash_hmac( 'sha256', 'rooster-lms-count|' . $body, wp_salt( 'nonce' ) ), 0, 32 ); } private static function scope_key( array $context ): string { $org_id = (int) ( $context['org'] ?? 0 ); if ( $org_id <= 0 ) { return ''; } $scope = OrgScope::for_user( get_current_user_id() ); $key = $scope->fingerprint( $org_id ); return array() === $scope->unit_roles( $org_id ) ? $key : $key . ':' . self::tree_key( $org_id ); } private static function tree_key( int $org_id ): string { $shape = array(); foreach ( Services::org_unit_repo()->for_org( $org_id, true ) as $unit ) { $shape[] = $unit->id . '>' . $unit->parent_id . ':' . $unit->state; } sort( $shape ); return substr( hash( 'sha256', implode( ',', $shape ) ), 0, 16 ); } private static function context_key( array $context ): string { ksort( $context ); $out = array(); foreach ( $context as $key => $value ) { $out[] = $key . '=' . (int) $value; } return implode( '&', $out ); } }