diff --git a/.phpstan.neon b/.phpstan.neon index fb731728b8..201d6acb39 100644 --- a/.phpstan.neon +++ b/.phpstan.neon @@ -2,6 +2,9 @@ # # SPDX-License-Identifier: CC0-1.0 +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + parameters: level: 3 @@ -23,6 +26,10 @@ parameters: dynamicConstantNames: - DB_UPDATE_VERSION + # See all rules at https://github.com/phpstan/phpstan-strict-rules/blob/2.0.x/rules.neon + strictRules: + allRules: false + ignoreErrors: - # Ignore missing GdImage class in PHP <= 7.4 @@ -38,3 +45,17 @@ parameters: # Ignore missing IMAP\Connection class in PHP <= 8.0 message: '(^Parameter .+ has invalid type IMAP\\Connection\.$)' path: src + + - + # #Fixme: Ignore type mismatch of BaseRepository::$factory in child classes + message: '#^PHPDoc type Friendica\\.+ of property Friendica\\.+\:\:\$factory is not the same as PHPDoc type Friendica\\Capabilities\\ICanCreateFromTableRow of overridden property Friendica\\BaseRepository\:\:\$factory\.$#' + identifier: property.phpDocType + count: 13 + path: src + + - + # #Fixme: Ignore type mismatch of BaseModule::$response in BaseApi module + message: '#^PHPDoc type Friendica\\Module\\Api\\ApiResponse of property Friendica\\Module\\BaseApi\:\:\$response is not the same as PHPDoc type Friendica\\Capabilities\\ICanCreateResponses of overridden property Friendica\\BaseModule\:\:\$response\.$#' + identifier: property.phpDocType + count: 1 + path: src/Module/BaseApi.php diff --git a/composer.json b/composer.json index 9973336590..fc26674ca7 100644 --- a/composer.json +++ b/composer.json @@ -157,6 +157,7 @@ "php-mock/php-mock-phpunit": "^2.10", "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9" }, "scripts": { diff --git a/composer.lock b/composer.lock index fb9fadddbd..ab66a33d05 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": "897b878d6db24b9a6437bd9f971478be", + "content-hash": "e93a8ac7e31cf3e5e0ca76134e5ffa0b", "packages": [ { "name": "asika/simple-console", @@ -5849,6 +5849,54 @@ ], "time": "2024-11-11T15:43:04+00:00" }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" + }, + "time": "2024-10-26T16:04:33+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.31", diff --git a/src/App.php b/src/App.php index ffcbfd1544..b10e474885 100644 --- a/src/App.php +++ b/src/App.php @@ -18,7 +18,6 @@ use Friendica\Capabilities\ICanCreateResponses; use Friendica\Capabilities\ICanHandleRequests; use Friendica\Content\Nav; 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; @@ -278,11 +277,15 @@ class App private function setupContainerForAddons(): void { - /** @var ICanLoadAddons $addonLoader */ - $addonLoader = $this->container->create(ICanLoadAddons::class); + /** @var AddonHelper $addonHelper */ + $addonHelper = $this->container->create(AddonHelper::class); - foreach ($addonLoader->getActiveAddonConfig('dependencies') as $name => $rule) { - $this->container->addRule($name, $rule); + $addonHelper->loadAddons(); + + foreach ($addonHelper->getEnabledAddons() as $addonId) { + foreach ($addonHelper->getAddonDependencyConfig($addonId) as $name => $rule) { + $this->container->addRule($name, $rule); + } } } diff --git a/src/Console/Worker.php b/src/Console/Worker.php index 97b7160d03..09ff5f6599 100644 --- a/src/Console/Worker.php +++ b/src/Console/Worker.php @@ -92,5 +92,7 @@ HELP; CoreWorker::unclaimProcess($process); $this->processRepo->delete($process); + + return; } } diff --git a/src/Content/Pager.php b/src/Content/Pager.php index 0cdf5c0e4a..1199ec758f 100644 --- a/src/Content/Pager.php +++ b/src/Content/Pager.php @@ -157,10 +157,10 @@ class Pager 'text' => $this->l10n->t('newer'), 'class' => 'previous' . ($this->getPage() == 1 ? ' disabled' : '') ], - 'next' => [ + 'next' => [ 'url' => Strings::ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)), 'text' => $this->l10n->t('older'), - 'class' => 'next' . ($displayedItemCount < $this->getItemsPerPage() ? ' disabled' : '') + 'class' => 'next' . ($displayedItemCount < $this->getItemsPerPage() ? ' disabled' : '') ] ]; @@ -208,15 +208,15 @@ class Pager 'class' => $this->getPage() == 1 ? 'disabled' : '' ]; - $numpages = $totalItemCount / $this->getItemsPerPage(); + $numpages = (int) ceil($totalItemCount / $this->getItemsPerPage()); $numstart = 1; - $numstop = $numpages; + $numstop = $numpages; // Limit the number of displayed page number buttons. if ($numpages > 8) { - $numstart = (($this->getPage() > 4) ? ($this->getPage() - 4) : 1); - $numstop = (($this->getPage() > ($numpages - 7)) ? $numpages : ($numstart + 8)); + $numstart = ($this->getPage() > 4) ? ($this->getPage() - 4) : 1; + $numstop = ($this->getPage() > ($numpages - 7)) ? $numpages : ($numstart + 8); } $pages = []; @@ -237,25 +237,9 @@ class Pager } } - if (($totalItemCount % $this->getItemsPerPage()) != 0) { - if ($i == $this->getPage()) { - $pages[$i] = [ - 'url' => '#', - 'text' => $i, - 'class' => 'current active' - ]; - } else { - $pages[$i] = [ - 'url' => Strings::ensureQueryParameter($this->baseQueryString . '&page=' . $i), - 'text' => $i, - 'class' => 'n' - ]; - } - } - $data['pages'] = $pages; - $lastpage = (($numpages > intval($numpages)) ? intval($numpages)+1 : $numpages); + $lastpage = (($numpages > intval($numpages)) ? intval($numpages) + 1 : $numpages); $data['next'] = [ 'url' => Strings::ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)), diff --git a/src/Content/Text/HTML.php b/src/Content/Text/HTML.php index 72c61f38cc..701125dd27 100644 --- a/src/Content/Text/HTML.php +++ b/src/Content/Text/HTML.php @@ -55,7 +55,7 @@ class HTML $xpath = new DOMXPath($doc); - /** @var \DOMNode[] $list */ + /** @var \DOMNodeList<\DOMNode>|false $list */ $list = $xpath->query("//" . $tag); foreach ($list as $node) { $attr = []; @@ -1018,7 +1018,7 @@ class HTML */ public static function checkRelMeLink(DOMDocument $doc, UriInterface $meUrl): bool { - $xpath = new \DOMXpath($doc); + $xpath = new \DOMXPath($doc); // This expression checks that "me" is among the space-delimited values of the "rel" attribute. // And that the href attribute contains exactly the provided URL diff --git a/src/Core/Addon/AddonHelper.php b/src/Core/Addon/AddonHelper.php index 500bf66f1d..0f4205696f 100644 --- a/src/Core/Addon/AddonHelper.php +++ b/src/Core/Addon/AddonHelper.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace Friendica\Core\Addon; +use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; + /** * Some functions to handle addons */ @@ -66,6 +68,17 @@ interface AddonHelper */ public function getAddonInfo(string $addonId): AddonInfo; + /** + * Returns a dependency config array for a given addon + * + * This will load a potential config-file from the static directory, like `addon/{addonId}/static/dependencies.config.php` + * + * @throws AddonInvalidConfigFileException If the config file doesn't return an array + * + * @return array the config as array or empty array if no config file was found + */ + public function getAddonDependencyConfig(string $addonId): array; + /** * Checks if the provided addon is enabled */ diff --git a/src/Core/Addon/AddonManagerHelper.php b/src/Core/Addon/AddonManagerHelper.php index 9c888f55da..0825e67b13 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\AddonInvalidConfigFileException; use Friendica\Core\Addon\Exception\InvalidAddonException; use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; @@ -270,6 +271,34 @@ final class AddonManagerHelper implements AddonHelper return AddonInfo::fromString($addonId, $matches[0]); } + /** + * Returns a dependency config array for a given addon + * + * This will load a potential config-file from the static directory, like `addon/{addonId}/static/dependencies.config.php` + * + * @throws AddonInvalidConfigFileException If the config file doesn't return an array + * + * @return array the config as array or empty array if no config file was found + */ + public function getAddonDependencyConfig(string $addonId): array + { + $addonId = Strings::sanitizeFilePathItem(trim($addonId)); + + $configFile = $this->getAddonPath() . '/' . $addonId . '/static/dependencies.config.php'; + + if (!file_exists($configFile)) { + return []; + } + + $config = include($configFile); + + if (!is_array($config)) { + throw new AddonInvalidConfigFileException('Error loading config file ' . $configFile); + } + + return $config; + } + /** * Checks if the provided addon is enabled */ diff --git a/src/Core/Addon/Capability/ICanLoadAddons.php b/src/Core/Addon/Capability/ICanLoadAddons.php index 993c431497..63978462d3 100644 --- a/src/Core/Addon/Capability/ICanLoadAddons.php +++ b/src/Core/Addon/Capability/ICanLoadAddons.php @@ -9,12 +9,16 @@ namespace Friendica\Core\Addon\Capability; /** * Interface for loading Addons specific content + * + * @deprecated 2025.02 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead. */ interface ICanLoadAddons { /** * Returns a merged config array of all active addons for a given config-name * + * @deprecated 2025.02 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead. + * * @param string $configName The config-name (config-file at the static directory, like 'hooks' => '{addon}/static/hooks.config.php) * * @return array the merged array diff --git a/src/Core/Addon/Model/AddonLoader.php b/src/Core/Addon/Model/AddonLoader.php index a608a66e3d..b56ba3eed8 100644 --- a/src/Core/Addon/Model/AddonLoader.php +++ b/src/Core/Addon/Model/AddonLoader.php @@ -14,6 +14,9 @@ use Friendica\Core\Logger\Factory\LoggerFactory; use Friendica\Util\Strings; use Psr\Log\LoggerInterface; +/** + * @deprecated 2025.02 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead. + */ class AddonLoader implements ICanLoadAddons { const STATIC_PATH = 'static'; @@ -24,13 +27,19 @@ class AddonLoader implements ICanLoadAddons public function __construct(string $basePath, IManageConfigValues $config) { + @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); + $this->basePath = $basePath; $this->config = $config; } - /** {@inheritDoc} */ + /** + * @deprecated 2025.02 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead. + */ public function getActiveAddonConfig(string $configName): array { + @trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.02 and will be removed after 5 months, use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead.', E_USER_DEPRECATED); + $addons = array_keys(array_filter($this->config->get('addons') ?? [])); $returnConfig = []; diff --git a/src/Core/Hooks/Util/StrategiesFileManager.php b/src/Core/Hooks/Util/StrategiesFileManager.php index a876dc832c..c56a13ee6a 100644 --- a/src/Core/Hooks/Util/StrategiesFileManager.php +++ b/src/Core/Hooks/Util/StrategiesFileManager.php @@ -7,9 +7,13 @@ namespace Friendica\Core\Hooks\Util; -use Friendica\Core\Addon\Capability\ICanLoadAddons; +use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Hooks\Capability\ICanRegisterStrategies; use Friendica\Core\Hooks\Exceptions\HookConfigException; +use Friendica\Core\Logger\Factory\LoggerFactory; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; /** * Manage all strategies.config.php files @@ -24,17 +28,15 @@ class StrategiesFileManager const STATIC_DIR = 'static'; const CONFIG_NAME = 'strategies'; - /** @var ICanLoadAddons */ - protected $addonLoader; - /** @var array */ - protected $config = []; + private IManageConfigValues $configuration; + protected array $config = []; /** @var string */ protected $basePath; - public function __construct(string $basePath, ICanLoadAddons $addonLoader) + public function __construct(string $basePath, IManageConfigValues $configuration) { - $this->basePath = $basePath; - $this->addonLoader = $addonLoader; + $this->basePath = $basePath; + $this->configuration = $configuration; } /** @@ -84,6 +86,50 @@ class StrategiesFileManager /** * @deprecated 2025.02 Providing strategies via addons is deprecated and will be removed in 5 months. */ - $this->config = array_merge_recursive($config, $this->addonLoader->getActiveAddonConfig(static::CONFIG_NAME)); + $this->config = array_merge_recursive($config, $this->getActiveAddonConfig()); + } + + private function getActiveAddonConfig(): array + { + $addons = array_keys(array_filter($this->configuration->get('addons') ?? [])); + $returnConfig = []; + + foreach ($addons as $addon) { + $addonName = Strings::sanitizeFilePathItem(trim($addon)); + + $configFile = $this->basePath . '/addon/' . $addonName . '/' . static::STATIC_DIR . '/strategies.config.php'; + + if (!file_exists($configFile)) { + // Addon unmodified, skipping + continue; + } + + $config = include $configFile; + + if (!is_array($config)) { + throw new AddonInvalidConfigFileException('Error loading config file ' . $configFile); + } + + foreach ($config as $classname => $rule) { + if ($classname === LoggerInterface::class) { + @trigger_error(sprintf( + 'Providing a strategy for `%s` is deprecated since 2025.02 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.', + $classname, + LoggerFactory::class, + $addonName, + ), \E_USER_DEPRECATED); + } else { + @trigger_error(sprintf( + 'Providing strategies for `%s` via addons is deprecated since 2025.02 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.', + $classname, + $addonName, + ), \E_USER_DEPRECATED); + } + } + + $returnConfig = array_merge_recursive($returnConfig, $config); + } + + return $returnConfig; } } diff --git a/src/Core/Lock/Type/AbstractLock.php b/src/Core/Lock/Type/AbstractLock.php index 9d8b5849b2..6854be267c 100644 --- a/src/Core/Lock/Type/AbstractLock.php +++ b/src/Core/Lock/Type/AbstractLock.php @@ -28,7 +28,7 @@ abstract class AbstractLock implements ICanLock */ protected function hasAcquiredLock(string $key): bool { - return isset($this->acquireLock[$key]) && $this->acquiredLocks[$key] === true; + return isset($this->acquiredLocks[$key]) && $this->acquiredLocks[$key] === true; } /** diff --git a/src/Core/Session/Handler/Cache.php b/src/Core/Session/Handler/Cache.php index 7629d9a164..65b87872a7 100644 --- a/src/Core/Session/Handler/Cache.php +++ b/src/Core/Session/Handler/Cache.php @@ -99,8 +99,11 @@ class Cache extends AbstractSessionHandler } #[\ReturnTypeWillChange] - public function gc($max_lifetime): bool + /** + * @return int|false + */ + public function gc($max_lifetime) { - return true; + return 0; // Cache does not support garbage collection, so we return 0 to indicate no action taken } } diff --git a/src/Core/Session/Handler/Database.php b/src/Core/Session/Handler/Database.php index 3ef64294b9..d8146139a1 100644 --- a/src/Core/Session/Handler/Database.php +++ b/src/Core/Session/Handler/Database.php @@ -124,13 +124,23 @@ class Database extends AbstractSessionHandler } #[\ReturnTypeWillChange] - public function gc($max_lifetime): bool + /** + * @return int|false + */ + public function gc($max_lifetime) { try { - return $this->dba->delete('session', ["`expire` < ?", time()]); + $result = $this->dba->delete('session', ["`expire` < ?", time()]); } catch (\Exception $exception) { $this->logger->warning('Cannot use garbage collector.', ['exception' => $exception]); return false; } + + if ($result !== false) { + // TODO: DBA::delete() returns true, but we need to return the number of deleted rows as interger + $result = 0; + } + + return $result; } } diff --git a/src/Core/Worker.php b/src/Core/Worker.php index fd33278502..fd2ab5c242 100644 --- a/src/Core/Worker.php +++ b/src/Core/Worker.php @@ -521,7 +521,7 @@ class Worker } if ($sleeping) { - DI::logger()->info('Cooldown ended.', ['max-load' => $load_cooldown, 'max-processes' => $processes_cooldown, 'load' => $load, 'called-by' => System::callstack(1)]); + DI::logger()->info('Cooldown ended.', ['max-load' => $load_cooldown, 'max-processes' => $processes_cooldown, 'load' => $load ?? [], 'called-by' => System::callstack(1)]); } } diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index 6aebda8096..031c2fec1d 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -193,7 +193,7 @@ class PostUpdate Contact::removeDuplicates($contact['nurl'], $contact['uid']); } - DBA::close($contact); + DBA::close($contacts); DI::keyValue()->set('post_update_version', 1322); DI::logger()->info('Done'); diff --git a/src/Module/Search/Index.php b/src/Module/Search/Index.php index 8d395ba880..e6e9f39a31 100644 --- a/src/Module/Search/Index.php +++ b/src/Module/Search/Index.php @@ -136,7 +136,9 @@ class Index extends BaseSearch // Tags don't look like an URL and the fulltext search does only work with natural words if (parse_url($search, PHP_URL_SCHEME) && parse_url($search, PHP_URL_HOST)) { $this->logger->info('Skipping tag and fulltext search since the search looks like a URL.', ['q' => $search]); - DI::sysmsg()->addNotice(DI::l10n()->t('No results.')); + $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [ + '$title' => DI::l10n()->t('No results.') + ]); return $o; } @@ -186,7 +188,9 @@ class Index extends BaseSearch if (empty($items)) { if (empty($last_uriid)) { - DI::sysmsg()->addNotice(DI::l10n()->t('No results.')); + $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [ + '$title' => DI::l10n()->t('No results.') + ]); } return $o; } diff --git a/src/Module/Settings/Display.php b/src/Module/Settings/Display.php index 97c163cea6..f4173e82c0 100644 --- a/src/Module/Settings/Display.php +++ b/src/Module/Settings/Display.php @@ -234,11 +234,11 @@ class Display extends BaseSettings $update_content = $this->pConfig->get($uid, 'system', 'update_content') ?? false; $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', true); $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); + $stay_local = $this->pConfig->get($uid, 'system', 'stay_local', true); $show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true); $display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true); diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 471f6e313a..92ee97b816 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -8,7 +8,7 @@ namespace Friendica\Network; use DOMDocument; -use DomXPath; +use DOMXPath; use Exception; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -1273,7 +1273,7 @@ class Probe return []; } - $xpath = new DomXPath($doc); + $xpath = new DOMXPath($doc); $vcards = $xpath->query("//div[contains(concat(' ', @class, ' '), ' vcard ')]"); if (!is_object($vcards)) { diff --git a/src/Object/Image.php b/src/Object/Image.php index 0aad761341..56c6f9f325 100644 --- a/src/Object/Image.php +++ b/src/Object/Image.php @@ -48,8 +48,8 @@ class Image public function __construct(string $data, string $type = '', string $filename = '', bool $imagick = true) { $this->filename = $filename; - $type = Images::addMimeTypeByDataIfInvalid($type, $data); - $type = Images::addMimeTypeByExtensionIfInvalid($type, $filename); + $type = Images::addMimeTypeByDataIfInvalid($type, $data); + $type = Images::addMimeTypeByExtensionIfInvalid($type, $filename); if (Images::isSupportedMimeType($type)) { $this->originType = $this->outputType = Images::getImageTypeByMimeType($type); @@ -108,7 +108,7 @@ class Image private function isAnimatedWebP(string $data) { $header_format = 'A4Riff/I1Filesize/A4Webp/A4Vp/A74Chunk'; - $header = @unpack($header_format, $data); + $header = @unpack($header_format, $data); if (!isset($header['Riff']) || strtoupper($header['Riff']) !== 'RIFF') { return false; @@ -348,7 +348,7 @@ class Image return false; } - $width = $this->getWidth(); + $width = $this->getWidth(); $height = $this->getHeight(); $scale = Images::getScalingDimensions($width, $height, $max); @@ -363,12 +363,11 @@ class Image * Rotates image * * @param integer $degrees degrees to rotate image - * @return mixed */ - public function rotate(int $degrees) + public function rotate(int $degrees): void { if (!$this->isValid()) { - return false; + return; } if ($this->isImagick()) { @@ -393,12 +392,11 @@ class Image * * @param boolean $horiz optional, default true * @param boolean $vert optional, default false - * @return mixed */ - public function flip(bool $horiz = true, bool $vert = false) + public function flip(bool $horiz = true, bool $vert = false): void { if (!$this->isValid()) { - return false; + return; } if ($this->isImagick()) { @@ -414,8 +412,8 @@ class Image return; } - $w = imagesx($this->image); - $h = imagesy($this->image); + $w = imagesx($this->image); + $h = imagesy($this->image); $flipped = imagecreate($w, $h); if ($horiz) { for ($x = 0; $x < $w; $x++) { @@ -523,7 +521,7 @@ class Image return false; } - $width = $this->getWidth(); + $width = $this->getWidth(); $height = $this->getHeight(); if ((!$width) || (!$height)) { @@ -532,22 +530,22 @@ class Image if ($width < $min && $height < $min) { if ($width > $height) { - $dest_width = $min; + $dest_width = $min; $dest_height = intval(($height * $min) / $width); } else { - $dest_width = intval(($width * $min) / $height); + $dest_width = intval(($width * $min) / $height); $dest_height = $min; } } else { if ($width < $min) { - $dest_width = $min; + $dest_width = $min; $dest_height = intval(($height * $min) / $width); } else { if ($height < $min) { - $dest_width = intval(($width * $min) / $height); + $dest_width = intval(($width * $min) / $height); $dest_height = $min; } else { - $dest_width = $width; + $dest_width = $width; $dest_height = $height; } } @@ -622,7 +620,7 @@ class Image imagedestroy($this->image); } - $this->image = $dest; + $this->image = $dest; $this->width = imagesx($this->image); $this->height = imagesy($this->image); } @@ -799,9 +797,9 @@ class Image } $row[] = [$colors['r'], $colors['g'], $colors['b']]; } else { - $index = imagecolorat($image->image, $x, $y); + $index = imagecolorat($image->image, $x, $y); $colors = @imagecolorsforindex($image->image, $index); - $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; } } $pixels[] = $row; @@ -830,7 +828,7 @@ class Image if ($this->isImagick()) { $this->image = new Imagick(); - $draw = new ImagickDraw(); + $draw = new ImagickDraw(); $this->image->newImage($scaled['width'], $scaled['height'], '', 'png'); } else { $this->image = imagecreatetruecolor($scaled['width'], $scaled['height']); @@ -838,7 +836,7 @@ class Image for ($y = 0; $y < $scaled['height']; ++$y) { for ($x = 0; $x < $scaled['width']; ++$x) { - [$r, $g, $b] = $pixels[$y][$x]; + list($r, $g, $b) = $pixels[$y][$x]; if ($draw !== null) { $draw->setFillColor("rgb($r, $g, $b)"); $draw->point($x, $y); diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index a779a59a86..c5a182f98e 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -592,7 +592,7 @@ class HTTPSignature return []; } - $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']); + $sig_block = self::parseSigheader($http_headers['HTTP_SIGNATURE']); if (empty($sig_block['keyId'])) { DI::logger()->debug('No keyId', ['sig_block' => $sig_block]); @@ -652,7 +652,7 @@ class HTTPSignature } } - $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']); + $sig_block = self::parseSigheader($http_headers['HTTP_SIGNATURE']); // Add fields from the signature block to the header. See issue 8845 if (!empty($sig_block['created']) && empty($headers['(created)'])) { diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 04e2dd2aee..1ce02a2411 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -39,7 +39,6 @@ return (function(string $basepath, array $getVars, array $serverVars, array $coo 'instanceOf' => \Friendica\Core\Addon\Model\AddonLoader::class, 'constructParams' => [ $basepath, - [Dice::INSTANCE => Dice::SELF], ], ], \Friendica\Core\Addon\AddonHelper::class => [ diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php index 882082ceb0..a63c605538 100644 --- a/tests/Unit/Core/Addon/AddonManagerHelperTest.php +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -12,6 +12,7 @@ namespace Friendica\Test\Unit\Core\Addon; use Exception; use Friendica\Core\Addon\AddonInfo; use Friendica\Core\Addon\AddonManagerHelper; +use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; use Friendica\Core\Addon\Exception\InvalidAddonException; use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; @@ -81,6 +82,76 @@ class AddonManagerHelperTest extends TestCase $addonManagerHelper->getAddonInfo('helloaddon'); } + public function testGetAddonDependencyConfigReturnsArray(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'static' => [ + 'dependencies.config.php' => << 'bar', + ]; + PHP, + ], + ] + ]); + + $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) + ); + + $this->assertSame(['foo' => 'bar'], $addonManagerHelper->getAddonDependencyConfig('helloaddon')); + } + + public function testGetAddonDependencyConfigWithoutConfigFileReturnsEmptyArray(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [] + ]); + + $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) + ); + + $this->assertSame([], $addonManagerHelper->getAddonDependencyConfig('helloaddon')); + } + + public function testGetAddonDependencyConfigWithoutReturningAnArrayThrowsException(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'static' => [ + 'dependencies.config.php' => 'url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(AddonInvalidConfigFileException::class); + $this->expectExceptionMessageMatches('#Error loading config file .+/helloaddon/static/dependencies\.config\.php#'); + + $addonManagerHelper->getAddonDependencyConfig('helloaddon'); + } + public function testEnabledAddons(): void { $config = $this->createStub(IManageConfigValues::class); diff --git a/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php b/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php index 633b636701..7adcfc0a97 100644 --- a/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php +++ b/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php @@ -7,7 +7,7 @@ namespace Friendica\Test\src\Core\Hooks\Util; -use Friendica\Core\Addon\Capability\ICanLoadAddons; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Hooks\Capability\ICanRegisterStrategies; use Friendica\Core\Hooks\Exceptions\HookConfigException; use Friendica\Core\Hooks\Util\StrategiesFileManager; @@ -33,49 +33,61 @@ class StrategiesFileManagerTest extends MockedTestCase return [ 'normal' => [ 'content' => << [ - \Psr\Log\NullLogger::class => [''], - ], -]; -EOF, - 'addonsArray' => [], + return [ + \Psr\Log\LoggerInterface::class => [ + \Psr\Log\NullLogger::class => [''], + ], + ]; + EOF, + 'addonContent' => << [ [LoggerInterface::class, NullLogger::class, ''], ], ], 'normalWithString' => [ 'content' => << [ - \Psr\Log\NullLogger::class => '', - ], -]; -EOF, - 'addonsArray' => [], + return [ + \Psr\Log\LoggerInterface::class => [ + \Psr\Log\NullLogger::class => '', + ], + ]; + EOF, + 'addonContent' => << [ [LoggerInterface::class, NullLogger::class, ''], ], ], 'withAddons' => [ 'content' => << [ - \Psr\Log\NullLogger::class => [''], - ], -]; -EOF, - 'addonsArray' => [ - \Psr\Log\LoggerInterface::class => [ - \Psr\Log\NullLogger::class => ['null'], - ], - ], + return [ + \Psr\Log\LoggerInterface::class => [ + \Psr\Log\NullLogger::class => [''], + ], + ]; + EOF, + 'addonContent' => << [ + \Psr\Log\NullLogger::class => ['null'], + ], + ]; + EOF, 'assertStrategies' => [ [LoggerInterface::class, NullLogger::class, ''], [LoggerInterface::class, NullLogger::class, 'null'], @@ -83,19 +95,23 @@ EOF, ], 'withAddonsWithString' => [ 'content' => << [ - \Psr\Log\NullLogger::class => [''], - ], -]; -EOF, - 'addonsArray' => [ - \Psr\Log\LoggerInterface::class => [ - \Psr\Log\NullLogger::class => 'null', - ], - ], + return [ + \Psr\Log\LoggerInterface::class => [ + \Psr\Log\NullLogger::class => [''], + ], + ]; + EOF, + 'addonContent' => << [ + \Psr\Log\NullLogger::class => ['null'], + ], + ]; + EOF, 'assertStrategies' => [ [LoggerInterface::class, NullLogger::class, ''], [LoggerInterface::class, NullLogger::class, 'null'], @@ -104,19 +120,23 @@ EOF, // This should work because unique name convention is part of the instance manager logic, not of the file-infrastructure layer 'withAddonsDoubleNamed' => [ 'content' => << [ - \Psr\Log\NullLogger::class => [''], - ], -]; -EOF, - 'addonsArray' => [ - \Psr\Log\LoggerInterface::class => [ - \Psr\Log\NullLogger::class => [''], - ], - ], + return [ + \Psr\Log\LoggerInterface::class => [ + \Psr\Log\NullLogger::class => [''], + ], + ]; + EOF, + 'addonContent' => << [ + \Psr\Log\NullLogger::class => [''], + ], + ]; + EOF, 'assertStrategies' => [ [LoggerInterface::class, NullLogger::class, ''], [LoggerInterface::class, NullLogger::class, ''], @@ -128,16 +148,20 @@ EOF, /** * @dataProvider dataHooks */ - public function testSetupHooks(string $content, array $addonsArray, array $assertStrategies) + public function testSetupHooks(string $content, string $addonContent, array $assertStrategies) { vfsStream::newFile(StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php') ->withContent($content) ->at($this->root); - $addonLoader = \Mockery::mock(ICanLoadAddons::class); - $addonLoader->shouldReceive('getActiveAddonConfig')->andReturn($addonsArray)->once(); + vfsStream::newFile('addon/testaddon/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php') + ->withContent($addonContent) + ->at($this->root); - $hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader); + $config = \Mockery::mock(IManageConfigValues::class); + $config->shouldReceive('get')->andReturn(['testaddon' => ['admin' => false]])->once(); + + $hookFileManager = new StrategiesFileManager($this->root->url(), $config); $instanceManager = \Mockery::mock(ICanRegisterStrategies::class); foreach ($assertStrategies as $assertStrategy) { @@ -155,13 +179,15 @@ EOF, */ public function testMissingStrategiesFile() { - $addonLoader = \Mockery::mock(ICanLoadAddons::class); + $config = \Mockery::mock(IManageConfigValues::class); $instanceManager = \Mockery::mock(ICanRegisterStrategies::class); - $hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader); + $hookFileManager = new StrategiesFileManager($this->root->url(), $config); self::expectException(HookConfigException::class); - self::expectExceptionMessage(sprintf('config file %s does not exist.', - $this->root->url() . '/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php')); + self::expectExceptionMessage(sprintf( + 'config file %s does not exist.', + $this->root->url() . '/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php' + )); $hookFileManager->loadConfig(); } @@ -171,17 +197,19 @@ EOF, */ public function testWrongStrategiesFile() { - $addonLoader = \Mockery::mock(ICanLoadAddons::class); + $config = \Mockery::mock(IManageConfigValues::class); $instanceManager = \Mockery::mock(ICanRegisterStrategies::class); - $hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader); + $hookFileManager = new StrategiesFileManager($this->root->url(), $config); vfsStream::newFile(StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php') ->withContent("at($this->root); self::expectException(HookConfigException::class); - self::expectExceptionMessage(sprintf('Error loading config file %s.', - $this->root->url() . '/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php')); + self::expectExceptionMessage(sprintf( + 'Error loading config file %s.', + $this->root->url() . '/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php' + )); $hookFileManager->loadConfig(); } diff --git a/view/global.css b/view/global.css index 0215e6d4df..86e8ccc933 100644 --- a/view/global.css +++ b/view/global.css @@ -806,6 +806,7 @@ summary.wall-item-summary { /* Margin to create space between the icon and the text */ .notif-image { margin-right: 10px; +} /* Styles to ensure the text wraps properly after 70 characters */ .notif-text { @@ -813,3 +814,16 @@ summary.wall-item-summary { max-width: 70ch; overflow-wrap: break-word; } + +/* Add alt tag indicators to the relevant images */ +a:has(img.has-alt-description)::after { + background: lightgray; + border-radius: 4px; + color: black; + content: "ALT"; + font-weight: bold; + padding: 3px 8px; + position: absolute; + bottom: 10px; + right: 10px; +} diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index 4aa51dc1cd..3988254f0a 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-14 15:06+0200\n" +"POT-Creation-Date: 2025-06-21 13:03+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -646,7 +646,7 @@ msgstr "" msgid "Map" msgstr "" -#: src/App.php:451 +#: src/App.php:454 msgid "Apologies but the website is unavailable at the moment." msgstr "" @@ -2227,11 +2227,11 @@ msgstr "" msgid "prev" msgstr "" -#: src/Content/Pager.php:262 src/Module/Calendar/Show.php:122 +#: src/Content/Pager.php:246 src/Module/Calendar/Show.php:122 msgid "next" msgstr "" -#: src/Content/Pager.php:267 +#: src/Content/Pager.php:251 msgid "last" msgstr "" @@ -6108,7 +6108,7 @@ msgstr "" msgid "Search your contacts" msgstr "" -#: src/Module/Contact.php:441 src/Module/Search/Index.php:202 +#: src/Module/Contact.php:441 src/Module/Search/Index.php:206 #, php-format msgid "Results for: %s" msgstr "" @@ -6698,8 +6698,8 @@ msgid "Unable to unfollow this contact, please contact your administrator" msgstr "" #: src/Module/Conversation/Channel.php:125 -#: src/Module/Conversation/Community.php:114 src/Module/Search/Index.php:139 -#: src/Module/Search/Index.php:189 +#: src/Module/Conversation/Community.php:114 src/Module/Search/Index.php:140 +#: src/Module/Search/Index.php:192 msgid "No results." msgstr "" @@ -8901,7 +8901,7 @@ msgstr "" msgid "Only one search per minute is permitted for not logged in users." msgstr "" -#: src/Module/Search/Index.php:200 +#: src/Module/Search/Index.php:204 #, php-format msgid "Items tagged with: %s" msgstr "" diff --git a/view/theme/frio/js/theme.js b/view/theme/frio/js/theme.js index ad59a3f633..d11f953127 100644 --- a/view/theme/frio/js/theme.js +++ b/view/theme/frio/js/theme.js @@ -174,9 +174,8 @@ $(document).ready(function () { // temporary workaround to avoid 'undefined' being displayed (issue #9789) // https://github.com/friendica/friendica/issues/9789 - // TODO: find a way to localize this string if (typeof searchText === "undefined") { - searchText = "No results"; + searchText = ""; } // insert the plain text in a

heading and give it a class var newText = '

' + searchText + "

"; diff --git a/view/theme/frio/templates/nav.tpl b/view/theme/frio/templates/nav.tpl index 37edb7118a..843a2cf00d 100644 --- a/view/theme/frio/templates/nav.tpl +++ b/view/theme/frio/templates/nav.tpl @@ -164,7 +164,7 @@