commit bda571fa773d8435da493dfc5d87c88c1ada5b12 Author: Netkas Date: Thu Feb 23 13:11:50 2023 -0500 Initial Commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca16ab2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright 2022-2023 Nosial All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f47d235 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# ConfigLib + +ConfigLib is a PHP library for managing configuration files and storing it +in NCC's data, while providing a command line interface for running functions +such as editing configuration files inline or importing/exporting +configuration files. + +One of the biggest advantages of using something like ConfigLib is that +it will allow for more complicated software to be configured more easily +by following the documented instructions on how to alter configuration +files, optionally you could use a builtin editor to edit the configuration +file manually. \ No newline at end of file diff --git a/main b/main new file mode 100644 index 0000000..40447a0 --- /dev/null +++ b/main @@ -0,0 +1,7 @@ +Path = Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . $name . '.conf'; + } + catch (Exception $e) + { + throw new RuntimeException('Unable to load package "net.nosial.configlib"', $e); + } + + // Set the name + $this->Name = $name; + + // Default Configuration + $this->Modified = false; + + + if(file_exists($this->Path)) + { + try + { + $this->load(true); + } + catch(Exception $e) + { + Log::error('net.nosial.configlib', sprintf('Unable to load configuration "%s", %s', $this->Name, $e->getMessage())); + throw new RuntimeException(sprintf('Unable to load configuration "%s"', $this->Name), $e); + } + } + else + { + $this->Configuration = []; + } + } + + /** + * Validates a key syntax (e.g. "key1.key2.key3") + * + * @param string $input + * @return bool + */ + private static function validateKey(string $input): bool + { + $pattern = '/^([a-zA-Z]+\.?)+$/'; + + if (preg_match($pattern, $input)) + return true; + + return false; + } + + /** + * Attempts to convert a string to the correct type (int, float, bool, string) + * + * @param $input + * @return float|int|mixed|string + * @noinspection PhpUnusedPrivateMethodInspection + */ + private static function cast($input): mixed + { + if (is_numeric($input)) + { + if (str_contains($input, '.')) + return (float)$input; + + if (ctype_digit($input)) + return (int)$input; + } + elseif (in_array(strtolower($input), ['true', 'false'])) + { + return filter_var($input, FILTER_VALIDATE_BOOLEAN); + } + + return (string)$input; + } + + /** + * Returns a value from the configuration + * + * @param string $key The key to retrieve (e.g. "key1.key2.key3") + * @param mixed|null $default The default value to return if the key is not found + * @return mixed The value of the key or the default value + * @noinspection PhpUnused + */ + public function get(string $key, mixed $default=null): mixed + { + if(!self::validateKey($key)) + return $default; + + $path = explode('.', $key); + $current = $this->Configuration; + + foreach ($path as $key) + { + if (is_array($current) && array_key_exists($key, $current)) + { + $current = $current[$key]; + } + else + { + return $default; + } + } + + // Return the value at the end of the path + return $current; + } + + /** + * Sets a value in the configuration + * + * @param string $key The key to set (e.g. "key1.key2.key3") + * @param mixed $value The value to set + * @param bool $create If true, the key will be created if it does not exist + * @return bool True if the value was set, false otherwise + */ + public function set(string $key, mixed $value, bool $create=false): bool + { + if(!self::validateKey($key)) + return false; + + $path = explode('.', $key); + $current = &$this->Configuration; + + // Navigate to the parent of the value to set + foreach ($path as $key) + { + if (is_array($current) && array_key_exists($key, $current)) + { + $current = &$current[$key]; + } + else + { + if ($create) + { + $current[$key] = []; + $current = &$current[$key]; + } + else + { + return false; + } + } + } + + // Set the value + $current = $value; + + $this->Modified = true; + return true; + } + + /** + * Sets the default value for a key if it does not exist + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function setDefault(string $key, mixed $value): bool + { + if($this->exists($key)) + return false; + + return $this->set($key, $value, true); + } + + /** + * Checks if the given key exists in the configuration + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + if(!self::validateKey($key)) + return false; + if(!isset($this->Configuration[$key])) + return false; + + $path = explode('.', $key); + $current = $this->Configuration; + + foreach ($path as $key) + { + if (is_array($current) && array_key_exists($key, $current)) + { + $current = $current[$key]; + } + else + { + return false; + } + } + + return true; + } + + /** + * Clears the current configuration data + * + * @return void + * @noinspection PhpUnused + */ + public function clear(): void + { + $this->Configuration = []; + } + + /** + * Saves the Configuration File to the disk + * + * @return void + * @throws Exception + */ + public function save(): void + { + if (!$this->Modified) + return; + + $json = json_encode($this->Configuration, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $fs = new Filesystem(); + + try + { + $fs->dumpFile($this->Path, $json); + } + catch (IOException $e) + { + throw new Exception('Unable to write configuration file', $e); + } + + $this->Modified = false; + Log::debug('net.nosial.configlib', sprintf('Configuration "%s" saved', $this->Name)); + } + + /** + * Loads the Configuration File from the disk + * + * @param bool $force + * @return void + * @throws Exception + * @noinspection PhpUnused + */ + public function load(bool $force=false): void + { + if (!$force && !$this->Modified) + return; + + $fs = new Filesystem(); + + if (!$fs->exists($this->Path)) + return; + + try + { + $json = file_get_contents($this->Path); + } + catch (IOException $e) + { + throw new Exception('Unable to read configuration file', $e); + } + + $this->Configuration = json_decode($json, true); + $this->Modified = false; + + Log::debug('net.nosial.configlib', 'Loaded configuration file: ' . $this->Path); + } + + + + /** + * Returns the name of the configuration + * + * @return string + * @noinspection PhpUnused + */ + public function getName(): string + { + return $this->Name; + } + + /** + * Returns the path of the configuration file on disk + * + * @return string + */ + public function getPath(): string + { + return $this->Path; + } + + /** + * @return array + * @noinspection PhpUnused + */ + public function getConfiguration(): array + { + return $this->Configuration; + } + + /** + * Public Destructor + */ + public function __destruct() + { + if($this->Modified) + { + try + { + $this->save(); + } + catch(Exception $e) + { + Log::error('net.nosial.configlib', sprintf('Unable to save configuration "%s" to disk, %s', $this->Name, $e->getMessage())); + } + } + } + } \ No newline at end of file diff --git a/src/ConfigLib/Program.php b/src/ConfigLib/Program.php new file mode 100644 index 0000000..843ab73 --- /dev/null +++ b/src/ConfigLib/Program.php @@ -0,0 +1,213 @@ +getConfiguration(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL); + } + else + { + if($value === null) + { + print(json_encode($configuration->get($property, '(not set)'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL); + return; + } + + $configuration->set($property, $value); + + try + { + $configuration->save(); + } + catch (Exception $e) + { + print($e->getMessage() . PHP_EOL); + exit(1); + } + } + + return; + } + + self::help(); + } + + /** + * Prints out the Help information for the program + * + * @return void + */ + private static function help(): void + { + print('Usage: configlib [options]' . PHP_EOL); + print(' -h, --help Displays the help menu' . PHP_EOL); + print(' -v, --version Displays the version of the program' . PHP_EOL); + print(' -n, --name The name of the configuration' . PHP_EOL); + print(' -p, --path The property name to select/read (eg; foo.bar.baz) (Inline)' . PHP_EOL); + print(' -v, --value The value to set the property (Inline)' . PHP_EOL); + print(' -e, --editor (Optional) The editor to use (eg; nano, vim, notepad) (External)' . PHP_EOL); + print(' --nc (Optional) Disables type casting (eg; \'true\' > True) will always be a string' . PHP_EOL); + print(' --export (Optional) Exports the configuration to a file' . PHP_EOL); + print(' --import (Optional) Imports the configuration from a file' . PHP_EOL); + print('Examples:' . PHP_EOL); + print(' configlib -n com.example.package' . PHP_EOL); + print(' configlib -n com.example.package -e nano' . PHP_EOL); + print(' configlib -n com.example.package -p foo.bar.baz -v 123' . PHP_EOL); + print(' configlib -n com.example.package -p foo.bar.baz -v 123 --nc' . PHP_EOL); + print(' configlib -n com.example.package --export config.json' . PHP_EOL); + print(' configlib -n com.example.package --import config.json' . PHP_EOL); + + exit(0); + } + + /** + * Edits an existing configuration file or creates a new one if it doesn't exist + * + * @param array $args + * @return void + */ + private static function edit(array $args): void + { + $editor = $args['editor'] ?? 'vi'; + if(isset($args['e'])) + $editor = $args['e']; + + $name = $args['name'] ?? 'default'; + + if($editor == null) + { + print('No editor specified' . PHP_EOL); + exit(1); + } + + // Determine the temporary path to use + $tempPath = null; + + if(function_exists('ini_get')) + { + $tempPath = ini_get('upload_tmp_dir'); + if($tempPath == null) + $tempPath = ini_get('session.save_path'); + if($tempPath == null) + $tempPath = ini_get('upload_tmp_dir'); + if($tempPath == null) + $tempPath = sys_get_temp_dir(); + } + + if($tempPath == null && function_exists('sys_get_temp_dir')) + $tempPath = sys_get_temp_dir(); + + if($tempPath == null) + { + print('Unable to determine the temporary path to use' . PHP_EOL); + exit(1); + } + + // Prepare the temporary file + + try + { + $configuration = new Configuration($name); + } + catch (Exception $e) + { + print($e->getMessage() . PHP_EOL); + exit(1); + } + + $fs = new Filesystem(); + $tempFile = $tempPath . DIRECTORY_SEPARATOR . $name . '.conf'; + $fs->copy($configuration->getPath(), $tempFile); + $original_hash = hash_file('sha1', $tempFile); + + // Open the editor + try + { + $process = new Process([$editor, $tempFile]); + $process->setTimeout(0); + $process->setTty(true); + $process->run(); + } + catch(Exception $e) + { + print('Unable to open the editor, ' . $e->getMessage() . PHP_EOL); + exit(1); + } + finally + { + $fs->remove($tempFile); + } + + // Check if the file has changed and if so, update the configuration + if($fs->exists($tempFile)) + { + $new_hash = hash_file('sha1', $tempFile); + if($original_hash != $new_hash) + { + $fs->copy($tempFile, $configuration->getPath()); + } + else + { + print('No changes detected' . PHP_EOL); + } + + $fs->remove($tempFile); + } + + } + + /** + * Prints out the version of the program + * + * @return void + */ + private static function version(): void + { + print('ConfigLib v1.0.0' . PHP_EOL); + exit(0); + } + } \ No newline at end of file diff --git a/tests/default.php b/tests/default.php new file mode 100644 index 0000000..a1db638 --- /dev/null +++ b/tests/default.php @@ -0,0 +1,12 @@ +setDefault('database.host', '127.0.0.1'); + $config->setDefault('database.port', 3306); + $config->setDefault('database.username', 'root'); + $config->setDefault('database.password', null); + $config->setDefault('database.name', 'test'); \ No newline at end of file