diff --git a/core/includes/config.inc b/core/includes/config.inc index 5e07c54..18a2078 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -3,6 +3,8 @@ use Drupal\Core\Config\Config; use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\NullStorage; +use Drupal\Core\Config\Metadata\ConfigWrapper; +use Drupal\Core\Config\Metadata\MetadataLookup; use Drupal\Core\Config\StorageInterface; /** @@ -104,6 +106,36 @@ function config_sync_get_changes(StorageInterface $source_storage, StorageInterf } /** + * Retrieves metadata for a configuration object or key. + * + * @param string $name + * The name or key of the configuration object. + * + * @return Drupal\Core\Config\ConfigMetadata + * A metadata array. + */ +function config_metadata($name) { + $metadata = &drupal_static(__FUNCTION__); + if (!$metadata) { + $metadata = new MetadataLookup(); + } + return $metadata[$name]; +} + +/** + * Retrieves a configuration wrapper object to access data as typed properties. + * + * @param string $name + * The name of the configuration object to retrieve. + * + * @return Drupal\Core\Config\Metadata\Wrapper + * A configuration wrapper object. + */ +function config_wrapper($name) { + return new ConfigWrapper($name, config($name)->get()); +} + +/** * Writes an array of config file changes from a source storage to a target storage. * * @param array $config_changes diff --git a/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php b/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php new file mode 100644 index 0000000..3ff15a7 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php @@ -0,0 +1,283 @@ +name = $name; + $this->data = $data; + $this->localeStorage = $localeStorage; + } + + /** + * Gets the configuration object name. + * + * @return string + * Name of the configuration object. + */ + public function getName() { + return $this->name; + } + + /** + * Translates string using the localization system. + * + * So far we only know how to translate strings from English so we check + * whether the source data is English. + * Unlike regular t() translations, strings will be added to the source + * tables only if this is marked as default data. + * + * @param $langcode + * Language code to translate to. + * @param string $source + * The source string + * @param array $metadata + * The element's metadata that may contain a 'string_context' + * + * @return string|FALSE + * Translated string if there is a translation, FALSE if not. + */ + public function getStringTranslation($langcode, $source, $metadata) { + if ($source && $this->language()->langcode == 'en' && isset($this->localeStorage)) { + $context = isset($metadata['string_context']) ? $metadata['string_context'] : ''; + $translation = $this->localeStorage->findTranslation(array( + 'source' => $source, + 'context' => $context, + 'language' => $langcode, + )); + if ($translation) { + return $translation->isTranslation() ? $translation->getString() : FALSE; + } + else { + // If this the default data, create source and add name as location. + $this->localeStorage->createString(array( + 'source' => $source, + 'context' => $context, + 'location' => $this->getName() . '.yml', + ))->save(); + } + } + return FALSE; + } + + /** + * Sets translation parameters. + * + * @param string $langcode + * The language code for the translation. + * @param Drupal\Core\Config\Metadata\ConfigWrapper $source + * Configuration wrapper used as translation source. + */ + public function setTranslation($langcode, $source) { + $this->translationLangcode = $langcode; + $this->translationSource = $source; + return $this; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getConfigWrapper(). + */ + public function getConfigWrapper() { + return $this; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getKey(). + */ + public function getKey() { + // This is the parent wrapper, so there is no key. + return ''; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getMetadata(). + */ + public function getMetadata() { + return config_metadata($this->name); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getBase(). + */ + public function getBase() { + return $this->buildDefinition($this->data, $this->getMetadata()); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::get(). + */ + public function get($property_name) { + if ($property = $this->getElement($property_name)) { + return $property; + } + else { + throw new InvalidArgumentException(format_string("The configuration property @key doesn't exist.", array( + '@key' => $property_name, + ))); + } + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + // Set the data into the configuration array but behave according to the + // interface specification when we've got a null value. + if (isset($value)) { + $this->setElementData($property_name, $value); + return $this->get($property_name); + } + else { + // In these objects, when clearing the value, the property is gone. + // As this needs to return a property, we get it before we delete it. + $property = $this->get($property_name); + $this->clearElementData($property_name); + $property->setValue($value); + return $property; + } + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + return $this->toArray(); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + return $this->getData(); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + foreach ($values as $name => $value) { + $this->set($name, $value); + } + return $this; + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definition = $this->getElementDefinition($name); + return $definition ? $definition : FALSE; + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + return $this->getChildrenMetadata($this->getMetadata()); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + return empty($this->data); + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). + */ + public function getTranslationLanguages($include_default = TRUE) { + // These will be all enabled languages. + $languages = language_list(LANGUAGE_CONFIGURABLE); + $default = $this->language(); + if ($include_default) { + $languages[$default->translationLangcode] = $default; + } + else { + unset($language[$default->translationLangcode]); + } + return $languages; + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslation(). + */ + public function getTranslation($translationLangcode, $strict = TRUE) { + $source = isset($this->translationSource) ? $this->translationSource : $this; + $data = $source->getTranslatedData($translationLangcode, $strict); + $translation = new ConfigWrapper($this->name, $data); + $translation->setTranslation($translationLangcode, $strict, $this); + return $translation; + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::language(). + */ + public function language() { + if ($this->translationLangcode) { + $langcode = $this->translationLangcode; + } + else { + $meta = $this->getMetadata(); + // The default language will be English as this is hardcoded information. + $langcode = isset($meta['language']) ? $meta['language'] : 'en'; + } + return new Language(array('langcode' => $langcode)); + } +} diff --git a/core/lib/Drupal/Core/Config/Metadata/ListElement.php b/core/lib/Drupal/Core/Config/Metadata/ListElement.php new file mode 100644 index 0000000..595ad07 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/ListElement.php @@ -0,0 +1,67 @@ +getElementData($offset); + return isset($data); + } + + /** + * Implements ArrayAccess::offsetGet(). + */ + public function offsetGet($offset) { + return $this->buildElement($offset); + } + + /** + * Implements ArrayAccess::offsetSet(). + */ + public function offsetSet($offset, $value) { + if ($value instanceof TypedDataInterface) { + $value = $value->getValue(); + } + $this->setElementData($offset, $value); + } + + /** + * Implements ArrayAccess::offsetUnset(). + */ + public function offsetUnset($offset) { + $this->clearElementData($offset); + } + + /** + * Implements Countable::count(). + */ + public function count() { + return count($this->getData()); + } + + /** + * Implements Drupal\Core\TypedData\ListInterface::isEmpty(). + */ + public function isEmpty() { + return empty($this->data); + } +} diff --git a/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php b/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php new file mode 100644 index 0000000..48ac596 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php @@ -0,0 +1,112 @@ +getBaseName($offset)) { + $metadata = $this->offsetGet($basename); + } + // Merge current metadata on top of it. + if ($meta = $this->readMetadata($offset)) { + $metadata = NestedArray::mergeDeep($metadata, $meta); + } + $this->storage[$offset] = $metadata; + return $metadata; + } + + /** + * Gets parent metadata name. + * + * @param string $name + * Configuration name or key. + * + * @return string + * Same name with the last part replaced by the filesystem marker. + */ + protected static function getBaseName($name) { + $parts = explode('.', $name); + $last = array_pop($parts); + // If this is not the generic metadata (where the last component of the + // name is a '%'), try to load that first. + if ($last != self::BASE_MARK) { + return implode('.', array_merge($parts, array(self::BASE_MARK))); + } + } + + /** + * Reads metadata from file system. + * + * @param string $name + * Configuration name or key. + * + * @return array + * Metadata array if found or empty array if not. + */ + protected function readMetadata($name) { + if (!isset($this->metadataStorage)) { + $this->metadataStorage = new MetadataStorage(); + } + try { + return $this->metadataStorage->read($name); + } + catch (Symfony\Component\Yaml\Exception\ParseException $e) { + // @todo Just log an error, but return empty array. + } + return array(); + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php b/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php new file mode 100644 index 0000000..0776f18 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php @@ -0,0 +1,90 @@ +getMetadataMap()); + } + + /** + * Overrides Drupal\Core\Config\FileStorage::getFilePath(). + */ + public function getFilePath($name) { + $metamap = $this->getMetadataMap(); + $module = $metamap[$name]; + // We are looking for $module/meta/$name.yml + $directory = drupal_get_path('module', $module) . '/meta'; + return $directory . '/' . $name . '.yml'; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::listAll(). + */ + public function listAll($prefix = '') { + $names = array_keys($this->getMetadataMap()); + if (!$prefix) { + return $names; + } + else { + foreach ($names as $index => $name) { + if (strpos($name, $prefix) !== 0 ) { + unset($names[$index]); + } + } + return $names; + } + } + + /** + * Returns a map of all metadata names and the module they belong to. + * + * @return array + * An array mapping metadata names with module names. + */ + protected function getMetadataMap() { + if (!isset($this->metaMap)) { + $extension = '.' . self::getFileExtension(); + foreach (module_list() as $module) { + $directory = drupal_get_path('module', $module) . '/meta'; + if (file_exists($directory)) { + $files = glob($directory . '/*' . $extension); + foreach ($files as $filename) { + $name = basename($filename, $extension); + $this->metaMap[$name] = $module; + } + } + } + } + return $this->metaMap; + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/NestedElement.php b/core/lib/Drupal/Core/Config/Metadata/NestedElement.php new file mode 100644 index 0000000..4312cd4 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/NestedElement.php @@ -0,0 +1,182 @@ +definition = $definition; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setData(). + */ + public function setData($value) { + $this->data = $value; + $this->getParent()->setElementData($this->key, $this->data); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setElementData() + */ + public function setElementData($key, $value) { + parent::setElementData($key, $value); + $this->getParent()->setElementData($this->key, $this->data); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getConfigWrapper(). + */ + public function getConfigWrapper() { + return $this->getParent()->getConfigWrapper(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getKey(). + */ + public function getKey() { + $parent = $this->getParent()->getKey(); + return ($parent ? $parent . '.' : '') . $this->key; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getMetadata(). + */ + public function getMetadata() { + return $this->getParent()->getElementMetadata($this->key); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getBase(). + */ + public function getBase() { + return $this->getDefinition(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::clearElementData() + */ + public function clearElementData($key) { + parent::clearElementData($key); + $this->getParent()->setElementData($this->key, $data); + } + + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getType(). + */ + public function getType() { + return $this->definition['type']; + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getDefinition(). + */ + public function getDefinition() { + return $this->definition; + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getValue(). + */ + public function getValue() { + return $this->getData(); + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::setValue(). + */ + public function setValue($value) { + $this->data = $value; + // Due to how the constructor works, this function may get called before + // the parent is set. + // @see Drupal\Core\TypedData\TypedDataManager::createInstance() + if ($this->getParent()) { + $this->getParent()->set($this->key, $value); + } + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getString(). + */ + public function getString() { + return (string) $this->getValue(); + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::validate(). + */ + public function validate() { + // This will be ok if we have any config data at all. + return isset($this->data); + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::getName(). + */ + public function getName() { + return $this->key; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->key = $name; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::getParent(). + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php b/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php new file mode 100644 index 0000000..5ec8803 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php @@ -0,0 +1,348 @@ +getElementDefinition($key); + $value = $this->getElementData($key); + // If this is an end property, the value will be the configuration data. + // If it is a NestedElement, it won't be set, but retrieved from the parent + // element. See NestedElement::setValue() + $context = array('name' => $key, 'parent' => $this); + return typed_data()->create($definition, $value, $context); + } + + /** + * Builds metadata for a child element. + * + * @param $key + * Element's key. + * + * @return array + * Configuration metadata for the element. + */ + protected function buildElementMetadata($key) { + $metadata = $this->getMetadata(); + $meta = isset($metadata[$key]) ? $metadata[$key] : array(); + $definition = $this->getBase(); + if (isset($definition['elements.base']) && $base = config_metadata($definition['elements.base'])) { + // Load common base metadata for all elements. + $meta = NestedArray::mergeDeep($base, $meta); + } + if (isset($definition['elements.name'])) { + // Load specific metadata for each element. + $name = $definition['elements.name']; + $element_key = $key; + if (isset($definition['elements.key'])) { + // The search key for this one is in the data itself. + $key_name = str_replace('%', $key, $definition['elements.key']); + + $element_key = $this->getElementData($key_name); + } + if ($element_key) { + $name = str_replace('%', $element_key, $name); + } + if ($merge = config_metadata($name)) { + $meta = NestedArray::mergeDeep($meta, $merge); + } + } + if (isset($definition['elements.label']) && !isset($meta['.label'])) { + // Extract element's label from configuration data. + $key_name = str_replace('%', $key, $definition['elements.label']); + if ($label = $this->getElementData($key_name)) { + $meta['.label'] = $label; + } + } + return $meta; + } + + /** + * Translates element if it fits our translation criteria. + * + * @param Drupal\Core\TypedData\TypedDataInterface $element + * Element value. + * @param string $langcode + * The language code for the translation. + * @param bool $strict + * Whether this is a strict translation. + * + * @return bool + * Whether the element fits the translation criteria. + */ + protected function translateElement($element, $langcode, $strict) { + $definition = $element->getDefinition(); + $value = $element->getValue(); + if ($value && is_string($value) && !empty($definition['translatable'])) { + if ($translation = $this->getConfigWrapper()->getStringTranslation($langcode, $value, $definition)) { + $element->setValue($translation); + return TRUE; + } + } + // The element doesn't have a translation, if strict mode we drop it. + return !$strict; + } + + /** + * Builds element definition from metadata. + * + * @param mixed $data + * The configuration data for the element. + * + * @param array $metadata + * The raw metadata array for the element. + * + * @return array + * The elemement's data definition. + */ + protected function buildDefinition($data, $metadata) { + $element = $children = array(); + foreach ($metadata as $name => $value) { + if (strpos($name, '.') === 0) { + // Metadata properties will be prefixed by a dot. + $element[substr($name, 1)] = $value; + } + else { + // Children elements will be arrays of metadata themselves. + $children[$name] = $value; + } + } + // The default type will depend on whether we've got children or not. + $element['config.children'] = !empty($children); + if (!isset($element['type'])) { + $element['type'] = $element['config.children'] || is_array($data) ? 'config_list' : 'string'; + } + return $element; + } + + /** + * Gets valid configuration data keys. + * + * @return array + * Array of valid configuration data keys. + */ + protected function getAllKeys() { + return is_array($this->data) ? array_keys($this->data) : array(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getData(). + */ + public function getData() { + return $this->data; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setData(). + */ + public function setData($value) { + $this->data = $value; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getLabel(). + */ + public function getLabel() { + $definition = $this->getBase(); + return isset($definition['label']) ? $definition['label'] : $this->getKey(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getTranslatedData(). + */ + public function getTranslatedData($langcode, $strict) { + $translation = array(); + foreach ($this->getAllKeys() as $key) { + $element = $this->buildElement($key); + $value = NULL; + if ($element instanceof WrapperInterface) { + $value = $element->getTranslatedData($langcode, $strict); + } + elseif ($this->translateElement($element, $langcode, $strict)) { + $value = $element->getValue(); + } + elseif (!$strict) { + $value = $element->getValue(); + } + if ($value || !$strict) { + $translation[$key] = $value; + } + } + return $translation; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElement(). + */ + public function getElement($key) { + $parts = explode('.', $key); + $first = array_shift($parts); + $data = $this->getElementData($first); + // Only attempt to build the element if it actually has configuration data. + if (isset($data) && $element = $this->buildElement($first)) { + $subkey = implode('.', $parts); + // If not a nested key, return the element whatever it is. But if we need + // to find a nested key, this is only possible if it's a WrapperInterface. + if (!$subkey) { + return $element; + } + elseif ($element instanceof WrapperInterface) { + return $element->getElement($subkey); + } + } + return NULL; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setElementData(). + */ + public function setElementData($key, $value) { + $parts = explode('.', $key); + if (count($parts) == 1) { + $this->data[$key] = $value; + } + else { + NestedArray::setValue($this->data, $parts, $value); + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementData(). + */ + public function getElementData($key) { + if (!$key) { + return $this->data; + } + else { + $parts = explode('.', $key); + if (count($parts) == 1) { + return isset($this->data[$key]) ? $this->data[$key] : NULL; + } + else { + $value = NestedArray::getValue($this->data, $parts, $key_exists); + return $key_exists ? $value : NULL; + } + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementDefinition(). + */ + public function getElementDefinition($key) { + return $this->buildDefinition($this->getElementData($key), $this->getElementMetadata($key)); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementMetadata(). + */ + public function getElementMetadata($key) { + // Metadata for nested elements must be built before. + if (!isset($this->metadata[$key])) { + $this->metadata[$key] = $this->buildElementMetadata($key); + } + return $this->metadata[$key]; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementLabel(). + */ + public function getElementLabel($key) { + $definition = $this->getElementDefinition($key); + return isset($definition['label']) ? $definition['label'] : $this->getElementKey($key); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementKey(). + */ + public function getElementKey($key) { + $parent = $this->getKey(); + return $parent ? $parent . '.' . $key : $key; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::clearElementData(). + */ + public function clearElementData($key) { + $parts = explode('.', $key); + if (count($parts) == 1) { + unset($this->data[$key]); + } + else { + NestedArray::unsetValue($this->data, $parts); + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::toArray(). + */ + public function toArray() { + $array = array(); + foreach ($this->getAllKeys() as $key) { + $array[$key] = $this->buildElement($key); + } + return $array; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::toList(). + */ + public function toList($base_name = '') { + $list = array(); + foreach ($this->toArray() as $key => $element) { + $name = $base_name ? $base_name . '.' . $key : $key; + if ($element instanceof WrapperInterface) { + $list += $element->toList($name); + } + else { + $list[$name] = $element; + } + } + return $list; + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->toArray()); + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php b/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php new file mode 100644 index 0000000..5668056 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php @@ -0,0 +1,234 @@ + 'Configuration metadata', + 'description' => 'Tests Metadata for configuration objects.', + 'group' => 'Configuration', + ); + } + + /** + * Tests the basic metadata retrieval layer. + */ + function testBasicMetadata() { + // Simple case, straight metadata. + $metadata = config_metadata('system.maintenance'); + $expected = array( + 'enabled' => array( + '.label' => 'Put site into maintenance mode', + '.type' => 'boolean' + ), + 'message' => array( + '.label' => 'Message to display when in maintenance mode', + '.type' => 'text', + ), + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for system.maintenance'); + // More complex case, fallback to parent name. + $metadata = config_metadata('image.style.large'); + $expected = array( + 'name' => array( + '.label' => 'Machine name', + '.type' => 'string', + ), + 'label' => array( + '.label' => 'Label', + '.type' => 'label' + ), + 'effects' => array( + '.label' => 'Style effects', + '.type' => 'config_list', + '.elements.name' => 'image.style.effects.%', + '.elements.key' => '%.name' + ) + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for image.style.large'); + + // Most complex case, fallback to parent name with merging + $metadata = config_metadata('image.style.effects.image_scale'); + $expected = array( + '.label' => 'Image scale', + 'name' => array( + '.label' => 'Style name', + '.type' => 'string', + ), + 'data' => array( + '.label' => 'Data', + '.type' => 'config_list', + 'width' => array( + '.label' => 'Width', + '.type' => 'integer' + ), + 'height' => array( + '.label' => 'Height', + '.type' => 'integer', + ), + 'upscale' => array( + '.label' => 'Upscale', + '.type' => 'boolean', + ) + ), + 'weight' => array( + '.label' => 'Weight', + '.type' => 'integer' + ), + 'ieid' => array( + '.label' => 'IEID', + ), + + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for image.style.effects.scale'); + + } + + /** + * Tests metadata applied to configuration objects. + */ + function testConfigMetadata() { + // Try some simple properties. + $meta = config_wrapper('system.site'); + $property = $meta->get('name'); + $this->assertTrue(is_a($property, 'Drupal\Core\TypedData\Type\String'), 'Got the right wrapper fo the site name property.'); + $this->assertEqual($property->getType(), 'label', 'Got the right string type for site name data.'); + $this->assertEqual($property->getValue(), 'Drupal', 'Got the right string value for site name data.'); + + $property = $meta->get('page.front'); + $this->assertTrue(is_a($property, 'Drupal\Core\TypedData\Type\String'), 'Got the right wrapper fo the page.front property.'); + $this->assertEqual($property->getType(), 'string', 'Got the right type for page.front data (undefined).'); + $this->assertEqual($property->getValue(), 'user', 'Got the right value for page.front data.'); + + // Check nested array of properties. + $list = $meta->get('page'); + $this->assertEqual(count($list), 3, 'Got a list with the right number of properties for site page data'); + $this->assertTrue(isset($list['front']) && isset($list['403']) && isset($list['404']), 'Got a list with the right properties for site page data.'); + $this->assertEqual($list['front']->getValue(), 'user', 'Got the right value for page.front data from the list.'); + + // Now let's try something more complex, with nested objects. + $wrapper = config_wrapper('image.style.large'); + $effects = $wrapper->get('effects'); + // The function is_array() doesn't work with ArrayAccess, so we use count(). + $this->assertTrue(count($effects) == 1, 'Got an array with effects for image.style.large data'); + $ieid = key($effects->toArray()); + $effect = $wrapper->get('effects.' . $ieid); + $this->assertTrue(count($effect['data']) && $effect['name']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.'); + $this->assertEqual($effect['data']['width']->getType(), 'integer', 'Got the right type for the scale effect width.'); + $this->assertEqual($effect['data']['width']->getValue(), 480, 'Got the right value for the scale effect width.' ); + // Try config key and getting nested properties. + $key = 'effects.' . $ieid . '.data'; + $this->assertEqual($effect['data']->getKey(), $key, 'Got the right configuration key for a nested image styple effect'); + $object = $wrapper->get($key); + $this->assertEqual(get_class($effect['data']), get_class($object), 'Got the same object class using different keys with dot notation.'); + $this->assertEqual($effect['data']->getData(), $object->getData(), 'Got the same object values using different keys with dot notation.'); + + // Finally update some object using a configuration wrapper. + $new_slogan = 'Site slogan for testing configuration metadata'; + $wrapper = config_wrapper('system.site'); + $wrapper->set('slogan', $new_slogan); + $site_slogan = $wrapper->get('slogan'); + $this->assertEqual($site_slogan->getValue(), $new_slogan, 'Successfully updated the contained configuration data'); + } + + /** + * Tests metadata applied to configuration objects. + */ + function testConfigTranslation() { + /* + // Create translation for site name using string overrides. + $langcode = 'xx'; + $site_name = $this->randomName(20);; + $translation['']['Drupal'] = $site_name; + variable_set('locale_custom_strings_' . $langcode, $translation); + $this->refreshVariables(); + + $wrapper = config_wrapper('system.site'); + + // Get strict translation + $translation = $wrapper->getTranslation($langcode, TRUE); + $properties = $translation->toList(); + $this->assertEqual(count($properties), 1, 'Got the right number of properties with strict translation'); + $this->assertEqual($properties['name']->getValue(), $site_name, 'Got the right translation for site name with strict translation'); + // Get non strict translation + $translation = $wrapper->getTranslation($langcode, FALSE); + $properties = $translation->toList(); + $this->assertEqual(count($properties), 6, 'Got the right number of properties with non strict translation'); + $this->assertEqual($properties['name']->getValue(), $site_name, 'Got the right translation for site name with non strict translation'); +*/ + } +} diff --git a/core/modules/image/meta/image.style.%.yml b/core/modules/image/meta/image.style.%.yml new file mode 100644 index 0000000..99b54ba --- /dev/null +++ b/core/modules/image/meta/image.style.%.yml @@ -0,0 +1,11 @@ +name: + .label: 'Machine name' + .type: string +label: + .label: 'Label' + .type: label +effects: + .label: 'Style effects' + .type: config_list + .elements.name: 'image.style.effects.%' + .elements.key: '%.name' diff --git a/core/modules/image/meta/image.style.effects.%.yml b/core/modules/image/meta/image.style.effects.%.yml new file mode 100644 index 0000000..7191867 --- /dev/null +++ b/core/modules/image/meta/image.style.effects.%.yml @@ -0,0 +1,11 @@ +name: + .label: 'Style name' + .type: string +data: + .label: 'Data' + .type: config_list +weight: + .label: 'Weight' + .type: integer +ieid: + .label: 'IEID' diff --git a/core/modules/image/meta/image.style.effects.image_scale.yml b/core/modules/image/meta/image.style.effects.image_scale.yml new file mode 100644 index 0000000..30b017e --- /dev/null +++ b/core/modules/image/meta/image.style.effects.image_scale.yml @@ -0,0 +1,11 @@ +.label: 'Image scale' +data: + width: + .label: 'Width' + .type: 'integer' + height: + .label: 'Height' + .type: 'integer' + upscale: + .label: 'Upscale' + .type: 'boolean' diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index b7850db..26ad373 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -549,6 +549,8 @@ function locale_translate_batch_finished($success, $results) { // Clear cache and force refresh of JavaScript translations. _locale_invalidate_js(); + // Refresh configuration translations. + _locale_invalidate_config(); cache()->invalidateTags(array('locale' => TRUE)); } } diff --git a/core/modules/locale/locale.config.inc b/core/modules/locale/locale.config.inc new file mode 100644 index 0000000..e62627e --- /dev/null +++ b/core/modules/locale/locale.config.inc @@ -0,0 +1,189 @@ +get('system.translate_configuration') ?: array(); + $names = array_keys(locale_config_default_folder()); + + if (empty($langcode)) { + // Invalidate all languages. + $languages = locale_translatable_language_list(); + foreach ($languages as $lcode => $data) { + $translate[$lcode] = $names; + } + } + else { + // Invalidate single language. + $translate[$langcode] = $names; + } + + state()->set('system.translate_configuration', $translate); + return $translate; +} + +/** + * Translate a few configuration objects for given language. + * + * @param string $langcode. + */ +function locale_config_translate_step($langcode, $number) { + $count = 0; + $translate = state()->get('system.translate_configuration') ?: array(); + if (!empty($translate[$langcode])) { + while (($name = array_shift($translate[$langcode])) && $count < $number) { + $names[] = $name; + $count++; + } + locale_config_update_multiple($names, array($langcode)); + state()->set('system.translate_configuration', $translate); + } + return $count; +} + +/** + * Update configuration when strings changed. + * + * This will be the function to use once this patch lands: + * http://drupal.org/node/1777070 + * + * @param $lids + * String identifiers for updated / created strings. + */ +function locale_config_update_translations($langcode, $lids) { + // Collect configuration names for these strings. + $locations = locale_storage()->getLocations(array('lid' => $lids, 'type' => 'configuration')); + + $name_strings = array(); + foreach ($locations as $location) { + $name_strings[$locations->name][] = $location->lid; + } + // Update configuration translation data. + if ($name_strings) { + locale_config_update_multiple(array_keys($name_strings), array($langcode)); + } +} + +/** + * Create translated configuration for module, all languages. + * + * This will run: + * - When the module is installed. + * - When a language is added (then we translate config for all installed modules) + */ +function locale_config_import_module($module) { + $directory = drupal_get_path('module', $module) . '/config'; + if (file_exists($directory)) { + $storage = new FileStorage($directory); + if ($names = $storage->listAll()) { + $langcodes = array_keys(language_list()); + return locale_config_update_multiple($names, $langcodes); + } + } +} + +/** + * Enable language, thus update all modules for that language. + */ +function locale_config_refresh_language($langcode) { + $names = array_keys(locale_config_default_folder()); + return locale_config_update_multiple($names, array($langcode)); +} + +/** + * Update all configuration for names / languages. + */ +function locale_config_update_multiple($names, $langcodes) { + $count = 0; + foreach ($names as $name) { + if ($data = locale_config_default_data($name)) { + $wrapper = new ConfigWrapper($name, $data, locale_storage()); + foreach ($langcodes as $lang) { + $translation = $wrapper->getTranslation($lang, TRUE)->getData(); + locale_config_update_storage($name, $lang, $translation); + $count++; + } + unset($wrapper); + } + } + return $count; +} + +/** + * Get a default configuration wrapper for config name. + * + * @todo The config objects are useless for this, ask some changes from the guys. + * @todo Format the previous @todo to 80 chars ;-) + */ +function locale_config_update_storage($name, $langcode, $data) { + $locale_name = 'locale.config.' . $langcode . '.' . $name; + $storage = drupal_container()->get('config.storage'); + // If there are existing translations for whatever reason we don't want to lose them + $current = $storage->read($locale_name); + $updated = $current ? NestedArray::mergeDeep($current, $data) : $data; + if ($updated) { + $storage->write($locale_name, $updated); + } + else { + $storage->delete($locale_name); + } + return $updated; +} + +/** + * Get default data for configuration name. + */ +function locale_config_default_data($name) { + if ($directory = locale_config_default_folder($name)) { + $storage = new FileStorage($directory); + return $storage->read($name); + } + else { + return array(); + } +} + +/** + * Get all default configuration names and the path on which they are. + * + * @todo Do the same for themes? + */ +function locale_config_default_folder($name = NULL) { + $folders = &drupal_static(__FUNCTION__); + if (!$folders) { + foreach (module_list() as $module) { + $directory = drupal_get_path('module', $module) . '/config'; + if (file_exists($directory)) { + $storage = new FileStorage($directory); + foreach ($storage->listAll() as $config_name) { + $folders[$config_name] = $directory; + } + } + } + } + if ($name) { + return isset($folders[$name]) ? $folders[$name] : NULL; + } + else { + return $folders; + } +} + diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 3d3ce1f..e7a05b0 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -242,6 +242,8 @@ function locale_language_insert($language) { cache('page')->flush(); // Force JavaScript translation file re-creation for the new language. _locale_invalidate_js($language->langcode); + // Force configuration translation to be re-created. + _locale_invalidate_config($language->langcode); } /** @@ -267,6 +269,7 @@ function locale_language_delete($language) { locale_translate_delete_translation_files($language->langcode); _locale_invalidate_js($language->langcode); + _locale_invalidate_config($language->langcode); // Changing the language settings impacts the interface: cache('page')->flush(); @@ -424,6 +427,14 @@ function locale_modules_installed($modules) { } /** + * Implements hook_modules_enabled(). + */ +function locale_modules_enabled($modules) { + module_load_include('inc', 'locale', 'locale.config'); + array_map('locale_config_import_module', $modules); +} + +/** * Implements hook_themes_enabled(). * * @todo This is technically wrong. We must not import upon enabling, but upon @@ -917,6 +928,25 @@ function _locale_invalidate_js($langcode = NULL) { } /** + * Force the configuration translations to be refreshed. + * + * This function sets a refresh flag for a specified language, or all + * languages except English, if none specified. JavaScript translation + * files are rebuilt (with locale_update_js_files()) the next time a + * request is served in that language. + * + * @param $langcode + * The language code for which the configuration needs to be retranslated. + * + * @return + * New content of the 'system.configuration_translated' variable. + */ +function _locale_invalidate_config($langcode = NULL) { + module_load_include('inc', 'locale', 'locale.config'); + locale_config_invalidate_language($langcode); +} + +/** * (Re-)Creates the JavaScript translation file for a language. * * @param $langcode @@ -1037,6 +1067,14 @@ function _locale_rebuild_js($langcode = NULL) { * Implements hook_language_init(). */ function locale_language_init() { + // Check whether configuration for this language needs to be rebuilt. + $translate = state()->get('system.translate_configuration') ?: array(); + $langcode = language(LANGUAGE_TYPE_INTERFACE)->langcode; + if (!empty($translate[$langcode])) { + module_load_include('inc', 'locale', 'locale.config'); + $count = locale_config_translate_step($langcode, 4); + } + // Add locale helper to configuration subscribers. drupal_container()->get('dispatcher')->addSubscriber(new LocaleConfigSubscriber()); } diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 71aa14d..6e9146a 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -403,6 +403,7 @@ function locale_translate_edit_form_validate($form, &$form_state) { */ function locale_translate_edit_form_submit($form, &$form_state) { $langcode = $form_state['values']['langcode']; + foreach ($form_state['values']['strings'] as $lid => $translations) { // Get target string, that may be NULL if there's no translation. $target = locale_storage()->findTranslation(array('language' => $langcode, 'lid' => $lid)); @@ -437,6 +438,8 @@ function locale_translate_edit_form_submit($form, &$form_state) { // Force JavaScript translation file recreation for this language. _locale_invalidate_js($langcode); + // Rebuild configuration translations for this language. + _locale_invalidate_config($langcode); // Clear locale cache. cache()->invalidateTags(array('locale' => TRUE)); } diff --git a/core/modules/system/meta/system.maintenance.yml b/core/modules/system/meta/system.maintenance.yml new file mode 100644 index 0000000..96be6e2 --- /dev/null +++ b/core/modules/system/meta/system.maintenance.yml @@ -0,0 +1,6 @@ +enabled: + .label: 'Put site into maintenance mode' + .type: boolean +message: + .label: 'Message to display when in maintenance mode' + .type: text diff --git a/core/modules/system/meta/system.site.yml b/core/modules/system/meta/system.site.yml new file mode 100644 index 0000000..7917269 --- /dev/null +++ b/core/modules/system/meta/system.site.yml @@ -0,0 +1,17 @@ +name: + .label: 'Site name' + .type: label +mail: + .label: 'Site mail' +slogan: + .label: 'Site slogan' + .type: text +page: + .type: config_list + .label: 'Default pages' + 403: + .label: 'Default 403 (access denied) page' + 404: + .label: 'Default 404 (not found) page' + front: + .label: 'Default front page' diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ea3dadc..17e0080 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2090,6 +2090,31 @@ function system_data_type_info() { 'class' => '\Drupal\Core\Entity\Field\Type\EntityReferenceItem', 'list class' => '\Drupal\Core\Entity\Field\Type\Field', ), + 'config_list' => array( + 'label' => t('Configuration list'), + 'description' => t('List of configuration data'), + 'class' => 'Drupal\Core\Config\Metadata\ListElement', + ), + 'config_element' => array( + 'label' => t('Nested configuration'), + 'description' => t('Nested configuration data'), + 'class' => 'Drupal\Core\Config\Metadata\NestedElement', + ), + // These are UI string types. + 'label' => array( + 'label' => t('Label'), + 'description' => t('Short object name.'), + 'class' => '\Drupal\Core\TypedData\Type\String', + 'primitive type' => Primitive::STRING, + 'translatable' => TRUE, + ), + 'text' => array( + 'label' => t('Text'), + 'description' => t('Textual content.'), + 'class' => '\Drupal\Core\TypedData\Type\String', + 'primitive type' => Primitive::STRING, + 'translatable' => TRUE, + ), ); } diff --git a/core/modules/user/meta/user.mail.%.yml b/core/modules/user/meta/user.mail.%.yml new file mode 100644 index 0000000..11b479b --- /dev/null +++ b/core/modules/user/meta/user.mail.%.yml @@ -0,0 +1,7 @@ +.label: 'Mail text' +subject: + .label: 'Subject' + .type: 'text' +body: + .label: 'Body' + .type: 'text' diff --git a/core/modules/user/meta/user.mail.yml b/core/modules/user/meta/user.mail.yml new file mode 100644 index 0000000..35cbd52 --- /dev/null +++ b/core/modules/user/meta/user.mail.yml @@ -0,0 +1,3 @@ +.label: 'User mails' +.type: config_list +.elements.name: 'user.mail.%'