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 366d20fbdb..d42f63436c 100644 --- a/src/App.php +++ b/src/App.php @@ -21,13 +21,10 @@ use Friendica\Core\Addon\AddonHelper; 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; -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; @@ -36,9 +33,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\ConfigLoadedEvent; +use Friendica\Event\Event; +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; @@ -46,6 +49,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; @@ -154,6 +158,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); @@ -175,6 +181,7 @@ class App $this->mode, $this->config, $this->profiler, + $this->container->create(EventDispatcherInterface::class), $this->appHelper, $addonHelper, ); @@ -182,6 +189,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), @@ -207,6 +215,8 @@ class App $this->registerErrorHandler(); + $this->registerEventDispatcher(); + $this->load( $serverParams, $this->container->create(DbaDefinition::class), @@ -214,6 +224,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), $this->container->create(AddonHelper::class), ); @@ -236,6 +247,8 @@ class App $this->registerErrorHandler(); + $this->registerEventDispatcher(); + $this->load( $serverParams, $this->container->create(DbaDefinition::class), @@ -243,6 +256,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), $this->container->create(AddonHelper::class), ); @@ -308,6 +322,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'); @@ -323,6 +347,7 @@ class App Mode $mode, IManageConfigValues $config, Profiler $profiler, + EventDispatcherInterface $eventDispatcher, AppHelper $appHelper, AddonHelper $addonHelper ): void { @@ -347,7 +372,8 @@ class App if ($mode->has(Mode::DBAVAILABLE)) { Core\Hook::loadHooks(); $loader = (new Config())->createConfigFileManager($appHelper->getBasePath(), $addonHelper->getAddonPath(), $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); @@ -393,6 +419,7 @@ class App * @throws \ImagickException */ private function runFrontend( + EventDispatcherInterface $eventDispatcher, IManagePersonalConfigValues $pconfig, Page $page, Nav $nav, @@ -433,7 +460,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..6937a69240 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,9 @@ 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 +357,9 @@ 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 +384,9 @@ 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 +484,9 @@ 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/Content/Feature.php b/src/Content/Feature.php index 8b5e56eba4..641340133c 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -7,8 +7,8 @@ namespace Friendica\Content; -use Friendica\Core\Hook; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; class Feature { @@ -41,15 +41,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']; } @@ -86,55 +94,58 @@ class Feature */ public static function get($filtered = true) { - $arr = [ + $l10n = DI::l10n(); + $config = DI::config(); + $eventDispatcher = DI::eventDispatcher(); + $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)], ] ]; @@ -144,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; @@ -159,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/Content/Nav.php b/src/Content/Nav.php index fdfcd4bde6..695d32ffdd 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -10,11 +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; @@ -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; } /** @@ -114,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; } @@ -151,9 +158,11 @@ 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']; + $appMenu = $arr['app_menu'] ?? []; } return $appMenu; @@ -337,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/Core/Hooks/HookEventBridge.php b/src/Core/Hooks/HookEventBridge.php new file mode 100644 index 0000000000..2dad2d2b23 --- /dev/null +++ b/src/Core/Hooks/HookEventBridge.php @@ -0,0 +1,113 @@ + '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 + */ + 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; + } +} diff --git a/src/DI.php b/src/DI.php index 6a663e430f..bcbc17651b 100644 --- a/src/DI.php +++ b/src/DI.php @@ -795,4 +795,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); + } } diff --git a/src/Event/ArrayFilterEvent.php b/src/Event/ArrayFilterEvent.php new file mode 100644 index 0000000000..839aa40b75 --- /dev/null +++ b/src/Event/ArrayFilterEvent.php @@ -0,0 +1,45 @@ +array = $array; + } + + public function getArray(): array + { + return $this->array; + } + + public function setArray(array $array): void + { + $this->array = $array; + } +} diff --git a/src/Event/ConfigLoadedEvent.php b/src/Event/ConfigLoadedEvent.php new file mode 100644 index 0000000000..67f1407389 --- /dev/null +++ b/src/Event/ConfigLoadedEvent.php @@ -0,0 +1,36 @@ +config = $config; + } + + public function getConfig(): ConfigFileManager + { + return $this->config; + } +} diff --git a/src/Event/Event.php b/src/Event/Event.php new file mode 100644 index 0000000000..90defc3204 --- /dev/null +++ b/src/Event/Event.php @@ -0,0 +1,35 @@ +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..da92a9b76c --- /dev/null +++ b/src/Event/EventDispatcher.php @@ -0,0 +1,37 @@ +getName(); + } + + return parent::dispatch($event, $eventName); + } +} diff --git a/src/Event/HtmlFilterEvent.php b/src/Event/HtmlFilterEvent.php new file mode 100644 index 0000000000..7d1bbb2b55 --- /dev/null +++ b/src/Event/HtmlFilterEvent.php @@ -0,0 +1,47 @@ +html = $html; + } + + 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..3f94959cf6 --- /dev/null +++ b/src/Event/NamedEvent.php @@ -0,0 +1,20 @@ + $this->configCache->get('system', 'url'), '$dbhost' => ['database-hostname', $this->t('Database Server Name'), - $this->configCache->get('database', 'hostname'), + $this->configCache->get('database', 'hostname') ? : getenv('MYSQL_HOST') ? : 'localhost', '', $this->t('Required')], '$dbuser' => ['database-username', $this->t('Database Login Name'), - $this->configCache->get('database', 'username'), + $this->configCache->get('database', 'username') ? : getenv('MYSQL_USER') ? : '', '', $this->t('Required'), 'autofocus'], '$dbpass' => ['database-password', $this->t('Database Login Password'), - $this->configCache->get('database', 'password'), + $this->configCache->get('database', 'password') ? : getenv('MYSQL_PASSWORD') ? : '', $this->t("For security reasons the password must not be empty"), $this->t('Required')], '$dbdata' => ['database-database', - $this->t('Database Name'), + $this->t('Database Name') ? : getenv('MYSQL_DATABASE') ? : '', $this->configCache->get('database', 'database'), '', $this->t('Required')], diff --git a/src/Module/Settings/Channels.php b/src/Module/Settings/Channels.php index 6aee2f99d7..85ee2ccefa 100644 --- a/src/Module/Settings/Channels.php +++ b/src/Module/Settings/Channels.php @@ -127,7 +127,7 @@ class Channels extends BaseSettings throw new HTTPException\ForbiddenException($this->t('Permission denied.')); } - $user = User::getById($uid, ['account-type']); + $user = User::getById($uid, ['account-type']); $account_type = $user['account-type']; if (in_array($account_type, [User::ACCOUNT_TYPE_COMMUNITY, User::ACCOUNT_TYPE_RELAY])) { @@ -151,7 +151,7 @@ class Channels extends BaseSettings $circles[$circle['id']] = $circle['name']; } - $languages = $this->l10n->getLanguageCodes(true); + $languages = $this->l10n->getLanguageCodes(true); $channel_languages = User::getWantedLanguages($uid); $channels = []; @@ -185,7 +185,7 @@ class Channels extends BaseSettings 'image' => ["image[$channel->code]", $this->t("Images"), $channel->mediaType & 1], 'video' => ["video[$channel->code]", $this->t("Videos"), $channel->mediaType & 2], 'audio' => ["audio[$channel->code]", $this->t("Audio"), $channel->mediaType & 4], - 'languages' => ["languages[$channel->code][]", $this->t('Languages'), $channel->languages ?? $channel_languages, $this->t('Select all languages that you want to see in this channel.'), $languages, 'multiple'], + 'languages' => ["languages[$channel->code][]", $this->t('Languages'), $channel->languages ?? $channel_languages, $this->t('Select all languages that you want to see in this channel. "Unspecified" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list.'), $languages, 'multiple'], 'publish' => $publish, 'delete' => ["delete[$channel->code]", $this->t("Delete channel") . ' (' . $channel->label . ')', false, $this->t("Check to delete this entry from the channel list")] ]; diff --git a/src/Module/Settings/Display.php b/src/Module/Settings/Display.php index b918dfa9db..9a936c3d92 100644 --- a/src/Module/Settings/Display.php +++ b/src/Module/Settings/Display.php @@ -208,7 +208,7 @@ class Display extends BaseSettings $allowed_themes = Theme::getAllowedList(); - $themes = []; + $themes = []; $mobile_themes = ['---' => $this->t('No special theme for mobile devices')]; foreach ($allowed_themes as $theme) { $is_experimental = file_exists('view/theme/' . $theme . '/experimental'); @@ -233,8 +233,8 @@ class Display extends BaseSettings $theme_selected = $user['theme'] ?: $default_theme; $mobile_theme_selected = $this->session->get('mobile-theme', $default_mobile_theme); - $itemspage_network = intval($this->pConfig->get($uid, 'system', 'itemspage_network')); - $itemspage_network = (($itemspage_network > 0 && $itemspage_network < 101) ? $itemspage_network : $this->config->get('system', 'itemspage_network')); + $itemspage_network = intval($this->pConfig->get($uid, 'system', 'itemspage_network')); + $itemspage_network = (($itemspage_network > 0 && $itemspage_network < 101) ? $itemspage_network : $this->config->get('system', 'itemspage_network')); $itemspage_mobile_network = intval($this->pConfig->get($uid, 'system', 'itemspage_mobile_network')); $itemspage_mobile_network = (($itemspage_mobile_network > 0 && $itemspage_mobile_network < 101) ? $itemspage_mobile_network : $this->config->get('system', 'itemspage_network_mobile')); @@ -244,18 +244,18 @@ class Display extends BaseSettings } $enable_smile = !$this->pConfig->get($uid, 'system', 'no_smilies', false); - $infinite_scroll = $this->pConfig->get($uid, 'system', 'infinite_scroll', false); + $infinite_scroll = $this->pConfig->get($uid, 'system', 'infinite_scroll', false); $enable_smart_threading = !$this->pConfig->get($uid, 'system', 'no_smart_threading', false); $enable_dislike = !$this->pConfig->get($uid, 'system', 'hide_dislike', false); - $display_resharer = $this->pConfig->get($uid, 'system', 'display_resharer', false); - $stay_local = $this->pConfig->get($uid, 'system', 'stay_local', false); - $show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true); - $display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true); + $display_resharer = $this->pConfig->get($uid, 'system', 'display_resharer', false); + $stay_local = $this->pConfig->get($uid, 'system', 'stay_local', false); + $show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true); + $display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true); - $hide_empty_descriptions = $this->pConfig->get($uid, 'accessibility', 'hide_empty_descriptions', false); - $hide_custom_emojis = $this->pConfig->get($uid, 'accessibility', 'hide_custom_emojis', false); - $platform_icon_style = $this->pConfig->get($uid, 'accessibility', 'platform_icon_style', ContactSelector::SVG_COLOR_BLACK); - $platform_icon_styles = [ + $hide_empty_descriptions = $this->pConfig->get($uid, 'accessibility', 'hide_empty_descriptions', false); + $hide_custom_emojis = $this->pConfig->get($uid, 'accessibility', 'hide_custom_emojis', false); + $platform_icon_style = $this->pConfig->get($uid, 'accessibility', 'platform_icon_style', ContactSelector::SVG_COLOR_BLACK); + $platform_icon_styles = [ ContactSelector::SVG_DISABLED => $this->t('Disabled'), ContactSelector::SVG_COLOR_BLACK => $this->t('Color/Black'), ContactSelector::SVG_BLACK => $this->t('Black'), @@ -263,7 +263,7 @@ class Display extends BaseSettings ContactSelector::SVG_WHITE => $this->t('White'), ]; - $preview_mode = $this->pConfig->get($uid, 'system', 'preview_mode', BBCode::PREVIEW_LARGE); + $preview_mode = $this->pConfig->get($uid, 'system', 'preview_mode', BBCode::PREVIEW_LARGE); $preview_modes = [ BBCode::PREVIEW_NONE => $this->t('No preview'), BBCode::PREVIEW_NO_IMAGE => $this->t('No image'), @@ -273,16 +273,16 @@ class Display extends BaseSettings $bookmarked_timelines = $this->pConfig->get($uid, 'system', 'network_timelines', $this->getAvailableTimelines($uid, true)->column('code')); $enabled_timelines = $this->pConfig->get($uid, 'system', 'enabled_timelines', $this->getAvailableTimelines($uid, false)->column('code')); - $channel_languages = User::getWantedLanguages($uid); - $languages = $this->l10n->getLanguageCodes(true); + $channel_languages = User::getWantedLanguages($uid); + $languages = $this->l10n->getLanguageCodes(true); $timelines = []; foreach ($this->getAvailableTimelines($uid) as $timeline) { $timelines[] = [ - 'label' => $timeline->label, - 'description' => $timeline->description, - 'enable' => ["enable[{$timeline->code}]", '', in_array($timeline->code, $enabled_timelines)], - 'bookmark' => ["bookmark[{$timeline->code}]", '', in_array($timeline->code, $bookmarked_timelines)], + 'label' => $timeline->label, + 'description' => $timeline->description, + 'enable' => ["enable[{$timeline->code}]", '', in_array($timeline->code, $enabled_timelines)], + 'bookmark' => ["bookmark[{$timeline->code}]", '', in_array($timeline->code, $bookmarked_timelines)], ]; } @@ -326,14 +326,14 @@ class Display extends BaseSettings '$form_security_token' => self::getFormSecurityToken('settings_display'), '$uid' => $uid, - '$theme' => ['theme', $this->t('Display Theme:'), $theme_selected, '', $themes, true], - '$mobile_theme' => ['mobile_theme', $this->t('Mobile Theme:'), $mobile_theme_selected, '', $mobile_themes, false], + '$theme' => ['theme', $this->t('Display Theme:'), $theme_selected, '', $themes, true], + '$mobile_theme' => ['mobile_theme', $this->t('Mobile Theme:'), $mobile_theme_selected, '', $mobile_themes, false], '$theme_config' => $theme_config, '$itemspage_network' => ['itemspage_network', $this->t('Number of items to display per page:'), $itemspage_network, $this->t('Maximum of 100 items')], '$itemspage_mobile_network' => ['itemspage_mobile_network', $this->t('Number of items to display per page when viewed from mobile device:'), $itemspage_mobile_network, $this->t('Maximum of 100 items')], '$ajaxint' => ['browser_update', $this->t('Update browser every xx seconds'), $browser_update, $this->t('Minimum of 10 seconds. Enter -1 to disable it.')], - '$enable_smile' => ['enable_smile', $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')], + '$enable_smile' => ['enable_smile', $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')], '$infinite_scroll' => ['infinite_scroll', $this->t('Infinite scroll'), $infinite_scroll, $this->t('Automatic fetch new items when reaching the page end.')], '$enable_smart_threading' => ['enable_smart_threading', $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')], '$enable_dislike' => ['enable_dislike', $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')], @@ -353,7 +353,7 @@ class Display extends BaseSettings '$timelines' => $timelines, '$timeline_explanation' => $this->t('Enable timelines that you want to see in the channels widget. Bookmark timelines that you want to see in the top menu.'), - '$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all languages that you want to see in your channels.'), $languages, 'multiple'], + '$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all the languages you want to see in your channels. "Unspecified" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list.'), $languages, 'multiple'], '$first_day_of_week' => ['first_day_of_week', $this->t('Beginning of week:'), $first_day_of_week, '', $weekdays, false], '$calendar_default_view' => ['calendar_default_view', $this->t('Default calendar view:'), $calendar_default_view, '', $calendarViews, false], diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 6e6f7ecf52..5924ae512e 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -189,6 +189,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/Core/Hooks/HookEventBridgeTest.php b/tests/Unit/Core/Hooks/HookEventBridgeTest.php new file mode 100644 index 0000000000..671eb23e94 --- /dev/null +++ b/tests/Unit/Core/Hooks/HookEventBridgeTest.php @@ -0,0 +1,176 @@ + '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); + } +} 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/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()); + } +} 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/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; diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index 358aef9e2f..bd75b8803f 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2025.02-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-04 13:20-0500\n" +"POT-Creation-Date: 2025-02-04 05:51+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -9594,8 +9594,8 @@ msgstr "" msgid "Full Text Search" msgstr "" -#: src/Module/Settings/Channels.php:188 src/Module/Settings/Channels.php:209 -msgid "Select all languages that you want to see in this channel." +#: src/Module/Settings/Channels.php:188 +msgid "Select all languages that you want to see in this channel. \"Unspecified\" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list." msgstr "" #: src/Module/Settings/Channels.php:190 @@ -9655,6 +9655,10 @@ msgstr "" msgid "Check to display audio in the channel." msgstr "" +#: src/Module/Settings/Channels.php:209 +msgid "Select all languages that you want to see in this channel." +msgstr "" + #: src/Module/Settings/Channels.php:213 msgid "Add new entry to the channel list" msgstr "" @@ -10140,7 +10144,7 @@ msgid "Channel languages:" msgstr "" #: src/Module/Settings/Display.php:356 -msgid "Select all languages that you want to see in your channels." +msgid "Select all the languages you want to see in your channels. \"Unspecified\" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list." msgstr "" #: src/Module/Settings/Display.php:358