Skip to content

Commit

Permalink
tie into explicit model binding for broadcast authorizers
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorotwell committed Oct 28, 2016
1 parent 1a0f881 commit 515d97c
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 27 deletions.
107 changes: 87 additions & 20 deletions src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use ReflectionFunction;
use ReflectionParameter;
use Illuminate\Support\Str;
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;

Expand All @@ -18,6 +20,13 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected $channels = [];

/**
* The binding registrar instance.
*
* @var BindingRegistrar
*/
protected $bindingRegistrar;

/**
* Register a channel authenticator.
*
Expand Down Expand Up @@ -66,24 +75,87 @@ protected function verifyUserCanAccessChannel($request, $channel)
*/
protected function extractAuthParameters($pattern, $channel, $callback)
{
$parameters = [];
$callbackParameters = (new ReflectionFunction($callback))->getParameters();

$pattern = preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern);
return collect($this->extractChannelKeys($pattern, $channel))->reject(function ($value, $key) {
return is_numeric($key);
})->map(function ($value, $key) use ($callbackParameters) {
$newValue = $this->resolveExplicitBindingIfPossible($key, $value);

preg_match('/^'.$pattern.'/', $channel, $keys);
return $newValue === $value ? $this->resolveImplicitBindingIfPossible(
$key, $value, $callbackParameters
) : $newValue;
})->values()->all();
}

$callbackParameters = (new ReflectionFunction($callback))->getParameters();
/**
* Extract the channel keys from the incoming channel name.
*
* @param string $pattern
* @param string $channel
* @return array
*/
protected function extractChannelKeys($pattern, $channel)
{
preg_match('/^'.preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern).'/', $channel, $keys);

return $keys;
}

/**
* Resolve an explicit parameter binding if applicable.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function resolveExplicitBindingIfPossible($key, $value)
{
$binder = $this->binder();

if ($binder && $binder->getBindingCallback($key)) {
return call_user_func($binder->getBindingCallback($key), $value);
}

return $value;
}

/**
* Resolve an implicit parameter binding if applicable.
*
* @param string $key
* @param mixed $value
* @param array $callbackParameters
* @return mixed
*/
protected function resolveImplicitBindingIfPossible($key, $value, $callbackParameters)
{
foreach ($callbackParameters as $parameter) {
if ($parameter->getPosition() === 0) {
if (! $this->isImplicitlyBindable($key, $parameter)) {
continue;
}

$parameters[] = ! isset($keys[$parameter->getPosition()])
? null : $this->getAuthParameterFromKeys($parameter, $keys);
$model = $parameter->getClass()->newInstance();

return $model->where($model->getRouteKeyName(), $value)->firstOr(function () {
throw new HttpException(403);
});
}

return $parameters;
return $value;
}

/**
* Determine if a given key and parameter is implicitly bindable.
*
* @param string $key
* @param ReflectionParameter $parameter
* @return bool
*/
protected function isImplicitlyBindable($key, $parameter)
{
return $parameter->name === $key && $parameter->getClass() &&
$parameter->getClass()->isSubclassOf(Model::class);
}

/**
Expand All @@ -100,22 +172,17 @@ protected function formatChannels(array $channels)
}

/**
* Extract a parameter from the given keys.
* Get the model binding registrar instance.
*
* @param ReflectionParameter $parameter
* @param array $keys
* @return mixed
* @return BindingRegistrar
*/
protected function getAuthParameterFromKeys($parameter, $keys)
protected function binder()
{
$key = $keys[$parameter->getPosition()];

if ($parameter->getClass() && $parameter->getClass()->isSubclassOf(Model::class)) {
$model = $parameter->getClass()->newInstance();

return $model->where($model->getRouteKeyName(), $key)->first();
if (! $this->bindingRegistrar) {
$this->bindingRegistrar = Container::getInstance()->bound(BindingRegistrar::class)
? Container::getInstance()->make(BindingRegistrar::class) : null;
}

return $key;
return $this->bindingRegistrar;
}
}
23 changes: 23 additions & 0 deletions src/Illuminate/Contracts/Routing/BindingRegistrar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Illuminate\Contracts\Routing;

interface BindingRegistrar
{
/**
* Add a new route parameter binder.
*
* @param string $key
* @param string|callable $binder
* @return void
*/
public function bind($key, $binder);

/**
* Get the binding callback for a given binding.
*
* @param string $key
* @return \Closure
*/
public function getBindingCallback($key);
}
22 changes: 22 additions & 0 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,28 @@ public function firstOrFail($columns = ['*'])
throw (new ModelNotFoundException)->setModel(get_class($this->model));
}

