Merge pull request #13246 from nupplaphil/feat/addons

Introduce dynamic hook loading
This commit is contained in:
Hypolite Petovan 2023-07-23 06:58:46 -04:00 committed by GitHub
commit aee420152f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1871 additions and 821 deletions

View file

@ -7,9 +7,9 @@ matrix:
- PHP_MAJOR_VERSION: 8.0 - PHP_MAJOR_VERSION: 8.0
PHP_VERSION: 8.0.29 PHP_VERSION: 8.0.29
- PHP_MAJOR_VERSION: 8.1 - PHP_MAJOR_VERSION: 8.1
PHP_VERSION: 8.1.20 PHP_VERSION: 8.1.21
- PHP_MAJOR_VERSION: 8.2 - PHP_MAJOR_VERSION: 8.2
PHP_VERSION: 8.2.7 PHP_VERSION: 8.2.8
# This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...) # This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...)
labels: labels:

View file

@ -58,6 +58,7 @@ if (php_sapi_name() !== 'cli') {
use Dice\Dice; use Dice\Dice;
use Friendica\App\Mode; use Friendica\App\Mode;
use Friendica\Core\Logger\Capabilities\LogChannel;
use Friendica\Security\ExAuth; use Friendica\Security\ExAuth;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -78,7 +79,10 @@ chdir($directory);
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php');
$dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['auth_ejabberd']]); /** @var \Friendica\Core\Addon\Capabilities\ICanLoadAddons $addonLoader */
$addonLoader = $dice->create(\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class);
$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies'));
$dice = $dice->addRule(LoggerInterface::class,['constructParams' => [LogChannel::AUTH_JABBERED]]);
\Friendica\DI::init($dice); \Friendica\DI::init($dice);
\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); \Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class));

View file

@ -26,13 +26,17 @@ if (php_sapi_name() !== 'cli') {
} }
use Dice\Dice; use Dice\Dice;
use Friendica\Core\Logger\Capabilities\LogChannel;
use Friendica\DI; use Friendica\DI;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php');
$dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['console']]); /** @var \Friendica\Core\Addon\Capabilities\ICanLoadAddons $addonLoader */
$addonLoader = $dice->create(\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class);
$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies'));
$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [LogChannel::CONSOLE]]);
/// @fixme Necessary until Hooks inside the Logger can get loaded without the DI-class /// @fixme Necessary until Hooks inside the Logger can get loaded without the DI-class
DI::init($dice); DI::init($dice);

View file

@ -60,7 +60,10 @@ if (!file_exists('index.php') && (sizeof($_SERVER['argv']) != 0)) {
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php');
$dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['daemon']]); /** @var \Friendica\Core\Addon\Capabilities\ICanLoadAddons $addonLoader */
$addonLoader = $dice->create(\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class);
$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies'));
$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [Logger\Capabilities\LogChannel::DAEMON]]);
DI::init($dice); DI::init($dice);
\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); \Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class));

View file

@ -29,6 +29,7 @@ if (php_sapi_name() !== 'cli') {
use Dice\Dice; use Dice\Dice;
use Friendica\App; use Friendica\App;
use Friendica\App\Mode; use Friendica\App\Mode;
use Friendica\Core\Logger\Capabilities\LogChannel;
use Friendica\Core\Update; use Friendica\Core\Update;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\DI; use Friendica\DI;
@ -54,7 +55,10 @@ if (!file_exists("index.php") && (sizeof($_SERVER["argv"]) != 0)) {
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php');
$dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['worker']]); /** @var \Friendica\Core\Addon\Capabilities\ICanLoadAddons $addonLoader */
$addonLoader = $dice->create(\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class);
$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies'));
$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [LogChannel::WORKER]]);
DI::init($dice); DI::init($dice);
\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); \Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class));

View file

