Commit 9e8d1e85 authored by catch's avatar catch

Issue #1470824 by alexpott, sun: Fixed XML encoder can only handle a small...

Issue #1470824 by alexpott, sun: Fixed XML encoder can only handle a small subset of PHP arrays, so switch to YAML.
parent dc151c10
......@@ -28,7 +28,7 @@ function config_get_config_directory() {
}
/**
* Moves the default config supplied by a module to the live config directory.
* Installs the default configuration of a given module.
*
* @param
* The name of the module we are installing.
......@@ -40,55 +40,23 @@ function config_install_default_config($module) {
$module_config_dir = drupal_get_path('module', $module) . '/config';
$drupal_config_dir = config_get_config_directory();
if (is_dir(drupal_get_path('module', $module) . '/config')) {
$files = glob($module_config_dir . '/' . '*.xml');
$files = glob($module_config_dir . '/*.' . FileStorage::getFileExtension());
foreach ($files as $key => $file) {
// Load config data into the active store and write it out to the
// file system in the drupal config directory. Note the config name
// needs to be the same as the file name WITHOUT the extension.
$parts = explode('/', $file);
$file = array_pop($parts);
$config_name = str_replace('.xml', '', $file);
$config_name = basename($file, '.' . FileStorage::getFileExtension());
$storage = new DatabaseStorage($config_name);
$data = FileStorage::decode(file_get_contents($module_config_dir . '/' . $file));
$storage->write($data);
$database_storage = new DatabaseStorage($config_name);
$file_storage = new FileStorage($config_name);
$file_storage->setPath($module_config_dir);
$database_storage->write($file_storage->read());
}
}
}
/**
* Retrieves an iterable array which lists the children under a config 'branch'.
*
* Given the following configuration files:
* - core.entity.node_type.article.xml
* - core.entity.node_type.page.xml
*
* You can pass a prefix 'core.entity.node_type' and get back an array of the
* filenames that match. This allows you to iterate through all files in a
* branch.
*
* @param $prefix
* The prefix of the files we are searching for.
*
* @return
* An array of file names under a branch.
*/
function config_get_files_with_prefix($prefix = '') {
$files = glob(config_get_config_directory() . '/' . $prefix . '*.xml');
$clean_name = function ($value) {
return basename($value, '.xml');
};
return array_map($clean_name, $files);
}
/**
* @todo
*
* @param $prefix
* @todo
*
* @return
* @todo
* @todo http://drupal.org/node/1552396 renames this into config_load_all().
*/
function config_get_storage_names_with_prefix($prefix = '') {
return DatabaseStorage::getNamesWithPrefix($prefix);
......@@ -103,8 +71,8 @@ function config_get_storage_names_with_prefix($prefix = '') {
*
* @param $name
* The name of the configuration object to retrieve. The name corresponds to
* an XML configuration file. For @code config(book.admin) @endcode, the
* config object returned will contain the contents of book.admin.xml.
* a configuration file. For @code config(book.admin) @endcode, the config
* object returned will contain the contents of book.admin configuration file.
* @param $class
* The class name of the config object to be returned. Defaults to
* DrupalConfig.
......
......@@ -398,11 +398,9 @@ function drupal_uninstall_modules($module_list = array(), $uninstall_dependents
// by scanning its config directory.
$module_config_dir = drupal_get_path('module', $module) . '/config';
if (is_dir($module_config_dir)) {
$files = glob($module_config_dir . '/' . '*.xml');
$files = glob($module_config_dir . '/*.' . FileStorage::getFileExtension());
foreach ($files as $file) {
$parts = explode('/', $file);
$file = array_pop($parts);
$config_name = str_replace('.xml', '', $file);
$config_name = basename($file, '.' . FileStorage::getFileExtension());
$file_storage = new FileStorage($config_name);
// Delete the configuration from storage.
$file_storage->delete();
......
......@@ -17,6 +17,11 @@ class DrupalConfig {
*/
protected $storage;
/**
* The data of the configuration object.
*
* @var array
*/
protected $data = array();
/**
......@@ -36,16 +41,9 @@ public function __construct(StorageInterface $storage) {
* Reads config data from the active store into our object.
*/
public function read() {
$active = (array) $this->storage->read();
foreach ($active as $key => $value) {
// If the setting is empty, return an empty string rather than an array.
// This is necessary because SimpleXML's default behavior is to return
// an empty array instead of a string.
if (is_array($value) && empty($value)) {
$value = '';
}
$this->set($key, $value);
}
$data = $this->storage->read();
$this->setData($data !== FALSE ? $data : array());
return $this;
}
/**
......@@ -66,11 +64,13 @@ public function isOverridden($key) {
*
* @param $key
* A string that maps to a key within the configuration data.
* For instance in the following XML:
* For instance in the following configuation array:
* @code
* <foo>
* <bar>baz</bar>
* </foo>
* array(
* 'foo' => array(
* 'bar' => 'baz',
* ),
* );
* @endcode
* A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo'
* would return array('bar' => 'baz').
......@@ -113,6 +113,17 @@ public function get($key = '') {
}
}
/**
* Replaces the data of this configuration object.
*
* @param array $data
* The new configuration data.
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* Sets value in this config object.
*
......@@ -122,16 +133,11 @@ public function get($key = '') {
* @todo
*/
public function set($key, $value) {
// Remove all non-alphanumeric characters from the key.
// @todo Reverse this and throw an exception when encountering a key with
// invalid name. The identical validation also needs to happen in get().
// Furthermore, the dot/period is a reserved character; it may appear
// between keys, but not within keys.
$key = preg_replace('@[^a-zA-Z0-9_.-]@', '', $key);
// Type-cast value into a string.
$value = $this->castValue($value);
// The dot/period is a reserved character; it may appear between keys, but
// not within keys.
$parts = explode('.', $key);
if (count($parts) == 1) {
$this->data[$key] = $value;
......@@ -198,14 +204,14 @@ public function clear($key) {
}
/**
* Saves the configuration object to disk as XML.
* Saves the configuration object.
*/
public function save() {
$this->storage->write($this->data);
}
/**
* Deletes the configuration object on disk.
* Deletes the configuration object.
*/
public function delete() {
$this->data = array();
......
......@@ -2,69 +2,94 @@
namespace Drupal\Core\Config;
use Symfony\Component\Yaml\Yaml;
/**
* Represents the file storage interface.
* Represents the file storage controller.
*
* Classes implementing this interface allow reading and writing configuration
* data to and from disk.
* @todo Implement StorageInterface after removing DrupalConfig methods.
* @todo Consider to extend StorageBase.
*/
class FileStorage {
/**
* Constructs a FileStorage object.
* The name of the configuration object.
*
* @param string $name
* The name for the configuration data. Should be lowercase.
* @var string
*/
public function __construct($name) {
protected $name;
/**
* The filesystem path containing the configuration object.
*
* @var string
*/
protected $path;
/**
* Implements StorageInterface::__construct().
*/
public function __construct($name = NULL) {
$this->name = $name;
}
/**
* Reads and returns a file.
*
* @return
* The data of the file.
* Returns the path containing the configuration file.
*
* @throws
* Exception
* @return string
* The relative path to the configuration object.
*/
protected function readData() {
$data = file_get_contents($this->getFilePath());
if ($data === FALSE) {
throw new FileStorageReadException('Read file is invalid.');
public function getPath() {
// If the path has not been set yet, retrieve and assign the default path
// for configuration files.
if (!isset($this->path)) {
$this->setPath(config_get_config_directory());
}
return $data;
return $this->path;
}
/**
* Checks whether the XML configuration file already exists on disk.
*
* @return
* @todo
* Sets the path containing the configuration file.
*/
protected function exists() {
return file_exists($this->getFilePath());
public function setPath($directory) {
$this->path = $directory;
return $this;
}
/**
* Returns the path to the XML configuration file.
* Returns the path to the configuration file.
*
* @return
* @todo
* @return string
* The path to the configuration file.
*/
public function getFilePath() {
return config_get_config_directory() . '/' . $this->name . '.xml';
return $this->getPath() . '/' . $this->getName() . '.' . self::getFileExtension();
}
/**
* Returns the file extension used by the file storage for all configuration files.
*
* @return string
* The file extension.
*/
public static function getFileExtension() {
return 'yml';
}
/**
* Writes the contents of the configuration file to disk.
* Returns whether the configuration file exists.
*
* @param $data
* The data to be written to the file.
* @return bool
* TRUE if the configuration file exists, FALSE otherwise.
*/
protected function exists() {
return file_exists($this->getFilePath());
}
/**
* Implements StorageInterface::write().
*
* @throws
* Exception
* @throws FileStorageException
*/
public function write($data) {
$data = $this->encode($data);
......@@ -74,17 +99,21 @@ public function write($data) {
}
/**
* Returns the contents of the configuration file.
* Implements StorageInterface::read().
*
* @return
* @todo
* @throws FileStorageReadException
*/
public function read() {
if ($this->exists()) {
$data = $this->readData();
return $this->decode($data);
if (!$this->exists()) {
throw new FileStorageReadException('Configuration file does not exist.');
}
return FALSE;
$data = file_get_contents($this->getFilePath());
$data = $this->decode($data);
if ($data === FALSE) {
throw new FileStorageReadException('Unable to decode configuration file.');
}
return $data;
}
/**
......@@ -99,43 +128,9 @@ public function delete() {
* Implements StorageInterface::encode().
*/
public static function encode($data) {
// Convert the supplied array into a SimpleXMLElement.
$xml_object = new \SimpleXMLElement("<?xml version=\"1.0\"?><config></config>");
self::encodeArrayToXml($data, $xml_object);
// Pretty print the result.
$dom = new \DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml_object->asXML());
return $dom->saveXML();
}
/**
* Encodes an array into XML
*
* @param $array
* An associative array to encode.
*
* @return
* A representation of $array in XML.
*/
protected static function encodeArrayToXml($array, &$xml_object) {
foreach ($array as $key => $value) {
if (is_array($value)) {
if (!is_numeric($key)){
$subnode = $xml_object->addChild("$key");
self::encodeArrayToXml($value, $subnode);
}
else {
self::encodeArrayToXml($value, $xml_object);
}
}
else {
$xml_object->addChild($key, $value);
}
}
// The level where you switch to inline YAML is set to PHP_INT_MAX to ensure
// this does not occur.
return Yaml::dump($data, PHP_INT_MAX);
}
/**
......@@ -145,13 +140,33 @@ public static function decode($raw) {
if (empty($raw)) {
return array();
}
return Yaml::parse($raw);
}
/**
* Implements StorageInterface::getName().
*/
public function getName() {
return $this->name;
}
/**
* Implements StorageInterface::setName().
*/
public function setName($name) {
$this->name = $name;
}
// This is the fastest and easiest way to get from a string of XML to a PHP
// array since SimpleXML and json_decode()/encode() are native to PHP. Our
// only other choice would be a custom userspace implementation which would
// be a lot less performant and more complex.
$xml = new \SimpleXMLElement($raw);
$json = json_encode($xml);
return json_decode($json, TRUE);
/**
* Implements StorageInterface::getNamesWithPrefix().
*/
public static function getNamesWithPrefix($prefix = '') {
// @todo Use $this->getPath() to allow for contextual search of files in
// custom paths.
$files = glob(config_get_config_directory() . '/' . $prefix . '*.' . FileStorage::getFileExtension());
$clean_name = function ($value) {
return basename($value, '.' . FileStorage::getFileExtension());
};
return array_map($clean_name, $files);
}
}
......@@ -6,10 +6,15 @@
use Drupal\Core\Config\FileStorage;
/**
* @todo
* Base class for configuration storage controllers.
*/
abstract class StorageBase implements StorageInterface {
/**
* The name of the configuration object.
*
* @var string
*/
protected $name;
/**
......@@ -22,7 +27,7 @@ abstract class StorageBase implements StorageInterface {
/**
* Implements StorageInterface::__construct().
*/
function __construct($name) {
function __construct($name = NULL) {
$this->name = $name;
}
......@@ -106,4 +111,11 @@ public function delete() {
public function getName() {
return $this->name;
}
/**
* Implements StorageInterface::setName().
*/
public function setName($name) {
$this->name = $name;
}
}
......@@ -5,18 +5,20 @@
/**
* Defines an interface for configuration storage manipulation.
*
* This class allows reading and writing configuration data from/to the
* storage and copying to/from the file storing the same data.
* Classes implementing this interface allow reading and writing configuration
* data from and to the storage.
*
* @todo Remove all active/file methods. They belong onto DrupalConfig only.
*/
interface StorageInterface {
/**
* Constructs a storage manipulation class.
*
* @param $name
* Lowercase string, the name for the configuration data.
* @param string $name
* (optional) The name of a configuration object to load.
*/
function __construct($name);
function __construct($name = NULL);
/**
* Reads the configuration data from the storage.
......@@ -76,24 +78,58 @@ function writeToFile($data);
/**
* Encodes configuration data into the storage-specific format.
*
* @param array $data
* The configuration data to encode.
*
* @return string
* The encoded configuration data.
*
* This is a publicly accessible static method to allow for alternative
* usages in data conversion scripts and also tests.
*/
public static function encode($data);
/**
* Decodes configuration data from the storage-specific format.
*
* @param string $raw
* The raw configuration data string to decode.
*
* @return array
* The decoded configuration data as an associative array.
*
* This is a publicly accessible static method to allow for alternative
* usages in data conversion scripts and also tests.
*/
public static function decode($raw);
/**
* Gets names starting with this prefix.
*
* @param $prefix
* @todo
* Gets the name of this object.
*/
static function getNamesWithPrefix($prefix);
public function getName();
/**
* Gets the name of this object.
* Sets the name of this object.
*/
public function getName();
public function setName($name);
/**
* Gets configuration object names starting with a given prefix.
*
* Given the following configuration objects:
* - node.type.article
* - node.type.page
*
* Passing the prefix 'node.type' will return an array containing the above
* names.
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration object
* names that exist are returned.
*
* @return array
* An array containing matching configuration object names.
*/
static function getNamesWithPrefix($prefix = '');
}
<?xml version="1.0"?>
<config>
<block_cache>0</block_cache>
</config>
......@@ -52,7 +52,7 @@ class ConfigFileSecurityTestCase extends WebTestBase {
* Tests reading and writing file contents.
*/
class ConfigFileContentTestCase extends WebTestBase {
protected $fileExtension = 'xml';
protected $fileExtension;
public static function getInfo() {
return array(
......@@ -62,6 +62,12 @@ class ConfigFileContentTestCase extends WebTestBase {
);
}
function setUp() {
parent::setUp();
$this->fileExtension = FileStorage::getFileExtension();
}
/**
* Tests setting, writing, and reading of a configuration setting.
*/
......@@ -198,22 +204,22 @@ class ConfigFileContentTestCase extends WebTestBase {
// Get file listing for all files starting with 'foo'. Should return
// two elements.
$files = config_get_files_with_prefix('foo');
$files = FileStorage::getNamesWithPrefix('foo');
$this->assertEqual(count($files), 2, 'Two files listed with the prefix \'foo\'.');
// Get file listing for all files starting with 'biff'. Should return
// one element.
$files = config_get_files_with_prefix('biff');
$files = FileStorage::getNamesWithPrefix('biff');
$this->assertEqual(count($files), 1, 'One file listed with the prefix \'biff\'.');
// Get file listing for all files starting with 'foo.bar'. Should return
// one element.
$files = config_get_files_with_prefix('foo.bar');
$files = FileStorage::getNamesWithPrefix('foo.bar');
$this->assertEqual(count($files), 1, 'One file listed with the prefix \'foo.bar\'.');
// Get file listing for all files starting with 'bar'. Should return
// an empty array.
$files = config_get_files_with_prefix('bar');
$files = FileStorage::getNamesWithPrefix('bar');
$this->assertEqual($files, array(), 'No files listed with the prefix \'bar\'.');
// Delete the configuration.
......@@ -223,10 +229,61 @@ class ConfigFileContentTestCase extends WebTestBase {
// Verify the database entry no longer exists.
$db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch();
$this->assertIdentical($db_config, FALSE);
$this->assertFalse(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension));
$this->assertFalse(file_exists($config_dir . '/' . $name . $this->fileExtension));
// Attempt to delete non-existing configuration.
}
/**
* Tests serialization of configuration to file.
*/
function testConfigSerialization() {
$name = $this->randomName(10) . '.' . $this->randomName(10);
$config_data = array(
// Indexed arrays; the order of elements is essential.
'numeric keys' => array('i', 'n', 'd', 'e', 'x', 'e', 'd'),
// Infinitely nested keys using arbitrary element names.
'nested keys' => array(
// HTML/XML in values.
'HTML' => '<strong> <bold> <em> <blockquote>',
// UTF-8 in values.
'UTF-8' => 'FrançAIS is ÜBER-åwesome',
// Unicode in keys and values.
'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ' => 'αβγδεζηθικλμνξοσὠ',
),
'invalid xml' => '</title><script type="text/javascript">alert("Title XSS!");</script> & < > " \' ',
);
// Attempt to read non-existing configuration.
$config = config($name);
foreach ($config_data as $key => $value) {
$config->set($key, $value);
}
$config->save();
$config_filestorage = new FileStorage($name);
$config_parsed = $config_filestorage->read();
$key = 'numeric keys';
$this->assertIdentical($config_data[$key], $config_parsed[$key]);
$key = 'nested keys';
$this->assertIdentical($config_data[$key], $config_parsed[$key]);
$key = 'HTML';
$this->assertIdentical($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
$key = 'UTF-8';
$this->assertIdentical($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
$key = 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ';
$this->assertIdentical($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
$key = 'invalid xml';
$this->assertIdentical($config_data[$key], $config_parsed[$key]);
}
}
/**
......
<?xml version="1.0"?>
<config>
<name>large</name>
<effects>
<image_scale_480_480_1>
<name>image_scale</name>
<ieid>image_scale_480_480_1</ieid>
<data>
<width>480</width>
<height>480</height>
<upscale>1</upscale>
</data>
<weight>0</weight>
</image_scale_480_480_1>
</effects>
</config>
name: large
effects:
image_scale_480_480_1:
name: image_scale
data: