Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Broadcasting;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Broadcast;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class BroadcastController extends Controller
{
@@ -16,6 +17,28 @@ class BroadcastController extends Controller
*/
public function authenticate(Request $request)
{
if ($request->hasSession()) {
$request->session()->reflash();
}
return Broadcast::auth($request);
}
/**
* Authenticate the current user.
*
* See: https://pusher.com/docs/channels/server_api/authenticating-users/#user-authentication.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function authenticateUser(Request $request)
{
if ($request->hasSession()) {
$request->session()->reflash();
}
return Broadcast::resolveAuthenticatedUser($request)
?? throw new AccessDeniedHttpException;
}
}

View File

@@ -2,13 +2,13 @@
namespace Illuminate\Broadcasting;
use ReflectionClass;
use ReflectionProperty;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Broadcasting\Broadcaster;
use Illuminate\Support\Arr;
use ReflectionClass;
use ReflectionProperty;
class BroadcastEvent implements ShouldQueue
{
@@ -21,6 +21,27 @@ class BroadcastEvent implements ShouldQueue
*/
public $event;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries;
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout;
/**
* The number of seconds to wait before retrying the job when encountering an uncaught exception.
*
* @var int
*/
public $backoff;
/**
* Create a new job handler instance.
*
@@ -30,23 +51,40 @@ class BroadcastEvent implements ShouldQueue
public function __construct($event)
{
$this->event = $event;
$this->tries = property_exists($event, 'tries') ? $event->tries : null;
$this->timeout = property_exists($event, 'timeout') ? $event->timeout : null;
$this->backoff = property_exists($event, 'backoff') ? $event->backoff : null;
$this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null;
}
/**
* Handle the queued job.
*
* @param \Illuminate\Contracts\Broadcasting\Broadcaster $broadcaster
* @param \Illuminate\Contracts\Broadcasting\Factory $manager
* @return void
*/
public function handle(Broadcaster $broadcaster)
public function handle(BroadcastingFactory $manager)
{
$name = method_exists($this->event, 'broadcastAs')
? $this->event->broadcastAs() : get_class($this->event);
$broadcaster->broadcast(
array_wrap($this->event->broadcastOn()), $name,
$this->getPayloadFromEvent($this->event)
);
$channels = Arr::wrap($this->event->broadcastOn());
if (empty($channels)) {
return;
}
$connections = method_exists($this->event, 'broadcastConnections')
? $this->event->broadcastConnections()
: [null];
$payload = $this->getPayloadFromEvent($this->event);
foreach ($connections as $connection) {
$manager->connection($connection)->broadcast(
$channels, $name, $payload
);
}
}
/**
@@ -57,10 +95,9 @@ class BroadcastEvent implements ShouldQueue
*/
protected function getPayloadFromEvent($event)
{
if (method_exists($event, 'broadcastWith')) {
return array_merge(
$event->broadcastWith(), ['socket' => data_get($event, 'socket')]
);
if (method_exists($event, 'broadcastWith') &&
! is_null($payload = $event->broadcastWith())) {
return array_merge($payload, ['socket' => data_get($event, 'socket')]);
}
$payload = [];
@@ -98,4 +135,14 @@ class BroadcastEvent implements ShouldQueue
{
return get_class($this->event);
}
/**
* Prepare the instance for cloning.
*
* @return void
*/
public function __clone()
{
$this->event = clone $this->event;
}
}

View File

@@ -2,24 +2,34 @@
namespace Illuminate\Broadcasting;
use Pusher;
use Ably\AblyRest;
use Closure;
use Illuminate\Support\Arr;
use Psr\Log\LoggerInterface;
use InvalidArgumentException;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Broadcasting\Broadcasters\AblyBroadcaster;
use Illuminate\Broadcasting\Broadcasters\LogBroadcaster;
use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Broadcasting\Broadcasters\RedisBroadcaster;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Illuminate\Broadcasting\Broadcasters\RedisBroadcaster;
use Illuminate\Bus\UniqueLock;
use Illuminate\Contracts\Broadcasting\Factory as FactoryContract;
use Illuminate\Contracts\Broadcasting\ShouldBeUnique;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcherContract;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Foundation\CachesRoutes;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Pusher\Pusher;
/**
* @mixin \Illuminate\Contracts\Broadcasting\Broadcaster
*/
class BroadcastManager implements FactoryContract
{
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Container\Container
*/
protected $app;
@@ -40,7 +50,7 @@ class BroadcastManager implements FactoryContract
/**
* Create a new manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Container\Container $app
* @return void
*/
public function __construct($app)
@@ -49,24 +59,62 @@ class BroadcastManager implements FactoryContract
}
/**
* Register the routes for handling broadcast authentication and sockets.
* Register the routes for handling broadcast channel authentication and sockets.
*
* @param array|null $attributes
* @return void
*/
public function routes(array $attributes = null)
{
if ($this->app->routesAreCached()) {
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
return;
}
$attributes = $attributes ?: ['middleware' => ['web']];
$this->app['router']->group($attributes, function ($router) {
$router->post('/broadcasting/auth', BroadcastController::class.'@authenticate');
$router->match(
['get', 'post'], '/broadcasting/auth',
'\\'.BroadcastController::class.'@authenticate'
)->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
});
}
/**
* Register the routes for handling broadcast user authentication.
*
* @param array|null $attributes
* @return void
*/
public function userRoutes(array $attributes = null)
{
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
return;
}
$attributes = $attributes ?: ['middleware' => ['web']];
$this->app['router']->group($attributes, function ($router) {
$router->match(
['get', 'post'], '/broadcasting/user-auth',
'\\'.BroadcastController::class.'@authenticateUser'
)->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
});
}
/**
* Register the routes for handling broadcast authentication and sockets.
*
* Alias of "routes" method.
*
* @param array|null $attributes
* @return void
*/
public function channelRoutes(array $attributes = null)
{
return $this->routes($attributes);
}
/**
* Get the socket ID for the given request.
*
@@ -81,16 +129,14 @@ class BroadcastManager implements FactoryContract
$request = $request ?: $this->app['request'];
if ($request->hasHeader('X-Socket-ID')) {
return $request->header('X-Socket-ID');
}
return $request->header('X-Socket-ID');
}
/**
* Begin broadcasting an event.
*
* @param mixed|null $event
* @return \Illuminate\Broadcasting\PendingBroadcast|void
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function event($event = null)
{
@@ -105,10 +151,11 @@ class BroadcastManager implements FactoryContract
*/
public function queue($event)
{
$connection = $event instanceof ShouldBroadcastNow ? 'sync' : null;
if (is_null($connection) && isset($event->connection)) {
$connection = $event->connection;
if ($event instanceof ShouldBroadcastNow ||
(is_object($event) &&
method_exists($event, 'shouldBroadcastNow') &&
$event->shouldBroadcastNow())) {
return $this->app->make(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event));
}
$queue = null;
@@ -121,15 +168,40 @@ class BroadcastManager implements FactoryContract
$queue = $event->queue;
}
$this->app->make('queue')->connection($connection)->pushOn(
$queue, new BroadcastEvent(clone $event)
);
$broadcastEvent = new BroadcastEvent(clone $event);
if ($event instanceof ShouldBeUnique) {
$broadcastEvent = new UniqueBroadcastEvent(clone $event);
if ($this->mustBeUniqueAndCannotAcquireLock($broadcastEvent)) {
return;
}
}
$this->app->make('queue')
->connection($event->connection ?? null)
->pushOn($queue, $broadcastEvent);
}
/**
* Determine if the broadcastable event must be unique and determine if we can acquire the necessary lock.
*
* @param mixed $event
* @return bool
*/
protected function mustBeUniqueAndCannotAcquireLock($event)
{
return ! (new UniqueLock(
method_exists($event, 'uniqueVia')
? $event->uniqueVia()
: $this->app->make(Cache::class)
))->acquire($event);
}
/**
* Get a driver instance.
*
* @param string $driver
* @param string|null $driver
* @return mixed
*/
public function connection($driver = null)
@@ -140,7 +212,7 @@ class BroadcastManager implements FactoryContract
/**
* Get a driver instance.
*
* @param string $name
* @param string|null $name
* @return mixed
*/
public function driver($name = null)
@@ -158,11 +230,11 @@ class BroadcastManager implements FactoryContract
*/
protected function get($name)
{
return isset($this->drivers[$name]) ? $this->drivers[$name] : $this->resolve($name);
return $this->drivers[$name] ?? $this->resolve($name);
}
/**
* Resolve the given store.
* Resolve the given broadcaster.
*
* @param string $name
* @return \Illuminate\Contracts\Broadcasting\Broadcaster
@@ -174,7 +246,7 @@ class BroadcastManager implements FactoryContract
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Broadcaster [{$name}] is not defined.");
throw new InvalidArgumentException("Broadcast connection [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
@@ -209,10 +281,54 @@ class BroadcastManager implements FactoryContract
*/
protected function createPusherDriver(array $config)
{
return new PusherBroadcaster(
new Pusher($config['key'], $config['secret'],
$config['app_id'], Arr::get($config, 'options', []))
return new PusherBroadcaster($this->pusher($config));
}
/**
* Get a Pusher instance for the given configuration.
*
* @param array $config
* @return \Pusher\Pusher
*/
public function pusher(array $config)
{
$pusher = new Pusher(
$config['key'],
$config['secret'],
$config['app_id'],
$config['options'] ?? [],
isset($config['client_options']) && ! empty($config['client_options'])
? new GuzzleClient($config['client_options'])
: null,
);
if ($config['log'] ?? false) {
$pusher->setLogger($this->app->make(LoggerInterface::class));
}
return $pusher;
}
/**
* Create an instance of the driver.
*
* @param array $config
* @return \Illuminate\Contracts\Broadcasting\Broadcaster
*/
protected function createAblyDriver(array $config)
{
return new AblyBroadcaster($this->ably($config));
}
/**
* Get an Ably instance for the given configuration.
*
* @param array $config
* @return \Ably\AblyRest
*/
public function ably(array $config)
{
return new AblyRest($config);
}
/**
@@ -224,7 +340,8 @@ class BroadcastManager implements FactoryContract
protected function createRedisDriver(array $config)
{
return new RedisBroadcaster(
$this->app->make('redis'), Arr::get($config, 'connection')
$this->app->make('redis'), $config['connection'] ?? null,
$this->app['config']->get('database.redis.options.prefix', '')
);
}
@@ -260,7 +377,11 @@ class BroadcastManager implements FactoryContract
*/
protected function getConfig($name)
{
return $this->app['config']["broadcasting.connections.{$name}"];
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["broadcasting.connections.{$name}"];
}
return ['driver' => 'null'];
}
/**
@@ -284,10 +405,23 @@ class BroadcastManager implements FactoryContract
$this->app['config']['broadcasting.default'] = $name;
}
/**
* Disconnect the given disk and remove from local cache.
*
* @param string|null $name
* @return void
*/
public function purge($name = null)
{
$name ??= $this->getDefaultDriver();
unset($this->drivers[$name]);
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param string $driver
* @param \Closure $callback
* @return $this
*/
@@ -298,11 +432,46 @@ class BroadcastManager implements FactoryContract
return $this;
}
/**
* Get the application instance used by the manager.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public function getApplication()
{
return $this->app;
}
/**
* Set the application instance used by the manager.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return $this
*/
public function setApplication($app)
{
$this->app = $app;
return $this;
}
/**
* Forget all of the resolved driver instances.
*
* @return $this
*/
public function forgetDrivers()
{
$this->drivers = [];
return $this;
}
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)