@ -134,6 +134,7 @@
"scripts": { "scripts": {
"test": "phpunit", "test": "phpunit",
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l", "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l",
"docker:translate": "docker run --rm -v $PWD:/data -w /data friendicaci/transifex bin/run_xgettext.sh",
"cs:install": "@composer install --working-dir=bin/dev/php-cs-fixer", "cs:install": "@composer install --working-dir=bin/dev/php-cs-fixer",
"cs:check": [ "cs:check": [
"@cs:install", "@cs:install",

89
doc/StrategyHooks.md Normal file
View file

@ -0,0 +1,89 @@
Friendica strategy Hooks
===========================================
* [Home](help)
## Strategy hooks
This type of hook is based on the [Strategy Design Pattern](https://refactoring.guru/design-patterns/strategy).
A strategy class defines a possible implementation of a given interface based on a unique name.
Every name is possible as long as it's unique and not `null`.
Using an empty name (`''`) is possible as well and should be used as the "default" implementation.
To register a strategy, use the [`ICanRegisterInstance`](../src/Core/Hooks/Capabilities/ICanRegisterInstances.php) interface.
After registration, a caller can automatically create this instance with the [`ICanCreateInstances`](../src/Core/Hooks/Capabilities/ICanCreateInstances.php) interface and the chosen name.
This is useful in case there are different, possible implementations for the same purpose, like for logging, locking, caching, ...
Normally, a config entry is used to choose the right implementation at runtime.
And if no config entry is set, the "default" implementation should be used.
### Example
```php
interface ExampleInterface
{
public function testMethod();
}
public class ConcreteClassA implements ExampleInterface
{
public function testMethod()
{
echo "concrete class A";
}
}
public class ConcreteClassB implements ExampleInterface
{
public function testMethod()
{
echo "concrete class B";
}
}
/** @var \Friendica\Core\Hooks\Capabilities\ICanRegisterStrategies $instanceRegister */
$instanceRegister->registerStrategy(ExampleInterface::class, ConcreteClassA::class, 'A');
$instanceRegister->registerStrategy(ExampleInterface::class, ConcreteClassB::class, 'B');
/** @var \Friendica\Core\Hooks\Capabilities\ICanCreateInstances $instanceManager */
/** @var ConcreteClassA $concreteClass */
$concreteClass = $instanceManager->create(ExampleInterface::class, 'A');
$concreteClass->testMethod();
// output:
// "concrete class A";
```
## hooks.config.php
To avoid registering all strategies manually inside the code, Friendica introduced the [`hooks.config.php`](../static/hooks.config.php) file.
There, you can register all kind of strategies in one file.
### [`HookType::STRATEGY`](../src/Core/Hooks/Capabilities/HookType.php)
For each given interface, a list of key-value pairs can be set, where the key is the concrete implementation class and the value is an array of unique names.
### Example
```php
use Friendica\Core\Hooks\Capabilities\BehavioralHookType as H;
return [
H::STRATEGY => [
ExampleInterface::class => [
ConcreteClassA::class => ['A'],
ConcreteClassB::class => ['B'],
],
],
];
```
## Addons
The hook logic is useful for decoupling the Friendica core logic, but its primary goal is to modularize Friendica in creating addons.
Therefor you can either use the interfaces directly as shown above, or you can place your own `hooks.config.php` file inside a `static` directory directly under your addon core directory.
Friendica will automatically search these config files for each **activated** addon and register the given hooks.

View file

@ -30,6 +30,9 @@ if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
$dice = (new Dice())->addRules(include __DIR__ . '/static/dependencies.config.php'); $dice = (new Dice())->addRules(include __DIR__ . '/static/dependencies.config.php');
/** @var \Friendica\Core\Addon\Capabilities\ICanLoadAddons $addonLoader */
$addonLoader = $dice->create(\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class);
$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies'));
$dice = $dice->addRule(Friendica\App\Mode::class, ['call' => [['determineRunMode', [false, $_SERVER], Dice::CHAIN_CALL]]]); $dice = $dice->addRule(Friendica\App\Mode::class, ['call' => [['determineRunMode', [false, $_SERVER], Dice::CHAIN_CALL]]]);
\Friendica\DI::init($dice); \Friendica\DI::init($dice);

View file

@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Addon\Capabilities;
/**
* Interface for loading Addons specific content
*/
interface ICanLoadAddons
{
/**
* Returns a merged config array of all active addons for a given config-name
*
* @param string $configName The config-name (config-file at the static directory, like 'hooks' => '{addon}/static/hooks.config.php)
*
* @return array the merged array
*/
public function getActiveAddonConfig(string $configName): array;
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Addon\Exception;
use Throwable;
/**
* Exception in case one or more config files of the addons are invalid
*/
class AddonInvalidConfigFileException extends \RuntimeException
{
public function __construct($message = '', $code = 0, Throwable $previous = null)
{
parent::__construct($message, 500, $previous);
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Addon\Model;
use Friendica\Core\Addon\Capabilities\ICanLoadAddons;
use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Util\Strings;
class AddonLoader implements ICanLoadAddons
{
const STATIC_PATH = 'static';
/** @var string */
protected $basePath;
/** @var IManageConfigValues */
protected $config;
public function __construct(string $basePath, IManageConfigValues $config)
{
$this->basePath = $basePath;
$this->config = $config;
}
/** {@inheritDoc} */
public function getActiveAddonConfig(string $configName): array
{
$addons = array_keys(array_filter($this->config->get('addons') ?? []));
$returnConfig = [];
foreach ($addons as $addon) {
$addonName = Strings::sanitizeFilePathItem(trim($addon));
$configFile = $this->basePath . '/addon/' . $addonName . '/' . static::STATIC_PATH . '/' . $configName . '.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);
}
$returnConfig = array_merge_recursive($returnConfig, $config);
}
return $returnConfig;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Capabilities;
/**
* An enum of hook types, based on behavioral design patterns
* @see https://refactoring.guru/design-patterns/behavioral-patterns
*/
interface BehavioralHookType
{
/**
* Defines the key for the list of strategy-hooks.
*
* @see https://refactoring.guru/design-patterns/strategy
*/
const STRATEGY = 'strategy';
const EVENT = 'event';
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Capabilities;
/**
* creates special instances for given classes
*/
interface ICanCreateInstances
{
/**
* Returns a new instance of a given class for the corresponding name
*
* The instance will be build based on the registered strategy and the (unique) name
*
* @param string $class The fully-qualified name of the given class or interface which will get returned
* @param string $strategy An arbitrary identifier to find a concrete instance strategy.
* @param array $arguments Additional arguments, which can be passed to the constructor of "$class" at runtime
*
* @return object The concrete instance of the type "$class"
*/
public function create(string $class, string $strategy, array $arguments = []): object;
}

View file

@ -1,81 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Capabilities;
use Friendica\Core\Hooks\Exceptions\HookInstanceException;
use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException;
/**
* Managing special instance and decorator treatments for classes
*/
interface ICanManageInstances
{
/**
* Register a class(strategy) for a given interface with a unique name.
*
* @see https://refactoring.guru/design-patterns/strategy
*
* @param string $interface The interface, which the given class implements
* @param string $name An arbitrary identifier for the given class, which will be used for factories, dependency injections etc.
* @param string $class The fully-qualified given class name
* @param ?array $arguments Additional arguments, which can be passed to the constructor
*
* @return $this This interface for chain-calls
*
* @throws HookRegisterArgumentException in case the given class for the interface isn't valid or already set
*/
public function registerStrategy(string $interface, string $name, string $class, array $arguments = null): self;
/**
* Register a new decorator for a given class or interface
* @see https://refactoring.guru/design-patterns/decorator
*
* @note Decorator attach new behaviors to classes without changing them or without letting them know about it.
*
* @param string $class The fully-qualified class or interface name, which gets decorated by a class
* @param string $decoratorClass The fully-qualified name of the class which mimics the given class or interface and adds new functionality
* @param array $arguments Additional arguments, which can be passed to the constructor of "decoratorClass"
*
* @return $this This interface for chain-calls
*
* @throws HookRegisterArgumentException in case the given class for the class or interface isn't valid
*/
public function registerDecorator(string $class, string $decoratorClass, array $arguments = []): self;
/**
* Returns a new instance of a given class for the corresponding name
*
* The instance will be build based on the registered strategy and the (unique) name
*
* In case, there are registered decorators for this class as well, all decorators of the list will be wrapped
* around the instance before returning it
*
* @param string $class The fully-qualified name of the given class or interface which will get returned
* @param string $name An arbitrary identifier to find a concrete instance strategy.
* @param array $arguments Additional arguments, which can be passed to the constructor of "$class" at runtime
*
* @return object The concrete instance of the type "$class"
*
* @throws HookInstanceException In case the class cannot get created
*/
public function getInstance(string $class, string $name, array $arguments = []): object;
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Capabilities;
use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException;
/**
* Register strategies for given classes
*/
interface ICanRegisterStrategies
{
/**
* Register a class(strategy) for a given interface with a unique name.
*
* @see https://refactoring.guru/design-patterns/strategy
*
* @param string $interface The interface, which the given class implements
* @param string $class The fully-qualified given class name
* A placeholder for dependencies is possible as well
* @param ?string $name An arbitrary identifier for the given strategy, which will be used for factories, dependency injections etc.
*
* @return $this This interface for chain-calls
*
* @throws HookRegisterArgumentException in case the given class for the interface isn't valid or already set
*/
public function registerStrategy(string $interface, string $class, ?string $name = null): self;
}

View file

@ -19,13 +19,11 @@
* *
*/ */
namespace Friendica\Core\Logger\Exception; namespace Friendica\Core\Hooks\Exceptions;
use Throwable; class HookConfigException extends \RuntimeException
class LoggerInvalidException extends \RuntimeException
{ {
public function __construct($message = "", Throwable $previous = null) public function __construct($message = '', \Throwable $previous = null)
{ {
parent::__construct($message, 500, $previous); parent::__construct($message, 500, $previous);
} }

View file

@ -0,0 +1,70 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Model;
use Dice\Dice;
use Friendica\Core\Hooks\Capabilities\ICanCreateInstances;
use Friendica\Core\Hooks\Capabilities\ICanRegisterStrategies;
use Friendica\Core\Hooks\Exceptions\HookInstanceException;
use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException;
use Friendica\Core\Hooks\Util\StrategiesFileManager;
/**
* This class represents an instance register, which uses Dice for creation
*
* @see Dice
*/
class DiceInstanceManager implements ICanCreateInstances, ICanRegisterStrategies
{
protected $instance = [];
/** @var Dice */
protected $dice;
public function __construct(Dice $dice, StrategiesFileManager $strategiesFileManager)
{
$this->dice = $dice;
$strategiesFileManager->setupStrategies($this);
}
/** {@inheritDoc} */
public function registerStrategy(string $interface, string $class, ?string $name = null): ICanRegisterStrategies
{
if (!empty($this->instance[$interface][$name])) {
throw new HookRegisterArgumentException(sprintf('A class with the name %s is already set for the interface %s', $name, $interface));
}
$this->instance[$interface][$name] = $class;
return $this;
}
/** {@inheritDoc} */
public function create(string $class, string $strategy, array $arguments = []): object
{
if (empty($this->instance[$class][$strategy])) {
throw new HookInstanceException(sprintf('The class with the name %s isn\'t registered for the class or interface %s', $strategy, $class));
}
return $this->dice->create($this->instance[$class][$strategy], $arguments);
}
}

View file

@ -1,104 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Model;
use Dice\Dice;
use Friendica\Core\Hooks\Capabilities\IAmAStrategy;
use Friendica\Core\Hooks\Capabilities\ICanManageInstances;
use Friendica\Core\Hooks\Exceptions\HookInstanceException;
use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException;
/** {@inheritDoc} */
class InstanceManager implements ICanManageInstances
{
protected $instance = [];
protected $instanceArguments = [];
protected $decorator = [];
/** @var Dice */
protected $dice;
public function __construct(Dice $dice)
{
$this->dice = $dice;
}
/** {@inheritDoc} */
public function registerStrategy(string $interface, string $name, string $class, array $arguments = null): ICanManageInstances
{
if (!is_a($class, $interface, true)) {
throw new HookRegisterArgumentException(sprintf('%s is not a valid class for the interface %s', $class, $interface));
}
if (!is_a($class, IAmAStrategy::class, true)) {
throw new HookRegisterArgumentException(sprintf('%s does not inherit from the marker interface %s', $class, IAmAStrategy::class));
}
if (!empty($this->instance[$interface][$name])) {
throw new HookRegisterArgumentException(sprintf('A class with the name %s is already set for the interface %s', $name, $interface));
}
$this->instance[$interface][$name] = $class;
$this->instanceArguments[$interface][$name] = $arguments;
return $this;
}
/** {@inheritDoc} */
public function registerDecorator(string $class, string $decoratorClass, array $arguments = []): ICanManageInstances
{
if (!is_a($decoratorClass, $class, true)) {
throw new HookRegisterArgumentException(sprintf('%s is not a valid substitution for the given class or interface %s', $decoratorClass, $class));
}
$this->decorator[$class][] = [
'class' => $decoratorClass,
'arguments' => $arguments,
];
return $this;
}
/** {@inheritDoc} */
public function getInstance(string $class, string $name, array $arguments = []): object
{
if (empty($this->instance[$class][$name])) {
throw new HookInstanceException(sprintf('The class with the name %s isn\'t registered for the class or interface %s', $name, $class));
}
$instance = $this->dice->create($this->instance[$class][$name], array_merge($this->instanceArguments[$class][$name] ?? [], $arguments));
foreach ($this->decorator[$class] ?? [] as $decorator) {
$this->dice = $this->dice->addRule($class, [
'instanceOf' => $decorator['class'],
'constructParams' => empty($decorator['arguments']) ? null : $decorator['arguments'],
/// @todo maybe support call structures for hooks as well in a later stage - could make factory calls easier
'call' => null,
'substitutions' => [$class => $instance],
]);
$instance = $this->dice->create($class);
}
return $instance;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Hooks\Util;
use Friendica\Core\Addon\Capabilities\ICanLoadAddons;
use Friendica\Core\Hooks\Capabilities\ICanRegisterStrategies;
use Friendica\Core\Hooks\Exceptions\HookConfigException;
/**
* Manage all strategies.config.php files
*/
class StrategiesFileManager
{
const STATIC_DIR = 'static';
const CONFIG_NAME = 'strategies';
/** @var ICanLoadAddons */
protected $addonLoader;
/** @var array */
protected $config = [];
/** @var string */
protected $basePath;
public function __construct(string $basePath, ICanLoadAddons $addonLoader)
{
$this->basePath = $basePath;
$this->addonLoader = $addonLoader;
}
/**
* Loads all kinds of hooks and registers the corresponding instances
*
* @param ICanRegisterStrategies $instanceRegister The instance register
*
* @return void
*/
public function setupStrategies(ICanRegisterStrategies $instanceRegister)
{
foreach ($this->config as $interface => $strategy) {
foreach ($strategy as $dependencyName => $names) {
if (is_array($names)) {
foreach ($names as $name) {
$instanceRegister->registerStrategy($interface, $dependencyName, $name);
}
} else {
$instanceRegister->registerStrategy($interface, $dependencyName, $names);
}
}
}
}
/**
* Reloads all hook config files into the config cache for later usage
*
* Merges all hook configs from every addon - if present - as well
*
* @return void
*/
public function loadConfig()
{
// load core hook config
$configFile = $this->basePath . '/' . static::STATIC_DIR . '/' . static::CONFIG_NAME . '.config.php';
if (!file_exists($configFile)) {
throw new HookConfigException(sprintf('config file %s does not exist.', $configFile));
}
$config = include $configFile;
if (!is_array($config)) {
throw new HookConfigException(sprintf('Error loading config file %s.', $configFile));
}
$this->config = array_merge_recursive($config, $this->addonLoader->getActiveAddonConfig(static::CONFIG_NAME));
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Capabilities;
/**
* Whenever a logging specific check is necessary, use this interface to encapsulate and centralize this logic
*/
interface ICheckLoggerSettings
{
/**
* Checks if the logfile is set and usable
*
* @return string|null null in case everything is ok, otherwise returns the error
*/
public function checkLogfile(): ?string;
/**
* Checks if the debugging logfile is usable in case it is set!
*
* @return string|null null in case everything is ok, otherwise returns the error
*/
public function checkDebugLogfile(): ?string;
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Capabilities;
/**
* An enum class for the Log channels
*/
interface LogChannel
{
/** @var string channel for the auth_ejabbered script */
public const AUTH_JABBERED = 'auth_ejabberd';
/** @var string Default channel in case it isn't set explicit */
public const DEFAULT = self::APP;
/** @var string channel for console execution */
public const CONSOLE = 'console';
/** @var string channel for developer focused logging */
public const DEV = 'dev';
/** @var string channel for daemon executions */
public const DAEMON = 'daemon';
/** @var string channel for worker execution */
public const WORKER = 'worker';
/** @var string channel for frontend app executions */
public const APP = 'app';
}

View file

@ -23,9 +23,12 @@ namespace Friendica\Core\Logger\Exception;
use Throwable; use Throwable;
/**
* Exception in case the loglevel isn't set or isn't valid
*/
class LogLevelException extends \InvalidArgumentException class LogLevelException extends \InvalidArgumentException
{ {
public function __construct($message = "", Throwable $previous = null) public function __construct($message = '', Throwable $previous = null)
{ {
parent::__construct($message, 500, $previous); parent::__construct($message, 500, $previous);
} }

View file

@ -23,9 +23,12 @@ namespace Friendica\Core\Logger\Exception;
use Throwable; use Throwable;
/**
* Exception in case an argument of a logger class isn't valid
*/
class LoggerArgumentException extends \InvalidArgumentException class LoggerArgumentException extends \InvalidArgumentException
{ {
public function __construct($message = "", Throwable $previous = null) public function __construct($message = '', Throwable $previous = null)
{ {
parent::__construct($message, 500, $previous); parent::__construct($message, 500, $previous);
} }

View file

@ -23,9 +23,12 @@ namespace Friendica\Core\Logger\Exception;
use Throwable; use Throwable;
/**
* A generic exception of the logging namespace
*/
class LoggerException extends \Exception class LoggerException extends \Exception
{ {
public function __construct($message = "", Throwable $previous = null) public function __construct($message = '', Throwable $previous = null)
{ {
parent::__construct($message, 500, $previous); parent::__construct($message, 500, $previous);
} }

View file

@ -0,0 +1,35 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Exception;
use Throwable;
/**
* Exception in case the used logging instance is unusable because of some circumstances
*/
class LoggerUnusableException extends \RuntimeException
{
public function __construct($message = '', Throwable $previous = null)
{
parent::__construct($message, 500, $previous);
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Factory;
use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections;
use Psr\Log\LogLevel;
/**
* Abstract class for creating logger types, which includes common necessary logic/content
*/
abstract class AbstractLoggerTypeFactory
{
/** @var string */
protected $channel;
/** @var IHaveCallIntrospections */
protected $introspection;
/**
* @param string $channel The channel for the logger
*/
public function __construct(IHaveCallIntrospections $introspection, string $channel)
{
$this->channel = $channel;
$this->introspection = $introspection;
}
/**
* Mapping a legacy level to the PSR-3 compliant levels
*
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#5-psrlogloglevel
*
* @param string $level the level to be mapped
*
* @return string the PSR-3 compliant level
*/
protected static function mapLegacyConfigDebugLevel(string $level): string
{
switch ($level) {
// legacy WARNING
case "0":
return LogLevel::ERROR;
// legacy INFO
case "1":
return LogLevel::WARNING;
// legacy TRACE
case "2":
return LogLevel::NOTICE;
// legacy DEBUG
case "3":
return LogLevel::INFO;
// legacy DATA
case "4":
// legacy ALL
case "5":
return LogLevel::DEBUG;
// default if nothing set
default:
return $level;
}
}
}

View file

@ -22,134 +22,46 @@
namespace Friendica\Core\Logger\Factory; namespace Friendica\Core\Logger\Factory;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Hooks\Capabilities\ICanManageInstances; use Friendica\Core\Hooks\Capabilities\ICanCreateInstances;
use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Capabilities\LogChannel;
use Friendica\Core\Logger\Type\ProfilerLogger; use Friendica\Core\Logger\Type\ProfilerLogger as ProfilerLoggerClass;
use Friendica\Core\Logger\Type\StreamLogger; use Friendica\Util\Profiler;
use Friendica\Core\Logger\Type\SyslogLogger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Throwable;
/** /**
* A logger factory * The logger factory for the core logging instances
*/ */
class Logger class Logger
{ {
const DEV_CHANNEL = 'dev'; /** @var string The channel */
/** @var string The log-channel (app, worker, ...) */
protected $channel; protected $channel;
/** @var ICanManageInstances */
protected $instanceManager;
/** @var IManageConfigValues */
protected $config;
public function __construct(string $channel, ICanManageInstances $instanceManager, IManageConfigValues $config, string $logfile = null) public function __construct(string $channel = LogChannel::DEFAULT)
{ {
$this->channel = $channel; $this->channel = $channel;
$this->instanceManager = $instanceManager;
$this->config = $config;
$this->instanceManager
->registerStrategy(LoggerInterface::class, 'syslog', SyslogLogger::class)
->registerStrategy(LoggerInterface::class, 'stream', StreamLogger::class, isset($logfile) ? [$logfile] : null);
if ($this->config->get('system', 'profiling') ?? false) {
$this->instanceManager->registerDecorator(LoggerInterface::class, ProfilerLogger::class);
}
} }
/** public function create(ICanCreateInstances $instanceCreator, IManageConfigValues $config, Profiler $profiler): LoggerInterface
* Creates a new PSR-3 compliant logger instances
*
* @param string|null $loglevel (optional) A given loglevel in case the loglevel in the config isn't applicable
*
* @return LoggerInterface The PSR-3 compliant logger instance
*/
public function create(string $loglevel = null): LoggerInterface
{ {
if (empty($this->config->get('system', 'debugging') ?? false)) { if (empty($config->get('system', 'debugging') ?? false)) {
return new NullLogger(); return new NullLogger();
} }
$loglevel = $loglevel ?? static::mapLegacyConfigDebugLevel($this->config->get('system', 'loglevel')); $name = $config->get('system', 'logger_config') ?? '';
$name = $this->config->get('system', 'logger_config') ?? 'stream';
try { try {
/** @var LoggerInterface */ /** @var LoggerInterface $logger */
return $this->instanceManager->getInstance(LoggerInterface::class, $name, [$this->channel, $loglevel]); $logger = $instanceCreator->create(LoggerInterface::class, $name, [$this->channel]);
} catch (LogLevelException $exception) { if ($config->get('system', 'profiling') ?? false) {
// If there's a wrong config value for loglevel, try again with standard return new ProfilerLoggerClass($logger, $profiler);
$logger = $this->create(LogLevel::NOTICE); } else {
$logger->warning('Invalid loglevel set in config.', ['loglevel' => $loglevel]); return $logger;
return $logger; }
} catch (\Throwable $e) { } catch (Throwable $e) {
// No logger ... // No logger ...
return new NullLogger(); return new NullLogger();
} }
} }
/**
* Creates a new PSR-3 compliant develop logger
*
* If you want to debug only interactions from your IP or the IP of a remote server for federation debug,
* you'll use this logger instance for the duration of your work.
*
* It should never get filled during normal usage of Friendica
*
* @return LoggerInterface The PSR-3 compliant logger instance
* @throws \Exception
*/
public function createDev()
{
$debugging = $this->config->get('system', 'debugging');
$stream = $this->config->get('system', 'dlogfile');
$developerIp = $this->config->get('system', 'dlogip');
if ((!isset($developerIp) || !$debugging) &&
(!is_file($stream) || is_writable($stream))) {
return new NullLogger();
}
$name = $this->config->get('system', 'logger_config') ?? 'stream';
/** @var LoggerInterface */
return $this->instanceManager->getInstance(LoggerInterface::class, $name, [self::DEV_CHANNEL, LogLevel::DEBUG, $stream]);
}
/**
* Mapping a legacy level to the PSR-3 compliant levels
*
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#5-psrlogloglevel
*
* @param string $level the level to be mapped
*
* @return string the PSR-3 compliant level
*/
private static function mapLegacyConfigDebugLevel(string $level): string
{
switch ($level) {
// legacy WARNING
case "0":
return LogLevel::ERROR;
// legacy INFO
case "1":
return LogLevel::WARNING;
// legacy TRACE
case "2":
return LogLevel::NOTICE;
// legacy DEBUG
case "3":
return LogLevel::INFO;
// legacy DATA
case "4":
// legacy ALL
case "5":
return LogLevel::DEBUG;
// default if nothing set
default:
return $level;
}
}
} }

View file

@ -0,0 +1,100 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Factory;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Logger\Capabilities\LogChannel;
use Friendica\Core\Logger\Exception\LoggerArgumentException;
use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Core\Logger\Type\StreamLogger as StreamLoggerClass;
use Friendica\Core\Logger\Util\FileSystem;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* The logger factory for the StreamLogger instance
*
* @see StreamLoggerClass
*/
class StreamLogger extends AbstractLoggerTypeFactory
{
/**
* Creates a new PSR-3 compliant stream logger instance
*
* @param IManageConfigValues $config The system configuration
* @param string|null $logfile (optional) A given logfile which should be used as stream (e.g. in case of
* developer logging)
* @param string|null $channel (optional) A given channel in case it is different from the default
*
* @return LoggerInterface The PSR-3 compliant logger instance
*
* @throws LoggerException in case the logger cannot get created
*/
public function create(IManageConfigValues $config, string $logfile = null, string $channel = null): LoggerInterface
{
$fileSystem = new FileSystem();
$logfile = $logfile ?? $config->get('system', 'logfile');
if (!@file_exists($logfile) || !@is_writable($logfile)) {
throw new LoggerArgumentException(sprintf('%s is not a valid logfile', $logfile));
}
$loglevel = static::mapLegacyConfigDebugLevel($config->get('system', 'loglevel'));
if (array_key_exists($loglevel, StreamLoggerClass::levelToInt)) {
$loglevel = StreamLoggerClass::levelToInt[$loglevel];
} else {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $loglevel));
}
$stream = $fileSystem->createStream($logfile);
return new StreamLoggerClass($channel ?? $this->channel, $this->introspection, $stream, $loglevel, getmypid());
}
/**
* Creates a new PSR-3 compliant develop logger
*
* If you want to debug only interactions from your IP or the IP of a remote server for federation debug,
* you'll use this logger instance for the duration of your work.
*
* It should never get filled during normal usage of Friendica
*
* @return LoggerInterface The PSR-3 compliant logger instance
*
* @throws LoggerException
*/
public function createDev(IManageConfigValues $config)
{
$debugging = $config->get('system', 'debugging');
$logfile = $config->get('system', 'dlogfile');
$developerIp = $config->get('system', 'dlogip');
if ((!isset($developerIp) || !$debugging) &&
(!is_file($logfile) || is_writable($logfile))) {
return new NullLogger();
}
return $this->create($config, $logfile, LogChannel::DEV);
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Factory;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Core\Logger\Type\SyslogLogger as SyslogLoggerClass;
use Psr\Log\LoggerInterface;
/**
* The logger factory for the SyslogLogger instance
*
* @see SyslogLoggerClass
*/
class SyslogLogger extends AbstractLoggerTypeFactory
{
/**
* Creates a new PSR-3 compliant syslog logger instance
*
* @param IManageConfigValues $config The system configuration
*
* @return LoggerInterface The PSR-3 compliant logger instance
*
* @throws LoggerException in case the logger cannot get created
*/
public function create(IManageConfigValues $config): LoggerInterface
{
$logOpts = $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS;
$logFacility = $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY;
$loglevel = SyslogLogger::mapLegacyConfigDebugLevel($config->get('system', 'loglevel'));
if (array_key_exists($loglevel, SyslogLoggerClass::logLevels)) {
$loglevel = SyslogLoggerClass::logLevels[$loglevel];
} else {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $loglevel));
}
return new SyslogLoggerClass($this->channel, $this->introspection, $loglevel, $logOpts, $logFacility);
}
}

View file

@ -1,26 +0,0 @@
## Friendica\Util\Logger
This namespace contains the different implementations of a Logger.
### Configuration guideline
The following settings are possible for `logger_config`:
- [`stream`](StreamLogger.php): A small logger for files or streams
- [`syslog`](SyslogLogger.php): Prints the logging output into the syslog
[`VoidLogger`](VoidLogger.php) is a fallback logger without any function if no debugging is enabled.
[`ProfilerLogger`](ProfilerLogger.php) is a wrapper around an existing logger in case profiling is enabled for Friendica.
Every log call will be saved to the `Profiler` with a timestamp.
### Implementation guideline
Each logging implementation should pe capable of printing at least the following information:
- An unique ID for each Request/Call
- The process ID (PID)
- A timestamp of the logging entry
- The critically of the log entry
- A log message
- A context of the log message (f.e which user)
If possible, a Logger should extend [`AbstractLogger`](AbstractLogger.php), because it contains additional, Friendica specific business logic for each logging call.

View file

@ -21,20 +21,16 @@
namespace Friendica\Core\Logger\Type; namespace Friendica\Core\Logger\Type;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Hooks\Capabilities\IAmAStrategy;
use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections; use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections;
use Friendica\Core\Logger\Exception\LoggerArgumentException;
use Friendica\Core\Logger\Exception\LoggerException; use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\FileSystem;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
/** /**
* A Logger instance for logging into a stream (file, stdout, stderr) * A Logger instance for logging into a stream (file, stdout, stderr)
*/ */
class StreamLogger extends AbstractLogger implements IAmAStrategy class StreamLogger extends AbstractLogger
{ {
/** /**
* The minimum loglevel at which this logger will be triggered * The minimum loglevel at which this logger will be triggered
@ -42,12 +38,6 @@ class StreamLogger extends AbstractLogger implements IAmAStrategy
*/ */
private $logLevel; private $logLevel;
/**
* The file URL of the stream (if needed)
* @var string
*/
private $url;
/** /**
* The stream, where the current logger is writing into * The stream, where the current logger is writing into
* @var resource * @var resource
@ -60,16 +50,11 @@ class StreamLogger extends AbstractLogger implements IAmAStrategy
*/ */
private $pid; private $pid;
/**
* @var FileSystem
*/
private $fileSystem;
/** /**
* Translates LogLevel log levels to integer values * Translates LogLevel log levels to integer values
* @var array * @var array
*/ */
private $levelToInt = [ public const levelToInt = [
LogLevel::EMERGENCY => 0, LogLevel::EMERGENCY => 0,
LogLevel::ALERT => 1, LogLevel::ALERT => 1,
LogLevel::CRITICAL => 2, LogLevel::CRITICAL => 2,
@ -84,41 +69,20 @@ class StreamLogger extends AbstractLogger implements IAmAStrategy
* {@inheritdoc} * {@inheritdoc}
* @param string $level The minimum loglevel at which this logger will be triggered * @param string $level The minimum loglevel at which this logger will be triggered
* *
* @throws LoggerArgumentException * @throws LoggerException
* @throws LogLevelException
*/ */
public function __construct(string $channel, IManageConfigValues $config, IHaveCallIntrospections $introspection, FileSystem $fileSystem, string $level = LogLevel::DEBUG) public function __construct(string $channel, IHaveCallIntrospections $introspection, $stream, int $logLevel, int $pid)
{ {
$this->fileSystem = $fileSystem;
$stream = $this->logfile ?? $config->get('system', 'logfile');
if ((@file_exists($stream) && !@is_writable($stream)) && !@is_writable(basename($stream))) {
throw new LoggerArgumentException(sprintf('%s is not a valid logfile', $stream));
}
parent::__construct($channel, $introspection); parent::__construct($channel, $introspection);
if (is_resource($stream)) { $this->stream = $stream;
$this->stream = $stream; $this->pid = $pid;
} elseif (is_string($stream)) { $this->logLevel = $logLevel;
$this->url = $stream;
} else {
throw new LoggerArgumentException('A stream must either be a resource or a string.');
}
$this->pid = getmypid();
if (array_key_exists($level, $this->levelToInt)) {
$this->logLevel = $this->levelToInt[$level];
} else {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $level));
}
$this->checkStream();
} }
public function close() public function close()
{ {
if ($this->url && is_resource($this->stream)) { if (is_resource($this->stream)) {
fclose($this->stream); fclose($this->stream);
} }
@ -139,18 +103,16 @@ class StreamLogger extends AbstractLogger implements IAmAStrategy
*/ */
protected function addEntry($level, string $message, array $context = []) protected function addEntry($level, string $message, array $context = [])
{ {
if (!array_key_exists($level, $this->levelToInt)) { if (!array_key_exists($level, static::levelToInt)) {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $level)); throw new LogLevelException(sprintf('The level "%s" is not valid.', $level));
} }
$logLevel = $this->levelToInt[$level]; $logLevel = static::levelToInt[$level];
if ($logLevel > $this->logLevel) { if ($logLevel > $this->logLevel) {
return; return;
} }
$this->checkStream();
$formattedLog = $this->formatLog($level, $message, $context); $formattedLog = $this->formatLog($level, $message, $context);
fwrite($this->stream, $formattedLog); fwrite($this->stream, $formattedLog);
} }
@ -185,27 +147,4 @@ class StreamLogger extends AbstractLogger implements IAmAStrategy
return $logMessage; return $logMessage;
} }
/**
* Checks the current stream
*
* @throws LoggerException
* @throws LoggerArgumentException
*/
private function checkStream()
{
if (is_resource($this->stream)) {
return;
}
if (empty($this->url)) {
throw new LoggerArgumentException('Missing stream URL.');
}
try {
$this->stream = $this->fileSystem->createStream($this->url);
} catch (\UnexpectedValueException $exception) {
throw new LoggerException('Cannot create stream.', $exception);
}
}
} }

View file

@ -21,8 +21,6 @@
namespace Friendica\Core\Logger\Type; namespace Friendica\Core\Logger\Type;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Hooks\Capabilities\IAmAStrategy;
use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections; use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections;
use Friendica\Core\Logger\Exception\LoggerException; use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Exception\LogLevelException;
@ -32,7 +30,7 @@ use Psr\Log\LogLevel;
* A Logger instance for syslogging (fast, but simple) * A Logger instance for syslogging (fast, but simple)
* @see http://php.net/manual/en/function.syslog.php * @see http://php.net/manual/en/function.syslog.php
*/ */
class SyslogLogger extends AbstractLogger implements IAmAStrategy class SyslogLogger extends AbstractLogger
{ {
const IDENT = 'Friendica'; const IDENT = 'Friendica';
@ -45,7 +43,7 @@ class SyslogLogger extends AbstractLogger implements IAmAStrategy
* Translates LogLevel log levels to syslog log priorities. * Translates LogLevel log levels to syslog log priorities.
* @var array * @var array
*/ */
private $logLevels = [ public const logLevels = [
LogLevel::DEBUG => LOG_DEBUG, LogLevel::DEBUG => LOG_DEBUG,
LogLevel::INFO => LOG_INFO, LogLevel::INFO => LOG_INFO,
LogLevel::NOTICE => LOG_NOTICE, LogLevel::NOTICE => LOG_NOTICE,
@ -60,7 +58,7 @@ class SyslogLogger extends AbstractLogger implements IAmAStrategy
* Translates log priorities to string outputs * Translates log priorities to string outputs
* @var array * @var array
*/ */
private $logToString = [ protected const logToString = [
LOG_DEBUG => 'DEBUG', LOG_DEBUG => 'DEBUG',
LOG_INFO => 'INFO', LOG_INFO => 'INFO',
LOG_NOTICE => 'NOTICE', LOG_NOTICE => 'NOTICE',
@ -101,19 +99,18 @@ class SyslogLogger extends AbstractLogger implements IAmAStrategy
/** /**
* {@inheritdoc} * {@inheritdoc}
* @param string $level The minimum loglevel at which this logger will be triggered
* *
* @throws LogLevelException * @param string $logLevel The minimum loglevel at which this logger will be triggered
* @throws LoggerException * @param string $logOptions
* @param string $logFacility
*/ */
public function __construct(string $channel, IManageConfigValues $config, IHaveCallIntrospections $introspection, string $level = LogLevel::NOTICE) public function __construct(string $channel, IHaveCallIntrospections $introspection, string $logLevel, string $logOptions, string $logFacility)
{ {
parent::__construct($channel, $introspection); parent::__construct($channel, $introspection);
$this->logOpts = $config->get('system', 'syslog_flags') ?? static::DEFAULT_FLAGS; $this->logOpts = $logOptions;
$this->logFacility = $config->get('system', 'syslog_facility') ?? static::DEFAULT_FACILITY; $this->logFacility = $logFacility;
$this->logLevel = $this->mapLevelToPriority($level); $this->logLevel = $logLevel;
$this->introspection->addClasses([self::class]);
} }
/** /**
@ -149,11 +146,11 @@ class SyslogLogger extends AbstractLogger implements IAmAStrategy
*/ */
public function mapLevelToPriority(string $level): int public function mapLevelToPriority(string $level): int
{ {
if (!array_key_exists($level, $this->logLevels)) { if (!array_key_exists($level, static::logLevels)) {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $level)); throw new LogLevelException(sprintf('The level "%s" is not valid.', $level));
} }
return $this->logLevels[$level]; return static::logLevels[$level];
} }
/** /**
@ -202,7 +199,7 @@ class SyslogLogger extends AbstractLogger implements IAmAStrategy
$record = array_merge($record, ['uid' => $this->logUid]); $record = array_merge($record, ['uid' => $this->logUid]);
$logMessage = $this->channel . ' '; $logMessage = $this->channel . ' ';
$logMessage .= '[' . $this->logToString[$level] . ']: '; $logMessage .= '[' . static::logToString[$level] . ']: ';
$logMessage .= $this->psrInterpolate($message, $context) . ' '; $logMessage .= $this->psrInterpolate($message, $context) . ' ';
$logMessage .= $this->jsonEncodeArray($context) . ' - '; $logMessage .= $this->jsonEncodeArray($context) . ' - ';
$logMessage .= $this->jsonEncodeArray($record); $logMessage .= $this->jsonEncodeArray($record);

View file

@ -19,10 +19,12 @@
* *
*/ */
namespace Friendica\Util; namespace Friendica\Core\Logger\Util;
use Friendica\Core\Logger\Exception\LoggerUnusableException;
/** /**
* Util class for filesystem manipulation * Util class for filesystem manipulation for Logger classes
*/ */
class FileSystem class FileSystem
{ {
@ -37,8 +39,10 @@ class FileSystem
* @param string $file The file * @param string $file The file
* *
* @return string The directory name (empty if no directory is found, like urls) * @return string The directory name (empty if no directory is found, like urls)
*
* @throws LoggerUnusableException
*/ */
public function createDir(string $file) public function createDir(string $file): string
{ {
$dirname = null; $dirname = null;
$pos = strpos($file, '://'); $pos = strpos($file, '://');
@ -57,7 +61,7 @@ class FileSystem
restore_error_handler(); restore_error_handler();
if (!$status && !is_dir($dirname)) { if (!$status && !is_dir($dirname)) {
throw new \UnexpectedValueException(sprintf('Directory "%s" cannot get created: ' . $this->errorMessage, $dirname)); throw new LoggerUnusableException(sprintf('Directory "%s" cannot get created: ' . $this->errorMessage, $dirname));
} }
return $dirname; return $dirname;
@ -75,7 +79,7 @@ class FileSystem
* *
* @return resource the open stream resource * @return resource the open stream resource
* *
* @throws \UnexpectedValueException * @throws LoggerUnusableException
*/ */
public function createStream(string $url) public function createStream(string $url)
{ {
@ -89,7 +93,7 @@ class FileSystem
restore_error_handler(); restore_error_handler();
if (!is_resource($stream)) { if (!is_resource($stream)) {
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: ' . $this->errorMessage, $url)); throw new LoggerUnusableException(sprintf('The stream or file "%s" could not be opened: ' . $this->errorMessage, $url));
} }
return $stream; return $stream;

View file

@ -0,0 +1,91 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Core\Logger\Util;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Core\Logger\Capabilities\ICheckLoggerSettings;
use Friendica\Core\Logger\Exception\LoggerUnusableException;
/** {@inheritDoc} */
class LoggerSettingsCheck implements ICheckLoggerSettings
{
/** @var IManageConfigValues */
protected $config;
/** @var $fileSystem */
protected $fileSystem;
/** @var L10n */
protected $l10n;
public function __construct(IManageConfigValues $config, FileSystem $fileSystem, L10n $l10n)
{
$this->config = $config;
$this->fileSystem = $fileSystem;
$this->l10n = $l10n;
}
/** {@inheritDoc} */
public function checkLogfile(): ?string
{
// Check logfile permission
if ($this->config->get('system', 'debugging')) {
$file = $this->config->get('system', 'logfile');
try {
$stream = $this->fileSystem->createStream($file);
if (!isset($stream)) {
throw new LoggerUnusableException('Stream is null.');
}
} catch (\Throwable $exception) {
return $this->l10n->t('The logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage());
}
}
return null;
}
/** {@inheritDoc} */
public function checkDebugLogfile(): ?string
{
// Check logfile permission
if ($this->config->get('system', 'debugging')) {
$file = $this->config->get('system', 'dlogfile');
if (empty($file)) {
return null;
}
try {
$stream = $this->fileSystem->createStream($file);
if (!isset($stream)) {
throw new LoggerUnusableException('Stream is null.');
}
} catch (\Throwable $exception) {
return $this->l10n->t('The debug logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage());
}
}
return null;
}
}

View file

@ -22,6 +22,8 @@
namespace Friendica; namespace Friendica;
use Dice\Dice; use Dice\Dice;
use Friendica\Core\Logger\Capabilities\ICheckLoggerSettings;
use Friendica\Core\Logger\Util\LoggerSettingsCheck;
use Friendica\Core\Session\Capability\IHandleSessions; use Friendica\Core\Session\Capability\IHandleSessions;
use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Navigation\SystemMessages; use Friendica\Navigation\SystemMessages;
@ -295,6 +297,11 @@ abstract class DI
static::init($flushDice); static::init($flushDice);
} }
public static function logCheck(): ICheckLoggerSettings
{
return self::$dice->create(LoggerSettingsCheck::class);
}
/** /**
* @return LoggerInterface * @return LoggerInterface
*/ */
@ -692,14 +699,6 @@ abstract class DI
return self::$dice->create(Util\DateTimeFormat::class); return self::$dice->create(Util\DateTimeFormat::class);
} }
/**
* @return Util\FileSystem
*/
public static function fs()
{
return self::$dice->create(Util\FileSystem::class);
}
/** /**
* @return Util\Profiler * @return Util\Profiler
*/ */

View file

@ -79,7 +79,7 @@ class Summary extends BaseAdmin
// Check if github.com/friendica/stable/VERSION is higher then // Check if github.com/friendica/stable/VERSION is higher then
// the local version of Friendica. Check is opt-in, source may be stable or develop branch // the local version of Friendica. Check is opt-in, source may be stable or develop branch
if (DI::config()->get('system', 'check_new_version_url', 'none') != 'none') { if (DI::config()->get('system', 'check_new_version_url', 'none') != 'none') {
$gitversion = DI::keyValue()->get('git_friendica_version') ?? ''; $gitversion = DI::keyValue()->get('git_friendica_version') ?? '';
if (version_compare(App::VERSION, $gitversion) < 0) { if (version_compare(App::VERSION, $gitversion) < 0) {
$warningtext[] = DI::l10n()->t('There is a new version of Friendica available for download. Your current version is %1$s, upstream version is %2$s', App::VERSION, $gitversion); $warningtext[] = DI::l10n()->t('There is a new version of Friendica available for download. Your current version is %1$s, upstream version is %2$s', App::VERSION, $gitversion);
@ -126,35 +126,11 @@ class Summary extends BaseAdmin
} }
// Check logfile permission // Check logfile permission
if (DI::config()->get('system', 'debugging')) { if (($return = DI::logCheck()->checkLogfile()) !== null) {
$file = DI::config()->get('system', 'logfile'); $warningtext[] = $return;
}
$fileSystem = DI::fs(); if (($return = DI::logCheck()->checkDebugLogfile()) !== null) {
$warningtext[] = $return;
try {
$stream = $fileSystem->createStream($file);
if (!isset($stream)) {
throw new ServiceUnavailableException('Stream is null.');
}
} catch (\Throwable $exception) {
$warningtext[] = DI::l10n()->t('The logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage());
}
$file = DI::config()->get('system', 'dlogfile');
try {
if (!empty($file)) {
$stream = $fileSystem->createStream($file);
if (!isset($stream)) {
throw new ServiceUnavailableException('Stream is null.');
}
}
} catch (\Throwable $exception) {
$warningtext[] = DI::l10n()->t('The debug logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage());
}
} }
// check legacy basepath settings // check legacy basepath settings

View file

@ -37,8 +37,9 @@ use Dice\Dice;
use Friendica\App; use Friendica\App;
use Friendica\Core\Cache; use Friendica\Core\Cache;
use Friendica\Core\Config; use Friendica\Core\Config;
use Friendica\Core\Hooks\Capabilities\ICanManageInstances; use Friendica\Core\Hooks\Capabilities\ICanCreateInstances;
use Friendica\Core\Hooks\Model\InstanceManager; use Friendica\Core\Hooks\Capabilities\ICanRegisterStrategies;
use Friendica\Core\Hooks\Model\DiceInstanceManager;
use Friendica\Core\PConfig; use Friendica\Core\PConfig;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Core\Lock; use Friendica\Core\Lock;
@ -62,6 +63,13 @@ return [
// one instance for the whole execution // one instance for the whole execution
'shared' => true, 'shared' => true,
], ],
\Friendica\Core\Addon\Capabilities\ICanLoadAddons::class => [
'instanceOf' => \Friendica\Core\Addon\Model\AddonLoader::class,
'constructParams' => [
[Dice::INSTANCE => '$basepath'],
[Dice::INSTANCE => Dice::SELF],
],
],
'$basepath' => [ '$basepath' => [
'instanceOf' => Util\BasePath::class, 'instanceOf' => Util\BasePath::class,
'call' => [ 'call' => [
@ -78,8 +86,27 @@ return [
$_SERVER $_SERVER
] ]
], ],
ICanManageInstances::class => [ DiceInstanceManager::class => [
'instanceOf' => InstanceManager::class, 'constructParams' => [
[Dice::INSTANCE => Dice::SELF],
]
],
\Friendica\Core\Hooks\Util\StrategiesFileManager::class => [
'constructParams' => [
[Dice::INSTANCE => '$basepath'],
],
'call' => [
['loadConfig'],
],
],
ICanRegisterStrategies::class => [
'instanceOf' => DiceInstanceManager::class,
'constructParams' => [
[Dice::INSTANCE => Dice::SELF],
],
],
ICanCreateInstances::class => [
'instanceOf' => DiceInstanceManager::class,
'constructParams' => [ 'constructParams' => [
[Dice::INSTANCE => Dice::SELF], [Dice::INSTANCE => Dice::SELF],
], ],
@ -156,40 +183,34 @@ return [
[Dice::INSTANCE => '$basepath'], [Dice::INSTANCE => '$basepath'],
], ],
], ],
/** \Psr\Log\LoggerInterface::class => [
* Create a Logger, which implements the LoggerInterface
*
* Same as:
* $loggerFactory = new Factory\LoggerFactory();
* $logger = $loggerFactory->create($channel, $configuration, $profiler);
*
* Attention1: We can use DICE for detecting dependencies inside "chained" calls too
* Attention2: The variable "$channel" is passed inside the creation of the dependencies per:
* $app = $dice->create(App::class, [], ['$channel' => 'index']);
* and is automatically passed as an argument with the same name
*/
LoggerInterface::class => [
'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class, 'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class,
'constructParams' => [
'index',
],
'call' => [ 'call' => [
['create', [], Dice::CHAIN_CALL], ['create', [], Dice::CHAIN_CALL],
], ],
], ],
'$devLogger' => [ \Friendica\Core\Logger\Type\SyslogLogger::class => [
'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class, 'instanceOf' => \Friendica\Core\Logger\Factory\SyslogLogger::class,
'constructParams' => [
'dev',
],
'call' => [ 'call' => [
['createDev', [], Dice::CHAIN_CALL], ['create', [], Dice::CHAIN_CALL],
] ],
],
\Friendica\Core\Logger\Type\StreamLogger::class => [
'instanceOf' => \Friendica\Core\Logger\Factory\StreamLogger::class,
'call' => [
['create', [], Dice::CHAIN_CALL],
],
], ],
\Friendica\Core\Logger\Capabilities\IHaveCallIntrospections::class => [ \Friendica\Core\Logger\Capabilities\IHaveCallIntrospections::class => [
'instanceOf' => \Friendica\Core\Logger\Util\Introspection::class, 'instanceOf' => \Friendica\Core\Logger\Util\Introspection::class,
'constructParams' => [ 'constructParams' => [
\Friendica\Core\Logger\Util\Introspection::IGNORE_CLASS_LIST, \Friendica\Core\Logger\Capabilities\IHaveCallIntrospections::IGNORE_CLASS_LIST,
],
],
'$devLogger' => [
'instanceOf' => \Friendica\Core\Logger\Factory\StreamLogger::class,
'call' => [
['createDev', [], Dice::CHAIN_CALL],
], ],
], ],
Cache\Capability\ICanCache::class => [ Cache\Capability\ICanCache::class => [

View file

@ -19,11 +19,14 @@
* *
*/ */
namespace Friendica\Core\Hooks\Capabilities; use Friendica\Core\Hooks\Capabilities\BehavioralHookType as H;
use Friendica\Core\Logger\Type;
use Psr\Log;
/** return [
* All classes, implementing this interface are valid Strategies for Hook calls Log\LoggerInterface::class => [
*/ Log\NullLogger::class => [''],
interface IAmAStrategy Type\SyslogLogger::class => ['syslog'],
{ Type\StreamLogger::class => ['stream'],
} ],
];

View file

@ -21,9 +21,7 @@
namespace Friendica\Test\Util\Hooks\InstanceMocks; namespace Friendica\Test\Util\Hooks\InstanceMocks;
use Friendica\Core\Hooks\Capabilities\IAmAStrategy; class FakeInstance implements IAmADecoratedInterface
class FakeInstance implements IAmADecoratedInterface, IAmAStrategy
{ {
protected $aText = null; protected $aText = null;
protected $cBool = null; protected $cBool = null;
@ -41,6 +39,8 @@ class FakeInstance implements IAmADecoratedInterface, IAmAStrategy
$this->aText = $aText; $this->aText = $aText;
$this->cBool = $cBool; $this->cBool = $cBool;
$this->bText = $bText; $this->bText = $bText;
return '';
} }
public function getAText(): ?string public function getAText(): ?string

View file

@ -25,14 +25,14 @@ class FakeInstanceDecorator implements IAmADecoratedInterface
{ {
public static $countInstance = 0; public static $countInstance = 0;
const PREFIX = 'prefix1';
/** @var IAmADecoratedInterface */ /** @var IAmADecoratedInterface */
protected $orig; protected $orig;
protected $prefix = '';
public function __construct(IAmADecoratedInterface $orig, string $prefix = '') public function __construct(IAmADecoratedInterface $orig)
{ {
$this->orig = $orig; $this->orig = $orig;
$this->prefix = $prefix;
self::$countInstance++; self::$countInstance++;
} }
@ -44,16 +44,16 @@ class FakeInstanceDecorator implements IAmADecoratedInterface
public function getAText(): ?string public function getAText(): ?string
{ {
return $this->prefix . $this->orig->getAText(); return static::PREFIX . $this->orig->getAText();
} }
public function getBText(): ?string public function getBText(): ?string
{ {
return $this->prefix . $this->orig->getBText(); return static::PREFIX . $this->orig->getBText();
} }
public function getCBool(): ?bool public function getCBool(): ?bool
{ {
return $this->prefix . $this->orig->getCBool(); return static::PREFIX . $this->orig->getCBool();
} }
} }

View file

@ -145,7 +145,7 @@ class DependencyCheckTest extends FixtureTest
$config->set('system', 'dlogfile', $this->root->url() . '/friendica.log'); $config->set('system', 'dlogfile', $this->root->url() . '/friendica.log');
/** @var LoggerInterface $logger */ /** @var LoggerInterface $logger */
$logger = $this->dice->create('$devLogger', [['$channel' => 'dev']]); $logger = $this->dice->create('$devLogger', ['dev']);
self::assertInstanceOf(LoggerInterface::class, $logger); self::assertInstanceOf(LoggerInterface::class, $logger);
} }

View file

@ -0,0 +1,212 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Core\Addon\Model;
use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException;
use Friendica\Core\Addon\Model\AddonLoader;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Test\MockedTest;
use Friendica\Test\Util\VFSTrait;
use org\bovigo\vfs\vfsStream;
class AddonLoaderTest extends MockedTest
{
use VFSTrait;
protected $structure = [
'addon' => [
'testaddon1' => [
'static' => [],
],
'testaddon2' => [
'static' => [],
],
'testaddon3' => [],
]
];
protected $addons = [
'testaddon1',
'testaddon2',
'testaddon3',
];
protected $content = <<<EOF
<?php
return [
\Friendica\Core\Hooks\Capabilities\BehavioralHookType::STRATEGY => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
],
];
EOF;
protected function setUp(): void
{
parent::setUp();
$this->setUpVfsDir();
}
public function dataHooks(): array
{
return [
'normal' => [
'structure' => $this->structure,
'enabled' => $this->addons,
'files' => [
'addon/testaddon1/static/hooks.config.php' => $this->content,
],
'assertion' => [
\Friendica\Core\Hooks\Capabilities\BehavioralHookType::STRATEGY => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
],
],
],
'double' => [
'structure' => $this->structure,
'enabled' => $this->addons,
'files' => [
'addon/testaddon1/static/hooks.config.php' => $this->content,
'addon/testaddon2/static/hooks.config.php' => $this->content,
],
'assertion' => [
\Friendica\Core\Hooks\Capabilities\BehavioralHookType::STRATEGY => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => ['', ''],
],
],
],
],
'wrongName' => [
'structure' => $this->structure,
'enabled' => $this->addons,
'files' => [
'addon/testaddon1/static/wrong.config.php' => $this->content,
],
'assertion' => [
],
],
'doubleNutOnlyOneEnabled' => [
'structure' => $this->structure,
'enabled' => ['testaddon1'],
'files' => [
'addon/testaddon1/static/hooks.config.php' => $this->content,
'addon/testaddon2/static/hooks.config.php' => $this->content,
],
'assertion' => [
\Friendica\Core\Hooks\Capabilities\BehavioralHookType::STRATEGY => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
],
],
]
];
}
/**
* @dataProvider dataHooks
*/
public function testAddonLoader(array $structure, array $enabledAddons, array $files, array $assertion)
{
vfsStream::create($structure)->at($this->root);
foreach ($files as $file => $content) {
vfsStream::newFile($file)
->withContent($content)
->at($this->root);
}
$configArray = [];
foreach ($enabledAddons as $enabledAddon) {
$configArray[$enabledAddon] = ['test' => []];
}
$config = \Mockery::mock(IManageConfigValues::class);
$config->shouldReceive('get')->with('addons')->andReturn($configArray)->once();
$addonLoader = new AddonLoader($this->root->url(), $config);
self::assertEquals($assertion, $addonLoader->getActiveAddonConfig('hooks'));
}
/**
* Test the exception in case of a wrong addon content
*/
public function testWrongContent()
{
$filename = 'addon/testaddon1/static/hooks.config.php';
$wrongContent = "<php return 'wrong';";
vfsStream::create($this->structure)->at($this->root);
vfsStream::newFile($filename)
->withContent($wrongContent)
->at($this->root);
$configArray = [];
foreach ($this->addons as $enabledAddon) {
$configArray[$enabledAddon] = ['test' => []];
}
$config = \Mockery::mock(IManageConfigValues::class);
$config->shouldReceive('get')->with('addons')->andReturn($configArray)->once();
$addonLoader = new AddonLoader($this->root->url(), $config);
self::expectException(AddonInvalidConfigFileException::class);
self::expectExceptionMessage(sprintf('Error loading config file %s', $this->root->getChild($filename)->url()));
$addonLoader->getActiveAddonConfig('hooks');
}
/**
* Test that nothing happens in case there are wrong addons files, but they're not used
*/
public function testNoHooksConfig()
{
$filename = 'addon/testaddon1/static/hooks.config.php';
$wrongContent = "<php return 'wrong';";
vfsStream::create($this->structure)->at($this->root);
vfsStream::newFile($filename)
->withContent($wrongContent)
->at($this->root);
$configArray = [];
foreach ($this->addons as $enabledAddon) {
$configArray[$enabledAddon] = ['test' => []];
}
$config = \Mockery::mock(IManageConfigValues::class);
$config->shouldReceive('get')->with('addons')->andReturn($configArray)->once();
$addonLoader = new AddonLoader($this->root->url(), $config);
self::assertEmpty($addonLoader->getActiveAddonConfig('anythingElse'));
}
}

View file

@ -22,25 +22,27 @@
namespace Friendica\Test\src\Core\Hooks\Model; namespace Friendica\Test\src\Core\Hooks\Model;
use Dice\Dice; use Dice\Dice;
use Friendica\Core\Hooks\Model\InstanceManager; use Friendica\Core\Hooks\Exceptions\HookInstanceException;
use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException;
use Friendica\Core\Hooks\Model\DiceInstanceManager;
use Friendica\Core\Hooks\Util\StrategiesFileManager;
use Friendica\Test\MockedTest; use Friendica\Test\MockedTest;
use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstance; use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstance;
use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstanceDecorator; use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstanceDecorator;
use Friendica\Test\Util\Hooks\InstanceMocks\IAmADecoratedInterface; use Friendica\Test\Util\Hooks\InstanceMocks\IAmADecoratedInterface;
use Mockery\MockInterface;
class InstanceManagerTest extends MockedTest class InstanceManagerTest extends MockedTest
{ {
public function testEqualButNotSameInstance() /** @var StrategiesFileManager|MockInterface */
protected $hookFileManager;
protected function setUp(): void
{ {
$instance = new InstanceManager(new Dice()); parent::setUp();
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class); $this->hookFileManager = \Mockery::mock(StrategiesFileManager::class);
$this->hookFileManager->shouldReceive('setupStrategies')->withAnyArgs();
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake');
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake');
self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB);
} }
protected function tearDown(): void protected function tearDown(): void
@ -50,6 +52,19 @@ class InstanceManagerTest extends MockedTest
parent::tearDown(); parent::tearDown();
} }
public function testEqualButNotSameInstance()
{
$instance = new DiceInstanceManager(new Dice(), $this->hookFileManager);
$instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
$getInstanceA = $instance->create(IAmADecoratedInterface::class, 'fake');
$getInstanceB = $instance->create(IAmADecoratedInterface::class, 'fake');
self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB);
}
public function dataTests(): array public function dataTests(): array
{ {
return [ return [
@ -79,9 +94,9 @@ class InstanceManagerTest extends MockedTest
/** /**
* @dataProvider dataTests * @dataProvider dataTests
*/ */
public function testInstanceWithConstructorAnonymArgs(string $aString = null, bool $cBool = null, string $bString = null) public function testInstanceWithArgs(string $aString = null, bool $cBool = null, string $bString = null)
{ {
$instance = new InstanceManager(new Dice()); $instance = new DiceInstanceManager(new Dice(), $this->hookFileManager);
$args = []; $args = [];
@ -95,45 +110,12 @@ class InstanceManagerTest extends MockedTest
$args[] = $cBool; $args[] = $cBool;
} }
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class, $args); $instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
/** @var IAmADecoratedInterface $getInstanceA */ /** @var IAmADecoratedInterface $getInstanceA */
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake'); $getInstanceA = $instance->create(IAmADecoratedInterface::class, 'fake', $args);
/** @var IAmADecoratedInterface $getInstanceB */ /** @var IAmADecoratedInterface $getInstanceB */
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake'); $getInstanceB = $instance->create(IAmADecoratedInterface::class, 'fake', $args);
self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB);
self::assertEquals($aString, $getInstanceA->getAText());
self::assertEquals($aString, $getInstanceB->getAText());
self::assertEquals($bString, $getInstanceA->getBText());
self::assertEquals($bString, $getInstanceB->getBText());
self::assertEquals($cBool, $getInstanceA->getCBool());
self::assertEquals($cBool, $getInstanceB->getCBool());
}
/**
* @dataProvider dataTests
*/
public function testInstanceConstructorAndGetInstanceWithNamedArgs(string $aString = null, bool $cBool = null, string $bString = null)
{
$instance = new InstanceManager(new Dice());
$args = [];
if (isset($aString)) {
$args[] = $aString;
}
if (isset($cBool)) {
$args[] = $cBool;
}
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class, $args);
/** @var IAmADecoratedInterface $getInstanceA */
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake', [$bString]);
/** @var IAmADecoratedInterface $getInstanceB */
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake', [$bString]);
self::assertEquals($getInstanceA, $getInstanceB); self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB); self::assertNotSame($getInstanceA, $getInstanceB);
@ -150,24 +132,27 @@ class InstanceManagerTest extends MockedTest
*/ */
public function testInstanceWithTwoStrategies(string $aString = null, bool $cBool = null, string $bString = null) public function testInstanceWithTwoStrategies(string $aString = null, bool $cBool = null, string $bString = null)
{ {
$instance = new InstanceManager(new Dice()); $instance = new DiceInstanceManager(new Dice(), $this->hookFileManager);
$args = []; $args = [];
if (isset($aString)) { if (isset($aString)) {
$args[] = $aString; $args[] = $aString;
} }
if (isset($bString)) {
$args[] = $bString;
}
if (isset($cBool)) { if (isset($cBool)) {
$args[] = $cBool; $args[] = $cBool;
} }
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class, $args); $instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake23', FakeInstance::class, $args); $instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake23');
/** @var IAmADecoratedInterface $getInstanceA */ /** @var IAmADecoratedInterface $getInstanceA */
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake', [$bString]); $getInstanceA = $instance->create(IAmADecoratedInterface::class, 'fake', $args);
/** @var IAmADecoratedInterface $getInstanceB */ /** @var IAmADecoratedInterface $getInstanceB */
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake23', [$bString]); $getInstanceB = $instance->create(IAmADecoratedInterface::class, 'fake23', $args);
self::assertEquals($getInstanceA, $getInstanceB); self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB); self::assertNotSame($getInstanceA, $getInstanceB);
@ -180,79 +165,74 @@ class InstanceManagerTest extends MockedTest
} }
/** /**
* @dataProvider dataTests * Test the exception in case the interface was already registered
*/ */
public function testDecorator(string $aString = null, bool $cBool = null, string $bString = null) public function testDoubleRegister()
{ {
$instance = new InstanceManager(new Dice()); self::expectException(HookRegisterArgumentException::class);
self::expectExceptionMessage(sprintf('A class with the name %s is already set for the interface %s', 'fake', IAmADecoratedInterface::class));
$args = []; $instance = new DiceInstanceManager(new Dice(), $this->hookFileManager);
$instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
if (isset($aString)) { $instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
$args[] = $aString;
}
if (isset($cBool)) {
$args[] = $cBool;
}
$prefix = 'prefix1';
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class, $args);
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake23', FakeInstance::class, $args);
$instance->registerDecorator(IAmADecoratedInterface::class, FakeInstanceDecorator::class, [$prefix]);
/** @var IAmADecoratedInterface $getInstanceA */
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake', [$bString]);
/** @var IAmADecoratedInterface $getInstanceB */
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake23', [$bString]);
self::assertEquals(2, FakeInstanceDecorator::$countInstance);
self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB);
self::assertEquals($prefix . $aString, $getInstanceA->getAText());
self::assertEquals($prefix . $aString, $getInstanceB->getAText());
self::assertEquals($prefix . $bString, $getInstanceA->getBText());
self::assertEquals($prefix . $bString, $getInstanceB->getBText());
self::assertEquals($prefix . $cBool, $getInstanceA->getCBool());
self::assertEquals($prefix . $cBool, $getInstanceB->getCBool());
} }
/** /**
* Test the exception in case the name of the instance isn't registered
*/
public function testWrongInstanceName()
{
self::expectException(HookInstanceException::class );
self::expectExceptionMessage(sprintf('The class with the name %s isn\'t registered for the class or interface %s', 'fake', IAmADecoratedInterface::class));
$instance = new DiceInstanceManager(new Dice(), $this->hookFileManager);
$instance->create(IAmADecoratedInterface::class, 'fake');
}
/**
* Test in case there are already some rules
*
* @dataProvider dataTests * @dataProvider dataTests
*/ */
public function testTwoDecoratorWithPrefix(string $aString = null, bool $cBool = null, string $bString = null) public function testWithGivenRules(string $aString = null, bool $cBool = null, string $bString = null)
{ {
$instance = new InstanceManager(new Dice());
$args = []; $args = [];
if (isset($aString)) { if (isset($aString)) {
$args[] = $aString; $args[] = $aString;
} }
if (isset($bString)) {
$args[] = $bString;
}
$dice = (new Dice())->addRules([
FakeInstance::class => [
'constructParams' => $args,
],
]);
$args = [];
if (isset($cBool)) { if (isset($cBool)) {
$args[] = $cBool; $args[] = $cBool;
} }
$prefix = 'prefix1'; $instance = new DiceInstanceManager($dice, $this->hookFileManager);
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake', FakeInstance::class, $args); $instance->registerStrategy(IAmADecoratedInterface::class, FakeInstance::class, 'fake');
$instance->registerStrategy(IAmADecoratedInterface::class, 'fake23', FakeInstance::class, $args);
$instance->registerDecorator(IAmADecoratedInterface::class, FakeInstanceDecorator::class, [$prefix]);
$instance->registerDecorator(IAmADecoratedInterface::class, FakeInstanceDecorator::class);
/** @var IAmADecoratedInterface $getInstanceA */ /** @var IAmADecoratedInterface $getInstanceA */
$getInstanceA = $instance->getInstance(IAmADecoratedInterface::class, 'fake', [$bString]); $getInstanceA = $instance->create(IAmADecoratedInterface::class, 'fake', $args);
/** @var IAmADecoratedInterface $getInstanceB */ /** @var IAmADecoratedInterface $getInstanceB */
$getInstanceB = $instance->getInstance(IAmADecoratedInterface::class, 'fake23', [$bString]); $getInstanceB = $instance->create(IAmADecoratedInterface::class, 'fake', $args);
self::assertEquals(4, FakeInstanceDecorator::$countInstance);
self::assertEquals($getInstanceA, $getInstanceB); self::assertEquals($getInstanceA, $getInstanceB);
self::assertNotSame($getInstanceA, $getInstanceB); self::assertNotSame($getInstanceA, $getInstanceB);
self::assertEquals($prefix . $aString, $getInstanceA->getAText()); self::assertEquals($aString, $getInstanceA->getAText());
self::assertEquals($prefix . $aString, $getInstanceB->getAText()); self::assertEquals($aString, $getInstanceB->getAText());
self::assertEquals($prefix . $bString, $getInstanceA->getBText()); self::assertEquals($bString, $getInstanceA->getBText());
self::assertEquals($prefix . $bString, $getInstanceB->getBText()); self::assertEquals($bString, $getInstanceB->getBText());
self::assertEquals($prefix . $cBool, $getInstanceA->getCBool()); self::assertEquals($cBool, $getInstanceA->getCBool());
self::assertEquals($prefix . $cBool, $getInstanceB->getCBool()); self::assertEquals($cBool, $getInstanceB->getCBool());
} }
} }

View file

@ -0,0 +1,202 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Core\Hooks\Util;
use Friendica\Core\Addon\Capabilities\ICanLoadAddons;
use Friendica\Core\Hooks\Capabilities\ICanRegisterStrategies;
use Friendica\Core\Hooks\Exceptions\HookConfigException;
use Friendica\Core\Hooks\Util\StrategiesFileManager;
use Friendica\Test\MockedTest;
use Friendica\Test\Util\VFSTrait;
use org\bovigo\vfs\vfsStream;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
class StrategiesFileManagerTest extends MockedTest
{
use VFSTrait;
protected function setUp(): void
{
parent::setUp();
$this->setUpVfsDir();
}
public function dataHooks(): array
{
return [
'normal' => [
'content' => <<<EOF
<?php
return [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
];
EOF,
'addonsArray' => [],
'assertStrategies' => [
[LoggerInterface::class, NullLogger::class, ''],
],
],
'normalWithString' => [
'content' => <<<EOF
<?php
return [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => '',
],
];
EOF,
'addonsArray' => [],
'assertStrategies' => [
[LoggerInterface::class, NullLogger::class, ''],
],
],
'withAddons' => [
'content' => <<<EOF
<?php
return [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
];
EOF,
'addonsArray' => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => ['null'],
],
],
'assertStrategies' => [
[LoggerInterface::class, NullLogger::class, ''],
[LoggerInterface::class, NullLogger::class, 'null'],
],
],
'withAddonsWithString' => [
'content' => <<<EOF
<?php
return [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
];
EOF,
'addonsArray' => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => 'null',
],
],
'assertStrategies' => [
[LoggerInterface::class, NullLogger::class, ''],
[LoggerInterface::class, NullLogger::class, 'null'],
],
],
// This should work because unique name convention is part of the instance manager logic, not of the file-infrastructure layer
'withAddonsDoubleNamed' => [
'content' => <<<EOF
<?php
return [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
];
EOF,
'addonsArray' => [
\Psr\Log\LoggerInterface::class => [
\Psr\Log\NullLogger::class => [''],
],
],
'assertStrategies' => [
[LoggerInterface::class, NullLogger::class, ''],
[LoggerInterface::class, NullLogger::class, ''],
],
],
];
}
/**
* @dataProvider dataHooks
*/
public function testSetupHooks(string $content, array $addonsArray, 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();
$hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader);
$instanceManager = \Mockery::mock(ICanRegisterStrategies::class);
foreach ($assertStrategies as $assertStrategy) {
$instanceManager->shouldReceive('registerStrategy')->withArgs($assertStrategy)->once();
}
$hookFileManager->loadConfig();
$hookFileManager->setupStrategies($instanceManager);
self::expectNotToPerformAssertions();
}
/**
* Test the exception in case the strategies.config.php file is missing
*/
public function testMissingStrategiesFile()
{
$addonLoader = \Mockery::mock(ICanLoadAddons::class);
$instanceManager = \Mockery::mock(ICanRegisterStrategies::class);
$hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader);
self::expectException(HookConfigException::class);
self::expectExceptionMessage(sprintf('config file %s does not exist.',
$this->root->url() . '/' . StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php'));
$hookFileManager->loadConfig();
}
/**
* Test the exception in case the strategies.config.php file is wrong
*/
public function testWrongStrategiesFile()
{
$addonLoader = \Mockery::mock(ICanLoadAddons::class);
$instanceManager = \Mockery::mock(ICanRegisterStrategies::class);
$hookFileManager = new StrategiesFileManager($this->root->url(), $addonLoader);
vfsStream::newFile(StrategiesFileManager::STATIC_DIR . '/' . StrategiesFileManager::CONFIG_NAME . '.config.php')
->withContent("<php return 'WRONG_CONTENT';")
->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'));
$hookFileManager->loadConfig();
}
}

View file

@ -22,9 +22,7 @@
namespace Friendica\Test\src\Core\Logger; namespace Friendica\Test\src\Core\Logger;
use Friendica\Core\Logger\Exception\LoggerArgumentException; use Friendica\Core\Logger\Exception\LoggerArgumentException;
use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Util\FileSystem;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Core\Logger\Type\StreamLogger; use Friendica\Core\Logger\Type\StreamLogger;
use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
@ -40,33 +38,26 @@ class StreamLoggerTest extends AbstractLoggerTest
*/ */
private $logfile; private $logfile;
/**
* @var Filesystem
*/
private $fileSystem;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->setUpVfsDir(); $this->setUpVfsDir();
$this->fileSystem = new FileSystem();
} }
/** /**
* {@@inheritdoc} * {@@inheritdoc}
*/ */
protected function getInstance($level = LogLevel::DEBUG) protected function getInstance($level = LogLevel::DEBUG, $logfile = 'friendica.log')
{ {
$this->logfile = vfsStream::newFile('friendica.log') $this->logfile = vfsStream::newFile($logfile)
->at($this->root); ->at($this->root);
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn($this->logfile->url())->once(); $this->config->shouldReceive('get')->with('system', 'logfile')->andReturn($this->logfile->url())->once();
$this->config->shouldReceive('get')->with('system', 'loglevel')->andReturn($level)->once();
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem, $level); $loggerFactory = new \Friendica\Core\Logger\Factory\StreamLogger($this->introspection, 'test');
return $loggerFactory->create($this->config);
return $logger;
} }
/** /**
@ -83,11 +74,12 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testNoUrl() public function testNoUrl()
{ {
$this->expectException(LoggerArgumentException::class); $this->expectException(LoggerArgumentException::class);
$this->expectExceptionMessage("Missing stream URL."); $this->expectExceptionMessage(' is not a valid logfile');
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn('')->once(); $this->config->shouldReceive('get')->with('system', 'logfile')->andReturn('')->once();
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem); $loggerFactory = new \Friendica\Core\Logger\Factory\StreamLogger($this->introspection, 'test');
$logger = $loggerFactory->create($this->config);
$logger->emergency('not working'); $logger->emergency('not working');
} }
@ -104,7 +96,8 @@ class StreamLoggerTest extends AbstractLoggerTest
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn($logfile->url())->once(); $this->config->shouldReceive('get')->with('system', 'logfile')->andReturn($logfile->url())->once();
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem); $loggerFactory = new \Friendica\Core\Logger\Factory\StreamLogger($this->introspection, 'test');
$logger = $loggerFactory->create($this->config);
$logger->emergency('not working'); $logger->emergency('not working');
} }
@ -119,7 +112,8 @@ class StreamLoggerTest extends AbstractLoggerTest
static::markTestIncomplete('We need a platform independent way to set directory to readonly'); static::markTestIncomplete('We need a platform independent way to set directory to readonly');
$logger = new StreamLogger('test', '/$%/wrong/directory/file.txt', $this->introspection, $this->fileSystem); $loggerFactory = new \Friendica\Core\Logger\Factory\StreamLogger($this->introspection, 'test');
$logger = $loggerFactory->create($this->config);
$logger->emergency('not working'); $logger->emergency('not working');
} }
@ -132,9 +126,7 @@ class StreamLoggerTest extends AbstractLoggerTest
$this->expectException(LogLevelException::class); $this->expectException(LogLevelException::class);
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); $this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn('file.text')->once(); $logger = $this->getInstance('NOPE');
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem, 'NOPE');
} }
/** /**
@ -145,29 +137,11 @@ class StreamLoggerTest extends AbstractLoggerTest
$this->expectException(LogLevelException::class); $this->expectException(LogLevelException::class);
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); $this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logfile = vfsStream::newFile('friendica.log') $logger = $this->getInstance('NOPE');
->at($this->root);
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn($logfile->url())->once();
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem);
$logger->log('NOPE', 'a test'); $logger->log('NOPE', 'a test');
} }
/**
* Test when the file is null
*/
public function testWrongFile()
{
$this->expectException(LoggerArgumentException::class);
$this->expectExceptionMessage("A stream must either be a resource or a string.");
$this->config->shouldReceive('get')->with('system', 'logfile')->andReturn(null)->once();
$logger = new StreamLogger('test', $this->config, $this->introspection, $this->fileSystem);
}
/** /**
* Test a relative path * Test a relative path
* @doesNotPerformAssertions * @doesNotPerformAssertions

View file

@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Core\Logger;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Core\Logger\Factory\SyslogLogger;
use Friendica\Core\Logger\Type\SyslogLogger as SyslogLoggerClass;
use Psr\Log\LoggerInterface;
class SyslogLoggerFactoryWrapper extends SyslogLogger
{
public function create(IManageConfigValues $config): LoggerInterface
{
$logOpts = $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS;
$logFacility = $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY;
$loglevel = SyslogLogger::mapLegacyConfigDebugLevel($config->get('system', 'loglevel'));
if (array_key_exists($loglevel, SyslogLoggerClass::logLevels)) {
$loglevel = SyslogLoggerClass::logLevels[$loglevel];
} else {
throw new LogLevelException(sprintf('The level "%s" is not valid.', $loglevel));
}
return new SyslogLoggerWrapper($this->channel, $this->introspection, $loglevel, $logOpts, $logFacility);
}
}

View file

@ -21,8 +21,6 @@
namespace Friendica\Test\src\Core\Logger; namespace Friendica\Test\src\Core\Logger;
use Friendica\Core\Logger\Exception\LoggerArgumentException;
use Friendica\Core\Logger\Exception\LoggerException;
use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Exception\LogLevelException;
use Friendica\Core\Logger\Type\SyslogLogger; use Friendica\Core\Logger\Type\SyslogLogger;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
@ -58,7 +56,10 @@ class SyslogLoggerTest extends AbstractLoggerTest
*/ */
protected function getInstance($level = LogLevel::DEBUG) protected function getInstance($level = LogLevel::DEBUG)
{ {
$this->logger = new SyslogLoggerWrapper('test', $this->config, $this->introspection, $level); $this->config->shouldReceive('get')->with('system', 'loglevel')->andReturn($level);
$loggerFactory = new SyslogLoggerFactoryWrapper($this->introspection, 'test');
$this->logger = $loggerFactory->create($this->config);
return $this->logger; return $this->logger;
} }
@ -71,8 +72,8 @@ class SyslogLoggerTest extends AbstractLoggerTest
{ {
$this->expectException(LogLevelException::class); $this->expectException(LogLevelException::class);
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); $this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logger = new SyslogLoggerWrapper('test', $this->config, $this->introspection, 'NOPE'); $logger = $this->getInstance('NOPE');
} }
/** /**
@ -83,7 +84,7 @@ class SyslogLoggerTest extends AbstractLoggerTest
$this->expectException(LogLevelException::class); $this->expectException(LogLevelException::class);
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); $this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logger = new SyslogLoggerWrapper('test', $this->config, $this->introspection); $logger = $this->getInstance();
$logger->log('NOPE', 'a test'); $logger->log('NOPE', 'a test');
} }
@ -94,7 +95,7 @@ class SyslogLoggerTest extends AbstractLoggerTest
*/ */
public function testClose() public function testClose()
{ {
$logger = new SyslogLoggerWrapper('test', $this->config, $this->introspection); $logger = $this->getInstance();
$logger->emergency('test'); $logger->emergency('test');
$logger->close(); $logger->close();
// Reopened itself // Reopened itself

View file

@ -21,10 +21,8 @@
namespace Friendica\Test\src\Core\Logger; namespace Friendica\Test\src\Core\Logger;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Logger\Capabilities\IHaveCallIntrospections;
use Friendica\Core\Logger\Type\SyslogLogger; use Friendica\Core\Logger\Type\SyslogLogger;
use Friendica\Core\Logger\Util\Introspection;
use Psr\Log\LogLevel;
/** /**
* Wraps the SyslogLogger for replacing the syslog call with a string field. * Wraps the SyslogLogger for replacing the syslog call with a string field.
@ -33,9 +31,9 @@ class SyslogLoggerWrapper extends SyslogLogger
{ {
private $content; private $content;
public function __construct($channel, IManageConfigValues $config, Introspection $introspection, $level = LogLevel::NOTICE) public function __construct(string $channel, IHaveCallIntrospections $introspection, string $logLevel, string $logOptions, string $logFacility)
{ {
parent::__construct($channel, $config, $introspection, $level); parent::__construct($channel, $introspection, $logLevel, $logOptions, $logFacility);
$this->content = ''; $this->content = '';
} }

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2023.09-dev\n" "Project-Id-Version: 2023.09-dev\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-09 18:36-0400\n" "POT-Creation-Date: 2023-07-16 16:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -292,9 +292,9 @@ msgid "Insert web link"
msgstr "" msgstr ""
#: mod/message.php:201 mod/message.php:357 mod/photos.php:1301 #: mod/message.php:201 mod/message.php:357 mod/photos.php:1301
#: src/Content/Conversation.php:392 src/Content/Conversation.php:1506 #: src/Content/Conversation.php:392 src/Content/Conversation.php:1508
#: src/Module/Item/Compose.php:206 src/Module/Post/Edit.php:145 #: src/Module/Item/Compose.php:206 src/Module/Post/Edit.php:145
#: src/Module/Profile/UnkMail.php:154 src/Object/Post.php:574 #: src/Module/Profile/UnkMail.php:154 src/Object/Post.php:571
msgid "Please wait" msgid "Please wait"
msgstr "" msgstr ""
@ -315,7 +315,7 @@ msgstr ""
#: src/Module/Moderation/Report/Create.php:211 #: src/Module/Moderation/Report/Create.php:211
#: src/Module/Moderation/Report/Create.php:263 #: src/Module/Moderation/Report/Create.php:263
#: src/Module/Profile/Profile.php:274 src/Module/Profile/UnkMail.php:155 #: src/Module/Profile/Profile.php:274 src/Module/Profile/UnkMail.php:155
#: src/Module/Settings/Profile/Index.php:230 src/Object/Post.php:1090 #: src/Module/Settings/Profile/Index.php:230 src/Object/Post.php:1087
#: view/theme/duepuntozero/config.php:85 view/theme/frio/config.php:171 #: view/theme/duepuntozero/config.php:85 view/theme/frio/config.php:171
#: view/theme/quattro/config.php:87 view/theme/vier/config.php:135 #: view/theme/quattro/config.php:87 view/theme/vier/config.php:135
msgid "Submit" msgid "Submit"
@ -600,33 +600,33 @@ msgstr ""
#: mod/photos.php:1139 mod/photos.php:1195 mod/photos.php:1275 #: mod/photos.php:1139 mod/photos.php:1195 mod/photos.php:1275
#: src/Module/Contact.php:619 src/Module/Item/Compose.php:188 #: src/Module/Contact.php:619 src/Module/Item/Compose.php:188
#: src/Object/Post.php:1087 #: src/Object/Post.php:1084
msgid "This is you" msgid "This is you"
msgstr "" msgstr ""
#: mod/photos.php:1141 mod/photos.php:1197 mod/photos.php:1277 #: mod/photos.php:1141 mod/photos.php:1197 mod/photos.php:1277
#: src/Object/Post.php:568 src/Object/Post.php:1089 #: src/Object/Post.php:565 src/Object/Post.php:1086
msgid "Comment" msgid "Comment"
msgstr "" msgstr ""
#: mod/photos.php:1143 mod/photos.php:1199 mod/photos.php:1279 #: mod/photos.php:1143 mod/photos.php:1199 mod/photos.php:1279
#: src/Content/Conversation.php:407 src/Module/Calendar/Event/Form.php:248 #: src/Content/Conversation.php:407 src/Module/Calendar/Event/Form.php:248
#: src/Module/Item/Compose.php:201 src/Module/Post/Edit.php:165 #: src/Module/Item/Compose.php:201 src/Module/Post/Edit.php:165
#: src/Object/Post.php:1103 #: src/Object/Post.php:1100
msgid "Preview" msgid "Preview"
msgstr "" msgstr ""
#: mod/photos.php:1144 src/Content/Conversation.php:360 #: mod/photos.php:1144 src/Content/Conversation.php:360
#: src/Module/Post/Edit.php:130 src/Object/Post.php:1091 #: src/Module/Post/Edit.php:130 src/Object/Post.php:1088
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
#: mod/photos.php:1236 src/Content/Conversation.php:1422 #: mod/photos.php:1236 src/Content/Conversation.php:1424
#: src/Object/Post.php:263 #: src/Object/Post.php:260
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: mod/photos.php:1237 src/Content/Conversation.php:1423 #: mod/photos.php:1237 src/Content/Conversation.php:1425
#: src/Module/Moderation/Users/Active.php:136 #: src/Module/Moderation/Users/Active.php:136
#: src/Module/Moderation/Users/Blocked.php:136 #: src/Module/Moderation/Users/Blocked.php:136
#: src/Module/Moderation/Users/Index.php:151 #: src/Module/Moderation/Users/Index.php:151
@ -634,19 +634,19 @@ msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: mod/photos.php:1298 src/Object/Post.php:405 #: mod/photos.php:1298 src/Object/Post.php:402
msgid "Like" msgid "Like"
msgstr "" msgstr ""
#: mod/photos.php:1299 src/Object/Post.php:405 #: mod/photos.php:1299 src/Object/Post.php:402
msgid "I like this (toggle)" msgid "I like this (toggle)"
msgstr "" msgstr ""
#: mod/photos.php:1300 src/Object/Post.php:406 #: mod/photos.php:1300 src/Object/Post.php:403
msgid "Dislike" msgid "Dislike"
msgstr "" msgstr ""
#: mod/photos.php:1302 src/Object/Post.php:406 #: mod/photos.php:1302 src/Object/Post.php:403
msgid "I don't like this (toggle)" msgid "I don't like this (toggle)"
msgstr "" msgstr ""
@ -662,97 +662,97 @@ msgstr ""
msgid "Apologies but the website is unavailable at the moment." msgid "Apologies but the website is unavailable at the moment."
msgstr "" msgstr ""
#: src/App/Page.php:247 #: src/App/Page.php:248
msgid "Delete this item?" msgid "Delete this item?"
msgstr "" msgstr ""
#: src/App/Page.php:248 #: src/App/Page.php:249
msgid "" msgid ""
"Block this author? They won't be able to follow you nor see your public " "Block this author? They won't be able to follow you nor see your public "
"posts, and you won't be able to see their posts and their notifications." "posts, and you won't be able to see their posts and their notifications."
msgstr "" msgstr ""
#: src/App/Page.php:249 #: src/App/Page.php:250
msgid "" msgid ""
"Ignore this author? You won't be able to see their posts and their " "Ignore this author? You won't be able to see their posts and their "
"notifications." "notifications."
msgstr "" msgstr ""
#: src/App/Page.php:250 #: src/App/Page.php:251
msgid "Collapse this author's posts?" msgid "Collapse this author's posts?"
msgstr "" msgstr ""
#: src/App/Page.php:252 #: src/App/Page.php:253
msgid "Like not successful" msgid "Like not successful"
msgstr "" msgstr ""
#: src/App/Page.php:253 #: src/App/Page.php:254
msgid "Dislike not successful" msgid "Dislike not successful"
msgstr "" msgstr ""
#: src/App/Page.php:254 #: src/App/Page.php:255
msgid "Sharing not successful" msgid "Sharing not successful"
msgstr "" msgstr ""
#: src/App/Page.php:255 #: src/App/Page.php:256
msgid "Attendance unsuccessful" msgid "Attendance unsuccessful"
msgstr "" msgstr ""
#: src/App/Page.php:256 #: src/App/Page.php:257
msgid "Backend error" msgid "Backend error"
msgstr "" msgstr ""
#: src/App/Page.php:257 #: src/App/Page.php:258
msgid "Network error" msgid "Network error"
msgstr "" msgstr ""
#: src/App/Page.php:260 #: src/App/Page.php:261
msgid "Drop files here to upload" msgid "Drop files here to upload"
msgstr "" msgstr ""
#: src/App/Page.php:261 #: src/App/Page.php:262
msgid "Your browser does not support drag and drop file uploads." msgid "Your browser does not support drag and drop file uploads."
msgstr "" msgstr ""
#: src/App/Page.php:262 #: src/App/Page.php:263
msgid "" msgid ""
"Please use the fallback form below to upload your files like in the olden " "Please use the fallback form below to upload your files like in the olden "
"days." "days."
msgstr "" msgstr ""
#: src/App/Page.php:263 #: src/App/Page.php:264
msgid "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB." msgid "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB."
msgstr "" msgstr ""
#: src/App/Page.php:264 #: src/App/Page.php:265
msgid "You can't upload files of this type." msgid "You can't upload files of this type."
msgstr "" msgstr ""
#: src/App/Page.php:265 #: src/App/Page.php:266
msgid "Server responded with {{statusCode}} code." msgid "Server responded with {{statusCode}} code."
msgstr "" msgstr ""
#: src/App/Page.php:266 #: src/App/Page.php:267
msgid "Cancel upload" msgid "Cancel upload"
msgstr "" msgstr ""
#: src/App/Page.php:267 #: src/App/Page.php:268
msgid "Upload canceled." msgid "Upload canceled."
msgstr "" msgstr ""
#: src/App/Page.php:268 #: src/App/Page.php:269
msgid "Are you sure you want to cancel this upload?" msgid "Are you sure you want to cancel this upload?"
msgstr "" msgstr ""
#: src/App/Page.php:269 #: src/App/Page.php:270
msgid "Remove file" msgid "Remove file"
msgstr "" msgstr ""
#: src/App/Page.php:270 #: src/App/Page.php:271
msgid "You can't upload any more files." msgid "You can't upload any more files."
msgstr "" msgstr ""
#: src/App/Page.php:348 #: src/App/Page.php:349
msgid "toggle mobile" msgid "toggle mobile"
msgstr "" msgstr ""
@ -769,31 +769,31 @@ msgstr ""
msgid "You must be logged in to use addons. " msgid "You must be logged in to use addons. "
msgstr "" msgstr ""
#: src/BaseModule.php:400 #: src/BaseModule.php:401
msgid "" msgid ""
"The form security token was not correct. This probably happened because the " "The form security token was not correct. This probably happened because the "
"form has been opened for too long (>3 hours) before submitting it." "form has been opened for too long (>3 hours) before submitting it."
msgstr "" msgstr ""
#: src/BaseModule.php:427 #: src/BaseModule.php:428
msgid "All contacts" msgid "All contacts"
msgstr "" msgstr ""
#: src/BaseModule.php:432 src/Content/Widget.php:243 src/Core/ACL.php:195 #: src/BaseModule.php:433 src/Content/Widget.php:243 src/Core/ACL.php:195
#: src/Module/Contact.php:415 src/Module/PermissionTooltip.php:127 #: src/Module/Contact.php:415 src/Module/PermissionTooltip.php:127
#: src/Module/PermissionTooltip.php:149 #: src/Module/PermissionTooltip.php:149
msgid "Followers" msgid "Followers"
msgstr "" msgstr ""
#: src/BaseModule.php:437 src/Content/Widget.php:244 src/Module/Contact.php:418 #: src/BaseModule.php:438 src/Content/Widget.php:244 src/Module/Contact.php:418
msgid "Following" msgid "Following"
msgstr "" msgstr ""
#: src/BaseModule.php:442 src/Content/Widget.php:245 src/Module/Contact.php:421 #: src/BaseModule.php:443 src/Content/Widget.php:245 src/Module/Contact.php:421
msgid "Mutual friends" msgid "Mutual friends"
msgstr "" msgstr ""
#: src/BaseModule.php:450 #: src/BaseModule.php:451
msgid "Common" msgid "Common"
msgstr "" msgstr ""
@ -1226,7 +1226,7 @@ msgid "Visible to <strong>everybody</strong>"
msgstr "" msgstr ""
#: src/Content/Conversation.php:330 src/Module/Item/Compose.php:200 #: src/Content/Conversation.php:330 src/Module/Item/Compose.php:200
#: src/Object/Post.php:1102 #: src/Object/Post.php:1099
msgid "Please enter a image/video/audio/webpage URL:" msgid "Please enter a image/video/audio/webpage URL:"
msgstr "" msgstr ""
@ -1271,52 +1271,52 @@ msgid "attach file"
msgstr "" msgstr ""
#: src/Content/Conversation.php:365 src/Module/Item/Compose.php:190 #: src/Content/Conversation.php:365 src/Module/Item/Compose.php:190
#: src/Module/Post/Edit.php:171 src/Object/Post.php:1092 #: src/Module/Post/Edit.php:171 src/Object/Post.php:1089
msgid "Bold" msgid "Bold"
msgstr "" msgstr ""
#: src/Content/Conversation.php:366 src/Module/Item/Compose.php:191 #: src/Content/Conversation.php:366 src/Module/Item/Compose.php:191
#: src/Module/Post/Edit.php:172 src/Object/Post.php:1093 #: src/Module/Post/Edit.php:172 src/Object/Post.php:1090
msgid "Italic" msgid "Italic"
msgstr "" msgstr ""
#: src/Content/Conversation.php:367 src/Module/Item/Compose.php:192 #: src/Content/Conversation.php:367 src/Module/Item/Compose.php:192
#: src/Module/Post/Edit.php:173 src/Object/Post.php:1094 #: src/Module/Post/Edit.php:173 src/Object/Post.php:1091
msgid "Underline" msgid "Underline"
msgstr "" msgstr ""
#: src/Content/Conversation.php:368 src/Module/Item/Compose.php:193 #: src/Content/Conversation.php:368 src/Module/Item/Compose.php:193
#: src/Module/Post/Edit.php:174 src/Object/Post.php:1096 #: src/Module/Post/Edit.php:174 src/Object/Post.php:1093
msgid "Quote" msgid "Quote"
msgstr "" msgstr ""
#: src/Content/Conversation.php:369 src/Module/Item/Compose.php:194 #: src/Content/Conversation.php:369 src/Module/Item/Compose.php:194
#: src/Module/Post/Edit.php:175 src/Object/Post.php:1097 #: src/Module/Post/Edit.php:175 src/Object/Post.php:1094
msgid "Add emojis" msgid "Add emojis"
msgstr "" msgstr ""
#: src/Content/Conversation.php:370 src/Module/Item/Compose.php:195 #: src/Content/Conversation.php:370 src/Module/Item/Compose.php:195
#: src/Object/Post.php:1095 #: src/Object/Post.php:1092
msgid "Content Warning" msgid "Content Warning"
msgstr "" msgstr ""
#: src/Content/Conversation.php:371 src/Module/Item/Compose.php:196 #: src/Content/Conversation.php:371 src/Module/Item/Compose.php:196
#: src/Module/Post/Edit.php:176 src/Object/Post.php:1098 #: src/Module/Post/Edit.php:176 src/Object/Post.php:1095
msgid "Code" msgid "Code"
msgstr "" msgstr ""
#: src/Content/Conversation.php:372 src/Module/Item/Compose.php:197 #: src/Content/Conversation.php:372 src/Module/Item/Compose.php:197
#: src/Object/Post.php:1099 #: src/Object/Post.php:1096
msgid "Image" msgid "Image"
msgstr "" msgstr ""
#: src/Content/Conversation.php:373 src/Module/Item/Compose.php:198 #: src/Content/Conversation.php:373 src/Module/Item/Compose.php:198
#: src/Module/Post/Edit.php:177 src/Object/Post.php:1100 #: src/Module/Post/Edit.php:177 src/Object/Post.php:1097
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: src/Content/Conversation.php:374 src/Module/Item/Compose.php:199 #: src/Content/Conversation.php:374 src/Module/Item/Compose.php:199
#: src/Module/Post/Edit.php:178 src/Object/Post.php:1101 #: src/Module/Post/Edit.php:178 src/Object/Post.php:1098
msgid "Link or Media" msgid "Link or Media"
msgstr "" msgstr ""
@ -1386,111 +1386,111 @@ msgstr ""
msgid "Delete Selected Items" msgid "Delete Selected Items"
msgstr "" msgstr ""
#: src/Content/Conversation.php:724 src/Content/Conversation.php:727 #: src/Content/Conversation.php:728 src/Content/Conversation.php:731
#: src/Content/Conversation.php:730 src/Content/Conversation.php:733 #: src/Content/Conversation.php:734 src/Content/Conversation.php:737
#: src/Content/Conversation.php:736 #: src/Content/Conversation.php:740
#, php-format #, php-format
msgid "You had been addressed (%s)." msgid "You had been addressed (%s)."
msgstr "" msgstr ""
#: src/Content/Conversation.php:739 #: src/Content/Conversation.php:743
#, php-format #, php-format
msgid "You are following %s." msgid "You are following %s."
msgstr "" msgstr ""
#: src/Content/Conversation.php:742 #: src/Content/Conversation.php:746
msgid "You subscribed to one or more tags in this post." msgid "You subscribed to one or more tags in this post."
msgstr "" msgstr ""
#: src/Content/Conversation.php:761 #: src/Content/Conversation.php:765
#, php-format #, php-format
msgid "%s reshared this." msgid "%s reshared this."
msgstr "" msgstr ""
#: src/Content/Conversation.php:763 #: src/Content/Conversation.php:767
msgid "Reshared" msgid "Reshared"
msgstr "" msgstr ""
#: src/Content/Conversation.php:763 #: src/Content/Conversation.php:767
#, php-format #, php-format
msgid "Reshared by %s <%s>" msgid "Reshared by %s <%s>"
msgstr "" msgstr ""
#: src/Content/Conversation.php:766 #: src/Content/Conversation.php:770
#, php-format #, php-format
msgid "%s is participating in this thread." msgid "%s is participating in this thread."
msgstr "" msgstr ""
#: src/Content/Conversation.php:769 #: src/Content/Conversation.php:773
msgid "Stored for general reasons" msgid "Stored for general reasons"
msgstr "" msgstr ""
#: src/Content/Conversation.php:772 #: src/Content/Conversation.php:776
msgid "Global post" msgid "Global post"
msgstr "" msgstr ""
#: src/Content/Conversation.php:775 #: src/Content/Conversation.php:779
msgid "Sent via an relay server" msgid "Sent via an relay server"
msgstr "" msgstr ""
#: src/Content/Conversation.php:775 #: src/Content/Conversation.php:779
#, php-format #, php-format
msgid "Sent via the relay server %s <%s>" msgid "Sent via the relay server %s <%s>"
msgstr "" msgstr ""
#: src/Content/Conversation.php:778 #: src/Content/Conversation.php:782
msgid "Fetched" msgid "Fetched"
msgstr "" msgstr ""
#: src/Content/Conversation.php:778 #: src/Content/Conversation.php:782
#, php-format #, php-format
msgid "Fetched because of %s <%s>" msgid "Fetched because of %s <%s>"
msgstr "" msgstr ""
#: src/Content/Conversation.php:781 #: src/Content/Conversation.php:785
msgid "Stored because of a child post to complete this thread." msgid "Stored because of a child post to complete this thread."
msgstr "" msgstr ""
#: src/Content/Conversation.php:784 #: src/Content/Conversation.php:788
msgid "Local delivery" msgid "Local delivery"
msgstr "" msgstr ""
#: src/Content/Conversation.php:787 #: src/Content/Conversation.php:791
msgid "Stored because of your activity (like, comment, star, ...)" msgid "Stored because of your activity (like, comment, star, ...)"
msgstr "" msgstr ""
#: src/Content/Conversation.php:790 #: src/Content/Conversation.php:794
msgid "Distributed" msgid "Distributed"
msgstr "" msgstr ""
#: src/Content/Conversation.php:793 #: src/Content/Conversation.php:797
msgid "Pushed to us" msgid "Pushed to us"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1450 src/Object/Post.php:248 #: src/Content/Conversation.php:1452 src/Object/Post.php:248
msgid "Pinned item" msgid "Pinned item"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1466 src/Object/Post.php:518 #: src/Content/Conversation.php:1468 src/Object/Post.php:515
#: src/Object/Post.php:519 #: src/Object/Post.php:516
#, php-format #, php-format
msgid "View %s's profile @ %s" msgid "View %s's profile @ %s"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1479 src/Object/Post.php:506 #: src/Content/Conversation.php:1481 src/Object/Post.php:503
msgid "Categories:" msgid "Categories:"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1480 src/Object/Post.php:507 #: src/Content/Conversation.php:1482 src/Object/Post.php:504
msgid "Filed under:" msgid "Filed under:"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1488 src/Object/Post.php:532 #: src/Content/Conversation.php:1490 src/Object/Post.php:529
#, php-format #, php-format
msgid "%s from %s" msgid "%s from %s"
msgstr "" msgstr ""
#: src/Content/Conversation.php:1504 #: src/Content/Conversation.php:1506
msgid "View in context" msgid "View in context"
msgstr "" msgstr ""
@ -1700,7 +1700,7 @@ msgstr ""
msgid "Collapse" msgid "Collapse"
msgstr "" msgstr ""
#: src/Content/Item.php:434 src/Object/Post.php:487 #: src/Content/Item.php:434 src/Object/Post.php:484
msgid "Languages" msgid "Languages"
msgstr "" msgstr ""
@ -2833,6 +2833,16 @@ msgstr ""
msgid "Dec" msgid "Dec"
msgstr "" msgstr ""
#: src/Core/Logger/Util/LoggerSettingsCheck.php:60
#, php-format
msgid "The logfile '%s' is not usable. No logging possible (error: '%s')"
msgstr ""
#: src/Core/Logger/Util/LoggerSettingsCheck.php:85
#, php-format
msgid "The debug logfile '%s' is not usable. No logging possible (error: '%s')"
msgstr ""
#: src/Core/Renderer.php:89 src/Core/Renderer.php:118 src/Core/Renderer.php:147 #: src/Core/Renderer.php:89 src/Core/Renderer.php:118 src/Core/Renderer.php:147
#: src/Core/Renderer.php:181 src/Render/FriendicaSmartyEngine.php:60 #: src/Core/Renderer.php:181 src/Render/FriendicaSmartyEngine.php:60
msgid "" msgid ""
@ -3407,7 +3417,7 @@ msgstr ""
msgid "Title/Description:" msgid "Title/Description:"
msgstr "" msgstr ""
#: src/Model/Profile.php:1025 src/Module/Admin/Summary.php:221 #: src/Model/Profile.php:1025 src/Module/Admin/Summary.php:197
#: src/Module/Moderation/Report/Create.php:280 #: src/Module/Moderation/Report/Create.php:280
#: src/Module/Moderation/Summary.php:77 #: src/Module/Moderation/Summary.php:77
msgid "Summary" msgid "Summary"
@ -3735,7 +3745,7 @@ msgstr ""
#: src/Module/Admin/Federation.php:210 src/Module/Admin/Logs/Settings.php:85 #: src/Module/Admin/Federation.php:210 src/Module/Admin/Logs/Settings.php:85
#: src/Module/Admin/Logs/View.php:83 src/Module/Admin/Queue.php:72 #: src/Module/Admin/Logs/View.php:83 src/Module/Admin/Queue.php:72
#: src/Module/Admin/Site.php:398 src/Module/Admin/Storage.php:138 #: src/Module/Admin/Site.php:398 src/Module/Admin/Storage.php:138
#: src/Module/Admin/Summary.php:220 src/Module/Admin/Themes/Details.php:90 #: src/Module/Admin/Summary.php:196 src/Module/Admin/Themes/Details.php:90
#: src/Module/Admin/Themes/Index.php:111 src/Module/Admin/Tos.php:77 #: src/Module/Admin/Themes/Index.php:111 src/Module/Admin/Tos.php:77
#: src/Module/Moderation/Users/Create.php:61 #: src/Module/Moderation/Users/Create.php:61
#: src/Module/Moderation/Users/Pending.php:96 #: src/Module/Moderation/Users/Pending.php:96
@ -5224,50 +5234,40 @@ msgid ""
"href=\"%s\">the installation page</a> for help." "href=\"%s\">the installation page</a> for help."
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:142 #: src/Module/Admin/Summary.php:148
#, php-format
msgid "The logfile '%s' is not usable. No logging possible (error: '%s')"
msgstr ""
#: src/Module/Admin/Summary.php:156
#, php-format
msgid "The debug logfile '%s' is not usable. No logging possible (error: '%s')"
msgstr ""
#: src/Module/Admin/Summary.php:172
#, php-format #, php-format
msgid "" msgid ""
"Friendica's system.basepath was updated from '%s' to '%s'. Please remove the " "Friendica's system.basepath was updated from '%s' to '%s'. Please remove the "
"system.basepath from your db to avoid differences." "system.basepath from your db to avoid differences."
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:180 #: src/Module/Admin/Summary.php:156
#, php-format #, php-format
msgid "" msgid ""
"Friendica's current system.basepath '%s' is wrong and the config file '%s' " "Friendica's current system.basepath '%s' is wrong and the config file '%s' "
"isn't used." "isn't used."
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:188 #: src/Module/Admin/Summary.php:164
#, php-format #, php-format
msgid "" msgid ""
"Friendica's current system.basepath '%s' is not equal to the config file " "Friendica's current system.basepath '%s' is not equal to the config file "
"'%s'. Please fix your configuration." "'%s'. Please fix your configuration."
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:199 #: src/Module/Admin/Summary.php:175
msgid "Message queues" msgid "Message queues"
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:205 #: src/Module/Admin/Summary.php:181
msgid "Server Settings" msgid "Server Settings"
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:223 #: src/Module/Admin/Summary.php:199
msgid "Version" msgid "Version"
msgstr "" msgstr ""
#: src/Module/Admin/Summary.php:227 #: src/Module/Admin/Summary.php:203
msgid "Active addons" msgid "Active addons"
msgstr "" msgstr ""
@ -5878,7 +5878,7 @@ msgid "Only show blocked contacts"
msgstr "" msgstr ""
#: src/Module/Contact.php:369 src/Module/Contact.php:441 #: src/Module/Contact.php:369 src/Module/Contact.php:441
#: src/Object/Post.php:365 #: src/Object/Post.php:362
msgid "Ignored" msgid "Ignored"
msgstr "" msgstr ""
@ -6562,7 +6562,7 @@ msgstr ""
msgid "Posts that mention or involve you" msgid "Posts that mention or involve you"
msgstr "" msgstr ""
#: src/Module/Conversation/Network.php:289 src/Object/Post.php:377 #: src/Module/Conversation/Network.php:289 src/Object/Post.php:374
msgid "Starred" msgid "Starred"
msgstr "" msgstr ""
@ -11500,234 +11500,234 @@ msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: src/Object/Post.php:252 #: src/Object/Post.php:261
msgid "Delete globally" msgid "Delete globally"
msgstr "" msgstr ""
#: src/Object/Post.php:252 #: src/Object/Post.php:261
msgid "Remove locally" msgid "Remove locally"
msgstr "" msgstr ""
#: src/Object/Post.php:271 #: src/Object/Post.php:268
#, php-format #, php-format
msgid "Block %s" msgid "Block %s"
msgstr "" msgstr ""
#: src/Object/Post.php:276 #: src/Object/Post.php:273
#, php-format #, php-format
msgid "Ignore %s" msgid "Ignore %s"
msgstr "" msgstr ""
#: src/Object/Post.php:281 #: src/Object/Post.php:278
#, php-format #, php-format
msgid "Collapse %s" msgid "Collapse %s"
msgstr "" msgstr ""
#: src/Object/Post.php:285 #: src/Object/Post.php:282
msgid "Report post" msgid "Report post"
msgstr "" msgstr ""
#: src/Object/Post.php:290 #: src/Object/Post.php:287
msgid "Save to folder" msgid "Save to folder"
msgstr "" msgstr ""
#: src/Object/Post.php:330 #: src/Object/Post.php:327
msgid "I will attend" msgid "I will attend"
msgstr "" msgstr ""
#: src/Object/Post.php:330 #: src/Object/Post.php:327
msgid "I will not attend" msgid "I will not attend"
msgstr "" msgstr ""
#: src/Object/Post.php:330 #: src/Object/Post.php:327
msgid "I might attend" msgid "I might attend"
msgstr "" msgstr ""
#: src/Object/Post.php:360 #: src/Object/Post.php:357
msgid "Ignore thread" msgid "Ignore thread"
msgstr "" msgstr ""
#: src/Object/Post.php:361 #: src/Object/Post.php:358
msgid "Unignore thread" msgid "Unignore thread"
msgstr "" msgstr ""
#: src/Object/Post.php:362 #: src/Object/Post.php:359
msgid "Toggle ignore status" msgid "Toggle ignore status"
msgstr "" msgstr ""
#: src/Object/Post.php:372 #: src/Object/Post.php:369
msgid "Add star" msgid "Add star"
msgstr "" msgstr ""
#: src/Object/Post.php:373 #: src/Object/Post.php:370
msgid "Remove star" msgid "Remove star"
msgstr "" msgstr ""
#: src/Object/Post.php:374 #: src/Object/Post.php:371
msgid "Toggle star status" msgid "Toggle star status"
msgstr "" msgstr ""
#: src/Object/Post.php:385 #: src/Object/Post.php:382
msgid "Pin" msgid "Pin"
msgstr "" msgstr ""
#: src/Object/Post.php:386 #: src/Object/Post.php:383
msgid "Unpin" msgid "Unpin"
msgstr "" msgstr ""
#: src/Object/Post.php:387 #: src/Object/Post.php:384
msgid "Toggle pin status" msgid "Toggle pin status"
msgstr "" msgstr ""
#: src/Object/Post.php:390 #: src/Object/Post.php:387
msgid "Pinned" msgid "Pinned"
msgstr "" msgstr ""
#: src/Object/Post.php:395 #: src/Object/Post.php:392
msgid "Add tag" msgid "Add tag"
msgstr "" msgstr ""
#: src/Object/Post.php:408 #: src/Object/Post.php:405
msgid "Quote share this" msgid "Quote share this"
msgstr "" msgstr ""
#: src/Object/Post.php:408 #: src/Object/Post.php:405
msgid "Quote Share" msgid "Quote Share"
msgstr "" msgstr ""
#: src/Object/Post.php:411 #: src/Object/Post.php:408
msgid "Reshare this" msgid "Reshare this"
msgstr "" msgstr ""
#: src/Object/Post.php:411 #: src/Object/Post.php:408
msgid "Reshare" msgid "Reshare"
msgstr "" msgstr ""
#: src/Object/Post.php:412 #: src/Object/Post.php:409
msgid "Cancel your Reshare" msgid "Cancel your Reshare"
msgstr "" msgstr ""
#: src/Object/Post.php:412 #: src/Object/Post.php:409
msgid "Unshare" msgid "Unshare"
msgstr "" msgstr ""
#: src/Object/Post.php:463 #: src/Object/Post.php:460
#, php-format #, php-format
msgid "%s (Received %s)" msgid "%s (Received %s)"
msgstr "" msgstr ""
#: src/Object/Post.php:469 #: src/Object/Post.php:466
msgid "Comment this item on your system" msgid "Comment this item on your system"
msgstr "" msgstr ""
#: src/Object/Post.php:469 #: src/Object/Post.php:466
msgid "Remote comment" msgid "Remote comment"
msgstr "" msgstr ""
#: src/Object/Post.php:491 #: src/Object/Post.php:488
msgid "Share via ..." msgid "Share via ..."
msgstr "" msgstr ""
#: src/Object/Post.php:491 #: src/Object/Post.php:488
msgid "Share via external services" msgid "Share via external services"
msgstr "" msgstr ""
#: src/Object/Post.php:520 #: src/Object/Post.php:517
msgid "to" msgid "to"
msgstr "" msgstr ""
#: src/Object/Post.php:521 #: src/Object/Post.php:518
msgid "via" msgid "via"
msgstr "" msgstr ""
#: src/Object/Post.php:522 #: src/Object/Post.php:519
msgid "Wall-to-Wall" msgid "Wall-to-Wall"
msgstr "" msgstr ""
#: src/Object/Post.php:523 #: src/Object/Post.php:520
msgid "via Wall-To-Wall:" msgid "via Wall-To-Wall:"
msgstr "" msgstr ""
#: src/Object/Post.php:569 #: src/Object/Post.php:566
#, php-format #, php-format
msgid "Reply to %s" msgid "Reply to %s"
msgstr "" msgstr ""
#: src/Object/Post.php:572 #: src/Object/Post.php:569
msgid "More" msgid "More"
msgstr "" msgstr ""
#: src/Object/Post.php:590 #: src/Object/Post.php:587
msgid "Notifier task is pending" msgid "Notifier task is pending"
msgstr "" msgstr ""
#: src/Object/Post.php:591 #: src/Object/Post.php:588
msgid "Delivery to remote servers is pending" msgid "Delivery to remote servers is pending"
msgstr "" msgstr ""
#: src/Object/Post.php:592 #: src/Object/Post.php:589
msgid "Delivery to remote servers is underway" msgid "Delivery to remote servers is underway"
msgstr "" msgstr ""
#: src/Object/Post.php:593 #: src/Object/Post.php:590
msgid "Delivery to remote servers is mostly done" msgid "Delivery to remote servers is mostly done"
msgstr "" msgstr ""
#: src/Object/Post.php:594 #: src/Object/Post.php:591
msgid "Delivery to remote servers is done" msgid "Delivery to remote servers is done"
msgstr "" msgstr ""
#: src/Object/Post.php:614 #: src/Object/Post.php:611
#, php-format #, php-format
msgid "%d comment" msgid "%d comment"
msgid_plural "%d comments" msgid_plural "%d comments"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: src/Object/Post.php:615 #: src/Object/Post.php:612
msgid "Show more" msgid "Show more"
msgstr "" msgstr ""
#: src/Object/Post.php:616 #: src/Object/Post.php:613
msgid "Show fewer" msgid "Show fewer"
msgstr "" msgstr ""
#: src/Object/Post.php:652 #: src/Object/Post.php:649
#, php-format #, php-format
msgid "Reshared by: %s" msgid "Reshared by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:657 #: src/Object/Post.php:654
#, php-format #, php-format
msgid "Viewed by: %s" msgid "Viewed by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:662 #: src/Object/Post.php:659
#, php-format #, php-format
msgid "Liked by: %s" msgid "Liked by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:667 #: src/Object/Post.php:664
#, php-format #, php-format
msgid "Disliked by: %s" msgid "Disliked by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:672 #: src/Object/Post.php:669
#, php-format #, php-format
msgid "Attended by: %s" msgid "Attended by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:677 #: src/Object/Post.php:674
#, php-format #, php-format
msgid "Maybe attended by: %s" msgid "Maybe attended by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:682 #: src/Object/Post.php:679
#, php-format #, php-format
msgid "Not attended by: %s" msgid "Not attended by: %s"
msgstr "" msgstr ""
#: src/Object/Post.php:687 #: src/Object/Post.php:684
#, php-format #, php-format
msgid "Reacted with %s by: %s" msgid "Reacted with %s by: %s"
msgstr "" msgstr ""