<?php
/**
* Part of the Joomla Framework Registry Package
*
* @copyright Copyright (C) 2013 Open Source Matters, Inc.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Registry\Format;
use Joomla\Registry\FormatInterface;
use Joomla\Utilities\ArrayHelper;
/**
* INI format handler for Registry.
*
* @since 1.0.0
*/
class Ini implements FormatInterface
{
/**
* Default options array
*
* @var array
* @since 1.3.0
*/
protected static $options = [
'supportArrayValues' => false,
'parseBooleanWords' => false,
'processSections' => false,
];
/**
* A cache used by stringToObject.
*
* @var array
* @since 1.0.0
*/
protected static $cache = [];
/**
* Converts an object into an INI formatted string
* - Unfortunately, there is no way to have ini values nested further than two
* levels deep. Therefore we will only go through the first two levels of
* the object.
*
* @param object $object Data source object.
* @param array $options Options used by the formatter.
*
* @return string INI formatted string.
*
* @since 1.0.0
*/
public function objectToString($object, array $options = [])
{
$options = \array_merge(static::$options, $options);
$supportArrayValues = $options['supportArrayValues'];
$local = [];
$global = [];
$variables = \get_object_vars($object);
$last = \count($variables);
// Assume that the first element is in section
$inSection = true;
// Iterate over the object to set the properties.
foreach ($variables as $key => $value) {
// If the value is an object then we need to put it in a local section.
if (\is_object($value)) {
// Add an empty line if previous string wasn't in a section
if (!$inSection) {
$local[] = '';
}
// Add the section line.
$local[] = '[' . $key . ']';
// Add the properties for this section.
foreach (\get_object_vars($value) as $k => $v) {
if (\is_array($v) && $supportArrayValues) {
$assoc = ArrayHelper::isAssociative($v);
foreach ($v as $arrayKey => $item) {
$arrayKey = $assoc ? $arrayKey : '';
$local[] = $k . '[' . $arrayKey . ']=' . $this->getValueAsIni($item);
}
} else {
$local[] = $k . '=' . $this->getValueAsIni($v);
}
}
// Add empty line after section if it is not the last one
if (--$last !== 0) {
$local[] = '';
}
} elseif (\is_array($value) && $supportArrayValues) {
$assoc = ArrayHelper::isAssociative($value);
foreach ($value as $arrayKey => $item) {
$arrayKey = $assoc ? $arrayKey : '';
$global[] = $key . '[' . $arrayKey . ']=' . $this->getValueAsIni($item);
}
} else {
// Not in a section so add the property to the global array.
$global[] = $key . '=' . $this->getValueAsIni($value);
$inSection = false;
}
}
return \implode("\n", \array_merge($global, $local));
}
/**
* Parse an INI formatted string and convert it into an object.
*
* @param string $data INI formatted string to convert.
* @param array $options An array of options used by the formatter, or a boolean setting to process sections.
*
* @return object Data object.
*
* @since 1.0.0
*/
public function stringToObject($data, array $options = [])
{
$options = \array_merge(static::$options, $options);
// Check the memory cache for already processed strings.
$hash = \md5($data . ':' . (int) $options['processSections']);
if (isset(static::$cache[$hash])) {
return static::$cache[$hash];
}
// If no lines present just return the object.
if (empty($data)) {
return new \stdClass();
}
$obj = new \stdClass();
$section = false;
$array = false;
$lines = \explode("\n", $data);
// Process the lines.
foreach ($lines as $line) {
// Trim any unnecessary whitespace.
$line = \trim($line);
// Ignore empty lines and comments.
if (empty($line) || ($line[0] === ';')) {
continue;
}
if ($options['processSections']) {
$length = \strlen($line);
// If we are processing sections and the line is a section add the object and continue.
if ($line[0] === '[' && ($line[$length - 1] === ']')) {
$section = \substr($line, 1, $length - 2);
$obj->$section = new \stdClass();
continue;
}
} elseif ($line[0] === '[') {
continue;
}
// Check that an equal sign exists and is not the first character of the line.
if (!\strpos($line, '=')) {
// Maybe throw exception?
continue;
}
// Get the key and value for the line.
[$key, $value] = \explode('=', $line, 2);
// If we have an array item
if (\substr($key, -1) === ']' && ($openBrace = \strpos($key, '[', 1)) !== false) {
if ($options['supportArrayValues']) {
$array = true;
$arrayKey = \substr($key, $openBrace + 1, -1);
// If we have a multi-dimensional array or malformed key
if (\strpos($arrayKey, '[') !== false || \strpos($arrayKey, ']') !== false) {
// Maybe throw exception?
continue;
}
$key = \substr($key, 0, $openBrace);
} else {
continue;
}
}
// Validate the key.
if (\preg_match('/[^A-Z\d_]/i', $key)) {
// Maybe throw exception?
continue;
}
// If the value is quoted then we assume it is a string.
$length = \strlen($value);
if ($length && ($value[0] === '"') && ($value[$length - 1] === '"')) {
// Strip the quotes and Convert the new line characters.
$value = \stripcslashes(\substr($value, 1, $length - 2));
$value = \str_replace('\n', "\n", $value);
} else {
// If the value is not quoted, we assume it is not a string.
// If the value is 'false' assume boolean false.
if ($value === 'false') {
$value = false;
} elseif ($value === 'true') {
// If the value is 'true' assume boolean true.
$value = true;
} elseif ($options['parseBooleanWords'] && \in_array(\strtolower($value), ['yes', 'no'], true)) {
// If the value is 'yes' or 'no' and option is enabled assume appropriate boolean
$value = (\strtolower($value) === 'yes');
} elseif (\is_numeric($value)) {
// If the value is numeric than it is either a float or int.
// If there is a period then we assume a float.
if (\strpos($value, '.') !== false) {
$value = (float) $value;
} else {
$value = (int) $value;
}
}
}
// If a section is set add the key/value to the section, otherwise top level.
if ($section) {
if ($array) {
if (!isset($obj->$section->$key)) {
$obj->$section->$key = [];
}
if (!empty($arrayKey)) {
$obj->$section->{$key}[$arrayKey] = $value;
} else {
$obj->$section->{$key}[] = $value;
}
} else {
$obj->$section->$key = $value;
}
} else {
if ($array) {
if (!isset($obj->$key)) {
$obj->$key = [];
}
if (!empty($arrayKey)) {
$obj->{$key}[$arrayKey] = $value;
} else {
$obj->{$key}[] = $value;
}
} else {
$obj->$key = $value;
}
}
$array = false;
}
// Cache the string to save cpu cycles -- thus the world :)
static::$cache[$hash] = clone $obj;
return $obj;
}
/**
* Method to get a value in an INI format.
*
* @param mixed $value The value to convert to INI format.
*
* @return string The value in INI format.
*
* @since 1.0.0
*/
protected function getValueAsIni($value)
{
$string = '';
switch (\gettype($value)) {
case 'integer':
case 'double':
$string = $value;
break;
case 'boolean':
$string = $value ? 'true' : 'false';
break;
case 'string':
// Sanitize any CRLF characters..
$string = '"' . \str_replace(["\r\n", "\n"], '\\n', $value) . '"';
break;
}
return $string;
}
}