diff --git a/app/Events/LinkCreated.php b/app/Events/LinkCreated.php new file mode 100644 index 000000000..5355b5912 --- /dev/null +++ b/app/Events/LinkCreated.php @@ -0,0 +1,16 @@ +handle() should contain exactly 1 parameter"); + } + + public static function noParameters(object|string $class): self + { + $className = self::resolveClassName($class); + return new static("{$className}->handle() does not specify any events to listen for"); + } + + public static function noHandleFunction(object|string $class): self + { + $className = self::resolveClassName($class); + return new static("required function {$className}->handle() does not exist"); + } + + public static function pluginClassNotFound(object|string $class): self + { + $className = self::resolveClassName($class); + return new static("plugin {$className} not found"); + } + + private static function resolveClassName(object|string $class) + { + return (is_object($class)) ? $class->getName() : $class; + } +} diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php new file mode 100644 index 000000000..33d5040a5 --- /dev/null +++ b/app/Helper/PluginManager.php @@ -0,0 +1,53 @@ +getParameterTypesFor($plugin); + foreach ($parameterTypes as $event) { + Event::listen($event, $plugin); + } + } + } + + /** + * @throws PluginException + */ + private function getParameterTypesFor(object|string $pluginClass): array + { + try { + $reflectionClass = new ReflectionClass($pluginClass); + } catch (ReflectionException) { + throw PluginException::pluginClassNotFound($pluginClass); + } + try { + $reflectionMethod = $reflectionClass->getMethod('handle'); + } catch (ReflectionException) { + throw PluginException::noHandleFunction($pluginClass); + } + $parameters = $reflectionMethod->getParameters(); + if (count($parameters) > 1) { + throw PluginException::tooManyParameters($pluginClass); + } + if (count($parameters) < 1) { + throw PluginException::noParameters($pluginClass); + } + return Reflector::getParameterClassNames($parameters[0]); + } +} diff --git a/app/Models/Link.php b/app/Models/Link.php index 980c443de..695dd2c00 100644 --- a/app/Models/Link.php +++ b/app/Models/Link.php @@ -239,27 +239,6 @@ public function addedAt(): string return $output; } - /** - * Dispatch the SaveLinkToWaybackmachine job, if Internet Archive backups - * are enabled. - * If the link is private, private Internet Archive backups must be enabled - * too. - */ - public function initiateInternetArchiveBackup(): void - { - if (usersettings('archive_backups_enabled') === false) { - return; - } - - if ($this->visibility === ModelAttribute::VISIBILITY_PRIVATE - && usersettings('archive_private_backups_enabled') === false - ) { - return; - } - - SaveLinkToWaybackmachine::dispatchAfterResponse($this); - } - /** * Create a base uri of the link url, consisting of a possible auth, the * hostname, a port if present, and the path. The scheme, fragments and diff --git a/app/Plugins/NewLinkToWaybackMachine.php b/app/Plugins/NewLinkToWaybackMachine.php new file mode 100644 index 000000000..f05510d37 --- /dev/null +++ b/app/Plugins/NewLinkToWaybackMachine.php @@ -0,0 +1,31 @@ +link->visibility === ModelAttribute::VISIBILITY_PRIVATE + && usersettings('archive_private_backups_enabled') === false + ) { + return; + } + + SaveLinkToWaybackmachine::dispatchAfterResponse($event->link); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f8a27f5e2..682e189e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Facades\App\Helper\PluginManager; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; @@ -18,6 +19,8 @@ public function boot(): void Schema::defaultStringLength(191); Paginator::useBootstrap(); + + PluginManager::registerPlugins(); } /** diff --git a/app/Repositories/LinkRepository.php b/app/Repositories/LinkRepository.php index 9a6d94cc8..3498ab6f9 100644 --- a/app/Repositories/LinkRepository.php +++ b/app/Repositories/LinkRepository.php @@ -3,6 +3,9 @@ namespace App\Repositories; use App\Enums\ModelAttribute; +use App\Events\LinkCreated; +use App\Events\LinkDeleted; +use App\Events\LinkUpdated; use App\Helper\HtmlMeta; use App\Helper\LinkIconMapper; use App\Models\Link; @@ -46,7 +49,7 @@ public static function create(array $data, bool $flashAlerts = false): Link self::processLinkTaxonomies($link, $data); - $link->initiateInternetArchiveBackup(); + LinkCreated::dispatch($link); return $link; } @@ -65,6 +68,7 @@ public static function update(Link $link, array $data): Link $link->update($data); self::processLinkTaxonomies($link, $data); + LinkUpdated::dispatch($link); return $link; } @@ -104,14 +108,15 @@ public static function bulkUpdate(array $models, array $data): Collection public static function delete(Link $link): bool { try { + $id = $link->id; $link->tags()->detach(); $link->lists()->detach(); $link->delete(); + LinkDeleted::dispatch($id); } catch (Exception $e) { Log::error($e); return false; } - return true; } diff --git a/config/linkace.php b/config/linkace.php index ff3160ca2..e2d05d1b3 100644 --- a/config/linkace.php +++ b/config/linkace.php @@ -37,4 +37,7 @@ 'g:i A', ], ], + 'plugins' => [ + \App\Plugins\NewLinkToWaybackMachine::class, + ], ]; diff --git a/tests/Controller/Models/LinkControllerTest.php b/tests/Controller/Models/LinkControllerTest.php index 5c1fc04cf..0189ac20c 100644 --- a/tests/Controller/Models/LinkControllerTest.php +++ b/tests/Controller/Models/LinkControllerTest.php @@ -2,6 +2,9 @@ namespace Tests\Controller\Models; +use App\Events\LinkCreated; +use App\Events\LinkDeleted; +use App\Events\LinkUpdated; use App\Enums\ModelAttribute; use App\Jobs\SaveLinkToWaybackmachine; use App\Models\Link; @@ -12,6 +15,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; use Kovah\HtmlMeta\Facades\HtmlMeta; @@ -239,12 +243,9 @@ public function test_store_request_with_continue(): void $this->assertEquals('https://example.com', $databaseLink->url); } - public function test_store_request_without_archive_backup(): void + public function test_store_request_fires_store_event(): void { - UserSettings::fake([ - 'archive_backups_enabled' => false, - ]); - + Event::fake(); $this->post('links', [ 'url' => 'https://example.com', 'title' => null, @@ -253,27 +254,7 @@ public function test_store_request_without_archive_backup(): void 'tags' => null, 'visibility' => 1, ]); - - Queue::assertNotPushed(SaveLinkToWaybackmachine::class); - } - - public function test_store_request_without_private_archive_backup(): void - { - UserSettings::fake([ - 'archive_backups_enabled' => true, - 'archive_private_backups_enabled' => false, - ]); - - $this->post('links', [ - 'url' => 'https://example.com', - 'title' => null, - 'description' => null, - 'lists' => null, - 'tags' => null, - 'visibility' => 3, - ]); - - Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + Event::assertDispatched(LinkCreated::class); } public function test_store_request_with_foreign_private_tag(): void @@ -505,6 +486,24 @@ public function test_validation_error_for_update(): void ]); } + public function test_update_request_fires_update_event(): void + { + Event::fake(); + + $baseLink = Link::factory()->create(); + + $this->patch('links/1', [ + 'link_id' => $baseLink->id, + 'url' => 'https://new-example.com', + 'title' => 'New Title', + 'description' => 'New Description', + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + Event::assertDispatched(LinkUpdated::class); + } + public function test_delete_response(): void { $this->createTestLinks(); @@ -518,6 +517,17 @@ public function test_delete_response(): void $this->delete('links/3')->assertForbidden(); } + public function test_delete_request_fires_event(): void + { + Event::fake(); + + $this->createTestLinks(); + + $this->delete('links/1'); + + Event::assertDispatched(LinkDeleted::class); + } + public function test_missing_model_error_for_delete(): void { $this->delete('links/1')->assertNotFound(); diff --git a/tests/Helper/PluginManagerTest.php b/tests/Helper/PluginManagerTest.php new file mode 100644 index 000000000..da4074267 --- /dev/null +++ b/tests/Helper/PluginManagerTest.php @@ -0,0 +1,84 @@ +assertTrue(true, 'no exception thrown'); + } + + public function test_plugin_manager_registers_single_listener() + { + Config::set('linkace.plugins', [SimplePlugin::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, SimplePlugin::class); + } + + public function test_plugin_manager_registers_all_events_for_union_types() + { + Config::set('linkace.plugins', [UnionPlugin::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, UnionPlugin::class); + Event::assertListening(LinkUpdated::class, UnionPlugin::class); + } + + public function test_plugin_manager_handles_multiple_plugins() + { + Config::set('linkace.plugins', [SimplePlugin::class, UnionPlugin::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, SimplePlugin::class); + Event::assertListening(LinkCreated::class, UnionPlugin::class); + Event::assertListening(LinkUpdated::class, UnionPlugin::class); + } + + public function test_plugin_manager_throws_plugin_exception_with_missing_plugin() + { + Config::set('linkace.plugins', ['noSuchPlugin']); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_missing_handle_function() + { + Config::set('linkace.plugins', [NoHandlePlugin::class]); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_too_many_parameters() + { + Config::set('linkace.plugins', [TooManyParametersPlugin::class]); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_not_enough_parameters() + { + Config::set('linkace.plugins', [NoParameterPlugin::class]); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } +} diff --git a/tests/Mocks/NoHandlePlugin.php b/tests/Mocks/NoHandlePlugin.php new file mode 100644 index 000000000..9d3600ebf --- /dev/null +++ b/tests/Mocks/NoHandlePlugin.php @@ -0,0 +1,7 @@ +create(); + $this->actingAs($user); + + Http::preventStrayRequests(); + Http::fake([ + 'example.com' => Http::response('ok'), + ]); + + Queue::fake(); + Config::set('linkace.plugins', [\App\Plugins\NewLinkToWaybackMachine::class]); + } + + public function test_store_request_archives_links(): void + { + UserSettings::fake([ + 'archive_backups_enabled' => true, + 'archive_private_backups_enabled' => false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + + Queue::assertPushed(SaveLinkToWaybackmachine::class); + } + + public function test_store_request_doesnt_archive_without_being_enabled(): void + { + UserSettings::fake([ + 'archive_backups_enabled' => false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + + Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + } + + public function test_store_request_doesnt_archive_private_links(): void + { + UserSettings::fake([ + 'archive_backups_enabled' => true, + 'archive_private_backups_enabled' => false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 3, + ]); + + Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + } +} diff --git a/tests/Plugins/SamplePlugin.php b/tests/Plugins/SamplePlugin.php new file mode 100644 index 000000000..84ef0e383 --- /dev/null +++ b/tests/Plugins/SamplePlugin.php @@ -0,0 +1,14 @@ +