mirror of
https://git.friendi.ca/friendica/friendica.git
synced 2025-06-07 20:04:32 +02:00
Merge 75f07fb7e3
into 415e7b5f8b
This commit is contained in:
commit
95e490782b
24 changed files with 1643 additions and 4 deletions
50
src/Addon/AddonBootstrap.php
Normal file
50
src/Addon/AddonBootstrap.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Addon;
|
||||
|
||||
use Friendica\Addon\Event\AddonStartEvent;
|
||||
|
||||
/**
|
||||
* Interface an Addon has to implement.
|
||||
*/
|
||||
interface AddonBootstrap
|
||||
{
|
||||
/**
|
||||
* Returns an array with the FQCN of required services.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```php
|
||||
* return [LoggerInterface::class];
|
||||
* ```
|
||||
*/
|
||||
public function getRequiredDependencies(): array;
|
||||
|
||||
/**
|
||||
* Return an array of events to subscribe to.
|
||||
*
|
||||
* The keys MUST be the event name.
|
||||
* The values MUST be the method of the implementing class to call.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```php
|
||||
* return [Event::NAME => 'onEvent'];
|
||||
* ```
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getSubscribedEvents(): array;
|
||||
|
||||
/**
|
||||
* Init the addon with the required dependencies.
|
||||
*/
|
||||
public function initAddon(AddonStartEvent $event): void;
|
||||
}
|
26
src/Addon/DependencyProvider.php
Normal file
26
src/Addon/DependencyProvider.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Addon;
|
||||
|
||||
/**
|
||||
* Interface that Addon can provide some dependencies and/or strategies.
|
||||
*/
|
||||
interface DependencyProvider
|
||||
{
|
||||
/**
|
||||
* Returns an array of Dice rules.
|
||||
*/
|
||||
public function provideDependencyRules(): array;
|
||||
|
||||
/**
|
||||
* Returns an array of strategy rules.
|
||||
*/
|
||||
public function provideStrategyRules(): array;
|
||||
}
|
30
src/Addon/Event/AddonStartEvent.php
Normal file
30
src/Addon/Event/AddonStartEvent.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Addon\Event;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Start an addon.
|
||||
*/
|
||||
final class AddonStartEvent
|
||||
{
|
||||
private ContainerInterface $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function getContainer(): ContainerInterface
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
}
|
26
src/Addon/InstallableAddon.php
Normal file
26
src/Addon/InstallableAddon.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Addon;
|
||||
|
||||
/**
|
||||
* Interface for an Addon that has to be installed.
|
||||
*/
|
||||
interface InstallableAddon
|
||||
{
|
||||
/**
|
||||
* Runs after AddonBootstrap::initAddon()
|
||||
*/
|
||||
public function install(): void;
|
||||
|
||||
/**
|
||||
* Runs after AddonBootstrap::initAddon()
|
||||
*/
|
||||
public function uninstall(): void;
|
||||
}
|
37
src/App.php
37
src/App.php
|
@ -44,6 +44,8 @@ use Friendica\Protocol\ATProtocol\DID;
|
|||
use Friendica\Security\Authentication;
|
||||
use Friendica\Security\ExAuth;
|
||||
use Friendica\Security\OpenWebAuth;
|
||||
use Friendica\Service\Addon\AddonContainer;
|
||||
use Friendica\Service\Addon\AddonManager;
|
||||
use Friendica\Util\BasePath;
|
||||
use Friendica\Util\DateTimeFormat;
|
||||
use Friendica\Util\HTTPInputData;
|
||||
|
@ -134,6 +136,8 @@ class App
|
|||
*/
|
||||
private $appHelper;
|
||||
|
||||
private AddonManager $addonManager;
|
||||
|
||||
private function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
@ -150,7 +154,7 @@ class App
|
|||
],
|
||||
]);
|
||||
|
||||
$this->setupContainerForAddons();
|
||||
$this->setupAddons();
|
||||
|
||||
$this->setupLogChannel(LogChannel::APP);
|
||||
|
||||
|
@ -207,7 +211,7 @@ class App
|
|||
{
|
||||
$argv = $serverParams['argv'] ?? [];
|
||||
|
||||
$this->setupContainerForAddons();
|
||||
$this->setupAddons();
|
||||
|
||||
$this->setupLogChannel($this->determineLogChannel($argv));
|
||||
|
||||
|
@ -239,7 +243,7 @@ class App
|
|||
*/
|
||||
public function processEjabberd(array $serverParams): void
|
||||
{
|
||||
$this->setupContainerForAddons();
|
||||
$this->setupAddons();
|
||||
|
||||
$this->setupLogChannel(LogChannel::AUTH_JABBERED);
|
||||
|
||||
|
@ -276,7 +280,7 @@ class App
|
|||
}
|
||||
}
|
||||
|
||||
private function setupContainerForAddons(): void
|
||||
private function setupAddons(): void
|
||||
{
|
||||
/** @var ICanLoadAddons $addonLoader */
|
||||
$addonLoader = $this->container->create(ICanLoadAddons::class);
|
||||
|
@ -284,6 +288,27 @@ class App
|
|||
foreach ($addonLoader->getActiveAddonConfig('dependencies') as $name => $rule) {
|
||||
$this->container->addRule($name, $rule);
|
||||
}
|
||||
|
||||
$config = $this->container->create(IManageConfigValues::class);
|
||||
|
||||
$this->addonManager = $this->container->create(AddonManager::class);
|
||||
|
||||
$this->addonManager->bootstrapAddons($config->get('addons') ?? []);
|
||||
|
||||
// At this place we should be careful because addons can change the container
|
||||
// Maybe we should create a new container especially for the addons
|
||||
foreach ($this->addonManager->getProvidedDependencyRules() as $name => $rule) {
|
||||
$this->container->addRule($name, $rule);
|
||||
}
|
||||
|
||||
$containers = [];
|
||||
|
||||
foreach ($this->addonManager->getRequiredDependencies() as $addonId => $dependencies) {
|
||||
// @TODO At this point we can filter or restrict the dependencies of addons
|
||||
$containers[$addonId] = AddonContainer::fromContainer($this->container, $dependencies);
|
||||
}
|
||||
|
||||
$this->addonManager->initAddons($containers);
|
||||
}
|
||||
|
||||
private function determineLogChannel(array $argv): string
|
||||
|
@ -330,6 +355,10 @@ class App
|
|||
foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) {
|
||||
$eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]);
|
||||
}
|
||||
|
||||
foreach ($this->addonManager->getAllSubscribedEvents() as $listener) {
|
||||
$eventDispatcher->addListener($listener[0], $listener[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private function registerTemplateEngine(): void
|
||||
|
|
113
src/EventSubscriber/HookEventBridge.php
Normal file
113
src/EventSubscriber/HookEventBridge.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\EventSubscriber;
|
||||
|
||||
use Friendica\Core\Hook;
|
||||
use Friendica\Event\ArrayFilterEvent;
|
||||
use Friendica\Event\ConfigLoadedEvent;
|
||||
use Friendica\Event\Event;
|
||||
use Friendica\Event\HtmlFilterEvent;
|
||||
use Friendica\Event\NamedEvent;
|
||||
|
||||
/**
|
||||
* Bridge between the EventDispatcher and the Hook class.
|
||||
*
|
||||
* @internal Provides BC
|
||||
*/
|
||||
final class HookEventBridge
|
||||
{
|
||||
/**
|
||||
* @internal This allows us to mock the Hook call in tests.
|
||||
*
|
||||
* @var \Closure|null
|
||||
*/
|
||||
private static $mockedCallHook = null;
|
||||
|
||||
/**
|
||||
* This maps the new event names to the legacy Hook names.
|
||||
*/
|
||||
private static array $eventMapper = [
|
||||
Event::INIT => 'init_1',
|
||||
ConfigLoadedEvent::CONFIG_LOADED => 'load_config',
|
||||
ArrayFilterEvent::APP_MENU => 'app_menu',
|
||||
ArrayFilterEvent::NAV_INFO => 'nav_info',
|
||||
ArrayFilterEvent::FEATURE_ENABLED => 'isEnabled',
|
||||
ArrayFilterEvent::FEATURE_GET => 'get',
|
||||
HtmlFilterEvent::HEAD => 'head',
|
||||
HtmlFilterEvent::FOOTER => 'footer',
|
||||
HtmlFilterEvent::PAGE_HEADER => 'page_header',
|
||||
HtmlFilterEvent::PAGE_CONTENT_TOP => 'page_content_top',
|
||||
HtmlFilterEvent::PAGE_END => 'page_end',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getStaticSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Event::INIT => 'onNamedEvent',
|
||||
ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent',
|
||||
ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::FEATURE_ENABLED => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::FEATURE_GET => 'onArrayFilterEvent',
|
||||
HtmlFilterEvent::HEAD => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent',
|
||||
];
|
||||
}
|
||||
|
||||
public static function onNamedEvent(NamedEvent $event): void
|
||||
{
|
||||
static::callHook($event->getName(), '');
|
||||
}
|
||||
|
||||
public static function onConfigLoadedEvent(ConfigLoadedEvent $event): void
|
||||
{
|
||||
static::callHook($event->getName(), $event->getConfig());
|
||||
}
|
||||
|
||||
public static function onArrayFilterEvent(ArrayFilterEvent $event): void
|
||||
{
|
||||
$event->setArray(
|
||||
static::callHook($event->getName(), $event->getArray())
|
||||
);
|
||||
}
|
||||
|
||||
public static function onHtmlFilterEvent(HtmlFilterEvent $event): void
|
||||
{
|
||||
$event->setHtml(
|
||||
static::callHook($event->getName(), $event->getHtml())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array|object $data
|
||||
*
|
||||
* @return string|array|object
|
||||
*/
|
||||
private static function callHook(string $name, $data)
|
||||
{
|
||||
// If possible, map the event name to the legacy Hook name
|
||||
$name = static::$eventMapper[$name] ?? $name;
|
||||
|
||||
// Little hack to allow mocking the Hook call in tests.
|
||||
if (static::$mockedCallHook instanceof \Closure) {
|
||||
return (static::$mockedCallHook)->__invoke($name, $data);
|
||||
}
|
||||
|
||||
Hook::callAll($name, $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
32
src/Service/Addon/Addon.php
Normal file
32
src/Service/Addon/Addon.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Interface to communicate with an addon.
|
||||
*/
|
||||
interface Addon
|
||||
{
|
||||
public function getId(): string;
|
||||
|
||||
public function getRequiredDependencies(): array;
|
||||
|
||||
public function getSubscribedEvents(): array;
|
||||
|
||||
public function getProvidedDependencyRules(): array;
|
||||
|
||||
public function initAddon(ContainerInterface $container): void;
|
||||
|
||||
public function installAddon(): void;
|
||||
|
||||
public function uninstallAddon(): void;
|
||||
}
|
75
src/Service/Addon/AddonContainer.php
Normal file
75
src/Service/Addon/AddonContainer.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Friendica\Core\Container;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* Subset of the Container for an addon.
|
||||
*/
|
||||
final class AddonContainer implements ContainerInterface
|
||||
{
|
||||
public static function fromContainer(Container $container, array $allowedServices): self
|
||||
{
|
||||
return new self($container, $allowedServices);
|
||||
}
|
||||
|
||||
private Container $container;
|
||||
|
||||
private array $allowedServices;
|
||||
|
||||
private function __construct(Container $container, array $allowedServices)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->allowedServices = $allowedServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entry of the container by its identifier and returns it.
|
||||
*
|
||||
* @param string $id Identifier of the entry to look for.
|
||||
*
|
||||
* @throws \Psr\Container\NotFoundExceptionInterface No entry was found for **this** identifier.
|
||||
* @throws \Psr\Container\ContainerExceptionInterface Error while retrieving the entry.
|
||||
*
|
||||
* @return mixed Entry.
|
||||
*/
|
||||
public function get(string $id)
|
||||
{
|
||||
if ($this->has($id)) {
|
||||
return $this->container->create($id);
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
'No entry was found for "%s"',
|
||||
$id,
|
||||
);
|
||||
|
||||
throw new class ($message) extends \RuntimeException implements NotFoundExceptionInterface {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the container can return an entry for the given identifier.
|
||||
* Returns false otherwise.
|
||||
*
|
||||
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
|
||||
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
|
||||
*
|
||||
* @param string $id Identifier of the entry to look for.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return in_array($id, $this->allowedServices);
|
||||
}
|
||||
}
|
74
src/Service/Addon/AddonFactory.php
Normal file
74
src/Service/Addon/AddonFactory.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Friendica\Addon\AddonBootstrap;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Factory for all addons.
|
||||
*/
|
||||
final class AddonFactory implements AddonLoader
|
||||
{
|
||||
private string $addonPath;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/** @var Addon[] */
|
||||
private array $addons = [];
|
||||
|
||||
public function __construct(string $addonPath, LoggerInterface $logger)
|
||||
{
|
||||
$this->addonPath = $addonPath;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Addon[] Returns an array of Addon instances.
|
||||
*/
|
||||
public function getAddons(array $addonNames): array
|
||||
{
|
||||
foreach ($addonNames as $addonName => $addonDetails) {
|
||||
try {
|
||||
$this->addons[$addonName] = $this->bootstrapAddon($addonName);
|
||||
} catch (\Throwable $th) {
|
||||
// @TODO Here we can check if we have a Legacy addon and try to load it
|
||||
// throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->addons;
|
||||
}
|
||||
|
||||
private function bootstrapAddon(string $addonName): Addon
|
||||
{
|
||||
$bootstrapFile = sprintf('%s/%s/bootstrap.php', $this->addonPath, $addonName);
|
||||
|
||||
if (!file_exists($bootstrapFile)) {
|
||||
throw new \RuntimeException(sprintf('Bootstrap file for addon "%s" not found.', $addonName));
|
||||
}
|
||||
|
||||
try {
|
||||
$bootstrap = require $bootstrapFile;
|
||||
} catch (\Throwable $th) {
|
||||
throw new \RuntimeException(sprintf('Something went wrong loading the Bootstrap file for addon "%s".', $addonName), $th->getCode(), $th);
|
||||
}
|
||||
|
||||
if (!($bootstrap instanceof AddonBootstrap)) {
|
||||
throw new \RuntimeException(sprintf('Bootstrap file for addon "%s" MUST return an instance of AddonBootstrap.', $addonName));
|
||||
}
|
||||
|
||||
$addon = new AddonProxy($addonName, $bootstrap);
|
||||
|
||||
$this->logger->info(sprintf('Addon "%s" loaded.', $addonName));
|
||||
|
||||
return $addon;
|
||||
}
|
||||
}
|
21
src/Service/Addon/AddonLoader.php
Normal file
21
src/Service/Addon/AddonLoader.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
/**
|
||||
* Interface for an addon loader.
|
||||
*/
|
||||
interface AddonLoader
|
||||
{
|
||||
/**
|
||||
* @return Addon[] Returns an array of Addon instances.
|
||||
*/
|
||||
public function getAddons(array $addonNames): array;
|
||||
}
|
84
src/Service/Addon/AddonManager.php
Normal file
84
src/Service/Addon/AddonManager.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Manager for all addons.
|
||||
*/
|
||||
final class AddonManager
|
||||
{
|
||||
private AddonLoader $addonFactory;
|
||||
|
||||
/** @var Addon[] */
|
||||
private array $addons = [];
|
||||
|
||||
public function __construct(AddonLoader $addonFactory)
|
||||
{
|
||||
$this->addonFactory = $addonFactory;
|
||||
}
|
||||
|
||||
public function bootstrapAddons(array $addonNames): void
|
||||
{
|
||||
$this->addons = $this->addonFactory->getAddons($addonNames);
|
||||
}
|
||||
|
||||
public function getRequiredDependencies(): array
|
||||
{
|
||||
$dependencies = [];
|
||||
|
||||
foreach ($this->addons as $addon) {
|
||||
// @TODO Here we can filter or deny dependencies from addons
|
||||
$dependencies[$addon->getId()] = $addon->getRequiredDependencies();
|
||||
}
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
public function getProvidedDependencyRules(): array
|
||||
{
|
||||
$dependencyRules = [];
|
||||
|
||||
foreach ($this->addons as $addon) {
|
||||
// @TODO At this point we can handle duplicate rules and handle possible conflicts
|
||||
$dependencyRules = array_merge($dependencyRules, $addon->getProvidedDependencyRules());
|
||||
}
|
||||
|
||||
return $dependencyRules;
|
||||
}
|
||||
|
||||
public function getAllSubscribedEvents(): array
|
||||
{
|
||||
$events = [];
|
||||
|
||||
foreach ($this->addons as $addon) {
|
||||
$events = array_merge($events, $addon->getSubscribedEvents());
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface[] $containers
|
||||
*/
|
||||
public function initAddons(array $containers): void
|
||||
{
|
||||
foreach ($this->addons as $addon) {
|
||||
$container = $containers[$addon->getId()] ?? null;
|
||||
|
||||
if ($container === null) {
|
||||
throw new \RuntimeException(sprintf('Container for addon "%s" is missing.', $addon->getId()));
|
||||
}
|
||||
|
||||
$addon->initAddon($container);
|
||||
}
|
||||
}
|
||||
}
|
91
src/Service/Addon/AddonProxy.php
Normal file
91
src/Service/Addon/AddonProxy.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Friendica\Addon\AddonBootstrap;
|
||||
use Friendica\Addon\DependencyProvider;
|
||||
use Friendica\Addon\Event\AddonStartEvent;
|
||||
use Friendica\Addon\InstallableAddon;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Proxy object for an addon.
|
||||
*/
|
||||
final class AddonProxy implements Addon
|
||||
{
|
||||
private string $id;
|
||||
|
||||
private AddonBootstrap $bootstrap;
|
||||
|
||||
private bool $isInit = false;
|
||||
|
||||
public function __construct(string $id, AddonBootstrap $bootstrap)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->bootstrap = $bootstrap;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRequiredDependencies(): array
|
||||
{
|
||||
return $this->bootstrap->getRequiredDependencies();
|
||||
}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
$events = [];
|
||||
|
||||
foreach ($this->bootstrap->getSubscribedEvents() as $eventName => $methodName) {
|
||||
$events[] = [$eventName, [$this->bootstrap, $methodName]];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function getProvidedDependencyRules(): array
|
||||
{
|
||||
if ($this->bootstrap instanceof DependencyProvider) {
|
||||
return $this->bootstrap->provideDependencyRules();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function initAddon(ContainerInterface $container): void
|
||||
{
|
||||
if ($this->isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isInit = true;
|
||||
|
||||
$event = new AddonStartEvent($container);
|
||||
|
||||
$this->bootstrap->initAddon($event);
|
||||
}
|
||||
|
||||
public function installAddon(): void
|
||||
{
|
||||
if ($this->bootstrap instanceof InstallableAddon) {
|
||||
$this->bootstrap->install();
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstallAddon(): void
|
||||
{
|
||||
if ($this->bootstrap instanceof InstallableAddon) {
|
||||
$this->bootstrap->uninstall();
|
||||
}
|
||||
}
|
||||
}
|
81
src/Service/Addon/LegacyAddonProxy.php
Normal file
81
src/Service/Addon/LegacyAddonProxy.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Service\Addon;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Proxy object for a legacy addon.
|
||||
*/
|
||||
final class LegacyAddonProxy implements Addon
|
||||
{
|
||||
private string $id;
|
||||
|
||||
private string $path;
|
||||
|
||||
private bool $isInit = false;
|
||||
|
||||
public function __construct(string $id, string $path)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRequiredDependencies(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getProvidedDependencyRules(): array
|
||||
{
|
||||
$fileName = $this->path . '/' . $this->id . '/static/dependencies.config.php';
|
||||
|
||||
if (is_file($fileName)) {
|
||||
return include_once($fileName);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function initAddon(ContainerInterface $container): void
|
||||
{
|
||||
if ($this->isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isInit = true;
|
||||
|
||||
include_once($this->path . '/' . $this->id . '/' . $this->id . '.php');
|
||||
}
|
||||
|
||||
public function installAddon(): void
|
||||
{
|
||||
if (function_exists($this->id . '_install')) {
|
||||
call_user_func($this->id . '_install');
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstallAddon(): void
|
||||
{
|
||||
if (function_exists($this->id . '_uninstall')) {
|
||||
call_user_func($this->id . '_uninstall');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,6 +48,12 @@ return (function(string $basepath, array $getVars, array $serverVars, array $coo
|
|||
$basepath . '/addon',
|
||||
],
|
||||
],
|
||||
\Friendica\Service\Addon\AddonLoader::class => [
|
||||
'instanceOf' => \Friendica\Service\Addon\AddonFactory::class,
|
||||
'constructParams' => [
|
||||
$basepath . '/addon',
|
||||
],
|
||||
],
|
||||
\Friendica\Util\BasePath::class => [
|
||||
'constructParams' => [
|
||||
$basepath,
|
||||
|
|
176
tests/Unit/EventSubscriber/HookEventBridgeTest.php
Normal file
176
tests/Unit/EventSubscriber/HookEventBridgeTest.php
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\EventSubscriber;
|
||||
|
||||
use Friendica\Core\Config\Util\ConfigFileManager;
|
||||
use Friendica\Event\ArrayFilterEvent;
|
||||
use Friendica\Event\ConfigLoadedEvent;
|
||||
use Friendica\Event\Event;
|
||||
use Friendica\Event\HtmlFilterEvent;
|
||||
use Friendica\EventSubscriber\HookEventBridge;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HookEventBridgeTest extends TestCase
|
||||
{
|
||||
public function testGetStaticSubscribedEventsReturnsStaticMethods(): void
|
||||
{
|
||||
$expected = [
|
||||
Event::INIT => 'onNamedEvent',
|
||||
ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent',
|
||||
ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::FEATURE_ENABLED => 'onArrayFilterEvent',
|
||||
ArrayFilterEvent::FEATURE_GET => 'onArrayFilterEvent',
|
||||
HtmlFilterEvent::HEAD => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent',
|
||||
HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent',
|
||||
];
|
||||
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
HookEventBridge::getStaticSubscribedEvents()
|
||||
);
|
||||
|
||||
foreach ($expected as $methodName) {
|
||||
$this->assertTrue(
|
||||
method_exists(HookEventBridge::class, $methodName),
|
||||
$methodName . '() is not defined'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
(new \ReflectionMethod(HookEventBridge::class, $methodName))->isStatic(),
|
||||
$methodName . '() is not static'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getNamedEventData(): array
|
||||
{
|
||||
return [
|
||||
['test', 'test'],
|
||||
[Event::INIT, 'init_1'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getNamedEventData
|
||||
*/
|
||||
public function testOnNamedEventCallsHook($name, $expected): void
|
||||
{
|
||||
$event = new Event($name);
|
||||
|
||||
$reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook');
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
$reflectionProperty->setValue(null, function (string $name, $data) use ($expected) {
|
||||
$this->assertSame($expected, $name);
|
||||
$this->assertSame('', $data);
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
HookEventBridge::onNamedEvent($event);
|
||||
}
|
||||
|
||||
public static function getConfigLoadedEventData(): array
|
||||
{
|
||||
return [
|
||||
['test', 'test'],
|
||||
[ConfigLoadedEvent::CONFIG_LOADED, 'load_config'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getConfigLoadedEventData
|
||||
*/
|
||||
public function testOnConfigLoadedEventCallsHookWithCorrectValue($name, $expected): void
|
||||
{
|
||||
$config = $this->createStub(ConfigFileManager::class);
|
||||
|
||||
$event = new ConfigLoadedEvent($name, $config);
|
||||
|
||||
$reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook');
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
$reflectionProperty->setValue(null, function (string $name, $data) use ($expected, $config) {
|
||||
$this->assertSame($expected, $name);
|
||||
$this->assertSame($config, $data);
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
HookEventBridge::onConfigLoadedEvent($event);
|
||||
}
|
||||
|
||||
public static function getArrayFilterEventData(): array
|
||||
{
|
||||
return [
|
||||
['test', 'test'],
|
||||
[ArrayFilterEvent::APP_MENU, 'app_menu'],
|
||||
[ArrayFilterEvent::NAV_INFO, 'nav_info'],
|
||||
[ArrayFilterEvent::FEATURE_ENABLED, 'isEnabled'],
|
||||
[ArrayFilterEvent::FEATURE_GET, 'get'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getArrayFilterEventData
|
||||
*/
|
||||
public function testOnArrayFilterEventCallsHookWithCorrectValue($name, $expected): void
|
||||
{
|
||||
$event = new ArrayFilterEvent($name, ['original']);
|
||||
|
||||
$reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook');
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
$reflectionProperty->setValue(null, function (string $name, $data) use ($expected) {
|
||||
$this->assertSame($expected, $name);
|
||||
$this->assertSame(['original'], $data);
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
HookEventBridge::onArrayFilterEvent($event);
|
||||
}
|
||||
|
||||
public static function getHtmlFilterEventData(): array
|
||||
{
|
||||
return [
|
||||
['test', 'test'],
|
||||
[HtmlFilterEvent::HEAD, 'head'],
|
||||
[HtmlFilterEvent::FOOTER, 'footer'],
|
||||
[HtmlFilterEvent::PAGE_HEADER, 'page_header'],
|
||||
[HtmlFilterEvent::PAGE_CONTENT_TOP, 'page_content_top'],
|
||||
[HtmlFilterEvent::PAGE_END, 'page_end'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getHtmlFilterEventData
|
||||
*/
|
||||
public function testOnHtmlFilterEventCallsHookWithCorrectValue($name, $expected): void
|
||||
{
|
||||
$event = new HtmlFilterEvent($name, 'original');
|
||||
|
||||
$reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook');
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
$reflectionProperty->setValue(null, function (string $name, $data) use ($expected) {
|
||||
$this->assertSame($expected, $name);
|
||||
$this->assertSame('original', $data);
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
HookEventBridge::onHtmlFilterEvent($event);
|
||||
}
|
||||
}
|
77
tests/Unit/Service/Addon/AddonContainerTest.php
Normal file
77
tests/Unit/Service/Addon/AddonContainerTest.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\Service\Addon;
|
||||
|
||||
use Friendica\Core\Container;
|
||||
use Friendica\Service\Addon\AddonContainer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class AddonContainerTest extends TestCase
|
||||
{
|
||||
public function testAddonContainerImplementsContainerInterface(): void
|
||||
{
|
||||
$container = AddonContainer::fromContainer(
|
||||
$this->createStub(Container::class),
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(ContainerInterface::class, $container);
|
||||
}
|
||||
|
||||
public function testHasReturnsFalse(): void
|
||||
{
|
||||
$container = AddonContainer::fromContainer(
|
||||
$this->createStub(Container::class),
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertFalse($container->has('foo'));
|
||||
}
|
||||
|
||||
public function testHasReturnsTrue(): void
|
||||
{
|
||||
$container = AddonContainer::fromContainer(
|
||||
$this->createStub(Container::class),
|
||||
['foo']
|
||||
);
|
||||
|
||||
$this->assertTrue($container->has('foo'));
|
||||
}
|
||||
|
||||
public function testGetReturnsEntry(): void
|
||||
{
|
||||
$object = new \stdClass();
|
||||
|
||||
$parent = $this->createMock(Container::class);
|
||||
$parent->expects($this->once())->method('create')->with('foo')->willReturn($object);
|
||||
|
||||
$container = AddonContainer::fromContainer(
|
||||
$parent,
|
||||
['foo']
|
||||
);
|
||||
|
||||
$this->assertSame($object, $container->get('foo'));
|
||||
}
|
||||
|
||||
public function testGetThrowsNotFoundExceptionInterface(): void
|
||||
{
|
||||
$container = AddonContainer::fromContainer(
|
||||
$this->createStub(Container::class),
|
||||
[]
|
||||
);
|
||||
|
||||
$this->expectException(NotFoundExceptionInterface::class);
|
||||
$this->expectExceptionMessage('No entry was found for "foo"');
|
||||
|
||||
$container->get('foo');
|
||||
}
|
||||
}
|
45
tests/Unit/Service/Addon/AddonFactoryTest.php
Normal file
45
tests/Unit/Service/Addon/AddonFactoryTest.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\Service\Addon;
|
||||
|
||||
use Friendica\Service\Addon\Addon;
|
||||
use Friendica\Service\Addon\AddonFactory;
|
||||
use Friendica\Service\Addon\AddonLoader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class AddonFactoryTest extends TestCase
|
||||
{
|
||||
public function testAddonFactoryImplementsAddonLoaderInterface(): void
|
||||
{
|
||||
$factory = new AddonFactory(
|
||||
dirname(__DIR__, 3) . '/Util',
|
||||
$this->createStub(LoggerInterface::class)
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(AddonLoader::class, $factory);
|
||||
}
|
||||
|
||||
public function testLoadAddonsLoadsTheAddon(): void
|
||||
{
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())->method('info')->with('Addon "helloaddon" loaded.');
|
||||
|
||||
$factory = new AddonFactory(
|
||||
dirname(__DIR__, 3) . '/Util',
|
||||
$logger
|
||||
);
|
||||
|
||||
$addons = $factory->getAddons(['helloaddon' => []]);
|
||||
|
||||
$this->assertArrayHasKey('helloaddon', $addons);
|
||||
$this->assertInstanceOf(Addon::class, $addons['helloaddon']);
|
||||
}
|
||||
}
|
66
tests/Unit/Service/Addon/AddonManagerTest.php
Normal file
66
tests/Unit/Service/Addon/AddonManagerTest.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\Service\Addon;
|
||||
|
||||
use Friendica\Event\HtmlFilterEvent;
|
||||
use Friendica\Service\Addon\Addon;
|
||||
use Friendica\Service\Addon\AddonLoader;
|
||||
use Friendica\Service\Addon\AddonManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AddonManagerTest extends TestCase
|
||||
{
|
||||
public function testBootstrapAddonsLoadsTheAddon(): void
|
||||
{
|
||||
$loader = $this->createMock(AddonLoader::class);
|
||||
$loader->expects($this->once())->method('getAddons')->willReturn(['helloaddon' => $this->createMock(Addon::class)]);
|
||||
|
||||
$manager = new AddonManager($loader);
|
||||
|
||||
$manager->bootstrapAddons(['helloaddon' => []]);
|
||||
}
|
||||
|
||||
public function testGetAllSubscribedEventsReturnsEvents(): void
|
||||
{
|
||||
$addon = $this->createStub(Addon::class);
|
||||
$addon->method('getSubscribedEvents')->willReturn([[HtmlFilterEvent::PAGE_END, [Addon::class, 'onPageEnd']]]);
|
||||
|
||||
$loader = $this->createStub(AddonLoader::class);
|
||||
$loader->method('getAddons')->willReturn(['helloaddon' => $addon]);
|
||||
|
||||
$manager = new AddonManager($loader);
|
||||
|
||||
$manager->bootstrapAddons(['helloaddon' => []]);
|
||||
|
||||
$this->assertSame(
|
||||
[[HtmlFilterEvent::PAGE_END, [Addon::class, 'onPageEnd']]],
|
||||
$manager->getAllSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetRequiredDependenciesReturnsArray(): void
|
||||
{
|
||||
$addon = $this->createStub(Addon::class);
|
||||
$addon->method('getId')->willReturn('helloaddon');
|
||||
$addon->method('getRequiredDependencies')->willReturn(['foo', 'bar']);
|
||||
|
||||
$loader = $this->createStub(AddonLoader::class);
|
||||
$loader->method('getAddons')->willReturn(['helloaddon' => $addon]);
|
||||
|
||||
$manager = new AddonManager($loader);
|
||||
|
||||
$manager->bootstrapAddons(['helloaddon' => []]);
|
||||
|
||||
$this->assertSame(
|
||||
['helloaddon' => ['foo', 'bar']],
|
||||
$manager->getRequiredDependencies()
|
||||
);
|
||||
}
|
||||
}
|
151
tests/Unit/Service/Addon/AddonProxyTest.php
Normal file
151
tests/Unit/Service/Addon/AddonProxyTest.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\Service\Addon;
|
||||
|
||||
use Friendica\Addon\AddonBootstrap;
|
||||
use Friendica\Addon\DependencyProvider;
|
||||
use Friendica\Addon\Event\AddonStartEvent;
|
||||
use Friendica\Addon\InstallableAddon;
|
||||
use Friendica\Service\Addon\Addon;
|
||||
use Friendica\Service\Addon\AddonProxy;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Helper interface to combine AddonBootstrap and DependencyProvider.
|
||||
*/
|
||||
interface CombinedAddonBootstrapDependencyProvider extends AddonBootstrap, DependencyProvider
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper interface to combine AddonBootstrap and InstallableAddon.
|
||||
*/
|
||||
interface CombinedAddonBootstrapInstallableAddon extends AddonBootstrap, InstallableAddon
|
||||
{
|
||||
}
|
||||
|
||||
class AddonProxyTest extends TestCase
|
||||
{
|
||||
public function testCreateWithAddonBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createStub(AddonBootstrap::class);
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$this->assertInstanceOf(Addon::class, $addon);
|
||||
}
|
||||
|
||||
public function testGetIdReturnsId(): void
|
||||
{
|
||||
$bootstrap = $this->createStub(AddonBootstrap::class);
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$this->assertSame('id', $addon->getId());
|
||||
}
|
||||
|
||||
public function testGetRequiredDependenciesCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(AddonBootstrap::class);
|
||||
$bootstrap->expects($this->once())->method('getRequiredDependencies')->willReturn([]);
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->getRequiredDependencies();
|
||||
}
|
||||
|
||||
public function testGetProvidedDependencyRulesCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(CombinedAddonBootstrapDependencyProvider::class);
|
||||
$bootstrap->expects($this->once())->method('provideDependencyRules')->willReturn([]);
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->getProvidedDependencyRules();
|
||||
}
|
||||
|
||||
public function testGetSubscribedEventsCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(AddonBootstrap::class);
|
||||
$bootstrap->expects($this->once())->method('getSubscribedEvents')->willReturn(['foo' => 'bar']);
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
['foo', [$bootstrap, 'bar']],
|
||||
],
|
||||
$addon->getSubscribedEvents()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public function testInitAddonCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(AddonBootstrap::class);
|
||||
$bootstrap->expects($this->once())->method('initAddon')->willReturnCallback(function ($event) {
|
||||
$this->assertInstanceOf(AddonStartEvent::class, $event);
|
||||
});
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
}
|
||||
|
||||
public function testInitAddonMultipleTimesWillCallBootstrapOnce(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(AddonBootstrap::class);
|
||||
$bootstrap->expects($this->once())->method('initAddon')->willReturnCallback(function ($event) {
|
||||
$this->assertInstanceOf(AddonStartEvent::class, $event);
|
||||
});
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
}
|
||||
|
||||
public function testInitAddonCallsBootstrapWithDependencies(): void
|
||||
{
|
||||
$container = $this->createStub(ContainerInterface::class);
|
||||
|
||||
$bootstrap = $this->createMock(AddonBootstrap::class);
|
||||
$bootstrap->expects($this->once())->method('initAddon')->willReturnCallback(function (AddonStartEvent $event) use ($container) {
|
||||
$this->assertSame($container, $event->getContainer());
|
||||
});
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->initAddon($container);
|
||||
}
|
||||
|
||||
public function testInstallAddonCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(CombinedAddonBootstrapInstallableAddon::class);
|
||||
$bootstrap->expects($this->once())->method('install');
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
$addon->installAddon();
|
||||
}
|
||||
|
||||
public function testUninstallAddonCallsBootstrap(): void
|
||||
{
|
||||
$bootstrap = $this->createMock(CombinedAddonBootstrapInstallableAddon::class);
|
||||
$bootstrap->expects($this->once())->method('uninstall');
|
||||
|
||||
$addon = new AddonProxy('id', $bootstrap);
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
$addon->uninstallAddon();
|
||||
}
|
||||
}
|
162
tests/Unit/Service/Addon/LegacyAddonProxyTest.php
Normal file
162
tests/Unit/Service/Addon/LegacyAddonProxyTest.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
// Copyright (C) 2010-2024, the Friendica project
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Test\Unit\Service\Addon;
|
||||
|
||||
use Friendica\Service\Addon\Addon;
|
||||
use Friendica\Service\Addon\LegacyAddonProxy;
|
||||
use org\bovigo\vfs\vfsStream;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class LegacyAddonProxyTest extends TestCase
|
||||
{
|
||||
public function testCreateWithIdAndPath(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons', 0777, ['helloaddon' => []]);
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$this->assertInstanceOf(Addon::class, $addon);
|
||||
}
|
||||
|
||||
public function testGetIdReturnsId(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons', 0777, ['helloaddon' => []]);
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$this->assertSame('helloaddon', $addon->getId());
|
||||
}
|
||||
|
||||
public function testGetRequiredDependenciesReturnsEmptyArray(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons', 0777, ['helloaddon' => []]);
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$addon->getRequiredDependencies()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetProvidedDependencyIncludesConfigFile(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons_4', 0777, ['helloaddon' => ['static' => []]]);
|
||||
|
||||
vfsStream::newFile('dependencies.config.php')
|
||||
->at($root->getChild('helloaddon/static'))
|
||||
->setContent('<?php return [\'name\' => []];');
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$this->assertSame(
|
||||
['name' => []],
|
||||
$addon->getProvidedDependencyRules()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEventsReturnsEmptyArray(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons', 0777, ['helloaddon' => []]);
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$addon->getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testInitAddonIncludesAddonFile(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons_1', 0777, ['helloaddon' => []]);
|
||||
|
||||
vfsStream::newFile('helloaddon.php')
|
||||
->at($root->getChild('helloaddon'))
|
||||
->setContent('<?php throw new \Exception("Addon loaded");');
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
try {
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
} catch (\Throwable $th) {
|
||||
$this->assertSame(
|
||||
'Addon loaded',
|
||||
$th->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testInitAddonMultipleTimesWillIncludeFileOnlyOnce(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons_2', 0777, ['helloaddon' => []]);
|
||||
|
||||
vfsStream::newFile('helloaddon.php')
|
||||
->at($root->getChild('helloaddon'))
|
||||
->setContent('<?php throw new \Exception("Addon loaded");');
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
try {
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
} catch (\Exception $th) {
|
||||
$this->assertSame(
|
||||
'Addon loaded',
|
||||
$th->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
}
|
||||
|
||||
public function testInstallAddonWillCallInstallFunction(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons_3', 0777, ['helloaddon' => []]);
|
||||
|
||||
vfsStream::newFile('helloaddon.php')
|
||||
->at($root->getChild('helloaddon'))
|
||||
->setContent('<?php function helloaddon_install() { throw new \Exception("Addon installed"); }');
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
try {
|
||||
$addon->installAddon();
|
||||
} catch (\Exception $th) {
|
||||
$this->assertSame(
|
||||
'Addon installed',
|
||||
$th->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUninstallAddonWillCallUninstallFunction(): void
|
||||
{
|
||||
$root = vfsStream::setup('addons_4', 0777, ['helloaddon' => []]);
|
||||
|
||||
vfsStream::newFile('helloaddon.php')
|
||||
->at($root->getChild('helloaddon'))
|
||||
->setContent('<?php function helloaddon_uninstall() { throw new \Exception("Addon uninstalled"); }');
|
||||
|
||||
$addon = new LegacyAddonProxy('helloaddon', $root->url());
|
||||
|
||||
$addon->initAddon($this->createStub(ContainerInterface::class));
|
||||
try {
|
||||
$addon->uninstallAddon();
|
||||
} catch (\Exception $th) {
|
||||
$this->assertSame(
|
||||
'Addon uninstalled',
|
||||
$th->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
49
tests/Util/helloaddon/bootstrap.php
Normal file
49
tests/Util/helloaddon/bootstrap.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Name: Hello Addon
|
||||
* Description: For testing purpose only
|
||||
* Version: 1.0
|
||||
* Author: Artur Weigandt <dont-mail-me@example.com>
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PSR-4 autoloader.
|
||||
*
|
||||
* @param string $class The fully-qualified class name.
|
||||
*/
|
||||
spl_autoload_register(function (string $class): void {
|
||||
// addon namespace prefix
|
||||
$prefix = 'FriendicaAddons\\HelloAddon\\';
|
||||
|
||||
// base directory for the namespace prefix
|
||||
$base_dir = __DIR__ . '/src/';
|
||||
|
||||
// does the class use the namespace prefix?
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
// no, move to the next registered autoloader
|
||||
return;
|
||||
}
|
||||
|
||||
// get the relative class name
|
||||
$relative_class = substr($class, $len);
|
||||
|
||||
// replace the namespace prefix with the base directory, replace namespace
|
||||
// separators with directory separators in the relative class name, append
|
||||
// with .php
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
// if the file exists, require it
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
|
||||
return new \FriendicaAddons\HelloAddon\HelloAddon();
|
29
tests/Util/helloaddon/helloaddon.php
Normal file
29
tests/Util/helloaddon/helloaddon.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Name: Hello Addon
|
||||
* Description: For testing purpose only
|
||||
* Version: 1.0
|
||||
* Author: Artur Weigandt <dont-mail-me@example.com>
|
||||
*/
|
||||
|
||||
use Friendica\Core\Hook;
|
||||
|
||||
function helloaddon_install()
|
||||
{
|
||||
Hook::register('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end');
|
||||
}
|
||||
|
||||
function helloaddon_uninstall()
|
||||
{
|
||||
Hook::unregister('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end');
|
||||
}
|
||||
|
||||
function helloaddon_page_end(&$html)
|
||||
{
|
||||
$html .= '<p>Hello, World!</p>';
|
||||
}
|
117
tests/Util/helloaddon/src/HelloAddon.php
Normal file
117
tests/Util/helloaddon/src/HelloAddon.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Name: Hello Addon
|
||||
* Description: For testing purpose only
|
||||
* Version: 1.0
|
||||
* Author: Artur Weigandt <dont-mail-me@example.com>
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FriendicaAddons\HelloAddon;
|
||||
|
||||
use Friendica\Addon\AddonBootstrap;
|
||||
use Friendica\Addon\DependencyProvider;
|
||||
use Friendica\Addon\Event\AddonStartEvent;
|
||||
use Friendica\Addon\InstallableAddon;
|
||||
use Friendica\Event\HtmlFilterEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class HelloAddon implements AddonBootstrap, DependencyProvider, InstallableAddon
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* Returns an array of services that are required by this addon.
|
||||
*
|
||||
* The array should contain FQCN of the required services.
|
||||
*
|
||||
* The dependencies will be passed as a PSR-11 Container to the initAddon() method via AddonStartEvent::getContainer().
|
||||
*/
|
||||
public function getRequiredDependencies(): array
|
||||
{
|
||||
return [
|
||||
LoggerInterface::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of events to subscribe to.
|
||||
*
|
||||
* The keys MUST be the event name.
|
||||
* The values MUST be the method of the implementing class to call.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```php
|
||||
* return [Event::NAME => 'onEvent'];
|
||||
* ```
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
HtmlFilterEvent::PAGE_END => 'onPageEnd',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Dice rules.
|
||||
*/
|
||||
public function provideDependencyRules(): array
|
||||
{
|
||||
// replaces require($path_to_dependencies_file);
|
||||
return [
|
||||
LoggerInterface::class => [
|
||||
'instanceOf' => NullLogger::class,
|
||||
'call' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of strategy rules.
|
||||
*/
|
||||
public function provideStrategyRules(): array
|
||||
{
|
||||
// replaces require($path_to_strategies_file);
|
||||
return [];
|
||||
}
|
||||
|
||||
public function initAddon(AddonStartEvent $event): void
|
||||
{
|
||||
$container = $event->getContainer();
|
||||
|
||||
$this->logger = $container->get(LoggerInterface::class);
|
||||
|
||||
$this->logger->info('Hello from HelloAddon');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after AddonBootstrap::initAddon()
|
||||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$this->logger->info('HelloAddon installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after AddonBootstrap::initAddon()
|
||||
*/
|
||||
public function uninstall(): void
|
||||
{
|
||||
$this->logger->info('HelloAddon uninstalled');
|
||||
}
|
||||
|
||||
public function onPageEnd(HtmlFilterEvent $event): void
|
||||
{
|
||||
$event->setHtml($event->getHtml() . '<p>Hello, World!</p>');
|
||||
}
|
||||
}
|
29
tests/Util/helloaddon/static/dependencies.config.php
Normal file
29
tests/Util/helloaddon/static/dependencies.config.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2024, the Friendica project
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
return [
|
||||
\Psr\Log\LoggerInterface::class => [
|
||||
'instanceOf' => \Psr\Log\NullLogger::class,
|
||||
'call' => null,
|
||||
],
|
||||
];
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue