Add already existing codebase

This commit is contained in:
linarphy 2025-03-28 22:15:09 +01:00
commit 80c62f33ce
Signed by: linarphy
GPG key ID: D7AB13634CFBEE19
14 changed files with 941 additions and 0 deletions

69
class/core/Path.class.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace core;
/**
* Generic class to specify string type
*/
class Path
{
/**
* Path value
*
* @var \string
*/
public \array $parts;
/**
* Constructor of Path
*
* @param \string $value Path string
*/
public function __construct(\string $value)
{
$parts = \mb_split(\DIRECTORY_SEPARATOR, $value);
if ($parts === False)
{
\throw \core\PathException(_('cannot extract path components for the given value'), 1);
}
$this->parts = $parts;
}
/**
* Append a child path to this parent path without caring that the child is inside the parent
*
* @param \core\Path | \string $path Child path
*/
public function append(path: \core\Path | \string: $path) : self
{
if (\is_string($path))
{
$path = \core\Path($path);
}
$this->parts = \array_merge($this->parts, $path->parts);
return $this;
}
/**
* Append a child path to this parent path, checking if the result path is inside the parent path as it should
*
* @param \core\Path | \string $path Child path
*/
public function safe_append(path: \core\Path | \string $path) : self
{
$old_realpath = \realpath($this->__tostring());
$new_realpath = \realpath($this->append($path)->__tostring());
if (\strpos($new_realpath, $old_realpath) !== 0)
{
$this = \core\Path($old_realpath);
}
return $this;
}
public function __toString() : \string
{
return \DIRECTORY_SEPARATOR . \implode(\DIRECTORY_SEPARATOR, $this->parts);
}
}
?>

View file

@ -0,0 +1,78 @@
<?php
namespace core\config;
/**
* Configuration
*/
class Configuration
{
/**
* Configuration constructor
*
* @param \array[\string] $configuration Array containing configuration value string
*/
public function __construct(configuration: \array[\string] $configuration)
{
$this->configuration = $configuration;
}
/**
* Get a subpart of this configuration or a value in this configuration
*
* @param \string $keys,... Ordered tuple of key, which give the address of the wanted part
*
* @return \core\config\Configuration | \string
*/
public function get(keys: \string ...$keys) : \core\config\Configuration | \string
{
$part = $this->configuration;
foreach ($keys as $key)
{
if (\key_exists($key, $part))
{
\throw new \core\config\ConfigException(
\core\substitute(_('the key {key} does not exist in this part of the configuration'), array('key' => $key))
);
}
$part = $part[$key];
}
if (\is_array($part))
{
return new \core\config\Configuration($part);
}
if (\is_string($part))
{
return $part
}
\throw new \core\config\ConfigException(
\core\substitute(_('the type {type} should not be in a configuration'), array('type' => \gettype($part)))
);
}
/**
* Get a subpart of this configuration or a value in this configuration
*
* @param \string $keys,... Ordered tuple of key, which give the address of the wanted part
*
* @return \bool
*/
public function exist(keys: \string ...$keys) : \bool
{
$part = $this->configuration;
foreach ($keys as $key)
{
if (\key_exists($key, $part))
{
return False;
}
$part = $part[$key];
}
return True;
}
}
?>

View file

@ -0,0 +1,68 @@
<?php
namespace core\config;
/**
* Special configuration children, used especialy for the whole configuration
*/
class GlobalConfiguration
{
/**
* Equivalent to get method of \core\config\Configuration, but load the whole configuration
*
* @param \string $keys,... Ordered tuple of keys
*
* @return \core\config\Configuration | \string
*/
public static function read(...$keys) : \core\config\Configuration | \string
{
$part = self::load();
foreach ($keys as $key)
{
if (\key_exists($key, $part))
{
\throw new \core\config\ConfigException(
\core\substitute(_('the key {key} does not exist in this part of the configuration'), array('key' => $key))
);
}
$part = $part[$key];
}
if (\is_array($part))
{
return new \core\config\Configuration($part);
}
if (\is_string($part))
{
return $part
}
\throw new \core\config\ConfigException(
\core\substitute(_('the type {type} should not be in a configuration'), array('type' => \gettype($part)))
);
}
/**
* Get a subpart of this configuration or a value in this configuration
*
* @param \string $keys,... Ordered tuple of key, which give the address of the wanted part
*
* @return \bool
*/
public function exist(keys: \string ...$keys) : \bool
{
$part = self::load();
foreach ($keys as $key)
{
if (\key_exists($key, $part))
{
return False;
}
$part = $part[$key];
}
return True;
}
}
?>

