commit 80c62f33ce5611ae4e85565a8d352380f57c1ccf Author: linarphy Date: Fri Mar 28 22:15:09 2025 +0100 Add already existing codebase diff --git a/class/core/Path.class.php b/class/core/Path.class.php new file mode 100644 index 0000000..8b0f4e7 --- /dev/null +++ b/class/core/Path.class.php @@ -0,0 +1,69 @@ +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); + } +} + +?> diff --git a/class/core/config/Configuration.class.php b/class/core/config/Configuration.class.php new file mode 100644 index 0000000..50208a0 --- /dev/null +++ b/class/core/config/Configuration.class.php @@ -0,0 +1,78 @@ +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; + } +} + +?> diff --git a/class/core/config/GlobalConfiguration.class.php b/class/core/config/GlobalConfiguration.class.php new file mode 100644 index 0000000..532a1d2 --- /dev/null +++ b/class/core/config/GlobalConfiguration.class.php @@ -0,0 +1,68 @@ + $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; + } +} + +?> diff --git a/class/core/exception/CustomException.class.php b/class/core/exception/CustomException.class.php new file mode 100644 index 0000000..0d93121 --- /dev/null +++ b/class/core/exception/CustomException.class.php @@ -0,0 +1,49 @@ +message) . + ' in ' . \htmlspecialchars($this->file) . + '(' . \htmlspecialchars($this->line) . ')\n' . + \htmlspecialchars($this->getTraceAsString()); + } +} + +?> diff --git a/class/core/log/Event.class.php b/class/core/log/Event.class.php new file mode 100644 index 0000000..59bacd2 --- /dev/null +++ b/class/core/log/Event.class.php @@ -0,0 +1,85 @@ +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; + } +} + +?> diff --git a/class/core/log/Logger.class.php b/class/core/log/Logger.class.php new file mode 100644 index 0000000..505bd1e --- /dev/null +++ b/class/core/log/Logger.class.php @@ -0,0 +1,85 @@ +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 + } +} + +?> diff --git a/class/core/log/Stream.class.php b/class/core/log/Stream.class.php new file mode 100644 index 0000000..c3d8bee --- /dev/null +++ b/class/core/log/Stream.class.php @@ -0,0 +1,61 @@ + diff --git a/class/core/log/StreamException.class.php b/class/core/log/StreamException.class.php new file mode 100644 index 0000000..03e782b --- /dev/null +++ b/class/core/log/StreamException.class.php @@ -0,0 +1,21 @@ + diff --git a/class/core/log/StreamFactory.class.php b/class/core/log/StreamFactory.class.php new file mode 100644 index 0000000..09f8de9 --- /dev/null +++ b/class/core/log/StreamFactory.class.php @@ -0,0 +1,60 @@ + 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); + } +} + +?> diff --git a/class/core/log/Tag.class.php b/class/core/log/Tag.class.php new file mode 100644 index 0000000..ced010f --- /dev/null +++ b/class/core/log/Tag.class.php @@ -0,0 +1,59 @@ +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; + } +} + +?> diff --git a/class/core/log/client/MQTT.class.php b/class/core/log/client/MQTT.class.php new file mode 100644 index 0000000..bce25c7 --- /dev/null +++ b/class/core/log/client/MQTT.class.php @@ -0,0 +1,60 @@ +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() + } +} + +?> diff --git a/class/core/log/client/file.class.php b/class/core/log/client/file.class.php new file mode 100644 index 0000000..e68e0b9 --- /dev/null +++ b/class/core/log/client/file.class.php @@ -0,0 +1,48 @@ +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); + } +} + +?> diff --git a/class/core/log/client/redis.class.php b/class/core/log/client/redis.class.php new file mode 100644 index 0000000..addd2af --- /dev/null +++ b/class/core/log/client/redis.class.php @@ -0,0 +1,46 @@ +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()); + } +} + +?> diff --git a/func/core/utils.class.php b/func/core/utils.class.php new file mode 100644 index 0000000..020c690 --- /dev/null +++ b/func/core/utils.class.php @@ -0,0 +1,152 @@ + $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; +} + +?>