From 9e6f77c4b1289e2b713044fa693aa16339d403a1 Mon Sep 17 00:00:00 2001 From: Art4 Date: Fri, 24 Jan 2025 14:36:05 +0000 Subject: [PATCH 01/16] EventDispatcher proof of concept --- composer.json | 2 + composer.lock | 296 +++++++++++++++++- src/App.php | 32 +- src/App/Page.php | 32 +- src/Event/Event.php | 33 ++ src/Event/EventDispatcher.php | 35 +++ src/Event/HtmlFilterEvent.php | 49 +++ src/Event/NamedEvent.php | 18 ++ src/EventSubscriber/HookEventBridge.php | 90 ++++++ src/EventSubscriber/StaticEventSubscriber.php | 32 ++ static/dependencies.config.php | 3 + tests/Unit/Event/EventDispatcherTest.php | 37 +++ tests/Unit/Event/EventTest.php | 46 +++ tests/Unit/Event/HtmlFilterEventTest.php | 69 ++++ .../EventSubscriber/HookEventBridgeTest.php | 114 +++++++ tests/Unit/Util/BasePathTest.php | 24 +- tests/Unit/Util/CryptoTest.php | 2 +- 17 files changed, 887 insertions(+), 27 deletions(-) create mode 100644 src/Event/Event.php create mode 100644 src/Event/EventDispatcher.php create mode 100644 src/Event/HtmlFilterEvent.php create mode 100644 src/Event/NamedEvent.php create mode 100644 src/EventSubscriber/HookEventBridge.php create mode 100644 src/EventSubscriber/StaticEventSubscriber.php create mode 100644 tests/Unit/Event/EventDispatcherTest.php create mode 100644 tests/Unit/Event/EventTest.php create mode 100644 tests/Unit/Event/HtmlFilterEventTest.php create mode 100644 tests/Unit/EventSubscriber/HookEventBridgeTest.php diff --git a/composer.json b/composer.json index 89c8a9f41d..ce332415b6 100644 --- a/composer.json +++ b/composer.json @@ -71,9 +71,11 @@ "pragmarx/recovery": "^0.2", "psr/clock": "^1.0", "psr/container": "^2.0", + "psr/event-dispatcher": "^1.0", "psr/log": "^1.1", "seld/cli-prompt": "^1.0", "smarty/smarty": "^4", + "symfony/event-dispatcher": "^5.4", "textalk/websocket": "^1.6", "ua-parser/uap-php": "^3.9", "xemlock/htmlpurifier-html5": "^0.1.11" diff --git a/composer.lock b/composer.lock index 11cc407661..3086ed7be9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "64436f375561718bb857e3e1b0e503c9", + "content-hash": "8ee8f9186d271b65b83c2ddbd12c5c03", "packages": [ { "name": "asika/simple-console", @@ -3234,6 +3234,56 @@ ], "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.3", @@ -3709,6 +3759,170 @@ ], "time": "2022-01-02T09:53:40+00:00" }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, { "name": "symfony/polyfill-php56", "version": "v1.20.0", @@ -3774,6 +3988,86 @@ ], "time": "2020-10-23T14:02:19+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "textalk/websocket", "version": "1.6.3", diff --git a/src/App.php b/src/App.php index 5623f9a413..ace4011720 100644 --- a/src/App.php +++ b/src/App.php @@ -23,10 +23,6 @@ use Friendica\Core\Container; use Friendica\Core\Logger\LoggerManager; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; -use Friendica\Database\Definition\DbaDefinition; -use Friendica\Database\Definition\ViewDefinition; -use Friendica\Module\Maintenance; -use Friendica\Security\Authentication; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\DiceContainer; use Friendica\Core\L10n; @@ -35,9 +31,15 @@ use Friendica\Core\Logger\Handler\ErrorHandler; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\System; use Friendica\Core\Update; +use Friendica\Database\Definition\DbaDefinition; +use Friendica\Database\Definition\ViewDefinition; +use Friendica\Event\Event; +use Friendica\EventSubscriber\HookEventBridge; +use Friendica\Module\Maintenance; use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Network\HTTPException; use Friendica\Protocol\ATProtocol\DID; +use Friendica\Security\Authentication; use Friendica\Security\ExAuth; use Friendica\Security\OpenWebAuth; use Friendica\Util\BasePath; @@ -45,6 +47,7 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPInputData; use Friendica\Util\HTTPSignature; use Friendica\Util\Profiler; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -153,6 +156,8 @@ class App $this->registerErrorHandler(); + $this->registerEventDispatcher(); + $this->requestId = $this->container->create(Request::class)->getRequestId(); $this->auth = $this->container->create(Authentication::class); $this->config = $this->container->create(IManageConfigValues::class); @@ -178,6 +183,7 @@ class App $this->registerTemplateEngine(); $this->runFrontend( + $this->container->create(EventDispatcherInterface::class), $this->container->create(IManagePersonalConfigValues::class), $this->container->create(Page::class), $this->container->create(Nav::class), @@ -202,6 +208,8 @@ class App $this->registerErrorHandler(); + $this->registerEventDispatcher(); + $this->load( $serverParams, $this->container->create(DbaDefinition::class), @@ -230,6 +238,8 @@ class App $this->registerErrorHandler(); + $this->registerEventDispatcher(); + $this->load( $serverParams, $this->container->create(DbaDefinition::class), @@ -301,6 +311,16 @@ class App ErrorHandler::register($this->container->create(LoggerInterface::class)); } + private function registerEventDispatcher(): void + { + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = $this->container->create(EventDispatcherInterface::class); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } + } + private function registerTemplateEngine(): void { Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); @@ -385,6 +405,7 @@ class App * @throws \ImagickException */ private function runFrontend( + EventDispatcherInterface $eventDispatcher, IManagePersonalConfigValues $pconfig, Page $page, Nav $nav, @@ -424,7 +445,8 @@ class App $serverVars['REQUEST_METHOD'] === 'GET') { System::externalRedirect($this->baseURL . '/' . $this->args->getQueryString()); } - Core\Hook::callAll('init_1'); + + $eventDispatcher->dispatch(new Event(Event::INIT)); } DID::routeRequest($this->args->getCommand(), $serverVars); diff --git a/src/App/Page.php b/src/App/Page.php index ca83173184..b5a00859f1 100644 --- a/src/App/Page.php +++ b/src/App/Page.php @@ -14,7 +14,6 @@ use Friendica\App; use Friendica\AppHelper; use Friendica\Content\Nav; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; @@ -22,12 +21,14 @@ use Friendica\Core\Session\Model\UserSession; use Friendica\Core\System; use Friendica\Core\Theme; use Friendica\DI; +use Friendica\Event\HtmlFilterEvent; use Friendica\Network\HTTPException; use Friendica\Util\Images; use Friendica\Util\Network; use Friendica\Util\Profiler; use Friendica\Util\Strings; use GuzzleHttp\Psr7\Utils; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; /** @@ -70,6 +71,8 @@ class Page implements ArrayAccess */ private $basePath; + private EventDispatcherInterface $eventDispatcher; + private $timestamp = 0; private $method = ''; private $module = ''; @@ -78,10 +81,11 @@ class Page implements ArrayAccess /** * @param string $basepath The Page basepath */ - public function __construct(string $basepath) + public function __construct(string $basepath, EventDispatcherInterface $eventDispatcher) { - $this->timestamp = microtime(true); - $this->basePath = $basepath; + $this->timestamp = microtime(true); + $this->basePath = $basepath; + $this->eventDispatcher = $eventDispatcher; } public function setLogging(string $method, string $module, string $command) @@ -229,7 +233,10 @@ class Page implements ArrayAccess $touch_icon = 'images/friendica-192.png'; } - Hook::callAll('head', $this->page['htmlhead']); + $this->page['htmlhead'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( + HtmlFilterEvent::HEAD, + $this->page['htmlhead'] + ))->getHtml(); $tpl = Renderer::getMarkupTemplate('head.tpl'); /* put the head template at the beginning of page['htmlhead'] @@ -351,7 +358,10 @@ class Page implements ArrayAccess ]); } - Hook::callAll('footer', $this->page['footer']); + $this->page['footer'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( + HtmlFilterEvent::FOOTER, + $this->page['footer'] + ))->getHtml(); $tpl = Renderer::getMarkupTemplate('footer.tpl'); $this->page['footer'] = Renderer::replaceMacros($tpl, [ @@ -376,7 +386,10 @@ class Page implements ArrayAccess { // initialise content region if ($mode->isNormal()) { - Hook::callAll('page_content_top', $this->page['content']); + $this->page['content'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( + HtmlFilterEvent::PAGE_CONTENT_TOP, + $this->page['content'] + ))->getHtml(); } $this->page['content'] .= (string)$response->getBody(); @@ -474,7 +487,10 @@ class Page implements ArrayAccess $profiler->set(microtime(true) - $timestamp, 'aftermath'); if (!$mode->isAjax()) { - Hook::callAll('page_end', $this->page['content']); + $this->page['content'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( + HtmlFilterEvent::PAGE_END, + $this->page['content'] + ))->getHtml(); } // Add the navigation (menu) template diff --git a/src/Event/Event.php b/src/Event/Event.php new file mode 100644 index 0000000000..248b310b78 --- /dev/null +++ b/src/Event/Event.php @@ -0,0 +1,33 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php new file mode 100644 index 0000000000..6175996bc6 --- /dev/null +++ b/src/Event/EventDispatcher.php @@ -0,0 +1,35 @@ +getName(); + } + + return parent::dispatch($event, $eventName); + } +} diff --git a/src/Event/HtmlFilterEvent.php b/src/Event/HtmlFilterEvent.php new file mode 100644 index 0000000000..d8b7b742b8 --- /dev/null +++ b/src/Event/HtmlFilterEvent.php @@ -0,0 +1,49 @@ +name = $name; + $this->html = $html; + } + + public function getName(): string + { + return $this->name; + } + + public function getHtml(): string + { + return $this->html; + } + + public function setHtml(string $html): void + { + $this->html = $html; + } +} diff --git a/src/Event/NamedEvent.php b/src/Event/NamedEvent.php new file mode 100644 index 0000000000..424649664c --- /dev/null +++ b/src/Event/NamedEvent.php @@ -0,0 +1,18 @@ + 'init_1', + HtmlFilterEvent::HEAD => 'head', + HtmlFilterEvent::FOOTER => 'footer', + HtmlFilterEvent::PAGE_CONTENT_TOP => 'page_content_top', + HtmlFilterEvent::PAGE_END => 'page_end', + ]; + + /** + * @return array + */ + public static function getStaticSubscribedEvents(): array + { + return [ + Event::INIT => 'onNamedEvent', + HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', + HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent', + ]; + } + + public static function onNamedEvent(NamedEvent $event): void + { + $name = $event->getName(); + + $name = static::$eventMapper[$name] ?? $name; + + static::callHook($name, ''); + } + + public static function onHtmlFilterEvent(HtmlFilterEvent $event): void + { + $name = $event->getName(); + + $name = static::$eventMapper[$name] ?? $name; + + $event->setHtml( + static::callHook($name, $event->getHtml()) + ); + } + + /** + * @param string|array $data + * + * @return string|array + */ + private static function callHook(string $name, $data) + { + // 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; + } +} diff --git a/src/EventSubscriber/StaticEventSubscriber.php b/src/EventSubscriber/StaticEventSubscriber.php new file mode 100644 index 0000000000..16b60fb66d --- /dev/null +++ b/src/EventSubscriber/StaticEventSubscriber.php @@ -0,0 +1,32 @@ + 'onEvent']; + * ``` + * + * @return array + */ + public static function getStaticSubscribedEvents(): array; +} diff --git a/static/dependencies.config.php b/static/dependencies.config.php index f1c2f4e52e..5ffaf134e4 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -182,6 +182,9 @@ return (function(string $basepath, array $getVars, array $serverVars, array $coo ['create', [], Dice::CHAIN_CALL], ], ], + \Psr\EventDispatcher\EventDispatcherInterface::class => [ + 'instanceOf' => \Friendica\Event\EventDispatcher::class, + ], \Friendica\Core\Logger\Capability\IHaveCallIntrospections::class => [ 'instanceOf' => \Friendica\Core\Logger\Util\Introspection::class, 'constructParams' => [ diff --git a/tests/Unit/Event/EventDispatcherTest.php b/tests/Unit/Event/EventDispatcherTest.php new file mode 100644 index 0000000000..2cc1527164 --- /dev/null +++ b/tests/Unit/Event/EventDispatcherTest.php @@ -0,0 +1,37 @@ +assertInstanceOf(EventDispatcherInterface::class, $eventDispatcher); + } + + public function testDispatchANamedEventUsesNameAsEventName(): void + { + $eventDispatcher = new EventDispatcher(); + + $eventDispatcher->addListener('test', function (NamedEvent $event) { + $this->assertSame('test', $event->getName()); + }); + + $eventDispatcher->dispatch(new Event('test')); + } +} diff --git a/tests/Unit/Event/EventTest.php b/tests/Unit/Event/EventTest.php new file mode 100644 index 0000000000..8d7f882451 --- /dev/null +++ b/tests/Unit/Event/EventTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [Event::INIT, 'friendica.init'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new Event('test'); + + $this->assertSame('test', $event->getName()); + } +} diff --git a/tests/Unit/Event/HtmlFilterEventTest.php b/tests/Unit/Event/HtmlFilterEventTest.php new file mode 100644 index 0000000000..ae1d27a5ad --- /dev/null +++ b/tests/Unit/Event/HtmlFilterEventTest.php @@ -0,0 +1,69 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [HtmlFilterEvent::HEAD, 'friendica.html.head'], + [HtmlFilterEvent::FOOTER, 'friendica.html.footer'], + [HtmlFilterEvent::PAGE_CONTENT_TOP, 'friendica.html.page_content_top'], + [HtmlFilterEvent::PAGE_END, 'friendica.html.page_end'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new HtmlFilterEvent('test', ''); + + $this->assertSame('test', $event->getName()); + } + + public function testGetHtmlReturnsCorrectString(): void + { + $data = 'original'; + + $event = new HtmlFilterEvent('test', $data); + + $this->assertSame($data, $event->getHtml()); + } + + public function testSetHtmlUpdatesHtml(): void + { + $event = new HtmlFilterEvent('test', 'original'); + + $expected = 'updated'; + + $event->setHtml($expected); + + $this->assertSame($expected, $event->getHtml()); + } +} diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php new file mode 100644 index 0000000000..a9cc237bb7 --- /dev/null +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -0,0 +1,114 @@ +assertTrue( + is_subclass_of(HookEventBridge::class, StaticEventSubscriber::class, true), + HookEventBridge::class . ' does not implement ' . StaticEventSubscriber::class + ); + } + + public function testGetStaticSubscribedEventsReturnsStaticMethods(): void + { + $expected = [ + Event::INIT => 'onNamedEvent', + HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', + HtmlFilterEvent::FOOTER => '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 getHtmlFilterEventData(): array + { + return [ + ['test', 'test'], + [HtmlFilterEvent::HEAD, 'head'], + [HtmlFilterEvent::FOOTER, 'footer'], + [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); + } +} diff --git a/tests/Unit/Util/BasePathTest.php b/tests/Unit/Util/BasePathTest.php index 26c495b7fe..dbdf9bd261 100644 --- a/tests/Unit/Util/BasePathTest.php +++ b/tests/Unit/Util/BasePathTest.php @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -declare(strict_types = 1); +declare(strict_types=1); namespace Friendica\Test\Unit\Util; @@ -16,48 +16,48 @@ class BasePathTest extends TestCase { public static function getDataPaths(): array { - $basePath = dirname(__DIR__, 3); + $basePath = dirname(__DIR__, 3); $configPath = $basePath . DIRECTORY_SEPARATOR . 'config'; return [ 'fullPath' => [ - 'server' => [], - 'baseDir' => $configPath, + 'server' => [], + 'baseDir' => $configPath, 'expected' => $configPath, ], 'relative' => [ - 'server' => [], - 'baseDir' => 'config', + 'server' => [], + 'baseDir' => 'config', 'expected' => $configPath, ], 'document_root' => [ 'server' => [ 'DOCUMENT_ROOT' => $configPath, ], - 'baseDir' => '/noooop', + 'baseDir' => '/noooop', 'expected' => $configPath, ], 'pwd' => [ 'server' => [ 'PWD' => $configPath, ], - 'baseDir' => '/noooop', + 'baseDir' => '/noooop', 'expected' => $configPath, ], 'no_overwrite' => [ 'server' => [ 'DOCUMENT_ROOT' => $basePath, - 'PWD' => $basePath, + 'PWD' => $basePath, ], - 'baseDir' => 'config', + 'baseDir' => 'config', 'expected' => $configPath, ], 'no_overwrite_if_invalid' => [ 'server' => [ 'DOCUMENT_ROOT' => '/nopopop', - 'PWD' => $configPath, + 'PWD' => $configPath, ], - 'baseDir' => '/noatgawe22fafa', + 'baseDir' => '/noatgawe22fafa', 'expected' => $configPath, ] ]; diff --git a/tests/Unit/Util/CryptoTest.php b/tests/Unit/Util/CryptoTest.php index 9dbffb29b4..41fb1e2826 100644 --- a/tests/Unit/Util/CryptoTest.php +++ b/tests/Unit/Util/CryptoTest.php @@ -5,7 +5,7 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -declare(strict_types = 1); +declare(strict_types=1); namespace Friendica\Test\Unit\Util; From 2e660f44f25e3b7e44099377823add68f40c246e Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 27 Jan 2025 15:10:04 +0000 Subject: [PATCH 02/16] Mark new event and eventdispatcher classes as internal --- src/Event/Event.php | 2 ++ src/Event/EventDispatcher.php | 2 ++ src/Event/HtmlFilterEvent.php | 2 ++ src/Event/NamedEvent.php | 2 ++ src/EventSubscriber/HookEventBridge.php | 2 ++ src/EventSubscriber/StaticEventSubscriber.php | 2 ++ 6 files changed, 12 insertions(+) diff --git a/src/Event/Event.php b/src/Event/Event.php index 248b310b78..fc2511ed11 100644 --- a/src/Event/Event.php +++ b/src/Event/Event.php @@ -11,6 +11,8 @@ namespace Friendica\Event; /** * One-way Event to inform listener about something happend. + * + * @internal */ final class Event implements NamedEvent { diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php index 6175996bc6..da92a9b76c 100644 --- a/src/Event/EventDispatcher.php +++ b/src/Event/EventDispatcher.php @@ -13,6 +13,8 @@ use Symfony\Component\EventDispatcher\EventDispatcher as SymfonyEventDispatcher; /** * Modified Event Dispatcher. + * + * @internal */ final class EventDispatcher extends SymfonyEventDispatcher { diff --git a/src/Event/HtmlFilterEvent.php b/src/Event/HtmlFilterEvent.php index d8b7b742b8..0831fd468b 100644 --- a/src/Event/HtmlFilterEvent.php +++ b/src/Event/HtmlFilterEvent.php @@ -11,6 +11,8 @@ namespace Friendica\Event; /** * Allow Event listener to modify HTML. + * + * @internal */ final class HtmlFilterEvent implements NamedEvent { diff --git a/src/Event/NamedEvent.php b/src/Event/NamedEvent.php index 424649664c..3f94959cf6 100644 --- a/src/Event/NamedEvent.php +++ b/src/Event/NamedEvent.php @@ -11,6 +11,8 @@ namespace Friendica\Event; /** * Interface for named events. + * + * @internal */ interface NamedEvent { diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index b1408caa40..bce4063ef2 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -16,6 +16,8 @@ use Friendica\Event\NamedEvent; /** * Bridge between the EventDispatcher and the Hook class. + * + * @internal Provides BC */ final class HookEventBridge implements StaticEventSubscriber { diff --git a/src/EventSubscriber/StaticEventSubscriber.php b/src/EventSubscriber/StaticEventSubscriber.php index 16b60fb66d..4606b4a6cc 100644 --- a/src/EventSubscriber/StaticEventSubscriber.php +++ b/src/EventSubscriber/StaticEventSubscriber.php @@ -11,6 +11,8 @@ namespace Friendica\EventSubscriber; /** * Define events that should be reacted to. + * + * @internal */ interface StaticEventSubscriber { From 2a02f588865aeba9b1ac39c6f66b8f8e0c6e4842 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 27 Jan 2025 15:11:01 +0000 Subject: [PATCH 03/16] Remove unused StaticEventSubscriber interface --- src/EventSubscriber/HookEventBridge.php | 2 +- src/EventSubscriber/StaticEventSubscriber.php | 34 ------------------- .../EventSubscriber/HookEventBridgeTest.php | 9 ----- 3 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 src/EventSubscriber/StaticEventSubscriber.php diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index bce4063ef2..f4da4f9dd5 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -19,7 +19,7 @@ use Friendica\Event\NamedEvent; * * @internal Provides BC */ -final class HookEventBridge implements StaticEventSubscriber +final class HookEventBridge { /** * This allows us to mock the Hook call in tests. diff --git a/src/EventSubscriber/StaticEventSubscriber.php b/src/EventSubscriber/StaticEventSubscriber.php deleted file mode 100644 index 4606b4a6cc..0000000000 --- a/src/EventSubscriber/StaticEventSubscriber.php +++ /dev/null @@ -1,34 +0,0 @@ - 'onEvent']; - * ``` - * - * @return array - */ - public static function getStaticSubscribedEvents(): array; -} diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index a9cc237bb7..ec04ded87e 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -12,19 +12,10 @@ namespace Friendica\Test\Unit\EventSubscriber; use Friendica\Event\Event; use Friendica\Event\HtmlFilterEvent; use Friendica\EventSubscriber\HookEventBridge; -use Friendica\EventSubscriber\StaticEventSubscriber; use PHPUnit\Framework\TestCase; class HookEventBridgeTest extends TestCase { - public function testCorrectImplementation(): void - { - $this->assertTrue( - is_subclass_of(HookEventBridge::class, StaticEventSubscriber::class, true), - HookEventBridge::class . ' does not implement ' . StaticEventSubscriber::class - ); - } - public function testGetStaticSubscribedEventsReturnsStaticMethods(): void { $expected = [ From 7e199f034b91a06cd59d8be5b36a254698cbe878 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 27 Jan 2025 15:39:22 +0000 Subject: [PATCH 04/16] Create DI::eventDispatcher() where constructor injection is not possible atm --- src/DI.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/DI.php b/src/DI.php index 94bb4c308a..43a60dae0b 100644 --- a/src/DI.php +++ b/src/DI.php @@ -789,4 +789,13 @@ abstract class DI { return self::$dice->create(Content\Post\Repository\PostMedia::class); } + + /** + * @internal The EventDispatcher should never called outside of the core, like in addons or themes + * @deprecated 2025.02 Use constructor injection instead + */ + public static function eventDispatcher(): \Psr\EventDispatcher\EventDispatcherInterface + { + return self::$dice->create(\Psr\EventDispatcher\EventDispatcherInterface::class); + } } From 0c406a36964640331a9748eeddc3ac9caf8244e7 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 27 Jan 2025 16:02:58 +0000 Subject: [PATCH 05/16] Create ConfigLoadedEvent --- src/App.php | 8 ++++- src/EventSubscriber/HookEventBridge.php | 14 +++++++- .../EventSubscriber/HookEventBridgeTest.php | 33 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/App.php b/src/App.php index ace4011720..738370ede3 100644 --- a/src/App.php +++ b/src/App.php @@ -33,6 +33,7 @@ use Friendica\Core\System; use Friendica\Core\Update; use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\ViewDefinition; +use Friendica\Event\ConfigLoadedEvent; use Friendica\Event\Event; use Friendica\EventSubscriber\HookEventBridge; use Friendica\Module\Maintenance; @@ -177,6 +178,7 @@ class App $this->mode, $this->config, $this->profiler, + $this->container->create(EventDispatcherInterface::class), $this->appHelper, ); @@ -217,6 +219,7 @@ class App $this->container->create(Mode::class), $this->container->create(IManageConfigValues::class), $this->container->create(Profiler::class), + $this->container->create(EventDispatcherInterface::class), $this->container->create(AppHelper::class), ); @@ -247,6 +250,7 @@ class App $this->container->create(Mode::class), $this->container->create(IManageConfigValues::class), $this->container->create(Profiler::class), + $this->container->create(EventDispatcherInterface::class), $this->container->create(AppHelper::class), ); @@ -336,6 +340,7 @@ class App Mode $mode, IManageConfigValues $config, Profiler $profiler, + EventDispatcherInterface $eventDispatcher, AppHelper $appHelper ): void { if ($config->get('system', 'ini_max_execution_time') !== false) { @@ -359,7 +364,8 @@ class App if ($mode->has(Mode::DBAVAILABLE)) { Core\Hook::loadHooks(); $loader = (new Config())->createConfigFileManager($appHelper->getBasePath(), $serverParams); - Core\Hook::callAll('load_config', $loader); + + $eventDispatcher->dispatch(new ConfigLoadedEvent(ConfigLoadedEvent::CONFIG_LOADED, $loader)); // Hooks are now working, reload the whole definitions with hook enabled $dbaDefinition->load(true); diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index f4da4f9dd5..3c4e6adc28 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace Friendica\EventSubscriber; use Friendica\Core\Hook; +use Friendica\Event\ConfigLoadedEvent; use Friendica\Event\Event; use Friendica\Event\HtmlFilterEvent; use Friendica\Event\NamedEvent; @@ -33,6 +34,7 @@ final class HookEventBridge */ private static array $eventMapper = [ Event::INIT => 'init_1', + ConfigLoadedEvent::CONFIG_LOADED => 'load_config', HtmlFilterEvent::HEAD => 'head', HtmlFilterEvent::FOOTER => 'footer', HtmlFilterEvent::PAGE_CONTENT_TOP => 'page_content_top', @@ -46,6 +48,7 @@ final class HookEventBridge { return [ Event::INIT => 'onNamedEvent', + ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', @@ -73,10 +76,19 @@ final class HookEventBridge ); } + public static function onConfigLoadedEvent(ConfigLoadedEvent $event): void + { + $name = $event->getName(); + + $name = static::$eventMapper[$name] ?? $name; + + static::callHook($name, $event->getConfig()); + } + /** * @param string|array $data * - * @return string|array + * @return string|array|object */ private static function callHook(string $name, $data) { diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index ec04ded87e..ea5460eca3 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace Friendica\Test\Unit\EventSubscriber; +use Friendica\Core\Config\Util\ConfigFileManager; +use Friendica\Event\ConfigLoadedEvent; use Friendica\Event\Event; use Friendica\Event\HtmlFilterEvent; use Friendica\EventSubscriber\HookEventBridge; @@ -20,6 +22,7 @@ class HookEventBridgeTest extends TestCase { $expected = [ Event::INIT => 'onNamedEvent', + ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', @@ -72,6 +75,36 @@ class HookEventBridgeTest extends TestCase 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 getHtmlFilterEventData(): array { return [ From 16fb80be1fed02beeecabec9c1b9df5d8fd345f3 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 28 Jan 2025 07:14:01 +0000 Subject: [PATCH 06/16] Add missing files --- src/Event/ConfigLoadedEvent.php | 42 ++++++++++++++++ tests/Unit/Event/ConfigLoadedEventTest.php | 56 ++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/Event/ConfigLoadedEvent.php create mode 100644 tests/Unit/Event/ConfigLoadedEventTest.php diff --git a/src/Event/ConfigLoadedEvent.php b/src/Event/ConfigLoadedEvent.php new file mode 100644 index 0000000000..8923c53681 --- /dev/null +++ b/src/Event/ConfigLoadedEvent.php @@ -0,0 +1,42 @@ +name = $name; + $this->config = $config; + } + + public function getName(): string + { + return $this->name; + } + + public function getConfig(): ConfigFileManager + { + return $this->config; + } +} diff --git a/tests/Unit/Event/ConfigLoadedEventTest.php b/tests/Unit/Event/ConfigLoadedEventTest.php new file mode 100644 index 0000000000..473b75ac89 --- /dev/null +++ b/tests/Unit/Event/ConfigLoadedEventTest.php @@ -0,0 +1,56 @@ +createStub(ConfigFileManager::class)); + + $this->assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [ConfigLoadedEvent::CONFIG_LOADED, 'friendica.config_loaded'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new ConfigLoadedEvent('test', $this->createStub(ConfigFileManager::class)); + + $this->assertSame('test', $event->getName()); + } + + public function testGetConfigReturnsCorrectString(): void + { + $config = $this->createStub(ConfigFileManager::class); + + $event = new ConfigLoadedEvent('test', $config); + + $this->assertSame($config, $event->getConfig()); + } +} From 4cbee618d6f433d3830f80f219a007f76aa0fb1d Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 28 Jan 2025 07:23:44 +0000 Subject: [PATCH 07/16] Refactor HookEventBridge --- src/EventSubscriber/HookEventBridge.php | 31 +++++++++---------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index 3c4e6adc28..a90c364103 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -58,31 +58,19 @@ final class HookEventBridge public static function onNamedEvent(NamedEvent $event): void { - $name = $event->getName(); - - $name = static::$eventMapper[$name] ?? $name; - - static::callHook($name, ''); - } - - public static function onHtmlFilterEvent(HtmlFilterEvent $event): void - { - $name = $event->getName(); - - $name = static::$eventMapper[$name] ?? $name; - - $event->setHtml( - static::callHook($name, $event->getHtml()) - ); + static::callHook($event->getName(), ''); } public static function onConfigLoadedEvent(ConfigLoadedEvent $event): void { - $name = $event->getName(); + static::callHook($event->getName(), $event->getConfig()); + } - $name = static::$eventMapper[$name] ?? $name; - - static::callHook($name, $event->getConfig()); + public static function onHtmlFilterEvent(HtmlFilterEvent $event): void + { + $event->setHtml( + static::callHook($event->getName(), $event->getHtml()) + ); } /** @@ -92,6 +80,9 @@ final class HookEventBridge */ 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); From 5b28b3d28ff391f73bc2f5dcc6cbcf65a9186236 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 28 Jan 2025 09:11:33 +0000 Subject: [PATCH 08/16] Let event classes extending Event --- src/Event/ConfigLoadedEvent.php | 12 +++--------- src/Event/Event.php | 2 +- src/Event/HtmlFilterEvent.php | 12 +++--------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Event/ConfigLoadedEvent.php b/src/Event/ConfigLoadedEvent.php index 8923c53681..67f1407389 100644 --- a/src/Event/ConfigLoadedEvent.php +++ b/src/Event/ConfigLoadedEvent.php @@ -16,23 +16,17 @@ use Friendica\Core\Config\Util\ConfigFileManager; * * @internal */ -final class ConfigLoadedEvent implements NamedEvent +final class ConfigLoadedEvent extends Event { public const CONFIG_LOADED = 'friendica.config_loaded'; - private string $name; - private ConfigFileManager $config; public function __construct(string $name, ConfigFileManager $config) { - $this->name = $name; - $this->config = $config; - } + parent::__construct($name); - public function getName(): string - { - return $this->name; + $this->config = $config; } public function getConfig(): ConfigFileManager diff --git a/src/Event/Event.php b/src/Event/Event.php index fc2511ed11..90defc3204 100644 --- a/src/Event/Event.php +++ b/src/Event/Event.php @@ -14,7 +14,7 @@ namespace Friendica\Event; * * @internal */ -final class Event implements NamedEvent +class Event implements NamedEvent { /** * Friendica is initialized. diff --git a/src/Event/HtmlFilterEvent.php b/src/Event/HtmlFilterEvent.php index 0831fd468b..6b71ffb5bc 100644 --- a/src/Event/HtmlFilterEvent.php +++ b/src/Event/HtmlFilterEvent.php @@ -14,7 +14,7 @@ namespace Friendica\Event; * * @internal */ -final class HtmlFilterEvent implements NamedEvent +final class HtmlFilterEvent extends Event { public const HEAD = 'friendica.html.head'; @@ -24,19 +24,13 @@ final class HtmlFilterEvent implements NamedEvent public const PAGE_END = 'friendica.html.page_end'; - private string $name; - private string $html; public function __construct(string $name, string $html) { - $this->name = $name; - $this->html = $html; - } + parent::__construct($name); - public function getName(): string - { - return $this->name; + $this->html = $html; } public function getHtml(): string From 715248d6a22c113dd6f4d14338ae10f3eaa8098c Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 28 Jan 2025 15:24:49 +0000 Subject: [PATCH 09/16] Add ArrayFilterEvent, replace app_menu hook --- src/Content/Nav.php | 21 +++--- src/Event/ArrayFilterEvent.php | 39 +++++++++++ src/EventSubscriber/HookEventBridge.php | 10 +++ tests/Unit/Event/ArrayFilterEventTest.php | 66 +++++++++++++++++++ .../EventSubscriber/HookEventBridgeTest.php | 30 +++++++++ 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/Event/ArrayFilterEvent.php create mode 100644 tests/Unit/Event/ArrayFilterEventTest.php diff --git a/src/Content/Nav.php b/src/Content/Nav.php index fdfcd4bde6..e5da7d878a 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -15,6 +15,7 @@ use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Model\User; use Friendica\Module\Conversation\Community; @@ -22,6 +23,7 @@ use Friendica\Module\Home; use Friendica\Module\Security\Login; use Friendica\Network\HTTPException; use Friendica\Security\OpenWebAuth; +use Psr\EventDispatcher\EventDispatcherInterface; class Nav { @@ -63,14 +65,17 @@ class Nav /** @var Router */ private $router; - public function __construct(BaseURL $baseUrl, L10n $l10n, IHandleUserSessions $session, Database $database, IManageConfigValues $config, Router $router) + private EventDispatcherInterface $eventDispatcher; + + public function __construct(BaseURL $baseUrl, L10n $l10n, IHandleUserSessions $session, Database $database, IManageConfigValues $config, Router $router, EventDispatcherInterface $eventDispatcher) { - $this->baseUrl = $baseUrl; - $this->l10n = $l10n; - $this->session = $session; - $this->database = $database; - $this->config = $config; - $this->router = $router; + $this->baseUrl = $baseUrl; + $this->l10n = $l10n; + $this->session = $session; + $this->database = $database; + $this->config = $config; + $this->router = $router; + $this->eventDispatcher = $eventDispatcher; } /** @@ -151,7 +156,7 @@ class Nav ) { $arr = ['app_menu' => $appMenu]; - Hook::callAll('app_menu', $arr); + $arr = $this->eventDispatcher->dispatch(new ArrayFilterEvent(ArrayFilterEvent::APP_MENU, $arr))->getArray(); $appMenu = $arr['app_menu']; } diff --git a/src/Event/ArrayFilterEvent.php b/src/Event/ArrayFilterEvent.php new file mode 100644 index 0000000000..7e9063398b --- /dev/null +++ b/src/Event/ArrayFilterEvent.php @@ -0,0 +1,39 @@ +array = $array; + } + + public function getArray(): array + { + return $this->array; + } + + public function setArray(array $array): void + { + $this->array = $array; + } +} diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index a90c364103..662df204fd 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -10,6 +10,7 @@ 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; @@ -35,6 +36,7 @@ final class HookEventBridge private static array $eventMapper = [ Event::INIT => 'init_1', ConfigLoadedEvent::CONFIG_LOADED => 'load_config', + ArrayFilterEvent::APP_MENU => 'app_menu', HtmlFilterEvent::HEAD => 'head', HtmlFilterEvent::FOOTER => 'footer', HtmlFilterEvent::PAGE_CONTENT_TOP => 'page_content_top', @@ -49,6 +51,7 @@ final class HookEventBridge return [ Event::INIT => 'onNamedEvent', ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', + ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', @@ -66,6 +69,13 @@ final class HookEventBridge 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( diff --git a/tests/Unit/Event/ArrayFilterEventTest.php b/tests/Unit/Event/ArrayFilterEventTest.php new file mode 100644 index 0000000000..c709e0ff0a --- /dev/null +++ b/tests/Unit/Event/ArrayFilterEventTest.php @@ -0,0 +1,66 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [ArrayFilterEvent::APP_MENU, 'friendica.data.app_menu'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new ArrayFilterEvent('test', []); + + $this->assertSame('test', $event->getName()); + } + + public function testGetArrayReturnsCorrectString(): void + { + $data = ['original']; + + $event = new ArrayFilterEvent('test', $data); + + $this->assertSame($data, $event->getArray()); + } + + public function testSetArrayUpdatesHtml(): void + { + $event = new ArrayFilterEvent('test', ['original']); + + $expected = ['updated']; + + $event->setArray($expected); + + $this->assertSame($expected, $event->getArray()); + } +} diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index ea5460eca3..3b9b0afba1 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -10,6 +10,7 @@ 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; @@ -23,6 +24,7 @@ class HookEventBridgeTest extends TestCase $expected = [ Event::INIT => 'onNamedEvent', ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', + ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', @@ -105,6 +107,34 @@ class HookEventBridgeTest extends TestCase HookEventBridge::onConfigLoadedEvent($event); } + public static function getArrayFilterEventData(): array + { + return [ + ['test', 'test'], + [ArrayFilterEvent::APP_MENU, 'app_menu'], + ]; + } + + /** + * @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 [ From b9a191f6d82b06ec61853333e1eef5e108871bcb Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 07:46:56 +0000 Subject: [PATCH 10/16] Replace Hook with EventDispatcher in Content Nav class --- src/Content/Nav.php | 16 +++++++++++----- src/Event/ArrayFilterEvent.php | 2 ++ src/Event/HtmlFilterEvent.php | 2 ++ src/EventSubscriber/HookEventBridge.php | 6 +++++- .../Unit/EventSubscriber/HookEventBridgeTest.php | 3 +++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Content/Nav.php b/src/Content/Nav.php index e5da7d878a..695d32ffdd 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -10,12 +10,12 @@ namespace Friendica\Content; use Friendica\App\BaseURL; use Friendica\App\Router; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; use Friendica\Event\ArrayFilterEvent; +use Friendica\Event\HtmlFilterEvent; use Friendica\Model\Contact; use Friendica\Model\User; use Friendica\Module\Conversation\Community; @@ -119,7 +119,9 @@ class Nav '$search_hint' => $this->l10n->t('@name, !group, #tags, content') ]); - Hook::callAll('page_header', $nav); + $nav = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::PAGE_HEADER, $nav) + )->getHtml(); return $nav; } @@ -156,9 +158,11 @@ class Nav ) { $arr = ['app_menu' => $appMenu]; - $arr = $this->eventDispatcher->dispatch(new ArrayFilterEvent(ArrayFilterEvent::APP_MENU, $arr))->getArray(); + $arr = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::APP_MENU, $arr) + )->getArray(); - $appMenu = $arr['app_menu']; + $appMenu = $arr['app_menu'] ?? []; } return $appMenu; @@ -342,7 +346,9 @@ class Nav 'userinfo' => $userinfo, ]; - Hook::callAll('nav_info', $nav_info); + $nav_info = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::NAV_INFO, $nav_info) + )->getArray(); return $nav_info; } diff --git a/src/Event/ArrayFilterEvent.php b/src/Event/ArrayFilterEvent.php index 7e9063398b..4c888dcb80 100644 --- a/src/Event/ArrayFilterEvent.php +++ b/src/Event/ArrayFilterEvent.php @@ -18,6 +18,8 @@ final class ArrayFilterEvent extends Event { public const APP_MENU = 'friendica.data.app_menu'; + public const NAV_INFO = 'friendica.data.nav_info'; + private array $array; public function __construct(string $name, array $array) diff --git a/src/Event/HtmlFilterEvent.php b/src/Event/HtmlFilterEvent.php index 6b71ffb5bc..7d1bbb2b55 100644 --- a/src/Event/HtmlFilterEvent.php +++ b/src/Event/HtmlFilterEvent.php @@ -20,6 +20,8 @@ final class HtmlFilterEvent extends Event public const FOOTER = 'friendica.html.footer'; + public const PAGE_HEADER = 'friendica.html.page_header'; + public const PAGE_CONTENT_TOP = 'friendica.html.page_content_top'; public const PAGE_END = 'friendica.html.page_end'; diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index 662df204fd..c7e3280125 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -24,7 +24,7 @@ use Friendica\Event\NamedEvent; final class HookEventBridge { /** - * This allows us to mock the Hook call in tests. + * @internal This allows us to mock the Hook call in tests. * * @var \Closure|null */ @@ -37,8 +37,10 @@ final class HookEventBridge Event::INIT => 'init_1', ConfigLoadedEvent::CONFIG_LOADED => 'load_config', ArrayFilterEvent::APP_MENU => 'app_menu', + ArrayFilterEvent::NAV_INFO => 'nav_info', HtmlFilterEvent::HEAD => 'head', HtmlFilterEvent::FOOTER => 'footer', + HtmlFilterEvent::PAGE_HEADER => 'page_header', HtmlFilterEvent::PAGE_CONTENT_TOP => 'page_content_top', HtmlFilterEvent::PAGE_END => 'page_end', ]; @@ -52,8 +54,10 @@ final class HookEventBridge Event::INIT => 'onNamedEvent', ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', + ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent', ]; diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index 3b9b0afba1..ce1ebc58a0 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -25,8 +25,10 @@ class HookEventBridgeTest extends TestCase Event::INIT => 'onNamedEvent', ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', + ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent', ]; @@ -141,6 +143,7 @@ class HookEventBridgeTest extends TestCase ['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'], ]; From f3ccd198a4d1052f370748094d6c8667883d80cf Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 08:12:08 +0000 Subject: [PATCH 11/16] Replace Hook with EventDispatcher in Feature class --- src/Content/Feature.php | 17 +++++++++++++---- src/Event/ArrayFilterEvent.php | 2 ++ src/EventSubscriber/HookEventBridge.php | 4 +++- .../EventSubscriber/HookEventBridgeTest.php | 3 +++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Content/Feature.php b/src/Content/Feature.php index 8b5e56eba4..92198ae2c9 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -9,6 +9,7 @@ namespace Friendica\Content; use Friendica\Core\Hook; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; class Feature { @@ -41,15 +42,23 @@ class Feature */ public static function isEnabled(int $uid, $feature): bool { - if (!DI::config()->get('feature_lock', $feature, false)) { - $enabled = DI::config()->get('feature', $feature) ?? self::getDefault($feature); - $enabled = DI::pConfig()->get($uid, 'feature', $feature) ?? $enabled; + $config = DI::config(); + $pConfig = DI::pConfig(); + $eventDispatcher = DI::eventDispatcher(); + + if (!$config->get('feature_lock', $feature, false)) { + $enabled = $config->get('feature', $feature) ?? self::getDefault($feature); + $enabled = $pConfig->get($uid, 'feature', $feature) ?? $enabled; } else { $enabled = true; } $arr = ['uid' => $uid, 'feature' => $feature, 'enabled' => $enabled]; - Hook::callAll('isEnabled', $arr); + + $arr = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::FEATURE_ENABLED, $arr) + )->getArray(); + return (bool)$arr['enabled']; } diff --git a/src/Event/ArrayFilterEvent.php b/src/Event/ArrayFilterEvent.php index 4c888dcb80..5d084e42d5 100644 --- a/src/Event/ArrayFilterEvent.php +++ b/src/Event/ArrayFilterEvent.php @@ -20,6 +20,8 @@ final class ArrayFilterEvent extends Event public const NAV_INFO = 'friendica.data.nav_info'; + public const FEATURE_ENABLED = 'friendica.data.feature_enabled'; + private array $array; public function __construct(string $name, array $array) diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index c7e3280125..ede5fe409f 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -38,6 +38,7 @@ final class HookEventBridge ConfigLoadedEvent::CONFIG_LOADED => 'load_config', ArrayFilterEvent::APP_MENU => 'app_menu', ArrayFilterEvent::NAV_INFO => 'nav_info', + ArrayFilterEvent::FEATURE_ENABLED => 'isEnabled', HtmlFilterEvent::HEAD => 'head', HtmlFilterEvent::FOOTER => 'footer', HtmlFilterEvent::PAGE_HEADER => 'page_header', @@ -55,6 +56,7 @@ final class HookEventBridge ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent', + ArrayFilterEvent::FEATURE_ENABLED => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent', @@ -88,7 +90,7 @@ final class HookEventBridge } /** - * @param string|array $data + * @param string|array|object $data * * @return string|array|object */ diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index ce1ebc58a0..df138f4577 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -26,6 +26,7 @@ class HookEventBridgeTest extends TestCase ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent', + ArrayFilterEvent::FEATURE_ENABLED => 'onArrayFilterEvent', HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent', @@ -114,6 +115,8 @@ class HookEventBridgeTest extends TestCase return [ ['test', 'test'], [ArrayFilterEvent::APP_MENU, 'app_menu'], + [ArrayFilterEvent::NAV_INFO, 'nav_info'], + [ArrayFilterEvent::FEATURE_ENABLED, 'isEnabled'], ]; } From 673fc71c21d91be69d4770a256c199a6eb2189c4 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 08:16:50 +0000 Subject: [PATCH 12/16] Stop calling DI container over and over again --- src/Content/Feature.php | 54 +++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Content/Feature.php b/src/Content/Feature.php index 92198ae2c9..67031a73c3 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -95,55 +95,57 @@ class Feature */ public static function get($filtered = true) { - $arr = [ + $l10n = DI::l10n(); + $config = DI::config(); + $arr = [ // General 'general' => [ - DI::l10n()->t('General Features'), - //array('expire', DI::l10n()->t('Content Expiration'), DI::l10n()->t('Remove old posts/comments after a period of time')), - [self::PHOTO_LOCATION, DI::l10n()->t('Photo Location'), DI::l10n()->t("Photo metadata is normally stripped. This extracts the location \x28if present\x29 prior to stripping metadata and links it to a map."), false, DI::config()->get('feature_lock', self::PHOTO_LOCATION, false)], - [self::COMMUNITY, DI::l10n()->t('Display the community in the navigation'), DI::l10n()->t('If enabled, the community can be accessed via the navigation menu. Independent from this setting, the community timelines can always be accessed via the channels.'), true, DI::config()->get('feature_lock', self::COMMUNITY, false)], + $l10n->t('General Features'), + //array('expire', $l10n->t('Content Expiration'), $l10n->t('Remove old posts/comments after a period of time')), + [self::PHOTO_LOCATION, $l10n->t('Photo Location'), $l10n->t("Photo metadata is normally stripped. This extracts the location \x28if present\x29 prior to stripping metadata and links it to a map."), false, $config->get('feature_lock', self::PHOTO_LOCATION, false)], + [self::COMMUNITY, $l10n->t('Display the community in the navigation'), $l10n->t('If enabled, the community can be accessed via the navigation menu. Independent from this setting, the community timelines can always be accessed via the channels.'), true, $config->get('feature_lock', self::COMMUNITY, false)], ], // Post composition 'composition' => [ - DI::l10n()->t('Post Composition Features'), - [self::EXPLICIT_MENTIONS, DI::l10n()->t('Explicit Mentions'), DI::l10n()->t('Add explicit mentions to comment box for manual control over who gets mentioned in replies.'), false, DI::config()->get('feature_lock', Feature::EXPLICIT_MENTIONS, false)], - [self::ADD_ABSTRACT, DI::l10n()->t('Add an abstract from ActivityPub content warnings'), DI::l10n()->t('Add an abstract when commenting on ActivityPub posts with a content warning. Abstracts are displayed as content warning on systems like Mastodon or Pleroma.'), false, DI::config()->get('feature_lock', self::ADD_ABSTRACT, false)], + $l10n->t('Post Composition Features'), + [self::EXPLICIT_MENTIONS, $l10n->t('Explicit Mentions'), $l10n->t('Add explicit mentions to comment box for manual control over who gets mentioned in replies.'), false, $config->get('feature_lock', Feature::EXPLICIT_MENTIONS, false)], + [self::ADD_ABSTRACT, $l10n->t('Add an abstract from ActivityPub content warnings'), $l10n->t('Add an abstract when commenting on ActivityPub posts with a content warning. Abstracts are displayed as content warning on systems like Mastodon or Pleroma.'), false, $config->get('feature_lock', self::ADD_ABSTRACT, false)], ], // Item tools 'tools' => [ - DI::l10n()->t('Post/Comment Tools'), - [self::CATEGORIES, DI::l10n()->t('Post Categories'), DI::l10n()->t('Add categories to your posts'), false, DI::config()->get('feature_lock', self::CATEGORIES, false)], + $l10n->t('Post/Comment Tools'), + [self::CATEGORIES, $l10n->t('Post Categories'), $l10n->t('Add categories to your posts'), false, $config->get('feature_lock', self::CATEGORIES, false)], ], // Widget visibility on the network stream 'network' => [ - DI::l10n()->t('Network Widgets'), - [self::CIRCLES, DI::l10n()->t('Circles'), DI::l10n()->t('Display posts that have been created by accounts of the selected circle.'), true, DI::config()->get('feature_lock', self::CIRCLES, false)], - [self::GROUPS, DI::l10n()->t('Groups'), DI::l10n()->t('Display posts that have been distributed by the selected group.'), true, DI::config()->get('feature_lock', self::GROUPS, false)], - [self::ARCHIVE, DI::l10n()->t('Archives'), DI::l10n()->t('Display an archive where posts can be selected by month and year.'), true, DI::config()->get('feature_lock', self::ARCHIVE, false)], - [self::NETWORKS, DI::l10n()->t('Protocols'), DI::l10n()->t('Display posts with the selected protocols.'), true, DI::config()->get('feature_lock', self::NETWORKS, false)], - [self::ACCOUNTS, DI::l10n()->t('Account Types'), DI::l10n()->t('Display posts done by accounts with the selected account type.'), true, DI::config()->get('feature_lock', self::ACCOUNTS, false)], - [self::CHANNELS, DI::l10n()->t('Channels'), DI::l10n()->t('Display posts in the system channels and user defined channels.'), true, DI::config()->get('feature_lock', self::CHANNELS, false)], - [self::SEARCHES, DI::l10n()->t('Saved Searches'), DI::l10n()->t('Display posts that contain subscribed hashtags.'), true, DI::config()->get('feature_lock', self::SEARCHES, false)], - [self::FOLDERS, DI::l10n()->t('Saved Folders'), DI::l10n()->t('Display a list of folders in which posts are stored.'), true, DI::config()->get('feature_lock', self::FOLDERS, false)], - [self::NOSHARER, DI::l10n()->t('Own Contacts'), DI::l10n()->t('Include or exclude posts from subscribed accounts. This widget is not visible on all channels.'), true, DI::config()->get('feature_lock', self::NOSHARER, false)], - [self::TRENDING_TAGS, DI::l10n()->t('Trending Tags'), DI::l10n()->t('Display a list of the most popular tags in recent public posts.'), false, DI::config()->get('feature_lock', self::TRENDING_TAGS, false)], + $l10n->t('Network Widgets'), + [self::CIRCLES, $l10n->t('Circles'), $l10n->t('Display posts that have been created by accounts of the selected circle.'), true, $config->get('feature_lock', self::CIRCLES, false)], + [self::GROUPS, $l10n->t('Groups'), $l10n->t('Display posts that have been distributed by the selected group.'), true, $config->get('feature_lock', self::GROUPS, false)], + [self::ARCHIVE, $l10n->t('Archives'), $l10n->t('Display an archive where posts can be selected by month and year.'), true, $config->get('feature_lock', self::ARCHIVE, false)], + [self::NETWORKS, $l10n->t('Protocols'), $l10n->t('Display posts with the selected protocols.'), true, $config->get('feature_lock', self::NETWORKS, false)], + [self::ACCOUNTS, $l10n->t('Account Types'), $l10n->t('Display posts done by accounts with the selected account type.'), true, $config->get('feature_lock', self::ACCOUNTS, false)], + [self::CHANNELS, $l10n->t('Channels'), $l10n->t('Display posts in the system channels and user defined channels.'), true, $config->get('feature_lock', self::CHANNELS, false)], + [self::SEARCHES, $l10n->t('Saved Searches'), $l10n->t('Display posts that contain subscribed hashtags.'), true, $config->get('feature_lock', self::SEARCHES, false)], + [self::FOLDERS, $l10n->t('Saved Folders'), $l10n->t('Display a list of folders in which posts are stored.'), true, $config->get('feature_lock', self::FOLDERS, false)], + [self::NOSHARER, $l10n->t('Own Contacts'), $l10n->t('Include or exclude posts from subscribed accounts. This widget is not visible on all channels.'), true, $config->get('feature_lock', self::NOSHARER, false)], + [self::TRENDING_TAGS, $l10n->t('Trending Tags'), $l10n->t('Display a list of the most popular tags in recent public posts.'), false, $config->get('feature_lock', self::TRENDING_TAGS, false)], ], // Advanced Profile Settings 'advanced_profile' => [ - DI::l10n()->t('Advanced Profile Settings'), - [self::TAGCLOUD, DI::l10n()->t('Tag Cloud'), DI::l10n()->t('Provide a personal tag cloud on your profile page'), false, DI::config()->get('feature_lock', self::TAGCLOUD, false)], - [self::MEMBER_SINCE, DI::l10n()->t('Display Membership Date'), DI::l10n()->t('Display membership date in profile'), false, DI::config()->get('feature_lock', self::MEMBER_SINCE, false)], + $l10n->t('Advanced Profile Settings'), + [self::TAGCLOUD, $l10n->t('Tag Cloud'), $l10n->t('Provide a personal tag cloud on your profile page'), false, $config->get('feature_lock', self::TAGCLOUD, false)], + [self::MEMBER_SINCE, $l10n->t('Display Membership Date'), $l10n->t('Display membership date in profile'), false, $config->get('feature_lock', self::MEMBER_SINCE, false)], ], //Advanced Calendar Settings 'advanced_calendar' => [ - DI::l10n()->t('Advanced Calendar Settings'), - [self::PUBLIC_CALENDAR, DI::l10n()->t('Allow anonymous access to your calendar'), DI::l10n()->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, DI::config()->get('feature_lock', self::PUBLIC_CALENDAR, false)], + $l10n->t('Advanced Calendar Settings'), + [self::PUBLIC_CALENDAR, $l10n->t('Allow anonymous access to your calendar'), $l10n->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, $config->get('feature_lock', self::PUBLIC_CALENDAR, false)], ] ]; From d9a2d676d28352332bb61e85792cd2867bef7a83 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 08:37:03 +0000 Subject: [PATCH 13/16] Replace Hook with EventDispatcher in Feature class --- src/Content/Feature.php | 11 +++++++---- src/Event/ArrayFilterEvent.php | 2 ++ src/EventSubscriber/HookEventBridge.php | 2 ++ tests/Unit/EventSubscriber/HookEventBridgeTest.php | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Content/Feature.php b/src/Content/Feature.php index 67031a73c3..89165e232c 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -7,7 +7,6 @@ namespace Friendica\Content; -use Friendica\Core\Hook; use Friendica\DI; use Friendica\Event\ArrayFilterEvent; @@ -95,8 +94,9 @@ class Feature */ public static function get($filtered = true) { - $l10n = DI::l10n(); - $config = DI::config(); + $l10n = DI::l10n(); + $config = DI::config(); + $eventDispatcher = DI::eventDispatcher(); $arr = [ // General @@ -170,7 +170,10 @@ class Feature } } - Hook::callAll('get', $arr); + $arr = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::FEATURE_GET, $arr) + )->getArray(); + return $arr; } } diff --git a/src/Event/ArrayFilterEvent.php b/src/Event/ArrayFilterEvent.php index 5d084e42d5..839aa40b75 100644 --- a/src/Event/ArrayFilterEvent.php +++ b/src/Event/ArrayFilterEvent.php @@ -22,6 +22,8 @@ final class ArrayFilterEvent extends Event public const FEATURE_ENABLED = 'friendica.data.feature_enabled'; + public const FEATURE_GET = 'friendica.data.feature_get'; + private array $array; public function __construct(string $name, array $array) diff --git a/src/EventSubscriber/HookEventBridge.php b/src/EventSubscriber/HookEventBridge.php index ede5fe409f..f5dde4fbb7 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/EventSubscriber/HookEventBridge.php @@ -39,6 +39,7 @@ final class HookEventBridge 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', @@ -57,6 +58,7 @@ final class HookEventBridge 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', diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/EventSubscriber/HookEventBridgeTest.php index df138f4577..07bfa81229 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/EventSubscriber/HookEventBridgeTest.php @@ -27,6 +27,7 @@ class HookEventBridgeTest extends TestCase 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', @@ -117,6 +118,7 @@ class HookEventBridgeTest extends TestCase [ArrayFilterEvent::APP_MENU, 'app_menu'], [ArrayFilterEvent::NAV_INFO, 'nav_info'], [ArrayFilterEvent::FEATURE_ENABLED, 'isEnabled'], + [ArrayFilterEvent::FEATURE_GET, 'get'], ]; } From 70a6c6ed013105fe2e6e78b936420359665021da Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 08:53:49 +0000 Subject: [PATCH 14/16] Change code style --- src/App/Page.php | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/App/Page.php b/src/App/Page.php index b5a00859f1..6937a69240 100644 --- a/src/App/Page.php +++ b/src/App/Page.php @@ -233,10 +233,9 @@ class Page implements ArrayAccess $touch_icon = 'images/friendica-192.png'; } - $this->page['htmlhead'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( - HtmlFilterEvent::HEAD, - $this->page['htmlhead'] - ))->getHtml(); + $this->page['htmlhead'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::HEAD, $this->page['htmlhead']) + )->getHtml(); $tpl = Renderer::getMarkupTemplate('head.tpl'); /* put the head template at the beginning of page['htmlhead'] @@ -358,10 +357,9 @@ class Page implements ArrayAccess ]); } - $this->page['footer'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( - HtmlFilterEvent::FOOTER, - $this->page['footer'] - ))->getHtml(); + $this->page['footer'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::FOOTER, $this->page['footer']) + )->getHtml(); $tpl = Renderer::getMarkupTemplate('footer.tpl'); $this->page['footer'] = Renderer::replaceMacros($tpl, [ @@ -386,10 +384,9 @@ class Page implements ArrayAccess { // initialise content region if ($mode->isNormal()) { - $this->page['content'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( - HtmlFilterEvent::PAGE_CONTENT_TOP, - $this->page['content'] - ))->getHtml(); + $this->page['content'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::PAGE_CONTENT_TOP, $this->page['content']) + )->getHtml(); } $this->page['content'] .= (string)$response->getBody(); @@ -487,10 +484,9 @@ class Page implements ArrayAccess $profiler->set(microtime(true) - $timestamp, 'aftermath'); if (!$mode->isAjax()) { - $this->page['content'] = $this->eventDispatcher->dispatch(new HtmlFilterEvent( - HtmlFilterEvent::PAGE_END, - $this->page['content'] - ))->getHtml(); + $this->page['content'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::PAGE_END, $this->page['content']) + )->getHtml(); } // Add the navigation (menu) template From 0837ad647a1c45aed546a719502cada667fea81d Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 29 Jan 2025 08:58:55 +0000 Subject: [PATCH 15/16] Fix code style --- src/Content/Feature.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/Feature.php b/src/Content/Feature.php index 89165e232c..641340133c 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -46,7 +46,7 @@ class Feature $eventDispatcher = DI::eventDispatcher(); if (!$config->get('feature_lock', $feature, false)) { - $enabled = $config->get('feature', $feature) ?? self::getDefault($feature); + $enabled = $config->get('feature', $feature) ?? self::getDefault($feature); $enabled = $pConfig->get($uid, 'feature', $feature) ?? $enabled; } else { $enabled = true; @@ -155,7 +155,7 @@ class Feature foreach ($arr as $k => $x) { $has_items = false; $kquantity = count($arr[$k]); - for ($y = 0; $y < $kquantity; $y ++) { + for ($y = 0; $y < $kquantity; $y++) { if (is_array($arr[$k][$y])) { if ($arr[$k][$y][4] === false) { $has_items = true; From f634ec4758ebf42bfd8b306aa140869c18adecba Mon Sep 17 00:00:00 2001 From: Art4 Date: Sat, 1 Feb 2025 16:12:25 +0000 Subject: [PATCH 16/16] Move HookEventBridge into Friendica\Core\Hooks namespace --- src/App.php | 2 +- src/{EventSubscriber => Core/Hooks}/HookEventBridge.php | 2 +- .../{EventSubscriber => Core/Hooks}/HookEventBridgeTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/{EventSubscriber => Core/Hooks}/HookEventBridge.php (98%) rename tests/Unit/{EventSubscriber => Core/Hooks}/HookEventBridgeTest.php (98%) diff --git a/src/App.php b/src/App.php index 738370ede3..7f8e03b5e8 100644 --- a/src/App.php +++ b/src/App.php @@ -20,6 +20,7 @@ use Friendica\Content\Nav; use Friendica\Core\Addon\Capability\ICanLoadAddons; use Friendica\Core\Config\Factory\Config; use Friendica\Core\Container; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\Core\Logger\LoggerManager; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; @@ -35,7 +36,6 @@ use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\ViewDefinition; use Friendica\Event\ConfigLoadedEvent; use Friendica\Event\Event; -use Friendica\EventSubscriber\HookEventBridge; use Friendica\Module\Maintenance; use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Network\HTTPException; diff --git a/src/EventSubscriber/HookEventBridge.php b/src/Core/Hooks/HookEventBridge.php similarity index 98% rename from src/EventSubscriber/HookEventBridge.php rename to src/Core/Hooks/HookEventBridge.php index f5dde4fbb7..2dad2d2b23 100644 --- a/src/EventSubscriber/HookEventBridge.php +++ b/src/Core/Hooks/HookEventBridge.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Friendica\EventSubscriber; +namespace Friendica\Core\Hooks; use Friendica\Core\Hook; use Friendica\Event\ArrayFilterEvent; diff --git a/tests/Unit/EventSubscriber/HookEventBridgeTest.php b/tests/Unit/Core/Hooks/HookEventBridgeTest.php similarity index 98% rename from tests/Unit/EventSubscriber/HookEventBridgeTest.php rename to tests/Unit/Core/Hooks/HookEventBridgeTest.php index 07bfa81229..671eb23e94 100644 --- a/tests/Unit/EventSubscriber/HookEventBridgeTest.php +++ b/tests/Unit/Core/Hooks/HookEventBridgeTest.php @@ -7,14 +7,14 @@ declare(strict_types=1); -namespace Friendica\Test\Unit\EventSubscriber; +namespace Friendica\Test\Unit\Core\Hooks; use Friendica\Core\Config\Util\ConfigFileManager; +use Friendica\Core\Hooks\HookEventBridge; 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