View file

@ -0,0 +1,49 @@
<?php
namespace core\exception;
abstract class CustomException extends \Exception
{
/**
* default tags associated to this exception
*
* @var \class[]
*/
const TAGS = [];
/**
* CustomException constructor
*
* @param ?\string $message Error message (default to null)
*
* @param \int $code User defined code (default to 0)
*
* @param ?\Throwable $previous throwable (default to null)
*
* @param ?\array $tags Tags of the generated log event (default to null)
*/
public function __construct(?\string $message = null, \int $code = 0, ?\Throwable $previous = null, ?\array $tags = null)
{
if (\is_null($message))
{
$message = _("empty message")
}
if (\is_null($tags))
{
$tags = $this::TAGS;
}
parent::__construct($message, $code, $previous);
}
public funtion __tostring() : string
{
return \htmlspecialchars(\get_class($this)) .
' ' . \htmlspecialchars($this->message) .
' in ' . \htmlspecialchars($this->file) .
'(' . \htmlspecialchars($this->line) . ')\n' .
\htmlspecialchars($this->getTraceAsString());
}
}
?>

View file

@ -0,0 +1,85 @@
<?php
namespace core\log;
/**
* A log event
*/
class Event
{
/**
* Message associated to this event
*
* @var \string
*/
protected \string $message;
/**
* Time associated to this event
*
* @var \int
*/
protected \int $timestamp;
/**
* Tags associated to this event
*
* @var \core\log\Tag[]
*/
protected \array $tags;
/**
* Backtrace associated to this event
*
* @var ?\array
*/
protected ?\array $backtrace;
public function __construct(message: \string $message, tags: \array $tags, backtrace: ?\array = null)
{
$this->message = $message; # not escaped
$this->tags = $tags;
$this->timestamp = \time();
if ($backtrace === null)
{
$backtrace = \debug_backtrace();
$key = \array_search(__FUNCTION__, \array_column($backtrace, 'function'),); # NOTE: manual search, can change with time
$backtrace = $backtrace[$key];
}
$this->backtrace = $backtrace;
}
/**
* Display this event for logging
*
* This can be changed from configuration.
* The EOL (end of line sequence) is automatically added at the end.
*/
public function display() : \string
{
$configuration = \core\config\GlobalConfiguration::read('class', 'core', 'log');
$format = '[{tags}] - {date}: {message} in {file} at line {line}'; # NOTE: hard-coded fallback
$date_format = 'Y-m-d H:i:s'; # NOTE: hard-coded fallback
if ($configuration->exist('format'))
{
$format = $configuration->get('format');
}
if ($configuration->exist('date-format'))
{
$date_format = $configuration->get('date-format');
}
return \core\substitute(
$format,
array(
'date' => \date($date_format),
'file' => $this->backtrace['file'],
'line' => $this->backtrace['line'],
'message' => $this->message,
'tags' => $this->tags,
),
) . \PHP_EOL;
}
}
?>

View file

