From 4b9a6746593f6e5b4083b214b1a44e4dfe522e91 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 29 Apr 2025 13:59:58 +0000 Subject: [PATCH 01/24] Implement parsing of addon files to AddonInfo --- src/Core/Addon/AddonInfo.php | 115 +++++++++++++++++------- tests/Unit/Core/Addon/AddonInfoTest.php | 105 +++++++++++++++++----- 2 files changed, 168 insertions(+), 52 deletions(-) diff --git a/src/Core/Addon/AddonInfo.php b/src/Core/Addon/AddonInfo.php index ce5f07fc0f..8b44b6f841 100644 --- a/src/Core/Addon/AddonInfo.php +++ b/src/Core/Addon/AddonInfo.php @@ -14,6 +14,76 @@ namespace Friendica\Core\Addon; */ final class AddonInfo { + /** + * Parse addon comment in search of addon infos. + * + * like + * \code + * * Name: addon + * * Description: An addon which plugs in + * . * Version: 1.2.3 + * * Author: John + * * Author: Jane + * * Maintainer: Jess without link + * * Maintainer: Robin + * * Status: in development + * \endcode + * + * @internal Never create this object by yourself, use `Friendica\Core\Addon\AddonHelper::getAddonInfo()` instead. + * @see Friendica\Core\Addon\AddonHelper::getAddonInfo() + * + * @param string $addonId the name of the addon + * @param string $raw The raw file content + */ + public static function fromString(string $addonId, string $raw): self + { + $data = [ + 'id' => $addonId, + ]; + + $result = preg_match("|/\*.*\*/|msU", $raw, $m); + + if ($result === false || $result === 0) { + return self::fromArray($data); + } + + $ll = explode("\n", $m[0]); + + foreach ($ll as $l) { + $l = trim($l, "\t\n\r */"); + if ($l !== '') { + $addon_info = array_map('trim', explode(":", $l, 2)); + if (count($addon_info) < 2) { + continue; + } + + list($type, $v) = $addon_info; + $type = strtolower($type); + + if ($type === 'author' || $type === 'maintainer') { + $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m); + if ($r === false || $r === 0) { + $data[$type][] = ['name' => trim($v)]; + } else { + $data[$type][] = ['name' => trim($m[1]), 'link' => $m[2]]; + } + } else { + $data[$type] = $v; + } + } + } + + // rename author to authors + $data['authors'] = $data['author']; + unset($data['author']); + + // rename maintainer to maintainers + $data['maintainers'] = $data['maintainer']; + unset($data['maintainer']); + + return self::fromArray($data); + } + /** * @internal Never create this object by yourself, use `Friendica\Core\Addon\AddonHelper::getAddonInfo()` instead. * @@ -21,25 +91,21 @@ final class AddonInfo */ public static function fromArray(array $info): self { - $id = array_key_exists('id', $info) ? (string) $info['id'] : ''; - $name = array_key_exists('name', $info) ? (string) $info['name'] : ''; - $description = array_key_exists('description', $info) ? (string) $info['description'] : ''; - $authors = array_key_exists('authors', $info) ? self::parseContributors($info['authors']) : []; - $maintainers = array_key_exists('maintainers', $info) ? self::parseContributors($info['maintainers']) : []; - $version = array_key_exists('version', $info) ? (string) $info['version'] : ''; - $status = array_key_exists('status', $info) ? (string) $info['status'] : ''; + $addonInfo = new self(); + $addonInfo->id = array_key_exists('id', $info) ? (string) $info['id'] : ''; + $addonInfo->name = array_key_exists('name', $info) ? (string) $info['name'] : ''; + $addonInfo->description = array_key_exists('description', $info) ? (string) $info['description'] : ''; + $addonInfo->authors = array_key_exists('authors', $info) ? self::parseContributors($info['authors']) : []; + $addonInfo->maintainers = array_key_exists('maintainers', $info) ? self::parseContributors($info['maintainers']) : []; + $addonInfo->version = array_key_exists('version', $info) ? (string) $info['version'] : ''; + $addonInfo->status = array_key_exists('status', $info) ? (string) $info['status'] : ''; - return new self( - $id, - $name, - $description, - $authors, - $maintainers, - $version, - $status - ); + return $addonInfo; } + /** + * @param mixed $entries + */ private static function parseContributors($entries): array { if (!is_array($entries)) { @@ -85,22 +151,7 @@ final class AddonInfo private string $status = ''; - private function __construct( - string $id, - string $name, - string $description, - array $authors, - array $maintainers, - string $version, - string $status - ) { - $this->id = $id; - $this->name = $name; - $this->description = $description; - $this->authors = $authors; - $this->maintainers = $maintainers; - $this->version = $version; - $this->status = $status; + private function __construct() { } public function getId(): string diff --git a/tests/Unit/Core/Addon/AddonInfoTest.php b/tests/Unit/Core/Addon/AddonInfoTest.php index a8e007b50a..a1887d5dfd 100644 --- a/tests/Unit/Core/Addon/AddonInfoTest.php +++ b/tests/Unit/Core/Addon/AddonInfoTest.php @@ -14,19 +14,65 @@ use PHPUnit\Framework\TestCase; class AddonInfoTest extends TestCase { + public function testFromStringCreatesObject(): void + { + $this->assertInstanceOf(AddonInfo::class, AddonInfo::fromString('addonId', '')); + } + + public static function getStringData(): array + { + return [ + 'minimal' => [ + 'test', + '', + ['id' => 'test'], + ], + 'complete' => [ + 'test', + << + * Maintainer: Robin + * Maintainer: Robin With Profile + * Status: beta + * Ignore: The "ignore" key is unsupported and will be ignored + */ + TEXT, + [ + 'id' => 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + 'authors' => [ + ['name' => 'Sam'], + ['name' => 'Sam With Mail', 'link' => 'mail@example.org'], + ], + 'maintainers' => [ + ['name' => 'Robin'], + ['name' => 'Robin With Profile', 'link' => 'https://example.org/profile/robin'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], + ]; + } + + /** + * @dataProvider getStringData + */ + public function testFromStringReturnsCorrectValues(string $addonId, string $raw, array $expected): void + { + $this->assertAddonInfoData($expected, AddonInfo::fromString($addonId, $raw)); + } + public function testFromArrayCreatesObject(): void { - $data = [ - 'id' => '', - 'name' => '', - 'description' => '', - 'authors' => [], - 'maintainers' => [], - 'version' => '', - 'status' => '', - ]; - - $this->assertInstanceOf(AddonInfo::class, AddonInfo::fromArray($data)); + $this->assertInstanceOf(AddonInfo::class, AddonInfo::fromArray([])); } public function testGetterReturningCorrectValues(): void @@ -41,15 +87,34 @@ class AddonInfoTest extends TestCase 'status' => 'In Development', ]; - $info = AddonInfo::fromArray($data); + $this->assertAddonInfoData($data, AddonInfo::fromArray($data)); + } - $this->assertSame($data['id'], $info->getId()); - $this->assertSame($data['name'], $info->getName()); - $this->assertSame($data['description'], $info->getDescription()); - $this->assertSame($data['description'], $info->getDescription()); - $this->assertSame($data['authors'], $info->getAuthors()); - $this->assertSame($data['maintainers'], $info->getMaintainers()); - $this->assertSame($data['version'], $info->getVersion()); - $this->assertSame($data['status'], $info->getStatus()); + private function assertAddonInfoData(array $expected, AddonInfo $info): void + { + $expected = array_merge( + [ + 'id' => '', + 'name' => '', + 'description' => '', + 'authors' => [], + 'maintainers' => [], + 'version' => '', + 'status' => '', + ], + $expected + ); + + $data = [ + 'id' => $info->getId(), + 'name' => $info->getName(), + 'description' => $info->getDescription(), + 'authors' => $info->getAuthors(), + 'maintainers' => $info->getMaintainers(), + 'version' => $info->getVersion(), + 'status' => $info->getStatus(), + ]; + + $this->assertSame($expected, $data); } } From 6a058793f08b0247d6c817f179346dce19ac0e7f Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 13 May 2025 08:14:09 +0000 Subject: [PATCH 02/24] Fix warning if author or maintainer is missing in addon info --- src/Core/Addon/AddonInfo.php | 12 ++++-- tests/Unit/Core/Addon/AddonInfoTest.php | 49 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/Core/Addon/AddonInfo.php b/src/Core/Addon/AddonInfo.php index 8b44b6f841..87bb5750f5 100644 --- a/src/Core/Addon/AddonInfo.php +++ b/src/Core/Addon/AddonInfo.php @@ -74,12 +74,16 @@ final class AddonInfo } // rename author to authors - $data['authors'] = $data['author']; - unset($data['author']); + if (array_key_exists('author', $data)) { + $data['authors'] = $data['author']; + unset($data['author']); + } // rename maintainer to maintainers - $data['maintainers'] = $data['maintainer']; - unset($data['maintainer']); + if (array_key_exists('maintainer', $data)) { + $data['maintainers'] = $data['maintainer']; + unset($data['maintainer']); + } return self::fromArray($data); } diff --git a/tests/Unit/Core/Addon/AddonInfoTest.php b/tests/Unit/Core/Addon/AddonInfoTest.php index a1887d5dfd..5e9d2cc79a 100644 --- a/tests/Unit/Core/Addon/AddonInfoTest.php +++ b/tests/Unit/Core/Addon/AddonInfoTest.php @@ -27,6 +27,55 @@ class AddonInfoTest extends TestCase '', ['id' => 'test'], ], + 'without-author' => [ + 'test', + << 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + + 'maintainers' => [ + ['name' => 'Robin'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], + 'without-maintainer' => [ + 'test', + << 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + 'authors' => [ + ['name' => 'Sam'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], 'complete' => [ 'test', << Date: Tue, 13 May 2025 10:48:41 +0000 Subject: [PATCH 03/24] Create AddonManagerHelper --- src/Core/Addon/AddonManagerHelper.php | 160 ++++++++++++++++++ .../Core/Addon/AddonManagerHelperTest.php | 32 ++++ tests/Util/addons/helloaddon/helloaddon.php | 29 ++++ 3 files changed, 221 insertions(+) create mode 100644 src/Core/Addon/AddonManagerHelper.php create mode 100644 tests/Unit/Core/Addon/AddonManagerHelperTest.php create mode 100644 tests/Util/addons/helloaddon/helloaddon.php diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php new file mode 100644 index 0000000000..1672be1de2 --- /dev/null +++ b/src/Core/Addon/AddonManagerHelper.php @@ -0,0 +1,160 @@ +addonPath = $addonPath; + $this->profiler = $profiler; + + $this->proxy = new AddonProxy($addonPath); + } + /** + * Returns the absolute path to the addon folder + * + * e.g. `/var/www/html/addon` + */ + public function getAddonPath(): string + { + return $this->addonPath; + } + + /** + * Returns the list of available addons. + * + * This list is made from scanning the addon/ folder. + * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set. + * + * @return string[] + */ + public function getAvailableAddons(): array + { + return $this->proxy->getAvailableAddons(); + } + + /** + * Installs an addon. + * + * @param string $addonId name of the addon + * + * @return bool true on success or false on failure + */ + public function installAddon(string $addonId): bool + { + return $this->proxy->installAddon($addonId); + } + + /** + * Uninstalls an addon. + * + * @param string $addonId name of the addon + */ + public function uninstallAddon(string $addonId): void + { + $this->proxy->uninstallAddon($addonId); + } + + /** + * Load addons. + * + * @internal + */ + public function loadAddons(): void + { + $this->proxy->loadAddons(); + } + + /** + * Reload (uninstall and install) all updated addons. + */ + public function reloadAddons(): void + { + $this->proxy->reloadAddons(); + } + + /** + * Get the comment block of an addon as value object. + */ + public function getAddonInfo(string $addonId): AddonInfo + { + $default = [ + 'id' => $addonId, + 'name' => $addonId, + ]; + + if (!is_file($this->getAddonPath() . "/$addonId/$addonId.php")) { + return AddonInfo::fromArray($default); + } + + $this->profiler->startRecording('file'); + + $raw = file_get_contents($this->getAddonPath() . "/$addonId/$addonId.php"); + + $this->profiler->stopRecording(); + + return AddonInfo::fromString($addonId, $raw); + } + + /** + * Checks if the provided addon is enabled + */ + public function isAddonEnabled(string $addonId): bool + { + return $this->proxy->isAddonEnabled($addonId); + } + + /** + * Returns a list with the IDs of the enabled addons + * + * @return string[] + */ + public function getEnabledAddons(): array + { + return $this->proxy->getEnabledAddons(); + } + + /** + * Returns a list with the IDs of the non-hidden enabled addons + * + * @return string[] + */ + public function getVisibleEnabledAddons(): array + { + return $this->proxy->getVisibleEnabledAddons(); + } + + /** + * Returns a list with the IDs of the enabled addons that provides admin settings. + * + * @return string[] + */ + public function getEnabledAddonsWithAdminSettings(): array + { + return $this->proxy->getEnabledAddonsWithAdminSettings(); + } +} diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php new file mode 100644 index 0000000000..9ee8bde3c8 --- /dev/null +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -0,0 +1,32 @@ +createStub(Profiler::class) + ); + + $info = $addonManagerHelper->getAddonInfo('helloaddon'); + + $this->assertInstanceOf(AddonInfo::class, $info); + + $this->assertEquals('Hello Addon', $info->getName()); + } +} diff --git a/tests/Util/addons/helloaddon/helloaddon.php b/tests/Util/addons/helloaddon/helloaddon.php new file mode 100644 index 0000000000..679298dfde --- /dev/null +++ b/tests/Util/addons/helloaddon/helloaddon.php @@ -0,0 +1,29 @@ + + */ + +use Friendica\Core\Hook; + +function helloaddon_install() +{ + Hook::register('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end'); +} + +function helloaddon_uninstall() +{ + Hook::unregister('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end'); +} + +function helloaddon_page_end(&$html) +{ + $html .= '

Hello, World!

'; +} From 93a171765abe1c25813efd665bad6215fd711201 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 13 May 2025 14:04:57 +0000 Subject: [PATCH 04/24] Implement loading addons in AddonManagerHelper --- src/Core/Addon/AddonManagerHelper.php | 14 +++++++--- .../Core/Addon/AddonManagerHelperTest.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 1672be1de2..294f44d85b 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace Friendica\Core\Addon; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Util\Profiler; /** @@ -20,16 +21,23 @@ final class AddonManagerHelper implements AddonHelper { private string $addonPath; + private IManageConfigValues $config; + private Profiler $profiler; + /** @var string[] */ + private array $addons = []; + /** @deprecated */ private AddonHelper $proxy; public function __construct( string $addonPath, + IManageConfigValues $config, Profiler $profiler ) { $this->addonPath = $addonPath; + $this->config = $config; $this->profiler = $profiler; $this->proxy = new AddonProxy($addonPath); @@ -86,7 +94,7 @@ final class AddonManagerHelper implements AddonHelper */ public function loadAddons(): void { - $this->proxy->loadAddons(); + $this->addons = array_keys(array_filter($this->config->get('addons') ?? [])); } /** @@ -125,7 +133,7 @@ final class AddonManagerHelper implements AddonHelper */ public function isAddonEnabled(string $addonId): bool { - return $this->proxy->isAddonEnabled($addonId); + return in_array($addonId, $this->addons); } /** @@ -135,7 +143,7 @@ final class AddonManagerHelper implements AddonHelper */ public function getEnabledAddons(): array { - return $this->proxy->getEnabledAddons(); + return $this->addons; } /** diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 9ee8bde3c8..9afdbb0cfd 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -11,6 +11,7 @@ namespace Friendica\Test\Unit\Core\Addon; use Friendica\Core\Addon\AddonInfo; use Friendica\Core\Addon\AddonManagerHelper; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Util\Profiler; use PHPUnit\Framework\TestCase; @@ -20,6 +21,7 @@ class AddonManagerHelperTest extends TestCase { $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(IManageConfigValues::class), $this->createStub(Profiler::class) ); @@ -29,4 +31,29 @@ class AddonManagerHelperTest extends TestCase $this->assertEquals('Hello Addon', $info->getName()); } + + public function testEnabledAddons(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + __DIR__ . '/../../../Util/addons', + $config, + $this->createStub(Profiler::class) + ); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + $this->assertFalse($addonManagerHelper->isAddonEnabled('helloaddon')); + + $addonManagerHelper->loadAddons(); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + $this->assertTrue($addonManagerHelper->isAddonEnabled('helloaddon')); + } } From 0599801b3766f83b8a4f05cfa9dfa53dd96f1a88 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 13 May 2025 14:16:10 +0000 Subject: [PATCH 05/24] implement getVisibleEnablesAddons and getEnabledAddonsWithAdminSettings --- src/Core/Addon/AddonManagerHelper.php | 22 +++++++++- .../Core/Addon/AddonManagerHelperTest.php | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 294f44d85b..a436c0e19b 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -153,7 +153,14 @@ final class AddonManagerHelper implements AddonHelper */ public function getVisibleEnabledAddons(): array { - return $this->proxy->getVisibleEnabledAddons(); + $visible_addons = []; + $addons = array_filter($this->config->get('addons') ?? []); + + foreach ($addons as $name => $data) { + $visible_addons[] = $name; + } + + return $visible_addons; } /** @@ -163,6 +170,17 @@ final class AddonManagerHelper implements AddonHelper */ public function getEnabledAddonsWithAdminSettings(): array { - return $this->proxy->getEnabledAddonsWithAdminSettings(); + $addons_admin = []; + $addons = array_filter($this->config->get('addons') ?? []); + + ksort($addons); + + foreach ($addons as $name => $data) { + if (array_key_exists('admin', $data) && $data['admin'] === true) { + $addons_admin[] = $name; + } + } + + return $addons_admin; } } diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 9afdbb0cfd..7c8f817594 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -56,4 +56,46 @@ class AddonManagerHelperTest extends TestCase $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); $this->assertTrue($addonManagerHelper->isAddonEnabled('helloaddon')); } + + public function testGetVisibleEnabledAddons(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + __DIR__ . '/../../../Util/addons', + $config, + $this->createStub(Profiler::class) + ); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getVisibleEnabledAddons()); + } + + public function testGetEnabledAddonsWithAdminSettings(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + 'addonwithadminsettings' => [ + 'last_update' => 1738760499, + 'admin' => true, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + __DIR__ . '/../../../Util/addons', + $config, + $this->createStub(Profiler::class) + ); + + $this->assertSame(['addonwithadminsettings'], $addonManagerHelper->getEnabledAddonsWithAdminSettings()); + } } From 8b40d65e6c5e41bd558b1589ab0140cfcfa05c5f Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 14 May 2025 07:52:53 +0000 Subject: [PATCH 06/24] Implement getAvailableAddons --- src/Core/Addon/AddonManagerHelper.php | 28 ++++++++++++++++++- .../Core/Addon/AddonManagerHelperTest.php | 11 ++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index a436c0e19b..66f22bf904 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -62,7 +62,33 @@ final class AddonManagerHelper implements AddonHelper */ public function getAvailableAddons(): array { - return $this->proxy->getAvailableAddons(); + $files = glob($this->getAddonPath() . '/*/'); + + if (!is_array($files)) { + return []; + } + + $addons = []; + + foreach ($files as $file) { + if (!is_dir($file)) { + continue; + } + + $addonId = basename($file); + + $addonInfo = $this->getAddonInfo($addonId); + + if ( + $this->config->get('system', 'show_unsupported_addons') + || strtolower($addonInfo->getStatus()) !== 'unsupported' + || $this->isAddonEnabled($addonId) + ) { + $addons[] = $addonId; + } + } + + return $addons; } /** diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 7c8f817594..6464987845 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -98,4 +98,15 @@ class AddonManagerHelperTest extends TestCase $this->assertSame(['addonwithadminsettings'], $addonManagerHelper->getEnabledAddonsWithAdminSettings()); } + + public function testGetAvailableAddons(): void + { + $addonManagerHelper = new AddonManagerHelper( + __DIR__ . '/../../../Util/addons', + $this->createStub(originalClassName: IManageConfigValues::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getAvailableAddons()); + } } From 5f7de9d028132a3fd7e1f65125098ed1817724b8 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 14 May 2025 08:20:49 +0000 Subject: [PATCH 07/24] remove name parameter --- tests/Unit/Core/Addon/AddonManagerHelperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 6464987845..1ad73524ad 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -103,7 +103,7 @@ class AddonManagerHelperTest extends TestCase { $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', - $this->createStub(originalClassName: IManageConfigValues::class), + $this->createStub(IManageConfigValues::class), $this->createStub(Profiler::class) ); From 88dcd755a90001ca1ea87607d7f7121845278b63 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 14 May 2025 09:52:33 +0000 Subject: [PATCH 08/24] Replace glob() with stream safe alternative scandir() --- src/Core/Addon/AddonManagerHelper.php | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 66f22bf904..d8873869b9 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -62,21 +62,29 @@ final class AddonManagerHelper implements AddonHelper */ public function getAvailableAddons(): array { - $files = glob($this->getAddonPath() . '/*/'); + $dirs = scandir($this->getAddonPath()); - if (!is_array($files)) { + if (!is_array($dirs)) { return []; } + $files = []; + + foreach ($dirs as $dirname) { + if (in_array($dirname, ['.', '..'])) { + continue; + } + + if (!is_dir($this->getAddonPath() . '/' . $dirname)) { + continue; + } + + $files[] = $dirname; + } + $addons = []; - foreach ($files as $file) { - if (!is_dir($file)) { - continue; - } - - $addonId = basename($file); - + foreach ($files as $addonId) { $addonInfo = $this->getAddonInfo($addonId); if ( From a39850871eca4dd0fa0a41f8402e60a935101125 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 14 May 2025 11:37:47 +0000 Subject: [PATCH 09/24] Implement installAddon --- src/Core/Addon/AddonManagerHelper.php | 37 +++++- .../Core/Addon/AddonManagerHelperTest.php | 109 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index d8873869b9..6bf12285ac 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -11,6 +11,8 @@ namespace Friendica\Core\Addon; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Util\Profiler; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; /** * helper functions to handle addons @@ -23,6 +25,8 @@ final class AddonManagerHelper implements AddonHelper private IManageConfigValues $config; + private LoggerInterface $logger; + private Profiler $profiler; /** @var string[] */ @@ -34,10 +38,12 @@ final class AddonManagerHelper implements AddonHelper public function __construct( string $addonPath, IManageConfigValues $config, + LoggerInterface $logger, Profiler $profiler ) { $this->addonPath = $addonPath; $this->config = $config; + $this->logger = $logger; $this->profiler = $profiler; $this->proxy = new AddonProxy($addonPath); @@ -108,7 +114,36 @@ final class AddonManagerHelper implements AddonHelper */ public function installAddon(string $addonId): bool { - return $this->proxy->installAddon($addonId); + $addonId = Strings::sanitizeFilePathItem($addonId); + + $addon_file_path = $this->getAddonPath() . '/' . $addonId . '/' . $addonId . '.php'; + + // silently fail if addon was removed or if $addonId is funky + if (!file_exists($addon_file_path)) { + return false; + } + + $this->logger->debug("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addonId]); + + $timestamp = @filemtime($addon_file_path); + + @include_once($addon_file_path); + + if (function_exists($addonId . '_install')) { + $func = $addonId . '_install'; + $func(); + } + + $this->config->set('addons', $addonId, [ + 'last_update' => $timestamp, + 'admin' => function_exists($addonId . '_addon_admin'), + ]); + + if (!$this->isAddonEnabled($addonId)) { + $this->addons[] = $addonId; + } + + return true; } /** diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 1ad73524ad..f7b4960fda 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -9,11 +9,14 @@ declare(strict_types=1); namespace Friendica\Test\Unit\Core\Addon; +use Exception; use Friendica\Core\Addon\AddonInfo; use Friendica\Core\Addon\AddonManagerHelper; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Util\Profiler; +use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; class AddonManagerHelperTest extends TestCase { @@ -22,6 +25,7 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -45,6 +49,7 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', $config, + $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -70,6 +75,7 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', $config, + $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -93,6 +99,7 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', $config, + $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -104,9 +111,111 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); $this->assertSame(['helloaddon'], $addonManagerHelper->getAvailableAddons()); } + + public function testInstallAddonIncludesAddonFile(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon file loaded'); + + $addonManagerHelper->installAddon('helloaddon'); + } + + public function testInstallAddonCallsInstallFunction(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<url(), + $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon installed'); + + $addonManagerHelper->installAddon($addonName); + } + + public function testInstallAddonUpdatesConfig(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'getChild('helloaddon/helloaddon.php')->lastModified(1234567890); + + $config = $this->createMock(IManageConfigValues::class); + $config->expects($this->once())->method('set')->with( + 'addons', + 'helloaddon', + ['last_update' => 1234567890, 'admin' => false] + ); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $config, + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->installAddon('helloaddon'); + } + + public function testInstallAddonEnablesAddon(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + + $this->assertTrue($addonManagerHelper->installAddon('helloaddon')); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + } } From 33398298d56d095dbdd708d3bdba74b1f02f6058 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 14 May 2025 14:54:14 +0000 Subject: [PATCH 10/24] Implement uninstallAddon --- src/Core/Addon/AddonManagerHelper.php | 36 ++++- .../Core/Addon/AddonManagerHelperTest.php | 137 ++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 6bf12285ac..d8ff19af93 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -9,7 +9,9 @@ declare(strict_types=1); namespace Friendica\Core\Addon; +use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Database\Database; use Friendica\Util\Profiler; use Friendica\Util\Strings; use Psr\Log\LoggerInterface; @@ -23,8 +25,12 @@ final class AddonManagerHelper implements AddonHelper { private string $addonPath; + private Database $database; + private IManageConfigValues $config; + private ICanCache $cache; + private LoggerInterface $logger; private Profiler $profiler; @@ -37,12 +43,16 @@ final class AddonManagerHelper implements AddonHelper public function __construct( string $addonPath, + Database $database, IManageConfigValues $config, + ICanCache $cache, LoggerInterface $logger, Profiler $profiler ) { $this->addonPath = $addonPath; + $this->database = $database; $this->config = $config; + $this->cache = $cache; $this->logger = $logger; $this->profiler = $profiler; @@ -153,7 +163,31 @@ final class AddonManagerHelper implements AddonHelper */ public function uninstallAddon(string $addonId): void { - $this->proxy->uninstallAddon($addonId); + $addonId = Strings::sanitizeFilePathItem($addonId); + + $this->logger->debug("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addonId]); + $this->config->delete('addons', $addonId); + + $addon_file_path = $this->getAddonPath() . '/' . $addonId . '/' . $addonId . '.php'; + + @include_once($addon_file_path); + + if (function_exists($addonId . '_uninstall')) { + $func = $addonId . '_uninstall'; + $func(); + } + + // Remove registered hooks for the addon + // Handles both relative and absolute file paths + $condition = ['`file` LIKE ?', "%/$addonId/$addonId.php"]; + + $result = $this->database->delete('hook', $condition); + + if ($result) { + $this->cache->delete('routerDispatchData'); + } + + unset($this->addons[array_search($addonId, $this->addons)]); } /** diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index f7b4960fda..951f9655ac 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -12,7 +12,9 @@ namespace Friendica\Test\Unit\Core\Addon; use Exception; use Friendica\Core\Addon\AddonInfo; use Friendica\Core\Addon\AddonManagerHelper; +use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Database\Database; use Friendica\Util\Profiler; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; @@ -24,7 +26,9 @@ class AddonManagerHelperTest extends TestCase { $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -48,7 +52,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(Database::class), $config, + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -74,7 +80,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(Database::class), $config, + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -98,7 +106,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(Database::class), $config, + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -110,7 +120,9 @@ class AddonManagerHelperTest extends TestCase { $addonManagerHelper = new AddonManagerHelper( __DIR__ . '/../../../Util/addons', + $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -128,7 +140,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( $root->url(), + $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -159,7 +173,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( $root->url(), + $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -189,7 +205,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( $root->url(), + $this->createStub(Database::class), $config, + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -207,7 +225,9 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper = new AddonManagerHelper( $root->url(), + $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), $this->createStub(LoggerInterface::class), $this->createStub(Profiler::class) ); @@ -218,4 +238,121 @@ class AddonManagerHelperTest extends TestCase $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); } + public function testUninstallAddonIncludesAddonFile(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon file loaded'); + + $addonManagerHelper->uninstallAddon('helloaddon'); + } + + public function testUninstallAddonCallsUninstallFunction(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon uninstalled'); + + $addonManagerHelper->uninstallAddon($addonName); + } + + public function testUninstallAddonRemovesHooksFromDatabase(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'createMock(Database::class); + $database->expects($this->once()) + ->method('delete') + ->with( + 'hook', + ['`file` LIKE ?', '%/helloaddon/helloaddon.php'] + ); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $database, + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->uninstallAddon('helloaddon'); + } + + public function testUninstallAddonDisablesAddon(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1234567890, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->loadAddons(); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + + $addonManagerHelper->uninstallAddon('helloaddon'); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + } } From 5cf2b7d7bdd8490e7f680bac5b4feb1319577d99 Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 06:59:37 +0000 Subject: [PATCH 11/24] Implement reloadAddons() --- src/Core/Addon/AddonHelper.php | 2 +- src/Core/Addon/AddonManagerHelper.php | 24 ++++++- .../Core/Addon/AddonManagerHelperTest.php | 71 +++++++++++++++---- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/Core/Addon/AddonHelper.php b/src/Core/Addon/AddonHelper.php index 84b0c5f89d..03a21232e5 100644 --- a/src/Core/Addon/AddonHelper.php +++ b/src/Core/Addon/AddonHelper.php @@ -55,7 +55,7 @@ interface AddonHelper public function loadAddons(): void; /** - * Reload (uninstall and install) all updated addons. + * Reload (uninstall and install) all installed and modified addons. */ public function reloadAddons(): void; diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index d8ff19af93..f2ff78d896 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -201,11 +201,31 @@ final class AddonManagerHelper implements AddonHelper } /** - * Reload (uninstall and install) all updated addons. + * Reload (uninstall and install) all installed and modified addons. */ public function reloadAddons(): void { - $this->proxy->reloadAddons(); + $addons = array_filter($this->config->get('addons') ?? []); + + foreach ($addons as $addonName => $data) { + $addonId = Strings::sanitizeFilePathItem(trim($addonName)); + + $addon_file_path = $this->getAddonPath() . '/' . $addonId . '/' . $addonId . '.php'; + + if (!file_exists($addon_file_path)) { + continue; + } + + if (array_key_exists('last_update', $data) && intval($data['last_update']) === filemtime($addon_file_path)) { + // Addon unmodified, skipping + continue; + } + + $this->logger->debug("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $addonId]); + + $this->uninstallAddon($addonId); + $this->installAddon($addonId); + } } /** diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 951f9655ac..41320361dd 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -162,12 +162,12 @@ class AddonManagerHelperTest extends TestCase $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ $addonName => [ $addonName . '.php' => << [ $addonName . '.php' => <<assertSame([], $addonManagerHelper->getEnabledAddons()); } + + public function testReloadAddonsInstallsAddon(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<getChild($addonName . '/' . $addonName . '.php')->lastModified(1234567890); + + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + $addonName => [ + 'last_update' => 0, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->loadAddons(); + + $this->assertSame([$addonName], $addonManagerHelper->getEnabledAddons()); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon reinstalled'); + + $addonManagerHelper->reloadAddons(); + } } From 367f7bd3779b128d7795fc24208df4a12efe8e28 Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:00:36 +0000 Subject: [PATCH 12/24] Remove unused AddonProxy --- src/Core/Addon/AddonManagerHelper.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index f2ff78d896..21573ab89c 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -38,9 +38,6 @@ final class AddonManagerHelper implements AddonHelper /** @var string[] */ private array $addons = []; - /** @deprecated */ - private AddonHelper $proxy; - public function __construct( string $addonPath, Database $database, @@ -55,8 +52,6 @@ final class AddonManagerHelper implements AddonHelper $this->cache = $cache; $this->logger = $logger; $this->profiler = $profiler; - - $this->proxy = new AddonProxy($addonPath); } /** * Returns the absolute path to the addon folder From 075e9eaaa91dba197183ca47a0976634ab936123 Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:14:11 +0000 Subject: [PATCH 13/24] Replace test addon with vfs files --- .../Core/Addon/AddonManagerHelperTest.php | 36 ++++++++++++++++--- tests/Util/addons/helloaddon/helloaddon.php | 29 --------------- 2 files changed, 31 insertions(+), 34 deletions(-) delete mode 100644 tests/Util/addons/helloaddon/helloaddon.php diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 41320361dd..9dbbbdfdd0 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -24,8 +24,22 @@ class AddonManagerHelperTest extends TestCase { public function testGetAddonInfoReturnsAddonInfo(): void { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => << + */ + PHP, + ] + ]); + $addonManagerHelper = new AddonManagerHelper( - __DIR__ . '/../../../Util/addons', + $root->url(), $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), $this->createStub(ICanCache::class), @@ -50,8 +64,10 @@ class AddonManagerHelperTest extends TestCase ], ]); + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + $addonManagerHelper = new AddonManagerHelper( - __DIR__ . '/../../../Util/addons', + $root->url(), $this->createStub(Database::class), $config, $this->createStub(ICanCache::class), @@ -78,8 +94,10 @@ class AddonManagerHelperTest extends TestCase ], ]); + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + $addonManagerHelper = new AddonManagerHelper( - __DIR__ . '/../../../Util/addons', + $root->url(), $this->createStub(Database::class), $config, $this->createStub(ICanCache::class), @@ -104,8 +122,10 @@ class AddonManagerHelperTest extends TestCase ], ]); + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + $addonManagerHelper = new AddonManagerHelper( - __DIR__ . '/../../../Util/addons', + $root->url(), $this->createStub(Database::class), $config, $this->createStub(ICanCache::class), @@ -118,8 +138,14 @@ class AddonManagerHelperTest extends TestCase public function testGetAvailableAddons(): void { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), $this->createStub(Database::class), $this->createStub(IManageConfigValues::class), $this->createStub(ICanCache::class), diff --git a/tests/Util/addons/helloaddon/helloaddon.php b/tests/Util/addons/helloaddon/helloaddon.php deleted file mode 100644 index 679298dfde..0000000000 --- a/tests/Util/addons/helloaddon/helloaddon.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - -use Friendica\Core\Hook; - -function helloaddon_install() -{ - Hook::register('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end'); -} - -function helloaddon_uninstall() -{ - Hook::unregister('page_end', 'addon/helloaddon/helloaddon.php', 'helloaddon_page_end'); -} - -function helloaddon_page_end(&$html) -{ - $html .= '

Hello, World!

'; -} From 44c8cd118caf07ecc6ea0ea5e7bcf882efcc9c41 Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:15:23 +0000 Subject: [PATCH 14/24] Fix code style --- src/Core/Addon/AddonInfo.php | 3 +- tests/Unit/Core/Addon/AddonInfoTest.php | 32 +++++++++---------- .../Core/Addon/AddonManagerHelperTest.php | 12 +++---- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Core/Addon/AddonInfo.php b/src/Core/Addon/AddonInfo.php index 87bb5750f5..60a4b6dc70 100644 --- a/src/Core/Addon/AddonInfo.php +++ b/src/Core/Addon/AddonInfo.php @@ -155,7 +155,8 @@ final class AddonInfo private string $status = ''; - private function __construct() { + private function __construct() + { } public function getId(): string diff --git a/tests/Unit/Core/Addon/AddonInfoTest.php b/tests/Unit/Core/Addon/AddonInfoTest.php index 5e9d2cc79a..20eb654f3a 100644 --- a/tests/Unit/Core/Addon/AddonInfoTest.php +++ b/tests/Unit/Core/Addon/AddonInfoTest.php @@ -41,15 +41,15 @@ class AddonInfoTest extends TestCase */ TEXT, [ - 'id' => 'test', - 'name' => 'Test Addon', + 'id' => 'test', + 'name' => 'Test Addon', 'description' => 'adds awesome features to friendica', 'maintainers' => [ ['name' => 'Robin'], ], 'version' => '100.4.50-beta.5', - 'status' => 'beta', + 'status' => 'beta', ], ], 'without-maintainer' => [ @@ -66,14 +66,14 @@ class AddonInfoTest extends TestCase */ TEXT, [ - 'id' => 'test', - 'name' => 'Test Addon', + 'id' => 'test', + 'name' => 'Test Addon', 'description' => 'adds awesome features to friendica', - 'authors' => [ + 'authors' => [ ['name' => 'Sam'], ], 'version' => '100.4.50-beta.5', - 'status' => 'beta', + 'status' => 'beta', ], ], 'complete' => [ @@ -93,10 +93,10 @@ class AddonInfoTest extends TestCase */ TEXT, [ - 'id' => 'test', - 'name' => 'Test Addon', + 'id' => 'test', + 'name' => 'Test Addon', 'description' => 'adds awesome features to friendica', - 'authors' => [ + 'authors' => [ ['name' => 'Sam'], ['name' => 'Sam With Mail', 'link' => 'mail@example.org'], ], @@ -105,7 +105,7 @@ class AddonInfoTest extends TestCase ['name' => 'Robin With Profile', 'link' => 'https://example.org/profile/robin'], ], 'version' => '100.4.50-beta.5', - 'status' => 'beta', + 'status' => 'beta', ], ], ]; @@ -155,13 +155,13 @@ class AddonInfoTest extends TestCase ); $data = [ - 'id' => $info->getId(), - 'name' => $info->getName(), + 'id' => $info->getId(), + 'name' => $info->getName(), 'description' => $info->getDescription(), - 'authors' => $info->getAuthors(), + 'authors' => $info->getAuthors(), 'maintainers' => $info->getMaintainers(), - 'version' => $info->getVersion(), - 'status' => $info->getStatus(), + 'version' => $info->getVersion(), + 'status' => $info->getStatus(), ]; $this->assertSame($expected, $data); diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 9dbbbdfdd0..fae0502474 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -60,7 +60,7 @@ class AddonManagerHelperTest extends TestCase $config->method('get')->willReturn([ 'helloaddon' => [ 'last_update' => 1738760499, - 'admin' => false, + 'admin' => false, ], ]); @@ -90,7 +90,7 @@ class AddonManagerHelperTest extends TestCase $config->method('get')->willReturn([ 'helloaddon' => [ 'last_update' => 1738760499, - 'admin' => false, + 'admin' => false, ], ]); @@ -114,11 +114,11 @@ class AddonManagerHelperTest extends TestCase $config->method('get')->willReturn([ 'helloaddon' => [ 'last_update' => 1738760499, - 'admin' => false, + 'admin' => false, ], 'addonwithadminsettings' => [ 'last_update' => 1738760499, - 'admin' => true, + 'admin' => true, ], ]); @@ -360,7 +360,7 @@ class AddonManagerHelperTest extends TestCase $config->method('get')->willReturn([ 'helloaddon' => [ 'last_update' => 1234567890, - 'admin' => false, + 'admin' => false, ], ]); @@ -406,7 +406,7 @@ class AddonManagerHelperTest extends TestCase $config->method('get')->willReturn([ $addonName => [ 'last_update' => 0, - 'admin' => false, + 'admin' => false, ], ]); From 7f55714296b5af3569a967d83ac90c4899f722f4 Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:18:34 +0000 Subject: [PATCH 15/24] Replace AddonProxy with AddonManagerHelper --- static/dependencies.config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 644cb5f765..04e2dd2aee 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -43,7 +43,7 @@ return (function(string $basepath, array $getVars, array $serverVars, array $coo ], ], \Friendica\Core\Addon\AddonHelper::class => [ - 'instanceOf' => \Friendica\Core\Addon\AddonProxy::class, + 'instanceOf' => \Friendica\Core\Addon\AddonManagerHelper::class, 'constructParams' => [ $basepath . '/addon', ], From 6b71010de6a4d6ac5bf1c44701aada6ae1c72b7c Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:23:37 +0000 Subject: [PATCH 16/24] Remove debug line from #14931 --- src/Module/Admin/Addons/Details.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Module/Admin/Addons/Details.php b/src/Module/Admin/Addons/Details.php index 671d23c199..9080eb861f 100644 --- a/src/Module/Admin/Addons/Details.php +++ b/src/Module/Admin/Addons/Details.php @@ -96,7 +96,6 @@ class Details extends BaseAdmin $addonAuthors = []; foreach ($addonInfo->getAuthors() as $addonAuthor) { - $addonAuthor['link'] = 'foo@bar.com'; if (array_key_exists('link', $addonAuthor) && empty(parse_url($addonAuthor['link'], PHP_URL_SCHEME))) { $contact = Contact::getByURL($addonAuthor['link'], false); From 7b7d542c8f4ad33035c1b0f703350ce1b41a787b Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:33:50 +0000 Subject: [PATCH 17/24] Remove unused AddonProxy class --- src/Core/Addon/AddonProxy.php | 154 ---------------------------------- 1 file changed, 154 deletions(-) delete mode 100644 src/Core/Addon/AddonProxy.php diff --git a/src/Core/Addon/AddonProxy.php b/src/Core/Addon/AddonProxy.php deleted file mode 100644 index c3f7e583d8..0000000000 --- a/src/Core/Addon/AddonProxy.php +++ /dev/null @@ -1,154 +0,0 @@ -addonPath = $addonPath; - } - - /** - * Returns the absolute path to the addon folder - * - * e.g. `/var/www/html/addon` - */ - public function getAddonPath(): string - { - return $this->addonPath; - } - - /** - * Returns the list of available addons. - * - * This list is made from scanning the addon/ folder. - * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set. - * - * @return string[] - */ - public function getAvailableAddons(): array - { - return array_map( - function (array $item) { - return $item[0]; - }, - Addon::getAvailableList() - ); - } - - /** - * Installs an addon. - * - * @param string $addonId name of the addon - * - * @return bool true on success or false on failure - */ - public function installAddon(string $addonId): bool - { - return Addon::install($addonId); - } - - /** - * Uninstalls an addon. - * - * @param string $addonId name of the addon - */ - public function uninstallAddon(string $addonId): void - { - Addon::uninstall($addonId); - } - - /** - * Load addons. - * - * @internal - */ - public function loadAddons(): void - { - Addon::loadAddons(); - } - - /** - * Reload (uninstall and install) all updated addons. - */ - public function reloadAddons(): void - { - Addon::reload(); - } - - /** - * Get the comment block of an addon as value object. - */ - public function getAddonInfo(string $addonId): AddonInfo - { - $data = Addon::getInfo($addonId); - - // add addon ID - $data['id'] = $addonId; - - // rename author to authors - $data['authors'] = $data['author']; - unset($data['author']); - - // rename maintainer to maintainers - $data['maintainers'] = $data['maintainer']; - unset($data['maintainer']); - - return AddonInfo::fromArray($data); - } - - /** - * Checks if the provided addon is enabled - */ - public function isAddonEnabled(string $addonId): bool - { - return Addon::isEnabled($addonId); - } - - /** - * Returns a list with the IDs of the enabled addons - * - * @return string[] - */ - public function getEnabledAddons(): array - { - return Addon::getEnabledList(); - } - - /** - * Returns a list with the IDs of the non-hidden enabled addons - * - * @return string[] - */ - public function getVisibleEnabledAddons(): array - { - return Addon::getVisibleList(); - } - - /** - * Returns a list with the IDs of the enabled addons that provides admin settings. - * - * @return string[] - */ - public function getEnabledAddonsWithAdminSettings(): array - { - return array_keys(Addon::getAdminList()); - } -} From 638496e55310a9028e416230ea0cbc73ed1f036f Mon Sep 17 00:00:00 2001 From: Art4 Date: Thu, 15 May 2025 07:34:17 +0000 Subject: [PATCH 18/24] Hard deprecate Friendica\Core\Addon class --- src/Core/Addon.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Core/Addon.php b/src/Core/Addon.php index f798021252..96f3fbc6f5 100644 --- a/src/Core/Addon.php +++ b/src/Core/Addon.php @@ -12,6 +12,8 @@ use Friendica\Util\Strings; /** * Some functions to handle addons + * + * @deprecated 2025.02 Use implementation of `Friendica\Core\Addon\AddonHelper` instead */ class Addon { @@ -43,6 +45,8 @@ class Addon */ public static function getAvailableList(): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addons = []; $files = glob('addon/*/'); if (is_array($files)) { @@ -75,6 +79,8 @@ class Addon */ public static function getAdminList(): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addons_admin = []; $addons = array_filter(DI::config()->get('addons') ?? []); @@ -109,6 +115,8 @@ class Addon */ public static function loadAddons() { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + self::$addons = array_keys(array_filter(DI::config()->get('addons') ?? [])); } @@ -123,6 +131,8 @@ class Addon */ public static function uninstall(string $addon) { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addon = Strings::sanitizeFilePathItem($addon); DI::logger()->debug("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]); @@ -151,6 +161,8 @@ class Addon */ public static function install(string $addon): bool { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addon = Strings::sanitizeFilePathItem($addon); $addon_file_path = 'addon/' . $addon . '/' . $addon . '.php'; @@ -191,6 +203,8 @@ class Addon */ public static function reload() { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addons = array_filter(DI::config()->get('addons') ?? []); foreach ($addons as $name => $data) { @@ -230,6 +244,8 @@ class Addon */ public static function getInfo(string $addon): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $addon = Strings::sanitizeFilePathItem($addon); $info = [ @@ -291,6 +307,8 @@ class Addon */ public static function isEnabled(string $addon): bool { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + return in_array($addon, self::$addons); } @@ -303,6 +321,8 @@ class Addon */ public static function getEnabledList(): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + return self::$addons; } @@ -316,6 +336,8 @@ class Addon */ public static function getVisibleList(): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); + $visible_addons = []; $addons = array_filter(DI::config()->get('addons') ?? []); From 0078423b485648b991a0950611aca4acdd513658 Mon Sep 17 00:00:00 2001 From: Art4 Date: Fri, 16 May 2025 08:46:16 +0000 Subject: [PATCH 19/24] Ignore hidden addon folders --- src/Core/Addon/AddonManagerHelper.php | 4 +++- tests/Unit/Core/Addon/AddonManagerHelperTest.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 21573ab89c..513baeb326 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -82,7 +82,9 @@ final class AddonManagerHelper implements AddonHelper $files = []; foreach ($dirs as $dirname) { - if (in_array($dirname, ['.', '..'])) { + // ignore hidden files and folders + // @TODO: Replace with str_starts_with() when PHP 8.0 is the minimum version + if (\strncmp($dirname, '.', 1) === 0) { continue; } diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index fae0502474..e46199ceee 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -141,6 +141,9 @@ class AddonManagerHelperTest extends TestCase $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ 'helloaddon' => [ 'helloaddon.php' => ' [ + '.hidden.php' => 'This folder should be ignored', ] ]); From da413283dae60985bc45cc532f3e0bd95eb446ba Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 20 May 2025 12:06:01 +0000 Subject: [PATCH 20/24] Remove function call of global namespace --- src/Core/Addon/AddonManagerHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 513baeb326..4b7ed1c4a0 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -84,7 +84,7 @@ final class AddonManagerHelper implements AddonHelper foreach ($dirs as $dirname) { // ignore hidden files and folders // @TODO: Replace with str_starts_with() when PHP 8.0 is the minimum version - if (\strncmp($dirname, '.', 1) === 0) { + if (strncmp($dirname, '.', 1) === 0) { continue; } From ab3e54f0e1853cc60e8a2ae0d1c98c7782de9855 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 4 Jun 2025 06:51:35 +0000 Subject: [PATCH 21/24] check type for matches --- src/Core/Addon/AddonInfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Addon/AddonInfo.php b/src/Core/Addon/AddonInfo.php index 60a4b6dc70..98e7fb3ed3 100644 --- a/src/Core/Addon/AddonInfo.php +++ b/src/Core/Addon/AddonInfo.php @@ -43,7 +43,7 @@ final class AddonInfo $result = preg_match("|/\*.*\*/|msU", $raw, $m); - if ($result === false || $result === 0) { + if ($result === false || $result === 0 || !is_array($m) || count($m) < 1) { return self::fromArray($data); } From f1143105d204c50e6d0e106e958b7281995235d6 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 4 Jun 2025 09:26:38 +0000 Subject: [PATCH 22/24] Let AddonHelper::getAddonInfo() throw exception on invalid addons --- src/Core/Addon/AddonHelper.php | 2 + src/Core/Addon/AddonInfo.php | 8 +--- src/Core/Addon/AddonManagerHelper.php | 32 +++++++++++++-- .../Addon/Exception/InvalidAddonException.php | 17 ++++++++ src/Module/Admin/Addons/Details.php | 12 +++++- src/Module/Admin/Addons/Index.php | 8 +++- tests/Unit/Core/Addon/AddonInfoTest.php | 5 +-- .../Core/Addon/AddonManagerHelperTest.php | 40 ++++++++++++++++++- 8 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 src/Core/Addon/Exception/InvalidAddonException.php diff --git a/src/Core/Addon/AddonHelper.php b/src/Core/Addon/AddonHelper.php index 03a21232e5..500bf66f1d 100644 --- a/src/Core/Addon/AddonHelper.php +++ b/src/Core/Addon/AddonHelper.php @@ -61,6 +61,8 @@ interface AddonHelper /** * Get the comment block of an addon as value object. + * + * @throws \Friendica\Core\Addon\Exception\InvalidAddonException if there is an error with the addon file */ public function getAddonInfo(string $addonId): AddonInfo; diff --git a/src/Core/Addon/AddonInfo.php b/src/Core/Addon/AddonInfo.php index 98e7fb3ed3..d0b46f11c6 100644 --- a/src/Core/Addon/AddonInfo.php +++ b/src/Core/Addon/AddonInfo.php @@ -41,13 +41,7 @@ final class AddonInfo 'id' => $addonId, ]; - $result = preg_match("|/\*.*\*/|msU", $raw, $m); - - if ($result === false || $result === 0 || !is_array($m) || count($m) < 1) { - return self::fromArray($data); - } - - $ll = explode("\n", $m[0]); + $ll = explode("\n", $raw); foreach ($ll as $l) { $l = trim($l, "\t\n\r */"); diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 4b7ed1c4a0..f19b8604ff 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace Friendica\Core\Addon; +use Friendica\Core\Addon\Exception\InvalidAddonException; use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Database\Database; @@ -98,7 +99,14 @@ final class AddonManagerHelper implements AddonHelper $addons = []; foreach ($files as $addonId) { - $addonInfo = $this->getAddonInfo($addonId); + try { + $addonInfo = $this->getAddonInfo($addonId); + } catch (InvalidAddonException $th) { + $this->logger->error('Invalid addon found: ' . $addonId, ['exception' => $th]); + + // skip invalid addons + continue; + } if ( $this->config->get('system', 'show_unsupported_addons') @@ -227,6 +235,8 @@ final class AddonManagerHelper implements AddonHelper /** * Get the comment block of an addon as value object. + * + * @throws \Friendica\Core\Addon\Exception\InvalidAddonException if there is an error with the addon file */ public function getAddonInfo(string $addonId): AddonInfo { @@ -235,17 +245,31 @@ final class AddonManagerHelper implements AddonHelper 'name' => $addonId, ]; - if (!is_file($this->getAddonPath() . "/$addonId/$addonId.php")) { + $addonFile = $this->getAddonPath() . "/$addonId/$addonId.php"; + + if (!is_file($addonFile)) { return AddonInfo::fromArray($default); } $this->profiler->startRecording('file'); - $raw = file_get_contents($this->getAddonPath() . "/$addonId/$addonId.php"); + $raw = file_get_contents($addonFile); $this->profiler->stopRecording(); - return AddonInfo::fromString($addonId, $raw); + if ($raw === false) { + throw new InvalidAddonException('Could not read addon file: ' . $addonFile); + } + + $result = preg_match("|/\*.*\*/|msU", $raw, $matches); + + var_dump($addonFile, $result, $matches); + + if ($result === false || $result === 0 || !is_array($matches) || count($matches) < 1) { + throw new InvalidAddonException('Could not find valid comment block in addon file: ' . $addonFile); + } + + return AddonInfo::fromString($addonId, $matches[0]); } /** diff --git a/src/Core/Addon/Exception/InvalidAddonException.php b/src/Core/Addon/Exception/InvalidAddonException.php new file mode 100644 index 0000000000..28079bb1e1 --- /dev/null +++ b/src/Core/Addon/Exception/InvalidAddonException.php @@ -0,0 +1,17 @@ +parameters['addon']); + if (!is_file("addon/$addon/$addon.php")) { DI::sysmsg()->addNotice(DI::l10n()->t('Addon not found.')); $addonHelper->uninstallAddon($addon); @@ -91,7 +94,14 @@ class Details extends BaseAdmin $func($admin_form); } - $addonInfo = $addonHelper->getAddonInfo($addon); + try { + $addonInfo = $addonHelper->getAddonInfo($addon); + } catch (InvalidAddonException $th) { + $this->logger->error('Invalid addon found: ' . $addon, ['exception' => $th]); + DI::sysmsg()->addNotice(DI::l10n()->t('Invalid Addon found.')); + + $addonInfo = AddonInfo::fromArray(['id' => $addon, 'name' => $addon]); + } $addonAuthors = []; diff --git a/src/Module/Admin/Addons/Index.php b/src/Module/Admin/Addons/Index.php index 6038373018..6cbd472c8c 100644 --- a/src/Module/Admin/Addons/Index.php +++ b/src/Module/Admin/Addons/Index.php @@ -7,6 +7,7 @@ namespace Friendica\Module\Admin\Addons; +use Friendica\Core\Addon\Exception\InvalidAddonException; use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\BaseAdmin; @@ -57,7 +58,12 @@ class Index extends BaseAdmin $addons = []; foreach ($addonHelper->getAvailableAddons() as $addonId) { - $addonInfo = $addonHelper->getAddonInfo($addonId); + try { + $addonInfo = $addonHelper->getAddonInfo($addonId); + } catch (InvalidAddonException $th) { + $this->logger->error('Invalid addon found: ' . $addonId, ['exception' => $th]); + continue; + } $info = [ 'name' => $addonInfo->getName(), diff --git a/tests/Unit/Core/Addon/AddonInfoTest.php b/tests/Unit/Core/Addon/AddonInfoTest.php index 20eb654f3a..7a342bc059 100644 --- a/tests/Unit/Core/Addon/AddonInfoTest.php +++ b/tests/Unit/Core/Addon/AddonInfoTest.php @@ -30,8 +30,7 @@ class AddonInfoTest extends TestCase 'without-author' => [ 'test', << [ 'test', << [ 'test', <<assertEquals('Hello Addon', $info->getName()); } + public function testGetAddonInfoThrowsInvalidAddonException(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => <<url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(InvalidAddonException::class); + $this->expectExceptionMessage('Could not find valid comment block in addon file:'); + + $addonManagerHelper->getAddonInfo('helloaddon'); + } + public function testEnabledAddons(): void { $config = $this->createStub(IManageConfigValues::class); @@ -140,7 +167,18 @@ class AddonManagerHelperTest extends TestCase { $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ 'helloaddon' => [ - 'helloaddon.php' => ' << + */ + PHP, + ], + 'invalidaddon' => [ + 'invalidaddon.php' => 'This addon should not be loaded, because it does not contain a valid comment section.', ], '.hidden' => [ '.hidden.php' => 'This folder should be ignored', From bec89a822a506dec4117d55fed06a01d94a5b69e Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 4 Jun 2025 09:27:29 +0000 Subject: [PATCH 23/24] Remove debug statement --- src/Core/Addon/AddonManagerHelper.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index f19b8604ff..9c888f55da 100644 --- a/src/Core/Addon/AddonManagerHelper.php +++ b/src/Core/Addon/AddonManagerHelper.php @@ -263,8 +263,6 @@ final class AddonManagerHelper implements AddonHelper $result = preg_match("|/\*.*\*/|msU", $raw, $matches); - var_dump($addonFile, $result, $matches); - if ($result === false || $result === 0 || !is_array($matches) || count($matches) < 1) { throw new InvalidAddonException('Could not find valid comment block in addon file: ' . $addonFile); } From 8083fb6f8e9eae84a0ec6391a1fe7c9a33520086 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 4 Jun 2025 10:40:04 +0000 Subject: [PATCH 24/24] recreate lang --- view/lang/C/messages.po | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index 6b0e5e433d..ea269e5c6f 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-06-03 20:17+0200\n" +"POT-Creation-Date: 2025-06-04 09:41+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -2171,7 +2171,7 @@ msgstr "" msgid "Manage other pages" msgstr "" -#: src/Content/Nav.php:323 src/Module/Admin/Addons/Details.php:131 +#: src/Content/Nav.php:323 src/Module/Admin/Addons/Details.php:140 #: src/Module/Admin/Themes/Details.php:85 src/Module/BaseSettings.php:177 #: src/Module/Welcome.php:38 view/theme/frio/theme.php:231 msgid "Settings" @@ -3861,31 +3861,35 @@ msgstr "" msgid "User with delegates can't be removed, please remove delegate users first" msgstr "" -#: src/Module/Admin/Addons/Details.php:49 +#: src/Module/Admin/Addons/Details.php:52 msgid "Addon not found." msgstr "" -#: src/Module/Admin/Addons/Details.php:60 src/Module/Admin/Addons/Index.php:43 +#: src/Module/Admin/Addons/Details.php:63 src/Module/Admin/Addons/Index.php:44 #, php-format msgid "Addon %s disabled." msgstr "" -#: src/Module/Admin/Addons/Details.php:63 src/Module/Admin/Addons/Index.php:45 +#: src/Module/Admin/Addons/Details.php:66 src/Module/Admin/Addons/Index.php:46 #, php-format msgid "Addon %s enabled." msgstr "" -#: src/Module/Admin/Addons/Details.php:72 +#: src/Module/Admin/Addons/Details.php:75 #: src/Module/Admin/Themes/Details.php:38 msgid "Disable" msgstr "" -#: src/Module/Admin/Addons/Details.php:75 +#: src/Module/Admin/Addons/Details.php:78 #: src/Module/Admin/Themes/Details.php:41 src/Module/Settings/Display.php:341 msgid "Enable" msgstr "" -#: src/Module/Admin/Addons/Details.php:128 src/Module/Admin/Addons/Index.php:77 +#: src/Module/Admin/Addons/Details.php:101 +msgid "Invalid Addon found." +msgstr "" + +#: src/Module/Admin/Addons/Details.php:137 src/Module/Admin/Addons/Index.php:83 #: src/Module/Admin/Federation.php:213 src/Module/Admin/Logs/Settings.php:74 #: src/Module/Admin/Logs/View.php:71 src/Module/Admin/Queue.php:59 #: src/Module/Admin/Site.php:446 src/Module/Admin/Storage.php:124 @@ -3896,36 +3900,36 @@ msgstr "" msgid "Administration" msgstr "" -#: src/Module/Admin/Addons/Details.php:129 src/Module/Admin/Addons/Index.php:78 +#: src/Module/Admin/Addons/Details.php:138 src/Module/Admin/Addons/Index.php:84 #: src/Module/BaseAdmin.php:77 src/Module/BaseSettings.php:127 msgid "Addons" msgstr "" -#: src/Module/Admin/Addons/Details.php:130 +#: src/Module/Admin/Addons/Details.php:139 #: src/Module/Admin/Themes/Details.php:84 msgid "Toggle" msgstr "" -#: src/Module/Admin/Addons/Details.php:143 +#: src/Module/Admin/Addons/Details.php:152 #: src/Module/Admin/Themes/Details.php:92 msgid "Author: " msgstr "" -#: src/Module/Admin/Addons/Details.php:144 +#: src/Module/Admin/Addons/Details.php:153 #: src/Module/Admin/Themes/Details.php:93 msgid "Maintainer: " msgstr "" -#: src/Module/Admin/Addons/Index.php:35 +#: src/Module/Admin/Addons/Index.php:36 msgid "Addons reloaded" msgstr "" -#: src/Module/Admin/Addons/Index.php:47 +#: src/Module/Admin/Addons/Index.php:48 #, php-format msgid "Addon %s failed to install." msgstr "" -#: src/Module/Admin/Addons/Index.php:79 src/Module/Admin/Features.php:69 +#: src/Module/Admin/Addons/Index.php:85 src/Module/Admin/Features.php:69 #: src/Module/Admin/Logs/Settings.php:76 src/Module/Admin/Site.php:449 #: src/Module/Admin/Themes/Index.php:105 src/Module/Admin/Tos.php:72 #: src/Module/Settings/Account.php:507 src/Module/Settings/Addons.php:64 @@ -3937,11 +3941,11 @@ msgstr "" msgid "Save Settings" msgstr "" -#: src/Module/Admin/Addons/Index.php:80 +#: src/Module/Admin/Addons/Index.php:86 msgid "Reload active addons" msgstr "" -#: src/Module/Admin/Addons/Index.php:84 +#: src/Module/Admin/Addons/Index.php:90 #, php-format msgid "There are currently no addons available on your node. You can find the official addon repository at %1$s." msgstr ""