View File

@@ -2,19 +2,13 @@
namespace Illuminate\Broadcasting;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
class BroadcastServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*

View File

@@ -0,0 +1,235 @@
<?php
namespace Illuminate\Broadcasting\Broadcasters;
use Ably\AblyRest;
use Ably\Exceptions\AblyException;
use Ably\Models\Message as AblyMessage;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @author Matthew Hall (matthall28@gmail.com)
* @author Taylor Otwell (taylor@laravel.com)
*/
class AblyBroadcaster extends Broadcaster
{
/**
* The AblyRest SDK instance.
*
* @var \Ably\AblyRest
*/
protected $ably;
/**
* Create a new broadcaster instance.
*
* @param \Ably\AblyRest $ably
* @return void
*/
public function __construct(AblyRest $ably)
{
$this->ably = $ably;
}
/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
$channelName = $this->normalizeChannelName($request->channel_name);
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
}
/**
* Return the valid authentication response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $result
* @return mixed
*/
public function validAuthenticationResponse($request, $result)
{
if (str_starts_with($request->channel_name, 'private')) {
$signature = $this->generateAblySignature(
$request->channel_name, $request->socket_id
);
return ['auth' => $this->getPublicToken().':'.$signature];
}
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
$signature = $this->generateAblySignature(
$request->channel_name,
$request->socket_id,
$userData = array_filter([
'user_id' => (string) $broadcastIdentifier,
'user_info' => $result,
])
);
return [
'auth' => $this->getPublicToken().':'.$signature,
'channel_data' => json_encode($userData),
];
}
/**
* Generate the signature needed for Ably authentication headers.
*
* @param string $channelName
* @param string $socketId
* @param array|null $userData
* @return string
*/
public function generateAblySignature($channelName, $socketId, $userData = null)
{
return hash_hmac(
'sha256',
sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':'.json_encode($userData) : ''),
$this->getPrivateToken(),
);
}
/**
* Broadcast the given event.
*
* @param array $channels
* @param string $event
* @param array $payload
* @return void
*
* @throws \Illuminate\Broadcasting\BroadcastException
*/
public function broadcast(array $channels, $event, array $payload = [])
{
try {
foreach ($this->formatChannels($channels) as $channel) {
$this->ably->channels->get($channel)->publish(
$this->buildAblyMessage($event, $payload)
);
}
} catch (AblyException $e) {
throw new BroadcastException(
sprintf('Ably error: %s', $e->getMessage())
);
}
}
/**
* Build an Ably message object for broadcasting.
*
* @param string $event
* @param array $payload
* @return \Ably\Models\Message
*/
protected function buildAblyMessage($event, array $payload = [])
{
return tap(new AblyMessage, function ($message) use ($event, $payload) {
$message->name = $event;
$message->data = $payload;
$message->connectionKey = data_get($payload, 'socket');
});
}
/**
* Return true if the channel is protected by authentication.
*
* @param string $channel
* @return bool
*/
public function isGuardedChannel($channel)
{
return Str::startsWith($channel, ['private-', 'presence-']);
}
/**
* Remove prefix from channel name.
*
* @param string $channel
* @return string
*/
public function normalizeChannelName($channel)
{
if ($this->isGuardedChannel($channel)) {
return str_starts_with($channel, 'private-')
? Str::replaceFirst('private-', '', $channel)
: Str::replaceFirst('presence-', '', $channel);
}
return $channel;
}
/**
* Format the channel array into an array of strings.
*
* @param array $channels
* @return array
*/
protected function formatChannels(array $channels)
{
return array_map(function ($channel) {
$channel = (string) $channel;
if (Str::startsWith($channel, ['private-', 'presence-'])) {
return str_starts_with($channel, 'private-')
? Str::replaceFirst('private-', 'private:', $channel)
: Str::replaceFirst('presence-', 'presence:', $channel);
}
return 'public:'.$channel;
}, $channels);
}
/**
* Get the public token value from the Ably key.
*
* @return mixed
*/
protected function getPublicToken()
{
return Str::before($this->ably->options->key, ':');
}
/**
* Get the private token value from the Ably key.
*
* @return mixed
*/
protected function getPrivateToken()
{
return Str::after($this->ably->options->key, ':');
}
/**
* Get the underlying Ably SDK instance.
*
* @return \Ably\AblyRest
*/
public function getAbly()
{
return $this->ably;
}
}