/**
* Execute the query and get the first result or call a callback.
*
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|static|mixed
*/
public function firstOr($columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;

$columns = ['*'];
}

if (! is_null($model = $this->first($columns))) {
return $model;
}

return call_user_func($callback);
}

/**
* Execute the query as a "select" statement.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Foundation/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ public function registerCoreContainerAliases()
'redirect' => ['Illuminate\Routing\Redirector'],
'redis' => ['Illuminate\Redis\Database', 'Illuminate\Contracts\Redis\Database'],
'request' => ['Illuminate\Http\Request', 'Symfony\Component\HttpFoundation\Request'],
'router' => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'],
'router' => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar', 'Illuminate\Contracts\Routing\BindingRegistrar'],
'session' => ['Illuminate\Session\SessionManager'],
'session.store' => ['Illuminate\Session\Store', 'Symfony\Component\HttpFoundation\Session\SessionInterface'],
'url' => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
Expand Down
16 changes: 15 additions & 1 deletion src/Illuminate/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Illuminate\Contracts\Routing\Registrar as RegistrarContract;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class Router implements RegistrarContract
class Router implements RegistrarContract, BindingRegistrar
{
use Macroable;

Expand Down Expand Up @@ -955,6 +956,19 @@ public function bind($key, $binder)
$this->binders[str_replace('-', '_', $key)] = $binder;
}

/**
* Get the binding callback for a given binding.
*
* @param string $key
* @return \Closure|null
*/
public function getBindingCallback($key)
{
if (isset($this->binders[$key = str_replace('-', '_', $key)])) {
return $this->binders[$key];
}
}

/**
* Create a class based binding using the IoC container.
*
Expand Down
35 changes: 30 additions & 5 deletions tests/Broadcasting/BroadcasterTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

use Mockery as m;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Broadcasting\Broadcasters\Broadcaster;

class BroadcasterTest extends PHPUnit_Framework_TestCase
Expand All @@ -22,7 +24,7 @@ public function testExtractingParametersWhileCheckingForUserAccess()

$callback = function ($user, BroadcasterTestEloquentModelStub $model, BroadcasterTestEloquentModelStub $model2, $something) {
};
$parameters = $broadcaster->extractAuthParameters('asd.{model}.{model}.{nonModel}', 'asd.1.uid.something', $callback);
$parameters = $broadcaster->extractAuthParameters('asd.{model}.{model2}.{nonModel}', 'asd.1.uid.something', $callback);
$this->assertEquals(['model.1.instance', 'model.uid.instance', 'something'], $parameters);

$callback = function ($user) {
Expand All @@ -33,12 +35,34 @@ public function testExtractingParametersWhileCheckingForUserAccess()
$callback = function ($user, $something) {
};
$parameters = $broadcaster->extractAuthParameters('asd', 'asd', $callback);
$this->assertEquals([null], $parameters);
$this->assertEquals([], $parameters);

/**
* Test Explicit Binding...
*/
$container = new Container;
Container::setInstance($container);
$binder = m::mock(BindingRegistrar::class);
$binder->shouldReceive('getBindingCallback')->with('model')->andReturn(function () {
return 'bound';
});
$container->instance(BindingRegistrar::class, $binder);
$callback = function ($user, $model) {
};
$parameters = $broadcaster->extractAuthParameters('something.{model}', 'something.1', $callback);
$this->assertEquals(['bound'], $parameters);
Container::setInstance(new Container);
}

/**
* @expectedException Symfony\Component\HttpKernel\Exception\HttpException
*/
public function testNotFoundThrowsHttpException()
{
$broadcaster = new FakeBroadcaster();
$callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) {
};
$parameters = $broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback);
$this->assertEquals([null], $parameters);
}
}

Expand Down Expand Up @@ -76,7 +100,7 @@ public function where($key, $value)
return $this;
}

public function first()
public function firstOr()
{
return "model.{$this->value}.instance";
}
Expand All @@ -96,7 +120,8 @@ public function where($key, $value)
return $this;
}

public function first()
public function firstOr($callback)
{
return call_user_func($callback);
}
}

0 comments on commit 515d97c

Please sign in to comment.