diff --git a/.gitignore b/.gitignore index 7db151f..0f9bc03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ build +.idea/ +.phpunit.result.cache composer.lock docs vendor diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb888d..33a4b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-bigbluebutton-webhooks` will be documented in this file +## 9.0.0 - 2022-01-26 + +- Drop support for PHP 7 +- Upgrade spatie/laravel-webhook-client to version 3.0 +- Test Laravel 9 integration + ## 1.0.0 - 2020-06-08 - initial release diff --git a/README.md b/README.md index 972ae27..ce5bbe6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ return [ /* * You can define the job that should be run when a certain webhook hits your application * here. The key is the name of the BigBlueButton event type with the `.` replaced by a `_`. + * + * The package will automatically convert the keys to lowercase, but you should + * be cognisant of the fact that array keys are case-sensitive */ 'jobs' => [ 'meeting-created' => \BinaryCats\BigBlueButtonWebhooks\Jobs\MeetingCreatedJob::class, @@ -103,10 +106,18 @@ Unless something goes terribly wrong, this package will always respond with a `2 If the signature is not valid, the request will NOT be logged in the `webhook_calls` table but a `BinaryCats\BigBlueButtonWebhooks\Exceptions\WebhookFailed` exception will be thrown. If something goes wrong during the webhook request the thrown exception will be saved in the `exception` column. In that case the controller will send a `500` instead of `200`. +**The package will ALWAYS cast events to lowercase - so your configured keys must be lowercase, too** + **N.B.: According to the docs:** > Hooks are only removed if a call to /hooks/destroy is made or if the callbacks for the hook fail too many times (~12) for a long period of time (~5min). +**N.N.B.: Payload structure:** + +> The payload that is sent from BigBlueButton is sort of split between into three sections. +> Out of the box the package will store whatever BBB sends back to you within payload via `$request->input()`. +> If you want to transform the payload, you may want to use custom model. [Advanced Usage](#advanced-usage) + There are two ways this package enables you to handle webhook requests: you can opt to queue a job or listen to the events the package will fire. ### Handling webhook requests using jobs @@ -206,6 +217,46 @@ The above example is only one way to handle events in Laravel. To learn the othe ## Advanced usage +### Transforming the payload + +If you want to change how the payload is saved into the database, for instance, to have the `event` name as a top tier element, you may want to use custom model: + +```php +use Illuminate\Http\Request; +use Illuminate\Support\Arr; +use Spatie\WebhookClient\Models\WebhookCall as Model; +use Spatie\WebhookClient\WebhookConfig; + +class WebhookCall extends Model +{ + /** + * @param \Spatie\WebhookClient\WebhookConfig $config + * @param \Illuminate\Http\Request $request + * @return \Spatie\WebhookClient\Models\WebhookCall + */ + public static function storeWebhook(WebhookConfig $config, Request $request): Model + { + // bigblubutton payload is build in expectation of multiple events + $payload = $request->input(); + // transform event + if ($event = Arr::get($payload, 'event', null) and is_string($event)) { + $payload['event'] = json_decode($event, true); + } + // take the headers form the top + $headers = self::headersToStore($config, $request); + // parse and return + return self::create([ + 'name' => $config->name, + 'url' => $request->fullUrl(), + 'headers' => $headers, + 'payload' => $payload, + ]); + } +} +``` + +and register it in `bitbluebutton-webhooks.model` config key. The above example is based on + ### Retry handling a webhook All incoming webhook requests are written to the database. This is incredibly valuable when something goes wrong while handling a webhook call. You can easily retry processing the webhook call, after you've investigated and fixed the cause of failure, like this: diff --git a/UPGRADING.md b/UPGRADING.md index 0d887bc..92aa269 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1 +1,5 @@ # Upgrading + +## v1.0 -> 9.0 + +If you are upgrading from previous version, please note that `spatie/laravel-webhook-client` has been upgraded to ^3.0 - which adds an extra field into the webhooks table. Read [upgrading instructions](https://github.com/spatie/laravel-webhook-client/blob/main/UPGRADING.md) for more details. diff --git a/composer.json b/composer.json index a321025..06c9076 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "keywords": [ "binary-cats", "laravel", + "bbb", "bigbluebutton", "webhooks" ], @@ -18,42 +19,40 @@ } ], "require": { - "php": "^7.2||^8.0", - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0", - "spatie/laravel-webhook-client": "^2.0" + "php": "^8.0", + "illuminate/support": "^8.0|^9.0", + "spatie/laravel-webhook-client": "^3.0" }, "require-dev": { - "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0", - "phpunit/phpunit": "^8.2|^9.0" + "orchestra/testbench": "^6.0|^7.0", + "phpunit/phpunit": "^9.4" }, "autoload": { "psr-4": { - "BinaryCats\\BigBlueButtonWebhooks\\": "src" + "BinaryCats\\BigBlueButtonWebhooks\\": "src/" } }, "autoload-dev": { "psr-4": { - "BinaryCats\\BigBlueButtonWebhooks\\Tests\\": "tests" + "Tests\\": "tests/" } }, "suggest": { - "binary-cats/laravel-mailgun-webhooks": "^1.0" + "binary-cats/laravel-lob-webhooks": "Handle lob.com webhooks in your Laravel application", + "binary-cats/laravel-mailgun-webhooks": "Handle mailgun.com webhooks in your Laravel application" }, "scripts": { - "test": "vendor/bin/phpunit --color=always", - "check": [ - "php-cs-fixer fix --ansi --dry-run --diff", - "phpcs --report-width=200 --report-summary --report-full src/ tests/ --standard=PSR2 -n", - "phpmd src/,tests/ text ./phpmd.xml.dist" - ], - "fix": [ - "php-cs-fixer fix --ansi" - ] + "coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage -d pcov.enabled", + "test": "./vendor/bin/phpunit --color=always -vvv" }, "config": { + "optimize-autoloader": true, "sort-packages": true }, "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + }, "laravel": { "providers": [ "BinaryCats\\BigBlueButtonWebhooks\\BigBlueButtonWebhooksServiceProvider" diff --git a/config/bigbluebutton-webhooks.php b/config/bigbluebutton-webhooks.php index 3407508..88276c3 100644 --- a/config/bigbluebutton-webhooks.php +++ b/config/bigbluebutton-webhooks.php @@ -10,6 +10,9 @@ /* * You can define the job that should be run when a certain webhook hits your application * here. The key is the name of the BigBlueButton event type with the `.` replaced by a `_`. + * + * The package will automatically convert the keys to lowercase, but you should + * be cognisant of the fact that array keys are case-sensitive */ 'jobs' => [ // 'meeting-created' => \BinaryCats\BigBlueButtonWebhooks\Jobs\MeetingCreatedJob::class, diff --git a/phpunit.xml b/phpunit.xml index aa3d4b0..fad77dd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,6 @@ - + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + + + src/ + + + + + + + tests - - - src/ - - - - - - - + diff --git a/src/BigBlueButtonSignatureValidator.php b/src/BigBlueButtonSignatureValidator.php index bdce734..0efc612 100644 --- a/src/BigBlueButtonSignatureValidator.php +++ b/src/BigBlueButtonSignatureValidator.php @@ -9,31 +9,18 @@ class BigBlueButtonSignatureValidator implements SignatureValidator { - /** - * Bind the implemetation. - * - * @var Illuminate\Http\Request - */ - protected $request; - - /** - * Inject the config. - * - * @var Spatie\WebhookClient\WebhookConfig - */ - protected $config; - /** * True if the signature has been valiates. * - * @param Illuminate\Http\Request $request - * @param Spatie\WebhookClient\WebhookConfig $config - * + * @param \Illuminate\Http\Request $request + * @param \Spatie\WebhookClient\WebhookConfig $config * @return bool */ public function isValid(Request $request, WebhookConfig $config): bool { + // idenfity signature $signature = $request->bearerToken(); + // "pretend" to fetch secret $secret = $config->signingSecret; // For the webhooks with a signature try { diff --git a/src/BigBlueButtonWebhooksController.php b/src/BigBlueButtonWebhooksController.php index dc47e18..a9f7188 100644 --- a/src/BigBlueButtonWebhooksController.php +++ b/src/BigBlueButtonWebhooksController.php @@ -12,9 +12,9 @@ class BigBlueButtonWebhooksController /** * Invoke controller method. * - * @param \Illuminate\Http\Request $request - * @param string|null $configKey - * @return \Illuminate\Http\Response + * @param \Illuminate\Http\Request $request + * @param string|null $configKey + * @return \Symfony\Component\HttpFoundation\Response */ public function __invoke(Request $request, string $configKey = null) { @@ -30,8 +30,6 @@ public function __invoke(Request $request, string $configKey = null) 'process_webhook_job' => config('bigbluebutton-webhooks.process_webhook_job'), ]); - (new WebhookProcessor($request, $webhookConfig))->process(); - - return response()->json(['message' => 'ok']); + return (new WebhookProcessor($request, $webhookConfig))->process(); } } diff --git a/src/BigBlueButtonWebhooksServiceProvider.php b/src/BigBlueButtonWebhooksServiceProvider.php index c5aa1eb..36b35d8 100644 --- a/src/BigBlueButtonWebhooksServiceProvider.php +++ b/src/BigBlueButtonWebhooksServiceProvider.php @@ -20,9 +20,7 @@ public function boot() ], 'config'); } - Route::macro('bigbluebuttonWebhooks', function ($url) { - return Route::post($url, '\BinaryCats\BigBlueButtonWebhooks\BigBlueButtonWebhooksController'); - }); + Route::macro('bigbluebuttonWebhooks', fn ($url) => Route::post($url, BigBlueButtonWebhooksController::class)); } /** diff --git a/src/Event.php b/src/Event.php index fa1d5f5..22f7460 100644 --- a/src/Event.php +++ b/src/Event.php @@ -4,19 +4,19 @@ use BinaryCats\BigBlueButtonWebhooks\Contracts\WebhookEvent; -class Event implements WebhookEvent +final class Event implements WebhookEvent { /** * Attributes from the event. * - * @var array + * @var string[] */ public $attributes = []; /** * Create new Event. * - * @param array $attributes + * @param string[] $attributes */ public function __construct($attributes) { @@ -24,11 +24,10 @@ public function __construct($attributes) } /** - * Construct the event. - * - * @return Event + * @param mixed[] $data + * @return static */ - public static function constructFrom($data): self + public static function constructFrom(array $data): self { return new static($data); } diff --git a/src/Exceptions/UnexpectedValueException.php b/src/Exceptions/UnexpectedValueException.php index c745e8b..feb5902 100644 --- a/src/Exceptions/UnexpectedValueException.php +++ b/src/Exceptions/UnexpectedValueException.php @@ -6,6 +6,10 @@ class UnexpectedValueException extends BaseUnexpectedValueException { + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response + */ public function render($request) { return response(['error' => $this->getMessage()], 400); diff --git a/src/Exceptions/WebhookFailed.php b/src/Exceptions/WebhookFailed.php index 1d2b84b..7a4440f 100644 --- a/src/Exceptions/WebhookFailed.php +++ b/src/Exceptions/WebhookFailed.php @@ -5,23 +5,39 @@ use Exception; use Spatie\WebhookClient\Models\WebhookCall; -class WebhookFailed extends Exception +final class WebhookFailed extends Exception { + /** + * @return static + */ public static function signingSecretNotSet(): self { return new static('The webhook signing secret is not set. Make sure that the `signing_secret` config key is set to the correct value.'); } + /** + * @param string $jobClass + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @return static + */ public static function jobClassDoesNotExist(string $jobClass, WebhookCall $webhookCall): self { - return new static("Could not process webhook id `{$webhookCall->id}` of type `{$webhookCall->type} because the configured jobclass `$jobClass` does not exist."); + return new static("Could not process webhook id `{$webhookCall->id}` of type `{$webhookCall->name} because the configured jobclass `$jobClass` does not exist."); } + /** + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @return static + */ public static function missingType(WebhookCall $webhookCall): self { return new static("Webhook call id `{$webhookCall->id}` did not contain a type. Valid BigBlueButton webhook calls should always contain a type."); } + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response + */ public function render($request) { return response(['error' => $this->getMessage()], 400); diff --git a/src/Jobs/Job.php b/src/Jobs/Job.php index b0dc65f..e29f59c 100644 --- a/src/Jobs/Job.php +++ b/src/Jobs/Job.php @@ -14,21 +14,21 @@ abstract class Job /** * Bind the implementation. * - * @var Spatie\WebhookClient\Models\WebhookCall + * @var \Spatie\WebhookClient\Models\WebhookCall */ - protected $webhookCall; + protected WebhookCall $webhookCall; /** * Location of the root. * * @var string */ - protected $root = 'event.0.data'; + protected string $root = 'event.0.data'; /** * Create new Job. * - * @param Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall */ public function __construct(WebhookCall $webhookCall) { @@ -38,7 +38,7 @@ public function __construct(WebhookCall $webhookCall) /** * Fetch Payload. * - * @return array + * @return mixed[] */ protected function payload(): array { @@ -46,12 +46,11 @@ protected function payload(): array } /** - * Get the value from the payload's event data. - * - * @param string $key - * @return mixed + * @param string $key + * @param mixed $default + * @return array|\ArrayAccess|mixed */ - public function get($key, $default = null) + public function get(string $key, $default = null) { return Arr::get($this->payload(), "{$this->root}.{$key}", $default); } diff --git a/src/ProcessBigBlueButtonWebhookJob.php b/src/ProcessBigBlueButtonWebhookJob.php index 5909b3d..9a70366 100644 --- a/src/ProcessBigBlueButtonWebhookJob.php +++ b/src/ProcessBigBlueButtonWebhookJob.php @@ -4,7 +4,8 @@ use BinaryCats\BigBlueButtonWebhooks\Exceptions\WebhookFailed; use Illuminate\Support\Arr; -use Spatie\WebhookClient\ProcessWebhookJob; +use Illuminate\Support\Str; +use Spatie\WebhookClient\Jobs\ProcessWebhookJob; class ProcessBigBlueButtonWebhookJob extends ProcessWebhookJob { @@ -28,7 +29,7 @@ public function handle() throw WebhookFailed::missingType($this->webhookCall); } - event("bigbluebutton-webhooks::{$type}", $this->webhookCall); + event($this->determineEventKey($type), $this->webhookCall); $jobClass = $this->determineJobClass($type); @@ -43,10 +44,35 @@ public function handle() dispatch(new $jobClass($this->webhookCall)); } + /** + * @param string $eventType + * @return string + */ protected function determineJobClass(string $eventType): string { - $jobConfigKey = str_replace('.', '_', $eventType); + return config($this->determineJobConfigKey($eventType), ''); + } - return config("bigbluebutton-webhooks.jobs.{$jobConfigKey}", ''); + /** + * @param string $eventType + * @return string + */ + protected function determineJobConfigKey(string $eventType): string + { + return Str::of($eventType) + ->replace('.', '_') + ->prepend('bigbluebutton-webhooks.jobs.') + ->lower(); + } + + /** + * @param string $eventType + * @return string + */ + protected function determineEventKey(string $eventType): string + { + return Str::of($eventType) + ->prepend('bigbluebutton-webhooks::') + ->lower(); } } diff --git a/src/Webhook.php b/src/Webhook.php index 41d8586..81bfe32 100644 --- a/src/Webhook.php +++ b/src/Webhook.php @@ -7,10 +7,10 @@ class Webhook /** * Validate and raise an appropriate event. * - * @param array $payload - * @param string $signature - * @param string $secret - * @return BinaryCats\BigBlueButtonWebhooks\Event + * @param mixed[] $payload + * @param string $signature + * @param string $secret + * @return \BinaryCats\BigBlueButtonWebhooks\Event */ public static function constructEvent(array $payload, string $signature, string $secret): Event { diff --git a/src/WebhookCall.php b/src/WebhookCall.php index 48b239e..03cac4f 100644 --- a/src/WebhookCall.php +++ b/src/WebhookCall.php @@ -9,17 +9,26 @@ class WebhookCall extends Model { + /** + * @param \Spatie\WebhookClient\WebhookConfig $config + * @param \Illuminate\Http\Request $request + * @return \Spatie\WebhookClient\Models\WebhookCall + */ public static function storeWebhook(WebhookConfig $config, Request $request): Model { - // payload is not proper JSON, rather is it split between three blocks + // bigblubutton payload is build in expectation of multiple events $payload = $request->input(); // transform event if ($event = Arr::get($payload, 'event', null) and is_string($event)) { $payload['event'] = json_decode($event, true); } - // create + // take the headers form the top + $headers = self::headersToStore($config, $request); + // parse and return return self::create([ 'name' => $config->name, + 'url' => $request->fullUrl(), + 'headers' => $headers, 'payload' => $payload, ]); } diff --git a/src/WebhookSignature.php b/src/WebhookSignature.php index f607ca2..ac471f0 100644 --- a/src/WebhookSignature.php +++ b/src/WebhookSignature.php @@ -2,27 +2,27 @@ namespace BinaryCats\BigBlueButtonWebhooks; -class WebhookSignature +final class WebhookSignature { /** * Signature. * * @var string */ - protected $signature; + protected string $signature; /** * Signature secret. * * @var string */ - protected $secret; + protected string $secret; /** * Create new Signature. * - * @param array $signatureArray - * @param string $secret + * @param string $signature + * @param string $secret */ public function __construct(string $signature, string $secret) { @@ -33,7 +33,7 @@ public function __construct(string $signature, string $secret) /** * Statis accessor into the class constructor. * - * @param string $secret + * @param string $secret * @return WebhookSignature static */ public static function make(string $signature, string $secret) diff --git a/tests/BigBlueButtonWebhookCallTest.php b/tests/BigBlueButtonWebhookCallTest.php index 8aae26e..e394b2a 100644 --- a/tests/BigBlueButtonWebhookCallTest.php +++ b/tests/BigBlueButtonWebhookCallTest.php @@ -1,6 +1,6 @@ '1591652302965', 'domain' => 'example.com', ], + 'url' => '/webhooks/bigbluebutton', ]); $this->processBigblueButtonwEbhookJob = new ProcessBigBlueButtonWebhookJob($this->webhookCall); diff --git a/tests/DummyJob.php b/tests/DummyJob.php index da05d0e..c24de23 100644 --- a/tests/DummyJob.php +++ b/tests/DummyJob.php @@ -1,6 +1,6 @@ assertEquals('my.type', $webhookCall->payload['event'][0]['data']['id']); $this->assertEquals($payload, $webhookCall->payload); $this->assertNull($webhookCall->exception); diff --git a/tests/PayloadDefinition.php b/tests/PayloadDefinition.php index 0c06fbb..969a928 100644 --- a/tests/PayloadDefinition.php +++ b/tests/PayloadDefinition.php @@ -1,11 +1,11 @@ set('database.default', 'sqlite'); - $app['config']->set('database.connections.sqlite', [ + config()->set('database.default', 'sqlite'); + config()->set('database.connections.sqlite', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); - - config(['bigbluebutton-webhooks.signing_secret' => 'test_signing_secret']); + config()->set('bigbluebutton-webhooks.signing_secret', 'test_signing_secret'); } + /** + * @return void + */ protected function setUpDatabase() { - include_once __DIR__.'/../vendor/spatie/laravel-webhook-client/database/migrations/create_webhook_calls_table.php.stub'; + $migration = include __DIR__.'/../vendor/spatie/laravel-webhook-client/database/migrations/create_webhook_calls_table.php.stub'; - (new CreateWebhookCallsTable())->up(); + $migration->up(); } /** - * @param \Illuminate\Foundation\Application $app - * - * @return array + * @param \Illuminate\Foundation\Application $app + * @return string[] */ protected function getPackageProviders($app) { @@ -54,6 +54,9 @@ protected function getPackageProviders($app) ]; } + /** + * @return void + */ protected function disableExceptionHandling() { $this->app->instance(ExceptionHandler::class, new class extends Handler @@ -73,6 +76,11 @@ public function render($request, Exception $exception) }); } + /** + * @param array $payload + * @param string|null $configKey + * @return string + */ protected function determineBigBlueButtonSignature(array $payload, string $configKey = null): string { $secret = ($configKey) ?