View File

@@ -2,17 +2,28 @@
namespace Illuminate\Broadcasting\Broadcasters;
use ReflectionFunction;
use ReflectionParameter;
use Illuminate\Support\Str;
use Closure;
use Exception;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use ReflectionClass;
use ReflectionFunction;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
abstract class Broadcaster implements BroadcasterContract
{
/**
* The callback to resolve the authenticated user information.
*
* @var \Closure|null
*/
protected $authenticatedUserCallback = null;
/**
* The registered channel authenticators.
*
@@ -20,24 +31,68 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected $channels = [];
/**
* The registered channel options.
*
* @var array
*/
protected $channelOptions = [];
/**
* The binding registrar instance.
*
* @var BindingRegistrar
* @var \Illuminate\Contracts\Routing\BindingRegistrar
*/
protected $bindingRegistrar;
/**
* Resolve the authenticated user payload for the incoming connection request.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication.
*
* @param \Illuminate\Http\Request $request
* @return array|null
*/
public function resolveAuthenticatedUser($request)
{
if ($this->authenticatedUserCallback) {
return $this->authenticatedUserCallback->__invoke($request);
}
}
/**
* Register the user retrieval callback used to authenticate connections.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication.
*
* @param \Closure $callback
* @return void
*/
public function resolveAuthenticatedUserUsing(Closure $callback)
{
$this->authenticatedUserCallback = $callback;
}
/**
* Register a channel authenticator.
*
* @param string $channel
* @param callable $callback
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel
* @param callable|string $callback
* @param array $options
* @return $this
*/
public function channel($channel, callable $callback)
public function channel($channel, $callback, $options = [])
{
if ($channel instanceof HasBroadcastChannel) {
$channel = $channel->broadcastChannelRoute();
} elseif (is_string($channel) && class_exists($channel) && is_a($channel, HasBroadcastChannel::class, true)) {
$channel = (new $channel)->broadcastChannelRoute();
}
$this->channels[$channel] = $callback;
$this->channelOptions[$channel] = $options;
return $this;
}
@@ -47,22 +102,30 @@ abstract class Broadcaster implements BroadcasterContract
* @param \Illuminate\Http\Request $request
* @param string $channel
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function verifyUserCanAccessChannel($request, $channel)
{
foreach ($this->channels as $pattern => $callback) {
if (! Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel)) {
if (! $this->channelNameMatchesPattern($channel, $pattern)) {
continue;
}
$parameters = $this->extractAuthParameters($pattern, $channel, $callback);
if ($result = $callback($request->user(), ...$parameters)) {
$handler = $this->normalizeChannelHandlerToCallable($callback);
$result = $handler($this->retrieveUser($request, $channel), ...$parameters);
if ($result === false) {
throw new AccessDeniedHttpException;
} elseif ($result) {
return $this->validAuthenticationResponse($request, $result);
}
}
throw new HttpException(403);
throw new AccessDeniedHttpException;
}
/**
@@ -70,12 +133,12 @@ abstract class Broadcaster implements BroadcasterContract
*
* @param string $pattern
* @param string $channel
* @param callable $callback
* @param callable|string $callback
* @return array
*/
protected function extractAuthParameters($pattern, $channel, $callback)
{
$callbackParameters = (new ReflectionFunction($callback))->getParameters();
$callbackParameters = $this->extractParameters($callback);
return collect($this->extractChannelKeys($pattern, $channel))->reject(function ($value, $key) {
return is_numeric($key);
@@ -84,6 +147,44 @@ abstract class Broadcaster implements BroadcasterContract
})->values()->all();
}
/**
* Extracts the parameters out of what the user passed to handle the channel authentication.
*
* @param callable|string $callback
* @return \ReflectionParameter[]
*
* @throws \Exception
*/
protected function extractParameters($callback)
{
if (is_callable($callback)) {
return (new ReflectionFunction($callback))->getParameters();
} elseif (is_string($callback)) {
return $this->extractParametersFromClass($callback);
}
throw new Exception('Given channel handler is an unknown type.');
}
/**
* Extracts the parameters out of a class channel's "join" method.
*
* @param string $callback
* @return \ReflectionParameter[]
*
* @throws \Exception
*/
protected function extractParametersFromClass($callback)
{
$reflection = new ReflectionClass($callback);
if (! $reflection->hasMethod('join')) {
throw new Exception('Class based channel must define a "join" method.');
}
return $reflection->getMethod('join')->getParameters();
}
/**
* Extract the channel keys from the incoming channel name.
*
@@ -140,6 +241,8 @@ abstract class Broadcaster implements BroadcasterContract
* @param mixed $value
* @param array $callbackParameters
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function resolveImplicitBindingIfPossible($key, $value, $callbackParameters)
{
@@ -148,11 +251,13 @@ abstract class Broadcaster implements BroadcasterContract
continue;
}
$model = $parameter->getClass()->newInstance();
$className = Reflector::getParameterClassName($parameter);
return $model->where($model->getRouteKeyName(), $value)->firstOr(function () {
throw new HttpException(403);
});
if (is_null($model = (new $className)->resolveRouteBinding($value))) {
throw new AccessDeniedHttpException;
}
return $model;
}
return $value;
@@ -162,13 +267,13 @@ abstract class Broadcaster implements BroadcasterContract
* Determine if a given key and parameter is implicitly bindable.
*
* @param string $key
* @param ReflectionParameter $parameter
* @param \ReflectionParameter $parameter
* @return bool
*/
protected function isImplicitlyBindable($key, $parameter)
{
return $parameter->name === $key && $parameter->getClass() &&
$parameter->getClass()->isSubclassOf(Model::class);
return $parameter->getName() === $key &&
Reflector::isParameterSubclassOf($parameter, UrlRoutable::class);
}
/**
@@ -187,7 +292,7 @@ abstract class Broadcaster implements BroadcasterContract
/**
* Get the model binding registrar instance.
*
* @return BindingRegistrar
* @return \Illuminate\Contracts\Routing\BindingRegistrar
*/
protected function binder()
{
@@ -198,4 +303,74 @@ abstract class Broadcaster implements BroadcasterContract
return $this->bindingRegistrar;
}
/**
* Normalize the given callback into a callable.
*
* @param mixed $callback
* @return callable
*/
protected function normalizeChannelHandlerToCallable($callback)
{
return is_callable($callback) ? $callback : function (...$args) use ($callback) {
return Container::getInstance()
->make($callback)
->join(...$args);
};
}
/**
* Retrieve the authenticated user using the configured guard (if any).
*
* @param \Illuminate\Http\Request $request
* @param string $channel
* @return mixed
*/
protected function retrieveUser($request, $channel)
{
$options = $this->retrieveChannelOptions($channel);
$guards = $options['guards'] ?? null;
if (is_null($guards)) {
return $request->user();
}
foreach (Arr::wrap($guards) as $guard) {
if ($user = $request->user($guard)) {
return $user;
}
}
}
/**
* Retrieve options for a certain channel.
*
* @param string $channel
* @return array
*/
protected function retrieveChannelOptions($channel)
{
foreach ($this->channelOptions as $pattern => $options) {
if (! $this->channelNameMatchesPattern($channel, $pattern)) {
continue;
}
return $options;
}
return [];
}
/**
* Check if the channel name from the request matches a pattern from registered channels.
*
* @param string $channel
* @param string $pattern
* @return bool
*/
protected function channelNameMatchesPattern($channel, $pattern)
{
return preg_match('/'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel);
}
}

View File

@@ -2,25 +2,28 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Pusher;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Broadcasting\BroadcastException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Pusher\ApiErrorException;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class PusherBroadcaster extends Broadcaster
{
use UsePusherChannelConventions;
/**
* The Pusher SDK instance.
*
* @var \Pusher
* @var \Pusher\Pusher
*/
protected $pusher;
/**
* Create a new broadcaster instance.
*
* @param \Pusher $pusher
* @param \Pusher\Pusher $pusher
* @return void
*/
public function __construct(Pusher $pusher)
@@ -28,22 +31,56 @@ class PusherBroadcaster extends Broadcaster
$this->pusher = $pusher;
}
/**
* Resolve the authenticated user payload for an incoming connection request.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication
* See: https://pusher.com/docs/channels/server_api/authenticating-users/#response
*
* @param \Illuminate\Http\Request $request
* @return array|null
*/
public function resolveAuthenticatedUser($request)
{
if (! $user = parent::resolveAuthenticatedUser($request)) {
return;
}
if (method_exists($this->pusher, 'authenticateUser')) {
return $this->pusher->authenticateUser($request->socket_id, $user);
}
$settings = $this->pusher->getSettings();
$encodedUser = json_encode($user);
$decodedString = "{$request->socket_id}::user::{$encodedUser}";
$auth = $settings['auth_key'].':'.hash_hmac(
'sha256', $decodedString, $settings['secret']
);
return [
'auth' => $auth,
'user_data' => $encodedUser,
];
}
/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
throw new HttpException(403);
}
$channelName = $this->normalizeChannelName($request->channel_name);
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
return parent::verifyUserCanAccessChannel(
$request, $channelName
@@ -59,27 +96,46 @@ class PusherBroadcaster extends Broadcaster
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'private')) {
if (str_starts_with($request->channel_name, 'private')) {
return $this->decodePusherResponse(
$this->pusher->socket_auth($request->channel_name, $request->socket_id)
$request,
method_exists($this->pusher, 'authorizeChannel')
? $this->pusher->authorizeChannel($request->channel_name, $request->socket_id)
: $this->pusher->socket_auth($request->channel_name, $request->socket_id)
);
}
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
return $this->decodePusherResponse(
$this->pusher->presence_auth(
$request->channel_name, $request->socket_id, $request->user()->getAuthIdentifier(), $result)
$request,
method_exists($this->pusher, 'authorizePresenceChannel')
? $this->pusher->authorizePresenceChannel($request->channel_name, $request->socket_id, $broadcastIdentifier, $result)
: $this->pusher->presence_auth($request->channel_name, $request->socket_id, $broadcastIdentifier, $result)
);
}
/**
* Decode the given Pusher response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $response
* @return array
*/
protected function decodePusherResponse($response)
protected function decodePusherResponse($request, $response)
{
return json_decode($response, true);
if (! $request->input('callback', false)) {
return json_decode($response, true);
}
return response()->json(json_decode($response, true))
->withCallback($request->callback);
}
/**
@@ -89,32 +145,46 @@ class PusherBroadcaster extends Broadcaster
* @param string $event
* @param array $payload
* @return void
*
* @throws \Illuminate\Broadcasting\BroadcastException
*/
public function broadcast(array $channels, $event, array $payload = [])
{
$socket = Arr::pull($payload, 'socket');
$response = $this->pusher->trigger(
$this->formatChannels($channels), $event, $payload, $socket, true
);
$parameters = $socket !== null ? ['socket_id' => $socket] : [];
if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299)
|| $response === true) {
return;
$channels = Collection::make($this->formatChannels($channels));
try {
$channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) {
$this->pusher->trigger($channels->toArray(), $event, $payload, $parameters);
});
} catch (ApiErrorException $e) {
throw new BroadcastException(
sprintf('Pusher error: %s.', $e->getMessage())
);
}
throw new BroadcastException(
is_bool($response) ? 'Failed to connect to Pusher.' : $response['body']
);
}
/**
* Get the Pusher SDK instance.
*
* @return \Pusher
* @return \Pusher\Pusher
*/
public function getPusher()
{
return $this->pusher;
}
/**
* Set the Pusher SDK instance.
*
* @param \Pusher\Pusher $pusher
* @return void
*/
public function setPusher($pusher)
{
$this->pusher = $pusher;
}
}

