/vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php

https://gitlab.com/madwanz64/laravel · PHP · 330 lines · 153 code · 45 blank · 132 comment · 12 complexity · 3a61fb06def73beef3d9b1410889d587 MD5 · raw file

  1. <?php
  2. namespace Illuminate\Broadcasting\Broadcasters;
  3. use Exception;
  4. use Illuminate\Container\Container;
  5. use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
  6. use Illuminate\Contracts\Routing\BindingRegistrar;
  7. use Illuminate\Contracts\Routing\UrlRoutable;
  8. use Illuminate\Support\Arr;
  9. use Illuminate\Support\Reflector;
  10. use Illuminate\Support\Str;
  11. use ReflectionClass;
  12. use ReflectionFunction;
  13. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  14. abstract class Broadcaster implements BroadcasterContract
  15. {
  16. /**
  17. * The registered channel authenticators.
  18. *
  19. * @var array
  20. */
  21. protected $channels = [];
  22. /**
  23. * The registered channel options.
  24. *
  25. * @var array
  26. */
  27. protected $channelOptions = [];
  28. /**
  29. * The binding registrar instance.
  30. *
  31. * @var \Illuminate\Contracts\Routing\BindingRegistrar
  32. */
  33. protected $bindingRegistrar;
  34. /**
  35. * Register a channel authenticator.
  36. *
  37. * @param string $channel
  38. * @param callable|string $callback
  39. * @param array $options
  40. * @return $this
  41. */
  42. public function channel($channel, $callback, $options = [])
  43. {
  44. $this->channels[$channel] = $callback;
  45. $this->channelOptions[$channel] = $options;
  46. return $this;
  47. }
  48. /**
  49. * Authenticate the incoming request for a given channel.
  50. *
  51. * @param \Illuminate\Http\Request $request
  52. * @param string $channel
  53. * @return mixed
  54. *
  55. * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
  56. */
  57. protected function verifyUserCanAccessChannel($request, $channel)
  58. {
  59. foreach ($this->channels as $pattern => $callback) {
  60. if (! $this->channelNameMatchesPattern($channel, $pattern)) {
  61. continue;
  62. }
  63. $parameters = $this->extractAuthParameters($pattern, $channel, $callback);
  64. $handler = $this->normalizeChannelHandlerToCallable($callback);
  65. if ($result = $handler($this->retrieveUser($request, $channel), ...$parameters)) {
  66. return $this->validAuthenticationResponse($request, $result);
  67. }
  68. }
  69. throw new AccessDeniedHttpException;
  70. }
  71. /**
  72. * Extract the parameters from the given pattern and channel.
  73. *
  74. * @param string $pattern
  75. * @param string $channel
  76. * @param callable|string $callback
  77. * @return array
  78. */
  79. protected function extractAuthParameters($pattern, $channel, $callback)
  80. {
  81. $callbackParameters = $this->extractParameters($callback);
  82. return collect($this->extractChannelKeys($pattern, $channel))->reject(function ($value, $key) {
  83. return is_numeric($key);
  84. })->map(function ($value, $key) use ($callbackParameters) {
  85. return $this->resolveBinding($key, $value, $callbackParameters);
  86. })->values()->all();
  87. }
  88. /**
  89. * Extracts the parameters out of what the user passed to handle the channel authentication.
  90. *
  91. * @param callable|string $callback
  92. * @return \ReflectionParameter[]
  93. *
  94. * @throws \Exception
  95. */
  96. protected function extractParameters($callback)
  97. {
  98. if (is_callable($callback)) {
  99. return (new ReflectionFunction($callback))->getParameters();
  100. } elseif (is_string($callback)) {
  101. return $this->extractParametersFromClass($callback);
  102. }
  103. throw new Exception('Given channel handler is an unknown type.');
  104. }
  105. /**
  106. * Extracts the parameters out of a class channel's "join" method.
  107. *
  108. * @param string $callback
  109. * @return \ReflectionParameter[]
  110. *
  111. * @throws \Exception
  112. */
  113. protected function extractParametersFromClass($callback)
  114. {
  115. $reflection = new ReflectionClass($callback);
  116. if (! $reflection->hasMethod('join')) {
  117. throw new Exception('Class based channel must define a "join" method.');
  118. }
  119. return $reflection->getMethod('join')->getParameters();
  120. }
  121. /**
  122. * Extract the channel keys from the incoming channel name.
  123. *
  124. * @param string $pattern
  125. * @param string $channel
  126. * @return array
  127. */
  128. protected function extractChannelKeys($pattern, $channel)
  129. {
  130. preg_match('/^'.preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern).'/', $channel, $keys);
  131. return $keys;
  132. }
  133. /**
  134. * Resolve the given parameter binding.
  135. *
  136. * @param string $key
  137. * @param string $value
  138. * @param array $callbackParameters
  139. * @return mixed
  140. */
  141. protected function resolveBinding($key, $value, $callbackParameters)
  142. {
  143. $newValue = $this->resolveExplicitBindingIfPossible($key, $value);
  144. return $newValue === $value ? $this->resolveImplicitBindingIfPossible(
  145. $key, $value, $callbackParameters
  146. ) : $newValue;
  147. }
  148. /**
  149. * Resolve an explicit parameter binding if applicable.
  150. *
  151. * @param string $key
  152. * @param mixed $value
  153. * @return mixed
  154. */
  155. protected function resolveExplicitBindingIfPossible($key, $value)
  156. {
  157. $binder = $this->binder();
  158. if ($binder && $binder->getBindingCallback($key)) {
  159. return call_user_func($binder->getBindingCallback($key), $value);
  160. }
  161. return $value;
  162. }
  163. /**
  164. * Resolve an implicit parameter binding if applicable.
  165. *
  166. * @param string $key
  167. * @param mixed $value
  168. * @param array $callbackParameters
  169. * @return mixed
  170. *
  171. * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
  172. */
  173. protected function resolveImplicitBindingIfPossible($key, $value, $callbackParameters)
  174. {
  175. foreach ($callbackParameters as $parameter) {
  176. if (! $this->isImplicitlyBindable($key, $parameter)) {
  177. continue;
  178. }
  179. $className = Reflector::getParameterClassName($parameter);
  180. if (is_null($model = (new $className)->resolveRouteBinding($value))) {
  181. throw new AccessDeniedHttpException;
  182. }
  183. return $model;
  184. }
  185. return $value;
  186. }
  187. /**
  188. * Determine if a given key and parameter is implicitly bindable.
  189. *
  190. * @param string $key
  191. * @param \ReflectionParameter $parameter
  192. * @return bool
  193. */
  194. protected function isImplicitlyBindable($key, $parameter)
  195. {
  196. return $parameter->getName() === $key &&
  197. Reflector::isParameterSubclassOf($parameter, UrlRoutable::class);
  198. }
  199. /**
  200. * Format the channel array into an array of strings.
  201. *
  202. * @param array $channels
  203. * @return array
  204. */
  205. protected function formatChannels(array $channels)
  206. {
  207. return array_map(function ($channel) {
  208. return (string) $channel;
  209. }, $channels);
  210. }
  211. /**
  212. * Get the model binding registrar instance.
  213. *
  214. * @return \Illuminate\Contracts\Routing\BindingRegistrar
  215. */
  216. protected function binder()
  217. {
  218. if (! $this->bindingRegistrar) {
  219. $this->bindingRegistrar = Container::getInstance()->bound(BindingRegistrar::class)
  220. ? Container::getInstance()->make(BindingRegistrar::class) : null;
  221. }
  222. return $this->bindingRegistrar;
  223. }
  224. /**
  225. * Normalize the given callback into a callable.
  226. *
  227. * @param mixed $callback
  228. * @return \Closure|callable
  229. */
  230. protected function normalizeChannelHandlerToCallable($callback)
  231. {
  232. return is_callable($callback) ? $callback : function (...$args) use ($callback) {
  233. return Container::getInstance()
  234. ->make($callback)
  235. ->join(...$args);
  236. };
  237. }
  238. /**
  239. * Retrieve the authenticated user using the configured guard (if any).
  240. *
  241. * @param \Illuminate\Http\Request $request
  242. * @param string $channel
  243. * @return mixed
  244. */
  245. protected function retrieveUser($request, $channel)
  246. {
  247. $options = $this->retrieveChannelOptions($channel);
  248. $guards = $options['guards'] ?? null;
  249. if (is_null($guards)) {
  250. return $request->user();
  251. }
  252. foreach (Arr::wrap($guards) as $guard) {
  253. if ($user = $request->user($guard)) {
  254. return $user;
  255. }
  256. }
  257. }
  258. /**
  259. * Retrieve options for a certain channel.
  260. *
  261. * @param string $channel
  262. * @return array
  263. */
  264. protected function retrieveChannelOptions($channel)
  265. {
  266. foreach ($this->channelOptions as $pattern => $options) {
  267. if (! $this->channelNameMatchesPattern($channel, $pattern)) {
  268. continue;
  269. }
  270. return $options;
  271. }
  272. return [];
  273. }
  274. /**
  275. * Check if channel name from request match a pattern from registered channels.
  276. *
  277. * @param string $channel
  278. * @param string $pattern
  279. * @return bool
  280. */
  281. protected function channelNameMatchesPattern($channel, $pattern)
  282. {
  283. return Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel);
  284. }
  285. }