Merge remote-tracking branch 'upstream/develop' into ap-delivery-failure

This commit is contained in:
Michael 2019-03-26 21:36:46 +00:00
commit 55325f191b
122 changed files with 5068 additions and 2006 deletions

View file

@ -0,0 +1,220 @@
<?php
namespace Friendica\Util\Config;
use Friendica\App;
use Friendica\Core\Addon;
use Friendica\Core\Config\Cache\IConfigCache;
/**
* The ConfigFileLoader loads config-files and stores them in a IConfigCache ( @see IConfigCache )
*
* It is capable of loading the following config files:
* - *.config.php (current)
* - *.ini.php (deprecated)
* - *.htconfig.php (deprecated)
*/
class ConfigFileLoader extends ConfigFileManager
{
/**
* @var App\Mode
*/
private $appMode;
public function __construct($baseDir, App\Mode $mode)
{
parent::__construct($baseDir);
$this->appMode = $mode;
}
/**
* Load the configuration files into an configuration cache
*
* First loads the default value for all the configuration keys, then the legacy configuration files, then the
* expected local.config.php
*
* @param IConfigCache $config The config cache to load to
* @param bool $raw Setup the raw config format
*
* @throws \Exception
*/
public function setupCache(IConfigCache $config, $raw = false)
{
$config->load($this->loadCoreConfig('defaults'));
$config->load($this->loadCoreConfig('settings'));
$config->load($this->loadLegacyConfig('htpreconfig'), true);
$config->load($this->loadLegacyConfig('htconfig'), true);
$config->load($this->loadCoreConfig('local'), true);
// In case of install mode, add the found basepath (because there isn't a basepath set yet
if (!$raw && ($this->appMode->isInstall() || empty($config->get('system', 'basepath')))) {
// Setting at least the basepath we know
$config->set('system', 'basepath', $this->baseDir);
}
}
/**
* Tries to load the specified core-configuration and returns the config array.
*
* @param string $name The name of the configuration (default is empty, which means 'local')
*
* @return array The config array (empty if no config found)
*
* @throws \Exception if the configuration file isn't readable
*/
public function loadCoreConfig($name = '')
{
if (!empty($this->getConfigFullName($name))) {
return $this->loadConfigFile($this->getConfigFullName($name));
} elseif (!empty($this->getIniFullName($name))) {
return $this->loadINIConfigFile($this->getIniFullName($name));
} else {
return [];
}
}
/**
* Tries to load the specified addon-configuration and returns the config array.
*
* @param string $name The name of the configuration
*
* @return array The config array (empty if no config found)
*
* @throws \Exception if the configuration file isn't readable
*/
public function loadAddonConfig($name)
{
$filepath = $this->baseDir . DIRECTORY_SEPARATOR . // /var/www/html/
Addon::DIRECTORY . DIRECTORY_SEPARATOR . // addon/
$name . DIRECTORY_SEPARATOR . // openstreetmap/
self::SUBDIRECTORY . DIRECTORY_SEPARATOR . // config/
$name . ".config.php"; // openstreetmap.config.php
if (file_exists($filepath)) {
return $this->loadConfigFile($filepath);
} else {
return [];
}
}
/**
* Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array.
*
* @param string $name The name of the config file (default is empty, which means .htconfig.php)
*
* @return array The configuration array (empty if no config found)
*
* @deprecated since version 2018.09
*/
private function loadLegacyConfig($name = '')
{
$config = [];
if (!empty($this->getHtConfigFullName($name))) {
$a = new \stdClass();
$a->config = [];
include $this->getHtConfigFullName($name);
$htConfigCategories = array_keys($a->config);
// map the legacy configuration structure to the current structure
foreach ($htConfigCategories as $htConfigCategory) {
if (is_array($a->config[$htConfigCategory])) {
$keys = array_keys($a->config[$htConfigCategory]);
foreach ($keys as $key) {
$config[$htConfigCategory][$key] = $a->config[$htConfigCategory][$key];
}
} else {
$config['config'][$htConfigCategory] = $a->config[$htConfigCategory];
}
}
unset($a);
if (isset($db_host)) {
$config['database']['hostname'] = $db_host;
unset($db_host);
}
if (isset($db_user)) {
$config['database']['username'] = $db_user;
unset($db_user);
}
if (isset($db_pass)) {
$config['database']['password'] = $db_pass;
unset($db_pass);
}
if (isset($db_data)) {
$config['database']['database'] = $db_data;
unset($db_data);
}
if (isset($config['system']['db_charset'])) {
$config['database']['charset'] = $config['system']['db_charset'];
}
if (isset($pidfile)) {
$config['system']['pidfile'] = $pidfile;
unset($pidfile);
}
if (isset($default_timezone)) {
$config['system']['default_timezone'] = $default_timezone;
unset($default_timezone);
}
if (isset($lang)) {
$config['system']['language'] = $lang;
unset($lang);
}
}
return $config;
}
/**
* Tries to load the specified legacy configuration file and returns the config array.
*
* @deprecated since version 2018.12
* @param string $filepath
*
* @return array The configuration array
* @throws \Exception
*/
private function loadINIConfigFile($filepath)
{
$contents = include($filepath);
$config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
if ($config === false) {
throw new \Exception('Error parsing INI config file ' . $filepath);
}
return $config;
}
/**
* Tries to load the specified configuration file and returns the config array.
*
* The config format is PHP array and the template for configuration files is the following:
*
* <?php return [
* 'section' => [
* 'key' => 'value',
* ],
* ];
*
* @param string $filepath The filepath of the
* @return array The config array0
*
* @throws \Exception if the config cannot get loaded.
*/
private function loadConfigFile($filepath)
{
$config = include($filepath);
if (!is_array($config)) {
throw new \Exception('Error loading config file ' . $filepath);
}
return $config;
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Friendica\Util\Config;
/**
* An abstract class in case of handling with config files
*/
abstract class ConfigFileManager
{
/**
* The Sub directory of the config-files
* @var string
*/
const SUBDIRECTORY = 'config';
/**
* The default name of the user defined config file
* @var string
*/
const CONFIG_LOCAL = 'local';
/**
* The default name of the user defined ini file
* @var string
*/
const CONFIG_INI = 'local';
/**
* The default name of the user defined legacy config file
* @var string
*/
const CONFIG_HTCONFIG = 'htconfig';
protected $baseDir;
protected $configDir;
/**
* @param string $baseDir The base directory of Friendica
*/
public function __construct($baseDir)
{
$this->baseDir = $baseDir;
$this->configDir = $baseDir . DIRECTORY_SEPARATOR . self::SUBDIRECTORY;
}
/**
* Gets the full name (including the path) for a *.config.php (default is local.config.php)
*
* @param string $name The config name (default is empty, which means local.config.php)
*
* @return string The full name or empty if not found
*/
protected function getConfigFullName($name = '')
{
$name = !empty($name) ? $name : self::CONFIG_LOCAL;
$fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.config.php';
return file_exists($fullName) ? $fullName : '';
}
/**
* Gets the full name (including the path) for a *.ini.php (default is local.ini.php)
*
* @param string $name The config name (default is empty, which means local.ini.php)
*
* @return string The full name or empty if not found
*/
protected function getIniFullName($name = '')
{
$name = !empty($name) ? $name : self::CONFIG_INI;
$fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.ini.php';
return file_exists($fullName) ? $fullName : '';
}
/**
* Gets the full name (including the path) for a .*.php (default is .htconfig.php)
*
* @param string $name The config name (default is empty, which means .htconfig.php)
*
* @return string The full name or empty if not found
*/
protected function getHtConfigFullName($name = '')
{
$name = !empty($name) ? $name : self::CONFIG_HTCONFIG;
$fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php';
return file_exists($fullName) ? $fullName : '';
}
}

View file

@ -0,0 +1,341 @@
<?php
namespace Friendica\Util\Config;
/**
* The ConfigFileSaver saves specific variables into the config-files
*
* It is capable of loading the following config files:
* - *.config.php (current)
* - *.ini.php (deprecated)
* - *.htconfig.php (deprecated)
*/
class ConfigFileSaver extends ConfigFileManager
{
/**
* The standard indentation for config files
* @var string
*/
const INDENT = "\t";
/**
* The settings array to save to
* @var array
*/
private $settings = [];
/**
* Adds a given value to the config file
* Either it replaces the current value or it will get added
*
* @param string $cat The configuration category
* @param string $key The configuration key
* @param string $value The new value
*/
public function addConfigValue($cat, $key, $value)
{
$settingsCount = count(array_keys($this->settings));
for ($i = 0; $i < $settingsCount; $i++) {
// if already set, overwrite the value
if ($this->settings[$i]['cat'] === $cat &&
$this->settings[$i]['key'] === $key) {
$this->settings[$i] = ['cat' => $cat, 'key' => $key, 'value' => $value];
return;
}
}
$this->settings[] = ['cat' => $cat, 'key' => $key, 'value' => $value];
}
/**
* Resetting all added configuration entries so far
*/
public function reset()
{
$this->settings = [];
}
/**
* Save all added configuration entries to the given config files
* After updating the config entries, all configuration entries will be reseted
*
* @param string $name The name of the configuration file (default is empty, which means the default name each type)
*
* @return bool true, if at least one configuration file was successfully updated or nothing to do
*/
public function saveToConfigFile($name = '')
{
// If no settings et, return true
if (count(array_keys($this->settings)) === 0) {
return true;
}
$saved = false;
// Check for the *.config.php file inside the /config/ path
list($reading, $writing) = $this->openFile($this->getConfigFullName($name));
if (isset($reading) && isset($writing)) {
$this->saveConfigFile($reading, $writing);
// Close the current file handler and rename them
if ($this->closeFile($this->getConfigFullName($name), $reading, $writing)) {
// just return true, if everything went fine
$saved = true;
}
}
// Check for the *.ini.php file inside the /config/ path
list($reading, $writing) = $this->openFile($this->getIniFullName($name));
if (isset($reading) && isset($writing)) {
$this->saveINIConfigFile($reading, $writing);
// Close the current file handler and rename them
if ($this->closeFile($this->getIniFullName($name), $reading, $writing)) {
// just return true, if everything went fine
$saved = true;
}
}
// Check for the *.php file (normally .htconfig.php) inside the / path
list($reading, $writing) = $this->openFile($this->getHtConfigFullName($name));
if (isset($reading) && isset($writing)) {
$this->saveToLegacyConfig($reading, $writing);
// Close the current file handler and rename them
if ($this->closeFile($this->getHtConfigFullName($name), $reading, $writing)) {
// just return true, if everything went fine
$saved = true;
}
}
$this->reset();
return $saved;
}
/**
* Opens a config file and returns two handler for reading and writing
*
* @param string $fullName The full name of the current config
*
* @return array An array containing the two reading and writing handler
*/
private function openFile($fullName)
{
if (empty($fullName)) {
return [null, null];
}
try {
$reading = fopen($fullName, 'r');
} catch (\Exception $exception) {
return [null, null];
}
if (!$reading) {
return [null, null];
}
try {
$writing = fopen($fullName . '.tmp', 'w');
} catch (\Exception $exception) {
fclose($reading);
return [null, null];
}
if (!$writing) {
fclose($reading);
return [null, null];
}
return [$reading, $writing];
}
/**
* Close and rename the config file
*
* @param string $fullName The full name of the current config
* @param resource $reading The reading resource handler
* @param resource $writing The writing resource handler
*
* @return bool True, if the close was successful
*/
private function closeFile($fullName, $reading, $writing)
{
fclose($reading);
fclose($writing);
try {
$renamed = rename($fullName, $fullName . '.old');
} catch (\Exception $exception) {
return false;
}
if (!$renamed) {
return false;
}
try {
$renamed = rename($fullName . '.tmp', $fullName);
} catch (\Exception $exception) {
// revert the move of the current config file to have at least the old config
rename($fullName . '.old', $fullName);
return false;
}
if (!$renamed) {
// revert the move of the current config file to have at least the old config
rename($fullName . '.old', $fullName);
return false;
}
return true;
}
/**
* Saves all configuration values to a config file
*
* @param resource $reading The reading handler
* @param resource $writing The writing handler
*/
private function saveConfigFile($reading, $writing)
{
$settingsCount = count(array_keys($this->settings));
$categoryFound = array_fill(0, $settingsCount, false);
$categoryBracketFound = array_fill(0, $settingsCount, false);;
$lineFound = array_fill(0, $settingsCount, false);;
$lineArrowFound = array_fill(0, $settingsCount, false);;
while (!feof($reading)) {
$line = fgets($reading);
// check for each added setting if we have to replace a config line
for ($i = 0; $i < $settingsCount; $i++) {
// find the first line like "'system' =>"
if (!$categoryFound[$i] && stristr($line, sprintf('\'%s\'', $this->settings[$i]['cat']))) {
$categoryFound[$i] = true;
}
// find the first line with a starting bracket ( "[" )
if ($categoryFound[$i] && !$categoryBracketFound[$i] && stristr($line, '[')) {
$categoryBracketFound[$i] = true;
}
// find the first line with the key like "'value'"
if ($categoryBracketFound[$i] && !$lineFound[$i] && stristr($line, sprintf('\'%s\'', $this->settings[$i]['key']))) {
$lineFound[$i] = true;
}
// find the first line with an arrow ("=>") after finding the key
if ($lineFound[$i] && !$lineArrowFound[$i] && stristr($line, '=>')) {
$lineArrowFound[$i] = true;
}
// find the current value and replace it
if ($lineArrowFound[$i] && preg_match_all('/\'(.*?)\'/', $line, $matches, PREG_SET_ORDER)) {
$lineVal = end($matches)[0];
$line = str_replace($lineVal, '\'' . $this->settings[$i]['value'] . '\'', $line);
$categoryFound[$i] = false;
$categoryBracketFound[$i] = false;
$lineFound[$i] = false;
$lineArrowFound[$i] = false;
// if a line contains a closing bracket for the category ( "]" ) and we didn't find the key/value pair,
// add it as a new line before the closing bracket
} elseif ($categoryBracketFound[$i] && !$lineArrowFound[$i] && stristr($line, ']')) {
$categoryFound[$i] = false;
$categoryBracketFound[$i] = false;
$lineFound[$i] = false;
$lineArrowFound[$i] = false;
$newLine = sprintf(self::INDENT . self::INDENT . '\'%s\' => \'%s\',' . PHP_EOL, $this->settings[$i]['key'], $this->settings[$i]['value']);
$line = $newLine . $line;
}
}
fputs($writing, $line);
}
}
/**
* Saves a value to a ini file
*
* @param resource $reading The reading handler
* @param resource $writing The writing handler
*/
private function saveINIConfigFile($reading, $writing)
{
$settingsCount = count(array_keys($this->settings));
$categoryFound = array_fill(0, $settingsCount, false);
while (!feof($reading)) {
$line = fgets($reading);
// check for each added setting if we have to replace a config line
for ($i = 0; $i < $settingsCount; $i++) {
// find the category of the current setting
if (!$categoryFound[$i] && stristr($line, sprintf('[%s]', $this->settings[$i]['cat']))) {
$categoryFound[$i] = true;
// check the current value
} elseif ($categoryFound[$i] && preg_match_all('/^' . $this->settings[$i]['key'] . '\s*=\s*(.*?)$/', $line, $matches, PREG_SET_ORDER)) {
$line = $this->settings[$i]['key'] . ' = ' . $this->settings[$i]['value'] . PHP_EOL;
$categoryFound[$i] = false;
// If end of INI file, add the line before the INI end
} elseif ($categoryFound[$i] && (preg_match_all('/^\[.*?\]$/', $line) || preg_match_all('/^INI;.*$/', $line))) {
$categoryFound[$i] = false;
$newLine = $this->settings[$i]['key'] . ' = ' . $this->settings[$i]['value'] . PHP_EOL;
$line = $newLine . $line;
}
}
fputs($writing, $line);
}
}
/**
* Saves a value to a .php file (normally .htconfig.php)
*
* @param resource $reading The reading handler
* @param resource $writing The writing handler
*/
private function saveToLegacyConfig($reading, $writing)
{
$settingsCount = count(array_keys($this->settings));
$found = array_fill(0, $settingsCount, false);
while (!feof($reading)) {
$line = fgets($reading);
// check for each added setting if we have to replace a config line
for ($i = 0; $i < $settingsCount; $i++) {
// check for a non plain config setting (use category too)
if ($this->settings[$i]['cat'] !== 'config' && preg_match_all('/^\$a\-\>config\[\'' . $this->settings[$i]['cat'] . '\'\]\[\'' . $this->settings[$i]['key'] . '\'\]\s*=\s\'*(.*?)\';$/', $line, $matches, PREG_SET_ORDER)) {
$line = '$a->config[\'' . $this->settings[$i]['cat'] . '\'][\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL;
$found[$i] = true;
// check for a plain config setting (don't use a category)
} elseif ($this->settings[$i]['cat'] === 'config' && preg_match_all('/^\$a\-\>config\[\'' . $this->settings[$i]['key'] . '\'\]\s*=\s\'*(.*?)\';$/', $line, $matches, PREG_SET_ORDER)) {
$line = '$a->config[\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL;
$found[$i] = true;
}
}
fputs($writing, $line);
}
for ($i = 0; $i < $settingsCount; $i++) {
if (!$found[$i]) {
if ($this->settings[$i]['cat'] !== 'config') {
$line = '$a->config[\'' . $this->settings[$i]['cat'] . '\'][\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL;
} else {
$line = '$a->config[\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL;
}
fputs($writing, $line);
}
}
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Friendica\Util;
/**
* Get Introspection information about the current call
*/
class Introspection
{
private $skipStackFramesCount;
private $skipClassesPartials;
private $skipFunctions = [
'call_user_func',
'call_user_func_array',
];
/**
* @param array $skipClassesPartials An array of classes to skip during logging
* @param int $skipStackFramesCount If the logger should use information from other hierarchy levels of the call
*/
public function __construct($skipClassesPartials = array(), $skipStackFramesCount = 0)
{
$this->skipClassesPartials = $skipClassesPartials;
$this->skipStackFramesCount = $skipStackFramesCount;
}
/**
* Adds new classes to get skipped
* @param array $classNames
*/
public function addClasses(array $classNames)
{
$this->skipClassesPartials = array_merge($this->skipClassesPartials, $classNames);
}
/**
* Returns the introspection record of the current call
*
* @return array
*/
public function getRecord()
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$i = 1;
while ($this->isTraceClassOrSkippedFunction($trace, $i)) {
$i++;
}
$i += $this->skipStackFramesCount;
return [
'file' => isset($trace[$i - 1]['file']) ? basename($trace[$i - 1]['file']) : null,
'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null,
'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null,
];
}
/**
* Checks if the current trace class or function has to be skipped
*
* @param array $trace The current trace array
* @param int $index The index of the current hierarchy level
*
* @return bool True if the class or function should get skipped, otherwise false
*/
private function isTraceClassOrSkippedFunction(array $trace, $index)
{
if (!isset($trace[$index])) {
return false;
}
if (isset($trace[$index]['class'])) {
foreach ($this->skipClassesPartials as $part) {
if (strpos($trace[$index]['class'], $part) !== false) {
return true;
}
}
} elseif (in_array($trace[$index]['function'], $this->skipFunctions)) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,158 @@
<?php
namespace Friendica\Util\Logger;
use Friendica\Util\Introspection;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* This class contains all necessary dependencies and calls for Friendica
* Every new Logger should extend this class and define, how addEntry() works
*
* Additional information for each Logger, who extends this class:
* - Introspection
* - UID for each call
* - Channel of the current call (i.e. index, worker, daemon, ...)
*/
abstract class AbstractLogger implements LoggerInterface
{
/**
* The output channel of this logger
* @var string
*/
protected $channel;
/**
* The Introspection for the current call
* @var Introspection
*/
protected $introspection;
/**
* The UID of the current call
* @var string
*/
protected $logUid;
/**
* Adds a new entry to the log
*
* @param int $level
* @param string $message
* @param array $context
*
* @return void
*/
abstract protected function addEntry($level, $message, $context = []);
/**
* @param string $channel The output channel
* @param Introspection $introspection The introspection of the current call
*
* @throws \Exception
*/
public function __construct($channel, Introspection $introspection)
{
$this->channel = $channel;
$this->introspection = $introspection;
$this->logUid = Strings::getRandomHex(6);
}
/**
* Simple interpolation of PSR-3 compliant replacements ( variables between '{' and '}' )
* @see https://www.php-fig.org/psr/psr-3/#12-message
*
* @param string $message
* @param array $context
*
* @return string the interpolated message
*/
protected function psrInterpolate($message, array $context = array())
{
$replace = [];
foreach ($context as $key => $value) {
// check that the value can be casted to string
if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
$replace['{' . $key . '}'] = $value;
} elseif (is_array($value)) {
$replace['{' . $key . '}'] = @json_encode($value);
}
}
return strtr($message, $replace);
}
/**
* {@inheritdoc}
*/
public function emergency($message, array $context = array())
{
$this->addEntry(LogLevel::EMERGENCY, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function alert($message, array $context = array())
{
$this->addEntry(LogLevel::ALERT, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function critical($message, array $context = array())
{
$this->addEntry(LogLevel::CRITICAL, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function error($message, array $context = array())
{
$this->addEntry(LogLevel::ERROR, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function warning($message, array $context = array())
{
$this->addEntry(LogLevel::WARNING, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function notice($message, array $context = array())
{
$this->addEntry(LogLevel::NOTICE, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function info($message, array $context = array())
{
$this->addEntry(LogLevel::INFO, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function debug($message, array $context = array())
{
$this->addEntry(LogLevel::DEBUG, (string) $message, $context);
}
/**
* {@inheritdoc}
*/
public function log($level, $message, array $context = array())
{
$this->addEntry($level, (string) $message, $context);
}
}

View file

@ -1,94 +0,0 @@
<?php
namespace Friendica\Util\Logger;
use Monolog\Logger;
use Monolog\Processor\ProcessorInterface;
/**
* Injects line/file//function where the log message came from
*
* Based on the class IntrospectionProcessor without the "class" information
* @see IntrospectionProcessor
*/
class FriendicaIntrospectionProcessor implements ProcessorInterface
{
private $level;
private $skipStackFramesCount;
private $skipClassesPartials;
private $skipFunctions = [
'call_user_func',
'call_user_func_array',
];
/**
* @param string|int $level The minimum logging level at which this Processor will be triggered
* @param array $skipClassesPartials An array of classes to skip during logging
* @param int $skipStackFramesCount If the logger should use information from other hierarchy levels of the call
*/
public function __construct($level = Logger::DEBUG, $skipClassesPartials = array(), $skipStackFramesCount = 0)
{
$this->level = Logger::toMonologLevel($level);
$this->skipClassesPartials = array_merge(array('Monolog\\'), $skipClassesPartials);
$this->skipStackFramesCount = $skipStackFramesCount;
}
public function __invoke(array $record)
{
// return if the level is not high enough
if ($record['level'] < $this->level) {
return $record;
}
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$i = 1;
while ($this->isTraceClassOrSkippedFunction($trace, $i)) {
$i++;
}
$i += $this->skipStackFramesCount;
// we should have the call source now
$record['extra'] = array_merge(
$record['extra'],
[
'file' => isset($trace[$i - 1]['file']) ? basename($trace[$i - 1]['file']) : null,
'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null,
'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null,
]
);
return $record;
}
/**
* Checks if the current trace class or function has to be skipped
*
* @param array $trace The current trace array
* @param int $index The index of the current hierarchy level
* @return bool True if the class or function should get skipped, otherwise false
*/
private function isTraceClassOrSkippedFunction(array $trace, $index)
{
if (!isset($trace[$index])) {
return false;
}
if (isset($trace[$index]['class'])) {
foreach ($this->skipClassesPartials as $part) {
if (strpos($trace[$index]['class'], $part) !== false) {
return true;
}
}
} elseif (in_array($trace[$index]['function'], $this->skipFunctions)) {
return true;
}
return false;
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Friendica\Util\Logger;
namespace Friendica\Util\Logger\Monolog;
use Monolog\Handler;
use Monolog\Logger;
@ -11,7 +11,7 @@ use Monolog\Logger;
* If you want to debug only interactions from your IP or the IP of a remote server for federation debug,
* you'll use Logger::develop() for the duration of your work, and you clean it up when you're done before submitting your PR.
*/
class FriendicaDevelopHandler extends Handler\AbstractHandler
class DevelopHandler extends Handler\AbstractHandler
{
/**
* @var string The IP of the developer who wants to debug

View file

@ -0,0 +1,43 @@
<?php
namespace Friendica\Util\Logger\Monolog;
use Friendica\Util\Introspection;
use Monolog\Logger;
use Monolog\Processor\ProcessorInterface;
/**
* Injects line/file//function where the log message came from
*/
class IntrospectionProcessor implements ProcessorInterface
{
private $level;
private $introspection;
/**
* @param Introspection $introspection Holds the Introspection of the current call
* @param string|int $level The minimum logging level at which this Processor will be triggered
*/
public function __construct(Introspection $introspection, $level = Logger::DEBUG)
{
$this->level = Logger::toMonologLevel($level);
$introspection->addClasses(array('Monolog\\'));
$this->introspection = $introspection;
}
public function __invoke(array $record)
{
// return if the level is not high enough
if ($record['level'] < $this->level) {
return $record;
}
// we should have the call source now
$record['extra'] = array_merge(
$record['extra'],
$this->introspection->getRecord()
);
return $record;
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace Friendica\Util\Logger;
use Friendica\Core\System;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* This Logger adds additional profiling data in case profiling is enabled.
* It uses a predefined logger.
*/
class ProfilerLogger implements LoggerInterface
{
/**
* The Logger of the current call
* @var LoggerInterface
*/
private $logger;
/**
* The Profiler for the current call
* @var Profiler
*/
protected $profiler;
/**
* ProfilerLogger constructor.
* @param LoggerInterface $logger The Logger of the current call
* @param Profiler $profiler The profiler of the current call
*/
public function __construct(LoggerInterface $logger, Profiler $profiler)
{
$this->logger = $logger;
$this->profiler = $profiler;
}
/**
* {@inheritdoc}
*/
public function emergency($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->emergency($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function alert($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->alert($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function critical($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->critical($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function error($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->error($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function warning($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->warning($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function notice($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->notice($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function info($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->info($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function debug($message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->debug($message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
/**
* {@inheritdoc}
*/
public function log($level, $message, array $context = array())
{
$stamp1 = microtime(true);
$this->logger->log($level, $message, $context);
$this->profiler->saveTimestamp($stamp1, 'file', System::callstack());
}
}

27
src/Util/Logger/README.md Normal file
View file

@ -0,0 +1,27 @@
## Friendica\Util\Logger
This namespace contains the different implementations of a Logger.
### Configuration guideline
The following settings are possible for `logger_config`:
- `monolog`: A Logging framework with lots of additions (see [Monolog](https://github.com/Seldaek/monolog/)). There are just Friendica additions inside the Monolog directory
- [`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

@ -0,0 +1,197 @@
<?php
namespace Friendica\Util\Logger;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Introspection;
use Psr\Log\LogLevel;
/**
* A Logger instance for logging into a stream (file, stdout, stderr)
*/
class StreamLogger extends AbstractLogger
{
/**
* The minimum loglevel at which this logger will be triggered
* @var string
*/
private $logLevel;
/**
* The file URL of the stream (if needed)
* @var string
*/
private $url;
/**
* The stream, where the current logger is writing into
* @var resource
*/
private $stream;
/**
* The current process ID
* @var int
*/
private $pid;
/**
* An error message
* @var string
*/
private $errorMessage;
/**
* Translates LogLevel log levels to integer values
* @var array
*/
private $levelToInt = [
LogLevel::EMERGENCY => 0,
LogLevel::ALERT => 1,
LogLevel::CRITICAL => 2,
LogLevel::ERROR => 3,
LogLevel::WARNING => 4,
LogLevel::NOTICE => 5,
LogLevel::INFO => 6,
LogLevel::DEBUG => 7,
];
/**
* {@inheritdoc}
* @param string|resource $stream The stream to write with this logger (either a file or a stream, i.e. stdout)
* @param string $level The minimum loglevel at which this logger will be triggered
*
* @throws \Exception
*/
public function __construct($channel, $stream, Introspection $introspection, $level = LogLevel::DEBUG)
{
parent::__construct($channel, $introspection);
if (is_resource($stream)) {
$this->stream = $stream;
} elseif (is_string($stream)) {
$this->url = $stream;
} else {
throw new \InvalidArgumentException('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 \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level));
}
}
public function close()
{
if ($this->url && is_resource($this->stream)) {
fclose($this->stream);
}
$this->stream = null;
}
/**
* Adds a new entry to the log
*
* @param int $level
* @param string $message
* @param array $context
*
* @return void
*/
protected function addEntry($level, $message, $context = [])
{
if (!array_key_exists($level, $this->levelToInt)) {
throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level));
}
$logLevel = $this->levelToInt[$level];
if ($logLevel > $this->logLevel) {
return;
}
$this->checkStream();
$formattedLog = $this->formatLog($level, $message, $context);
fwrite($this->stream, $formattedLog);
}
/**
* Formats a log record for the syslog output
*
* @param int $level The loglevel/priority
* @param string $message The message
* @param array $context The context of this call
*
* @return string the formatted syslog output
*/
private function formatLog($level, $message, $context = [])
{
$record = $this->introspection->getRecord();
$record = array_merge($record, ['uid' => $this->logUid, 'process_id' => $this->pid]);
$logMessage = '';
$logMessage .= DateTimeFormat::utcNow() . ' ';
$logMessage .= $this->channel . ' ';
$logMessage .= '[' . strtoupper($level) . ']: ';
$logMessage .= $this->psrInterpolate($message, $context) . ' ';
$logMessage .= @json_encode($context) . ' - ';
$logMessage .= @json_encode($record);
$logMessage .= PHP_EOL;
return $logMessage;
}
private function checkStream()
{
if (is_resource($this->stream)) {
return;
}
if (empty($this->url)) {
throw new \LogicException('Missing stream URL.');
}
$this->createDir();
set_error_handler([$this, 'customErrorHandler']);
$this->stream = fopen($this->url, 'ab');
restore_error_handler();
if (!is_resource($this->stream)) {
$this->stream = null;
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: ' . $this->errorMessage, $this->url));
}
}
private function createDir()
{
$dirname = null;
$pos = strpos($this->url, '://');
if (!$pos) {
$dirname = dirname($this->url);
}
if (substr($this->url, 0, 7) === 'file://') {
$dirname = dirname(substr($this->url, 7));
}
if (isset($dirname) && !is_dir($dirname)) {
set_error_handler([$this, 'customErrorHandler']);
$status = mkdir($dirname, 0777, true);
restore_error_handler();
if (!$status && !is_dir($dirname)) {
throw new \UnexpectedValueException(sprintf('Directory "%s" cannot get created: ' . $this->errorMessage, $dirname));
}
}
}
private function customErrorHandler($code, $msg)
{
$this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace Friendica\Util\Logger;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Util\Introspection;
use Psr\Log\LogLevel;
/**
* A Logger instance for syslogging (fast, but simple)
* @see http://php.net/manual/en/function.syslog.php
*/
class SyslogLogger extends AbstractLogger
{
const IDENT = 'Friendica';
/**
* Translates LogLevel log levels to syslog log priorities.
* @var array
*/
private $logLevels = [
LogLevel::DEBUG => LOG_DEBUG,
LogLevel::INFO => LOG_INFO,
LogLevel::NOTICE => LOG_NOTICE,
LogLevel::WARNING => LOG_WARNING,
LogLevel::ERROR => LOG_ERR,
LogLevel::CRITICAL => LOG_CRIT,
LogLevel::ALERT => LOG_ALERT,
LogLevel::EMERGENCY => LOG_EMERG,
];
/**
* Translates log priorities to string outputs
* @var array
*/
private $logToString = [
LOG_DEBUG => 'DEBUG',
LOG_INFO => 'INFO',
LOG_NOTICE => 'NOTICE',
LOG_WARNING => 'WARNING',
LOG_ERR => 'ERROR',
LOG_CRIT => 'CRITICAL',
LOG_ALERT => 'ALERT',
LOG_EMERG => 'EMERGENCY'
];
/**
* Indicates what logging options will be used when generating a log message
* @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
*
* @var int
*/
private $logOpts;
/**
* Used to specify what type of program is logging the message
* @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters
*
* @var int
*/
private $logFacility;
/**
* The minimum loglevel at which this logger will be triggered
* @var int
*/
private $logLevel;
/**
* A error message of the current operation
* @var string
*/
private $errorMessage;
/**
* {@inheritdoc}
* @param string $level The minimum loglevel at which this logger will be triggered
* @param int $logOpts Indicates what logging options will be used when generating a log message
* @param int $logFacility Used to specify what type of program is logging the message
*
* @throws \Exception
*/
public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER)
{
parent::__construct($channel, $introspection);
$this->logOpts = $logOpts;
$this->logFacility = $logFacility;
$this->logLevel = $this->mapLevelToPriority($level);
$this->introspection->addClasses(array(self::class));
}
/**
* Adds a new entry to the syslog
*
* @param int $level
* @param string $message
* @param array $context
*
* @throws InternalServerErrorException if the syslog isn't available
*/
protected function addEntry($level, $message, $context = [])
{
$logLevel = $this->mapLevelToPriority($level);
if ($logLevel > $this->logLevel) {
return;
}
$formattedLog = $this->formatLog($logLevel, $message, $context);
$this->write($logLevel, $formattedLog);
}
/**
* Maps the LogLevel (@see LogLevel ) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters )
*
* @param string $level A LogLevel
*
* @return int The SysLog priority
*
* @throws \Psr\Log\InvalidArgumentException If the loglevel isn't valid
*/
public function mapLevelToPriority($level)
{
if (!array_key_exists($level, $this->logLevels)) {
throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level));
}
return $this->logLevels[$level];
}
/**
* Closes the Syslog
*/
public function close()
{
closelog();
}
/**
* Writes a message to the syslog
* @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters
*
* @param int $priority The Priority
* @param string $message The message of the log
*
* @throws InternalServerErrorException if syslog cannot be used
*/
private function write($priority, $message)
{
set_error_handler([$this, 'customErrorHandler']);
$opened = openlog(self::IDENT, $this->logOpts, $this->logFacility);
restore_error_handler();
if (!$opened) {
throw new \UnexpectedValueException(sprintf('Can\'t open syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility));
}
$this->syslogWrapper($priority, $message);
}
/**
* Formats a log record for the syslog output
*
* @param int $level The loglevel/priority
* @param string $message The message
* @param array $context The context of this call
*
* @return string the formatted syslog output
*/
private function formatLog($level, $message, $context = [])
{
$record = $this->introspection->getRecord();
$record = array_merge($record, ['uid' => $this->logUid]);
$logMessage = '';
$logMessage .= $this->channel . ' ';
$logMessage .= '[' . $this->logToString[$level] . ']: ';
$logMessage .= $this->psrInterpolate($message, $context) . ' ';
$logMessage .= @json_encode($context) . ' - ';
$logMessage .= @json_encode($record);
return $logMessage;
}
private function customErrorHandler($code, $msg)
{
$this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
}
/**
* A syslog wrapper to make syslog functionality testable
*
* @param int $level The syslog priority
* @param string $entry The message to send to the syslog function
*/
protected function syslogWrapper($level, $entry)
{
set_error_handler([$this, 'customErrorHandler']);
$written = syslog($level, $entry);
restore_error_handler();
if (!$written) {
throw new \UnexpectedValueException(sprintf('Can\'t write into syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility));
}
}
}

View file

@ -0,0 +1,140 @@
<?php
namespace Friendica\Util\Logger;
use Psr\Log\LoggerInterface;
/**
* A Logger instance to not log
*/
class VoidLogger implements LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array())
{
return;
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array())
{
return;
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array())
{
return;
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array())
{
return;
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array())
{
return;
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array())
{
return;
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array())
{
return;
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array())
{
return;
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = array())
{
return;
}
}