@ -0,0 +1,85 @@
<?php
namespace core\log;
/**
* Manage server logs
*/
class Logger
{
/**
* Array of streams used by this logger
*
* @var \core\log\Stream[]
*/
protected \array $streams;
/**
* Constructor of Logger
*
* @param \core\path\Path $path Path to the folder where logs manaded by this object are stored
*/
public function __construct()
{
$this->streams = \array();
}
/**
* Add a stream to manage
*
* @param \core\log\Tag $tag Tag of the stream to manage
*
* @throws \core\log\StreamException
*
* @return \core\log\Stream
*/
protected function add_stream(\core\log\Tag $tag) : \core\log\Stream
{
if (!$this->stream_exist(tag: $tag))
{
$this->streams[$tag] = \core\log\StreamFactory::build(tag: $tag); # NOTE: can throw \core\log\StreamException too
return $this->streams[$tag];
}
\throw new \core\log\StreamException(_('the stream already exist, cannot add it', 1));
}
/**
* Check if stream associated to the tag already exist
*
* @param ?\core\log\Tag $tag Tag associated to a stream
*
* @param ?\core\log\Stream $stream Stream to search for
*
* @throws \core\log\LogException
*
* @return bool
*/
protected function stream_exist(tag: ?\core\log\Tag $tag = null, stream: ?\core\log\Stream $stream = null) : bool
{
if (\is_null($stream))
{
if (\is_null($tag))
{
\throw new \core\log\LogException(_("tag and stream arguments cannot be null at the same time"), 1);
}
if (\in_array($tag, \array_keys($this->streams)))
{
return True;
}
return False;
}
if (\is_null($tag))
{
if (\in_array($stream, $this->streams))
{
return True
}
return False
}
# this will never be reached
}
}
?>

View file

@ -0,0 +1,61 @@
<?php
namespace core\log;
/**
* Interface to manage streaming system
*/
interface Stream
{
/**
* Dispatcher configuration
*
* @var \core\config\Configuration
*/
protected \core\config\Configuration $configuration;
/**
* Tag associated to the stream
*
* @var \core\log\Tag
*/
protected \core\log\Tag $tag;
/**
* Stream constructor
*
* @param \core\config\Configuration $config Dispatcher configuration
*
* @throws \core\config\ConfigException
*/
public function __construct(config: \core\config\Configuration $configuration, tag: \core\log\Tag $tag);
/**
* Push an event to the stream
*
* @param \core\log\Event $event Event to push
*
* @throws \core\log\StreamException
*
* @return \core\log\Event Event pushed
*/
public function push(event: \core\log\Event $event) : \core\log\Event;
/**
* Connect to an existing stream running outside of PHP
*
* @param \core\log\StreamSettings $settings Settings to connect to the stream
*
* @throws \core\log\StreamException
*/
public function connect();
/**
* Close the connection to an existing stream running outside of PHP
*
* @throws \core\log\StreamException
*/
public function close();
}
?>

View file

@ -0,0 +1,21 @@
<?php
namespace core\log;
/**
* Exception on stream processing
*/
class StreamException extends \core\exception\CustomException
{
/**
* default tags associated to this exception
*
* @var \class[]
*/
const TAGS = [
\core\log\tag\Error,
\core\log\tag\Logging,
]
}
?>

View file

@ -0,0 +1,60 @@
<?php
namespace core\log;
/**
* Stream factory, to create stream with ease
*/
class StreamFactory
{
/**
* Available client NOTE: to update for "vanilla" client
*
* @var \string[]
*/
const AVAILABLE_CLIENTS = [
'\\core\\log\\client\\MQTT',
];
/**
* @throws \core\config\ConfigException
* @throws \core\log\StreamException
*/
public static function build(tag: \core\log\Tag $tag) : \core\log\Stream
{
$configuration = \core\config\Configuration(array(
'tags' => array(),
'client' => array('name' => '\\core\\log\\client\\File'),
));
if (\core\config\GlobalConfiguration::exist('class', 'core', 'log'))
{
$configuration = \core\config\GlobalConfiguration::read('class' 'core', 'log');
}
if (!\in_array($tag->__tostring(), $configuration.get('tags')))
{
\throw new \core\log\StreamException(_('this stream should not be created as the admin does not want to listen to it', 1)); # WARNING: this should only be seen at [DEBUG] level
}
$client = '\\core\\log\\client\\File'; # NOTE: hard-coded fallback
if ($configuration->exist('client', 'name'))
{
if (\mb_split('\\', $configuration->get('client', 'name'))[0] !== 'plugins') # WARNING: any plugin can add a client, but then the name should start with "plugins" (and then "\\foo\\Bar)"
{
if (!\in_array($configuration->get('client', 'name'), self::AVAILABLE_CLIENTS))
{
\throw new \core\log\StreamException(_('unknown client in configuration', 2));
}
$client = $configuration.get('client', 'name');
}
else
{
$client = $configuration.get('client', 'name');
}
}
$stream = new $client($configuration.get('client', 'settings'), $tag);
}
}
?>