View File

@@ -2,13 +2,17 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Contracts\Redis\Factory as Redis;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Arr;
use Predis\Connection\ConnectionException;
use RedisException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class RedisBroadcaster extends Broadcaster
{
use UsePusherChannelConventions;
/**
* The Redis instance.
*
@@ -19,20 +23,29 @@ class RedisBroadcaster extends Broadcaster
/**
* The Redis connection to use for broadcasting.
*
* @var ?string
*/
protected $connection = null;
/**
* The Redis key prefix.
*
* @var string
*/
protected $connection;
protected $prefix = '';
/**
* Create a new broadcaster instance.
*
* @param \Illuminate\Contracts\Redis\Factory $redis
* @param string $connection
* @param string|null $connection
* @param string $prefix
* @return void
*/
public function __construct(Redis $redis, $connection = null)
public function __construct(Redis $redis, $connection = null, $prefix = '')
{
$this->redis = $redis;
$this->prefix = $prefix;
$this->connection = $connection;
}
@@ -41,17 +54,20 @@ class RedisBroadcaster extends Broadcaster
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
throw new HttpException(403);
}
$channelName = $this->normalizeChannelName(
str_replace($this->prefix, '', $request->channel_name)
);
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
return parent::verifyUserCanAccessChannel(
$request, $channelName
@@ -71,8 +87,16 @@ class RedisBroadcaster extends Broadcaster
return json_encode($result);
}
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
return json_encode(['channel_data' => [
'user_id' => $request->user()->getAuthIdentifier(),
'user_id' => $broadcastIdentifier,
'user_info' => $result,
]]);
}
@@ -84,9 +108,15 @@ class RedisBroadcaster extends Broadcaster
* @param string $event
* @param array $payload
* @return void
*
* @throws \Illuminate\Broadcasting\BroadcastException
*/
public function broadcast(array $channels, $event, array $payload = [])
{
if (empty($channels)) {
return;
}
$connection = $this->redis->connection($this->connection);
$payload = json_encode([
@@ -95,8 +125,45 @@ class RedisBroadcaster extends Broadcaster
'socket' => Arr::pull($payload, 'socket'),
]);
foreach ($this->formatChannels($channels) as $channel) {
$connection->publish($channel, $payload);
try {
$connection->eval(
$this->broadcastMultipleChannelsScript(),
0, $payload, ...$this->formatChannels($channels)
);
} catch (ConnectionException|RedisException $e) {
throw new BroadcastException(
sprintf('Redis error: %s.', $e->getMessage())
);
}
}
/**
* Get the Lua script for broadcasting to multiple channels.
*
* ARGV[1] - The payload
* ARGV[2...] - The channels
*
* @return string
*/
protected function broadcastMultipleChannelsScript()
{
return <<<'LUA'
for i = 2, #ARGV do
redis.call('publish', ARGV[i], ARGV[1])
end
LUA;
}
/**
* Format the channel array into an array of strings.
*
* @param array $channels
* @return array
*/
protected function formatChannels(array $channels)
{
return array_map(function ($channel) {
return $this->prefix.$channel;
}, parent::formatChannels($channels));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Support\Str;
trait UsePusherChannelConventions
{
/**
* Return true if the channel is protected by authentication.
*
* @param string $channel
* @return bool
*/
public function isGuardedChannel($channel)
{
return Str::startsWith($channel, ['private-', 'presence-']);
}
/**
* Remove prefix from channel name.
*
* @param string $channel
* @return string
*/
public function normalizeChannelName($channel)
{
foreach (['private-encrypted-', 'private-', 'presence-'] as $prefix) {
if (Str::startsWith($channel, $prefix)) {
return Str::replaceFirst($prefix, '', $channel);
}
}
return $channel;
}
}

View File

@@ -2,6 +2,8 @@
namespace Illuminate\Broadcasting;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
class Channel
{
/**
@@ -14,12 +16,12 @@ class Channel
/**
* Create a new channel instance.
*
* @param string $name
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name
* @return void
*/
public function __construct($name)
{
$this->name = $name;
$this->name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name;
}
/**

View File

@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Broadcasting;
class EncryptedPrivateChannel extends Channel
{
/**
* Create a new channel instance.
*
* @param string $name
* @return void
*/
public function __construct($name)
{
parent::__construct('private-encrypted-'.$name);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Illuminate\Broadcasting;
use Illuminate\Support\Arr;
trait InteractsWithBroadcasting
{
/**
* The broadcaster connection to use to broadcast the event.
*
* @var array
*/
protected $broadcastConnection = [null];
/**
* Broadcast the event using a specific broadcaster.
*
* @param array|string|null $connection
* @return $this
*/
public function broadcastVia($connection = null)
{
$this->broadcastConnection = is_null($connection)
? [null]
: Arr::wrap($connection);
return $this;
}
/**
* Get the broadcaster connections the event should be broadcast on.
*
* @return array
*/
public function broadcastConnections()
{
return $this->broadcastConnection;
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -33,6 +33,21 @@ class PendingBroadcast
$this->events = $events;
}
/**
* Broadcast the event using a specific broadcaster.
*
* @param string|null $connection
* @return $this
*/
public function via($connection = null)
{
if (method_exists($this->event, 'broadcastVia')) {
$this->event->broadcastVia($connection);
}
return $this;
}
/**
* Broadcast the event to everyone except the current user.
*

View File

@@ -2,16 +2,20 @@
namespace Illuminate\Broadcasting;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
class PrivateChannel extends Channel
{
/**
* Create a new channel instance.
*
* @param string $name
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name
* @return void
*/
public function __construct($name)
{
$name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name;
parent::__construct('private-'.$name);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Illuminate\Broadcasting;
use Illuminate\Container\Container;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique
{
/**
* The unique lock identifier.
*
* @var mixed
*/
public $uniqueId;
/**
* The number of seconds the unique lock should be maintained.
*
* @var int
*/
public $uniqueFor;
/**
* Create a new event instance.
*
* @param mixed $event
* @return void
*/
public function __construct($event)
{
$this->uniqueId = get_class($event);
if (method_exists($event, 'uniqueId')) {
$this->uniqueId .= $event->uniqueId();
} elseif (property_exists($event, 'uniqueId')) {
$this->uniqueId .= $event->uniqueId;
}
if (method_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor();
} elseif (property_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor;
}
parent::__construct($event);
}
/**
* Resolve the cache implementation that should manage the event's uniqueness.
*
* @return \Illuminate\Contracts\Cache\Repository
*/
public function uniqueVia()
{
return method_exists($this->event, 'uniqueVia')
? $this->event->uniqueVia()
: Container::getInstance()->make(Repository::class);
}
}

View File

@@ -14,11 +14,15 @@
}
],
"require": {
"php": ">=5.6.4",
"illuminate/bus": "5.4.*",
"illuminate/contracts": "5.4.*",
"illuminate/queue": "5.4.*",
"illuminate/support": "5.4.*"
"php": "^8.0.2",
"ext-json": "*",
"psr/log": "^1.0|^2.0|^3.0",
"illuminate/bus": "^9.0",
"illuminate/collections": "^9.0",
"illuminate/container": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/queue": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
@@ -27,11 +31,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.4-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0)."
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)."
},
"config": {
"sort-packages": true