diff --git a/app/Actions/Webhook.php b/app/Actions/Webhook.php index 2d11584e6..55bc855ce 100644 --- a/app/Actions/Webhook.php +++ b/app/Actions/Webhook.php @@ -3,18 +3,67 @@ namespace BookStack\Actions; use BookStack\Interfaces\Loggable; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id * @property string $name * @property string $endpoint + * @property Collection $trackedEvents */ class Webhook extends Model implements Loggable { + protected $fillable = ['name', 'endpoint']; + use HasFactory; + /** + * Define the tracked event relation a webhook. + */ + public function trackedEvents(): HasMany + { + return $this->hasMany(WebhookTrackedEvent::class); + } + + /** + * Update the tracked events for a webhook from the given list of event types. + */ + public function updateTrackedEvents(array $events): void + { + $this->trackedEvents()->delete(); + + $eventsToStore = array_intersect($events, array_values(ActivityType::all())); + if (in_array('all', $events)) { + $eventsToStore = ['all']; + } + + $trackedEvents = []; + foreach ($eventsToStore as $event) { + $trackedEvents[] = new WebhookTrackedEvent(['event' => $event]); + } + + $this->trackedEvents()->saveMany($trackedEvents); + } + + /** + * Check if this webhook tracks the given event. + */ + public function tracksEvent(string $event): bool + { + return $this->trackedEvents->pluck('event')->contains($event); + } + + /** + * Get a URL for this webhook within the settings interface. + */ + public function getUrl(string $path = ''): string + { + return url('/settings/webhooks/' . $this->id . '/' . ltrim($path, '/')); + } + /** * Get the string descriptor for this item. */ diff --git a/app/Actions/WebhookTrackedEvent.php b/app/Actions/WebhookTrackedEvent.php new file mode 100644 index 000000000..a0530620a --- /dev/null +++ b/app/Actions/WebhookTrackedEvent.php @@ -0,0 +1,18 @@ +<?php + +namespace BookStack\Actions; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + +/** + * @property int $id + * @property int $webhook_id + * @property string $event + */ +class WebhookTrackedEvent extends Model +{ + protected $fillable = ['event']; + + use HasFactory; +} diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index 15a31f312..497d623b2 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -20,8 +20,11 @@ class WebhookController extends Controller */ public function index() { - // TODO - Get and pass webhooks - return view('settings.webhooks.index'); + $webhooks = Webhook::query() + ->orderBy('name', 'desc') + ->with('trackedEvents') + ->get(); + return view('settings.webhooks.index', ['webhooks' => $webhooks]); } /** @@ -37,7 +40,16 @@ class WebhookController extends Controller */ public function store(Request $request) { - // TODO - Create webhook + $validated = $this->validate($request, [ + 'name' => ['required', 'max:150'], + 'endpoint' => ['required', 'url', 'max:500'], + 'events' => ['required', 'array'] + ]); + + $webhook = new Webhook($validated); + $webhook->save(); + $webhook->updateTrackedEvents(array_values($validated['events'])); + $this->logActivity(ActivityType::WEBHOOK_CREATE, $webhook); return redirect('/settings/webhooks'); } @@ -48,7 +60,9 @@ class WebhookController extends Controller public function edit(string $id) { /** @var Webhook $webhook */ - $webhook = Webhook::query()->findOrFail($id); + $webhook = Webhook::query() + ->with('trackedEvents') + ->findOrFail($id); return view('settings.webhooks.edit', ['webhook' => $webhook]); } @@ -58,10 +72,17 @@ class WebhookController extends Controller */ public function update(Request $request, string $id) { + $validated = $this->validate($request, [ + 'name' => ['required', 'max:150'], + 'endpoint' => ['required', 'url', 'max:500'], + 'events' => ['required', 'array'] + ]); + /** @var Webhook $webhook */ $webhook = Webhook::query()->findOrFail($id); - // TODO - Update + $webhook->fill($validated)->save(); + $webhook->updateTrackedEvents($validated['events']); $this->logActivity(ActivityType::WEBHOOK_UPDATE, $webhook); return redirect('/settings/webhooks'); @@ -85,7 +106,7 @@ class WebhookController extends Controller /** @var Webhook $webhook */ $webhook = Webhook::query()->findOrFail($id); - // TODO - Delete event type relations + $webhook->trackedEvents()->delete(); $webhook->delete(); $this->logActivity(ActivityType::WEBHOOK_DELETE, $webhook); diff --git a/database/migrations/2021_12_07_111343_create_webhooks_table.php b/database/migrations/2021_12_07_111343_create_webhooks_table.php index 7ccfe693d..2ded0b949 100644 --- a/database/migrations/2021_12_07_111343_create_webhooks_table.php +++ b/database/migrations/2021_12_07_111343_create_webhooks_table.php @@ -18,6 +18,18 @@ class CreateWebhooksTable extends Migration $table->string('name', 150); $table->string('endpoint', 500); $table->timestamps(); + + $table->index('name'); + }); + + Schema::create('webhook_tracked_events', function (Blueprint $table) { + $table->increments('id'); + $table->integer('webhook_id'); + $table->string('event', 50); + $table->timestamps(); + + $table->index('event'); + $table->index('webhook_id'); }); } diff --git a/resources/js/components/webhook-events.js b/resources/js/components/webhook-events.js index 54080d36e..aa50aa9d8 100644 --- a/resources/js/components/webhook-events.js +++ b/resources/js/components/webhook-events.js @@ -8,7 +8,7 @@ class WebhookEvents { setup() { this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]'); - this.allCheckbox = this.$refs.all; + this.allCheckbox = this.$el.querySelector('input[type="checkbox"][value="all"]'); this.$el.addEventListener('change', event => { if (event.target.checked && event.target === this.allCheckbox) { diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 6812075b3..209702d0e 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -246,6 +246,7 @@ return [ 'webhooks_events_all' => 'All system events', 'webhooks_name' => 'Webhook Name', 'webhooks_endpoint' => 'Webhook Endpoint', + 'webhook_events_table_header' => 'Events', 'webhooks_delete' => 'Delete Webhook', 'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.', 'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?', diff --git a/resources/views/form/errors.blade.php b/resources/views/form/errors.blade.php new file mode 100644 index 000000000..03cd4be88 --- /dev/null +++ b/resources/views/form/errors.blade.php @@ -0,0 +1,3 @@ +@if($errors->has($name)) + <div class="text-neg text-small">{{ $errors->first($name) }}</div> +@endif \ No newline at end of file diff --git a/resources/views/settings/webhooks/create.blade.php b/resources/views/settings/webhooks/create.blade.php index b49afe415..d5fd1d38d 100644 --- a/resources/views/settings/webhooks/create.blade.php +++ b/resources/views/settings/webhooks/create.blade.php @@ -8,7 +8,7 @@ @include('settings.parts.navbar', ['selected' => 'webhooks']) </div> - <form action="{{ url("/settings/webhooks/new") }}" method="POST"> + <form action="{{ url("/settings/webhooks/create") }}" method="POST"> @include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')]) </form> </div> diff --git a/resources/views/settings/webhooks/delete.blade.php b/resources/views/settings/webhooks/delete.blade.php index a89b01171..65560f65f 100644 --- a/resources/views/settings/webhooks/delete.blade.php +++ b/resources/views/settings/webhooks/delete.blade.php @@ -13,7 +13,7 @@ <p>{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}</p> - <form action="{{ url("/settings/webhooks/{$role->id}") }}" method="POST"> + <form action="{{ $webhook->getUrl() }}" method="POST"> {!! csrf_field() !!} {!! method_field('DELETE') !!} @@ -25,7 +25,7 @@ </div> <div> <div class="form-group text-right"> - <a href="{{ url("/settings/webhooks/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a> + <a href="{{ $webhook->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a> <button type="submit" class="button">{{ trans('common.confirm') }}</button> </div> </div> diff --git a/resources/views/settings/webhooks/edit.blade.php b/resources/views/settings/webhooks/edit.blade.php index d4e60cc14..a221b4ce7 100644 --- a/resources/views/settings/webhooks/edit.blade.php +++ b/resources/views/settings/webhooks/edit.blade.php @@ -7,7 +7,7 @@ @include('settings.parts.navbar', ['selected' => 'webhooks']) </div> - <form action="{{ url("/settings/webhooks/{$webhook->id}") }}" method="POST"> + <form action="{{ $webhook->getUrl() }}" method="POST"> {!! method_field('PUT') !!} @include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')]) </form> diff --git a/resources/views/settings/webhooks/index.blade.php b/resources/views/settings/webhooks/index.blade.php index 8adf60835..999a458ec 100644 --- a/resources/views/settings/webhooks/index.blade.php +++ b/resources/views/settings/webhooks/index.blade.php @@ -14,10 +14,40 @@ <h1 class="list-heading">{{ trans('settings.webhooks') }}</h1> <div class="text-right"> - <a href="{{ url("/settings/webhooks/create") }}" class="button outline">{{ trans('settings.webhooks_create') }}</a> + <a href="{{ url("/settings/webhooks/create") }}" + class="button outline">{{ trans('settings.webhooks_create') }}</a> </div> </div> + @if(count($webhooks) > 0) + + <table class="table"> + <tr> + <th>{{ trans('common.name') }}</th> + <th>{{ trans('settings.webhook_events_table_header') }}</th> + </tr> + @foreach($webhooks as $webhook) + <tr> + <td> + <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br> + <span class="small text-muted italic">{{ $webhook->endpoint }}</span> + </td> + <td> + @if($webhook->tracksEvent('all')) + {{ trans('settings.webhooks_events_all') }} + @else + {{ $webhook->trackedEvents->count() }} + @endif + </td> + </tr> + @endforeach + </table> + @else + <p class="text-muted empty-text"> + {{ trans('common.no_items') }} + </p> + @endif + </div> </div> diff --git a/resources/views/settings/webhooks/parts/form.blade.php b/resources/views/settings/webhooks/parts/form.blade.php index 935b01992..e2b3fc34d 100644 --- a/resources/views/settings/webhooks/parts/form.blade.php +++ b/resources/views/settings/webhooks/parts/form.blade.php @@ -24,22 +24,32 @@ <div component="webhook-events"> <label class="setting-list-label">{{ trans('settings.webhooks_events') }}</label> + @include('form.errors', ['name' => 'events']) + <p class="small">{{ trans('settings.webhooks_events_desc') }}</p> <p class="text-warn small">{{ trans('settings.webhooks_events_warning') }}</p> - <div> - <label><input type="checkbox" - name="events[]" - value="all" - refs="webhook-events@all"> - {{ trans('settings.webhooks_events_all') }}</label> + <div class="toggle-switch-list"> + @include('form.custom-checkbox', [ + 'name' => 'events[]', + 'value' => 'all', + 'label' => trans('settings.webhooks_events_all'), + 'checked' => old('events') ? in_array('all', old('events')) : (isset($webhook) ? $webhook->tracksEvent('all') : false), + ]) </div> - <hr class="my-m"> + <hr class="my-s"> - <div class="dual-column-content"> + <div class="dual-column-content toggle-switch-list"> @foreach(\BookStack\Actions\ActivityType::all() as $activityType) - <label><input type="checkbox" name="events[]" value="{{ $activityType }}">{{ $activityType }}</label> + <div> + @include('form.custom-checkbox', [ + 'name' => 'events[]', + 'value' => $activityType, + 'label' => $activityType, + 'checked' => old('events') ? in_array($activityType, old('events')) : (isset($webhook) ? $webhook->tracksEvent($activityType) : false), + ]) + </div> @endforeach </div> </div> @@ -49,7 +59,7 @@ <div class="form-group text-right"> <a href="{{ url("/settings/webhooks") }}" class="button outline">{{ trans('common.cancel') }}</a> @if ($webhook->id ?? false) - <a href="{{ url("/settings/roles/delete/{$webhook->id}") }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a> + <a href="{{ $webhook->getUrl('/delete') }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a> @endif <button type="submit" class="button">{{ trans('settings.webhooks_save') }}</button> </div>