View file

@ -0,0 +1,59 @@
<?php
namespace core\log;
/**
* Base class for logging tag
*/
class Tag
{
/**
* Name of the tag
*
* @var \string
*/
protected \string $name;
/**
* Constructor of Tag
*
* @param \string $name Name of the tag
*/
public function __construct(\string $name)
{
if (\str_contains($name, '..'))
{
\throw \core\log\LogException(_('tag name cannot contains .. as it could be used to write outside the main log directory', 2))
}
else
{
$this->name = $name;
}
}
/**
* Generate safe id for general purpose (only [A-Z][a-Z]-_ chars)
*
* Replace whitespace character and non base latin character to "_".
* Keep [A-Z][a-z]- chars as it is
*/
public function generate_id() : \string
{
return \preg_replace(
'/[^a-zA-Z-]/gm',
'_',
\mb_convert_encoding(
$this->__tostring(),
'ASCII',
'UTF-8',
),
);
}
public function __toString() : \string
{
return $this->name;
}
}
?>

View file

@ -0,0 +1,60 @@
<?php
namespace core\log\client;
/**
* MQTT stream
*/
class MQTT implements \core\log\Stream
{
/**
* Client object from php-mqtt
*
* @var \PhpMqtt\Client\MqttClient
*/
protected \PhpMqtt\Client\MqttClient $client;
/**
* Connection settings from php-mqtt
*
* @var \PhpMqtt\Client\ConnectionSettings
*/
protected \PhpMqtt\Client\ConnectionSettings $connection_settings;
/**
* @throws \core\config\ConfigException
*/
public function __construct(config: \core\config\Configuration $configuration, tag: \core\log\Tag $tag)
{
$this->configuration = $configuration;
$this->tag = $tag;
$client_id = 'php-mqtt-client';
if ($this->configuration.exist('client_id'))
{
$client_id = $this->configuration.get('client_id'); # TODO: Check if we need to sanitize
}
$this->client = new \PhpMqtt\Client\MqttClient($this->configuration('hostname'), $this->configuration.get('port'), $client_id, \PhpMqtt\Client\MqttClient::MQTT_3_1, null);
$this->connection_settings = new \PhpMqtt\Client\ConnectionSettings;
if ($this->configuration.exist('auth', 'username'))
{
$this->connection_settings->setUsername($this->configuration.get('auth', 'username'));
}
if ($this->configuration.exist('auth', 'password'))
{
$this->connection_settings->setPassword($this->configuration.get('auth', 'password'));
}
}
public function push(event: \core\log\Event $event) : \core\log\Event
{
$this->client->connect($this->connection_settings, True);
$this->client->publish($tag->generate_id(), $event->display(), \PhpMqtt\Client\MqttClient::QO5_EXACTLY_ONCE);
$this->client->loop(True, True);
$this->client->disconnect()
}
}
?>

View file

@ -0,0 +1,48 @@
<?php
namespace core\log\client;
/**
* Store stream in files
*/
class File
{
/**
* File where event are stored
*
* @var \resource
*/
protected \resource $file;
/**
* @throws \core\config\ConfigException
* @throws \core\log\LogException
*/
public function __construct(config: \core\config\Configuration $configuration, tag: \core\log\Tag $tag)
{
$this->configuration = $configuration;
$this->tag = $tag;
$path = $configuration.get('path'); # WARNING: must be absolute path
$path->safe_append($tag->generate_id());
$this->file = \fopen($path->__tostring(), 'a');
if (!$this->file)
{
\throw new \core\log\LogException(_('cannot open log file'), 1);
}
}
public funtion push(event: \core\log\Event $event) : \core\log\Event
{
if (!\flock($this->file, \LOCK_EX))
{
\throw new \core\log\LogException(_('cannot guarantee that the file is not already being edited'));
}
\fwrite($this->file, $event->display());
\flock($this->file, LOCK_UN);
}
}
?>

