Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/Events/LinkCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Events;

use App\Models\Link;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class LinkCreated
{
use Dispatchable, SerializesModels;

public function __construct(public Link $link)
{
}
}
19 changes: 19 additions & 0 deletions app/Events/LinkDeleted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

/*
* Because this event is dispatched after the link is deleted, only the ID is available
* rather than the entire Link model (as found in LinkCreated and LinkUpdated)
*/
class LinkDeleted
{
use Dispatchable, SerializesModels;

public function __construct(public int $link_id)
{
}
}
16 changes: 16 additions & 0 deletions app/Events/LinkUpdated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Events;

use App\Models\Link;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class LinkUpdated
{
use Dispatchable, SerializesModels;

public function __construct(public Link $link)
{
}
}
37 changes: 37 additions & 0 deletions app/Exceptions/PluginException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Exceptions;

use Exception;

class PluginException extends Exception
{
public static function tooManyParameters(object|string $class): self
{
$className = self::resolveClassName($class);
return new static("{$className}->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;
}
}
53 changes: 53 additions & 0 deletions app/Helper/PluginManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Helper;

use App\Exceptions\PluginException;
use App\Plugins\NewLinkToWaybackMachine;
Comment thread
jtgrimes marked this conversation as resolved.
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Reflector;
use ReflectionClass;
use ReflectionException;

class PluginManager
{
/**
* @throws PluginException
*/
public function registerPlugins(): void
{
$plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]);
foreach ($plugins as $plugin) {
$parameterTypes = $this->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]);
}
}
21 changes: 0 additions & 21 deletions app/Models/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions app/Plugins/NewLinkToWaybackMachine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Plugins;

use App\Enums\ModelAttribute;
use App\Events\LinkCreated;
use App\Jobs\SaveLinkToWaybackmachine;

class NewLinkToWaybackMachine
{
/**
* 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 handle(LinkCreated $event) : void
{
if (usersettings('archive_backups_enabled') === false) {
return;
}

if ($event->link->visibility === ModelAttribute::VISIBILITY_PRIVATE
&& usersettings('archive_private_backups_enabled') === false
) {
return;
}

SaveLinkToWaybackmachine::dispatchAfterResponse($event->link);
}
}
3 changes: 3 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,8 @@ public function boot(): void
Schema::defaultStringLength(191);

Paginator::useBootstrap();

PluginManager::registerPlugins();
}

/**
Expand Down
9 changes: 7 additions & 2 deletions app/Repositories/LinkRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions config/linkace.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@
'g:i A',
],
],
'plugins' => [
\App\Plugins\NewLinkToWaybackMachine::class,
],
];
62 changes: 36 additions & 26 deletions tests/Controller/Models/LinkControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
Loading