View file

@ -0,0 +1,46 @@
<?php
namespace core\log\client;
/**
* Redis stream (only https://github.com/phpredis/phpredis for now)
*/
class Redis
{
/**
* Client object for redis
*
* @var \Redis
*/
protected \Redis $client;
/**
* @throws \core\config\ConfigException
*/
public function __construct(config: \core\config\Configuration $configuration, tag: \core\log\Tag $tag)
{
$this->configuration = $configuration;
$this->tag = $tag;
$config = array(
'host' => $this->configuration.get('hostname'),
'port' => $this->configuration.get('port'), // -1 if socket
);
if ($this->configuration.exist('auth'))
{
$config['auth'] = array();
if ($this->configuration.exist('auth', 'username'))
{
$config['auth'][] = $this->configuration.get('auth', 'username');
}
$config['auth'][] = $this->configuration.get('auth', 'password');
}
$this->client = new \Redis($config);
}
public function push(event: \core\log\Event $event) : \core\log\Event
{
$this->client->rpush($tag->generate_id(), $event->display());
}
}
?>

152
func/core/utils.class.php Normal file
View file

@ -0,0 +1,152 @@
<?php
namespace core;
/**
* convert nearly anything to a string (not protected for html, use html_special_chars)
*
* @param \mixed $var Variable to convert
*
* @return \string string conversion result
*/
function display(\mixed $var) : \string
{
if (\is_string($var))
{
return $var;
}
if (\is_bool($var))
{
return ($var ? 'True' : 'False');
}
if (\is_int($var) || \is_float($var))
{
return (string) $var;
}
if (\is_null($var))
{
return 'null';
}
if (\is_resource($var))
{
return \get_resource_type($var);
}
if (\is_array($var))
{
$ret = 'array (';
foreach ($var as $key => $el)
{
$ret .= ' ' . \core\display($key) . ' => ' . \core\display($el);
}
return $ret . ' )';
}
if (\is_object($var))
{
if (!\method_exists($var, '__toString'))
{
return \get_class($var) . ' : ' . \core\display(\core\table($var, 3)); // NOTE: arbitrary hard-coded recursion value
}
else
{
return $var->__toString();
}
}
if (\is_callable($var))
{
if ($var instanceOf \Closure)
{
return 'a closure';
}
return 'unknown callable';
}
if (\is_iterable($var))
{
return 'iterable';
}
return 'cannot convert the variable that should be here to a string';
}
/**
* substitution helper
*
* substitute {element} with element for each element in token
* Not safe to display without sanitization on a webpage
*
* @param \string $content
*
* @param \array $tokens
*
* @return \string
*/
function substitute(\string $content, \array $tokens) : \string
{
$parts = \preg_split('/({(?:\\}|[^\\}])+})/Um', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
if (\count($tokens) > 0)
{
foreach ($tokens as $name => $value)
{
if (\in_array('{' . $name . '}', $parts))
{
foreach (\array_keys($parts, '{' . $name . '}') as $key)
{
$parts[$key] = \core\display($value);
}
}
}
}
return \implode($parts);
}
/**
* convert any object into a table
*
* @param \object $object Object to convert
*
* @param \int $depth Depth of the recursion if there is an object. -1 for infinite, 0 for no recursion.
* Default to 0.
*
* @return \array
*/
function table(\object $object, \int $depth = 0) : \array
{
if (\in_array(\core\Base::class, \class_uses($object)))
{
return $object->table();
}
$attributes = [];
if ($depth < -1)
{
throw new \Exception('Depth cannot be below -1');
}
foreach (\array_keys(\get_class_vars(\get_class($object))) as $attribute)
{
if (\is_object($object->$attribute))
{
if ($depth === 0)
{
$attributes[$attribute] = $object->$attribute;
}
else if ($depth > 0)
{
$attributes[$attribute] = \core\table($object->$attribute, $depth - 1);
}
else // depth === -1
{
$attributes[$attribute] = \core\table($object->$attribute, $depth);
}
}
else
{
$attributes[$attribute] = $object->$attribute;
}
}
return $attributes;
}
?>