diff --git a/core/includes/form.inc b/core/includes/form.inc index 7498ea5..f162d85 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3533,6 +3533,7 @@ function form_process_machine_name($element, &$form_state) { // 'source' only) would leave all other properties undefined, if the defaults // were defined in hook_element_info(). Therefore, we apply the defaults here. $element['#machine_name'] += array( + // @todo Use 'label' by default. 'source' => array('name'), 'target' => '#' . $element['#id'], 'label' => t('Machine name'), diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index 2f4d14a..c5423ca 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -270,6 +270,7 @@ class Config { * Deletes the configuration object. */ public function delete() { + // @todo Consider to remove the pruning of data for Config::delete(). $this->data = array(); $this->storage->delete($this->name); $this->isNew = TRUE; diff --git a/core/modules/config/config.api.php b/core/modules/config/config.api.php index f0c3afa..6aef002 100644 --- a/core/modules/config/config.api.php +++ b/core/modules/config/config.api.php @@ -31,12 +31,12 @@ * A configuration object containing the old configuration data. */ function MODULE_config_import_create($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } - $config_test = new ConfigTest($new_config); + $config_test = entity_create('config_test', $new_config->get()); $config_test->save(); return TRUE; } @@ -60,13 +60,25 @@ function MODULE_config_import_create($name, $new_config, $old_config) { * A configuration object containing the old configuration data. */ function MODULE_config_import_change($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } - $config_test = new ConfigTest($new_config); - $config_test->setOriginal($old_config); + + // @todo Make this less ugly. + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + $config_test = entity_load('config_test', $id); + + $config_test->original = clone $config_test; + foreach ($old_config->get() as $property => $value) { + $config_test->original->$property = $value; + } + + foreach ($new_config->get() as $property => $value) { + $config_test->$property = $value; + } + $config_test->save(); return TRUE; } @@ -90,7 +102,7 @@ function MODULE_config_import_change($name, $new_config, $old_config) { * A configuration object containing the old configuration data. */ function MODULE_config_import_delete($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; @@ -100,8 +112,8 @@ function MODULE_config_import_delete($name, $new_config, $old_config) { // But that is impossible currently, since the config system only knows // about deleted and added changes. Introduce an 'old_ID' key within // config objects as a standard? - $config_test = new ConfigTest($old_config); - $config_test->delete(); + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + config_test_delete($id); return TRUE; } diff --git a/core/modules/config/lib/Drupal/config/ConfigStorageController.php b/core/modules/config/lib/Drupal/config/ConfigStorageController.php new file mode 100644 index 0000000..7b5ef0e --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigStorageController.php @@ -0,0 +1,346 @@ +entityType = $entityType; + $this->entityInfo = entity_get_info($entityType); + $this->hookLoadArguments = array(); + $this->idKey = $this->entityInfo['entity keys']['id']; + + // The UUID key and property is hard-coded for all configurables. + $this->uuidKey = 'uuid'; + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::resetCache(). + */ + public function resetCache(array $ids = NULL) { + // The configuration system is fast enough and/or implements its own + // (advanced) caching mechanism already. + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::load(). + */ + public function load($ids = array(), $conditions = array()) { + $entities = array(); + + // Create a new variable which is either a prepared version of the $ids + // array for later comparison with the entity cache, or FALSE if no $ids + // were passed. + $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; + + // Load any remaining entities. This is the case if $ids + // is set to FALSE (so we load all entities), + // or if $conditions was passed without $ids. + if ($ids === FALSE || $ids || ($conditions && !$passed_ids)) { + $queried_entities = $this->buildQuery($ids, $conditions); + } + + // Pass all entities loaded from the database through $this->attachLoad(), + // which calls the + // entity type specific load callback, for example hook_node_type_load(). + if (!empty($queried_entities)) { + $this->attachLoad($queried_entities); + $entities += $queried_entities; + } + + // Ensure that the returned array is ordered the same as the original + // $ids array if this was passed in and remove any invalid ids. + if ($passed_ids) { + // Remove any invalid ids from the array. + $passed_ids = array_intersect_key($passed_ids, $entities); + foreach ($entities as $entity) { + $passed_ids[$entity->{$this->idKey}] = $entity; + } + $entities = $passed_ids; + } + + return $entities; + } + + /** + * Builds the query to load the entity. + * + * This has full revision support. For entities requiring special queries, + * the class can be extended, and the default query can be constructed by + * calling parent::buildQuery(). This is usually necessary when the object + * being loaded needs to be augmented with additional data from another + * table, such as loading node type into comments or vocabulary machine name + * into terms, however it can also support $conditions on different tables. + * See Drupal\comment\CommentStorageController::buildQuery() or + * Drupal\taxonomy\TermStorageController::buildQuery() for examples. + * + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * An array of conditions in the form 'field' => $value. + * @param $revision_id + * The ID of the revision to load, or FALSE if this query is asking for the + * most current revision(s). + * + * @return SelectQuery + * A SelectQuery object for loading the entity. + */ + protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { + $config_class = $this->entityInfo['entity class']; + $prefix = $config_class::getConfigPrefix() . '.'; + + // @todo Handle $conditions? + if ($ids === FALSE) { + $names = drupal_container()->get('config.storage')->listAll($prefix); + $result = array(); + foreach ($names as $name) { + $config = config($name); + $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityType); + } + return $result; + } + else { + $result = array(); + foreach ($ids as $id) { + $config = config($prefix . $id); + if (!$config->isNew()) { + $result[$id] = new $config_class($config->get(), $this->entityType); + } + } + return $result; + } + } + + /** + * Attaches data to entities upon loading. + * + * This will attach fields, if the entity is fieldable. It calls + * hook_entity_load() for modules which need to add data to all entities. + * It also calls hook_TYPE_load() on the loaded entities. For example + * hook_node_load() or hook_user_load(). If your hook_TYPE_load() + * expects special parameters apart from the queried entities, you can set + * $this->hookLoadArguments prior to calling the method. + * See Drupal\node\NodeStorageController::attachLoad() for an example. + * + * @param $queried_entities + * Associative array of query results, keyed on the entity ID. + * @param $revision_id + * ID of the revision that was loaded, or FALSE if the most current revision + * was loaded. + */ + protected function attachLoad(&$queried_entities, $revision_id = FALSE) { + // Call hook_entity_load(). + foreach (module_implements('entity_load') as $module) { + $function = $module . '_entity_load'; + $function($queried_entities, $this->entityType); + } + // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are + // always the queried entities, followed by additional arguments set in + // $this->hookLoadArguments. + $args = array_merge(array($queried_entities), $this->hookLoadArguments); + foreach (module_implements($this->entityType . '_load') as $module) { + call_user_func_array($module . '_' . $this->entityType . '_load', $args); + } + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::create(). + */ + public function create(array $values) { + $class = isset($this->entityInfo['entity class']) ? $this->entityInfo['entity class'] : 'Drupal\entity\Entity'; + + $entity = new $class($values, $this->entityType); + + // Assign a new UUID if there is none yet. + if (!isset($entity->{$this->uuidKey})) { + $uuid = new Uuid(); + $entity->{$this->uuidKey} = $uuid->generate(); + } + + return $entity; + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::delete(). + */ + public function delete($ids) { + $entities = $ids ? $this->load($ids) : FALSE; + if (!$entities) { + // If no IDs or invalid IDs were passed, do nothing. + return; + } + + $this->preDelete($entities); + foreach ($entities as $id => $entity) { + $this->invokeHook('predelete', $entity); + } + + foreach ($entities as $id => $entity) { + $config = config($entity::getConfigPrefix() . '.' . $entity->id()); + $config->delete(); + } + + $this->postDelete($entities); + foreach ($entities as $id => $entity) { + $this->invokeHook('delete', $entity); + } + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::save(). + */ + public function save(EntityInterface $entity) { + $prefix = $entity::getConfigPrefix() . '.'; + + // Load the stored entity, if any. + if ($entity->getOriginalID()) { + $id = $entity->getOriginalID(); + } + else { + $id = $entity->id(); + } + $config = config($prefix . $id); + $config->setName($prefix . $entity->id()); + + if (!$config->isNew() && !isset($entity->original)) { + $entity->original = entity_load_unchanged($this->entityType, $id); + } + + $this->preSave($entity); + $this->invokeHook('presave', $entity); + + // Configuration objects do not have a schema. Extract all key names from + // class properties. + $class_info = new \ReflectionClass($entity); + foreach ($class_info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + $config->set($name, $entity->$name); + } + + if (!$config->isNew()) { + $return = SAVED_NEW; + $config->save(); + $this->postSave($entity, TRUE); + $this->invokeHook('update', $entity); + } + else { + $return = SAVED_UPDATED; + $config->save(); + $entity->enforceIsNew(FALSE); + $this->postSave($entity, FALSE); + $this->invokeHook('insert', $entity); + } + + unset($entity->original); + + return $return; + } + + /** + * Acts on an entity before the presave hook is invoked. + * + * Used before the entity is saved and before invoking the presave hook. + */ + protected function preSave(EntityInterface $entity) { + } + + /** + * Acts on a saved entity before the insert or update hook is invoked. + * + * Used after the entity is saved, but before invoking the insert or update + * hook. + * + * @param $update + * (bool) TRUE if the entity has been updated, or FALSE if it has been + * inserted. + */ + protected function postSave(EntityInterface $entity, $update) { + // Delete the original configurable entity, in case the entity ID was + // renamed. + if ($update && !empty($entity->original) && $entity->{$this->idKey} !== $entity->original->{$this->idKey}) { + // @todo This should just delete the original config object without going + // through the API, no? + $entity->original->delete(); + } + } + + /** + * Acts on entities before they are deleted. + * + * Used before the entities are deleted and before invoking the delete hook. + */ + protected function preDelete($entities) { + } + + /** + * Acts on deleted entities before the delete hook is invoked. + * + * Used after the entities are deleted but before invoking the delete hook. + */ + protected function postDelete($entities) { + } + + /** + * Invokes a hook on behalf of the entity. + * + * @param $hook + * One of 'presave', 'insert', 'update', 'predelete', or 'delete'. + * @param $entity + * The entity object. + */ + protected function invokeHook($hook, EntityInterface $entity) { + // Invoke the hook. + module_invoke_all($this->entityType . '_' . $hook, $entity); + // Invoke the respective entity-level hook. + module_invoke_all('entity_' . $hook, $entity, $this->entityType); + } +} diff --git a/core/modules/config/lib/Drupal/config/ConfigurableBase.php b/core/modules/config/lib/Drupal/config/ConfigurableBase.php new file mode 100644 index 0000000..f53be7b --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigurableBase.php @@ -0,0 +1,103 @@ +id()) { + $this->originalID = $original_id; + } + } + + /** + * Implements ConfigurableInterface::getOriginalID(). + */ + public function getOriginalID() { + return $this->originalID; + } + + /** + * Overrides Entity::isNew(). + * + * EntityInterface::enforceIsNew() is not supported by configurable entities, + * since each Configurable is unique. + */ + final public function isNew() { + return !$this->id(); + } + + /** + * Overrides Entity::bundle(). + * + * EntityInterface::bundle() is not supported by configurable entities, since + * a Configurable is a bundle. + */ + final public function bundle() { + return $this->entityType; + } + + /** + * Overrides Entity::get(). + * + * EntityInterface::get() implements support for fieldable entities, but + * configurable entities are not fieldable. + */ + public function get($property_name, $langcode = NULL) { + // @todo: Add support for translatable properties being not fields. + return isset($this->{$property_name}) ? $this->{$property_name} : NULL; + } + + /** + * Overrides Entity::set(). + * + * EntityInterface::set() implements support for fieldable entities, but + * configurable entities are not fieldable. + */ + public function set($property_name, $value, $langcode = NULL) { + // @todo: Add support for translatable properties being not fields. + $this->{$property_name} = $value; + } + + /** + * Helper callback for uasort() to sort Configurable entities by weight and label. + */ + public static function sort($a, $b) { + $a_weight = isset($a->weight) ? $a->weight : 0; + $b_weight = isset($b->weight) ? $b->weight : 0; + if ($a_weight == $b_weight) { + $a_label = $a->label(); + $b_label = $b->label(); + return strnatcasecmp($a_label, $b_label); + } + return ($a_weight < $b_weight) ? -1 : 1; + } +} diff --git a/core/modules/config/lib/Drupal/config/ConfigurableInterface.php b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php new file mode 100644 index 0000000..7d6075b --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php @@ -0,0 +1,32 @@ + 'Configurable entities', + 'description' => 'Tests configurable entities.', + 'group' => 'Configuration', + ); + } + + /** + * Tests basic CRUD operations through the UI. + */ + function testCRUD() { + // Create a configurable entity. + $id = 'thingie'; + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Update the configurable entity. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); + $edit = array( + 'label' => 'Thongie', + ); + $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertText('Thongie'); + + // Delete the configurable entity. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id . '/delete'); + $this->drupalPost('admin/structure/config_test/manage/' . $id . '/delete', array(), 'Delete'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertNoText('Thongie'); + + // Re-create a configurable entity. + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Rename the configurable entity's ID/machine name. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); + $new_id = 'zingie'; + $edit = array( + 'id' => $new_id, + 'label' => 'Zingie', + ); + $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertText('Zingie'); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php index ec92fdf..7265abaa 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -31,6 +31,33 @@ class ConfigImportTest extends WebTestBase { ); } + function setUp() { + parent::setUp(); + + // Clear out any possibly existing hook invocation records. + unset($GLOBALS['hook_config_test']); + } + + /** + * Tests omission of module APIs for bare configuration operations. + */ + function testNoImport() { + $dynamic_name = 'config_test.dynamic.default'; + + // Verify the default configuration values exist. + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), 'default'); + + // Verify that a bare config() does not involve module APIs. + $this->assertFalse(isset($GLOBALS['hook_config_test'])); + + // Export. + config_export(); + + // Verify that config_export() does not involve module APIs. + $this->assertFalse(isset($GLOBALS['hook_config_test'])); + } + /** * Tests deletion of configuration during import. */ @@ -64,6 +91,14 @@ class ConfigImportTest extends WebTestBase { $this->assertIdentical($config->get('foo'), NULL); $config = config($dynamic_name); $this->assertIdentical($config->get('id'), NULL); + + // Verify that appropriate module API hooks have been invoked. + $this->assertTrue(isset($GLOBALS['hook_config_test']['load'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); } /** @@ -100,6 +135,14 @@ class ConfigImportTest extends WebTestBase { $this->assertIdentical($config->get('add_me'), 'new value'); $config = config($dynamic_name); $this->assertIdentical($config->get('label'), 'New'); + + // Verify that appropriate module API hooks have been invoked. + $this->assertFalse(isset($GLOBALS['hook_config_test']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); } /** @@ -138,5 +181,13 @@ class ConfigImportTest extends WebTestBase { $this->assertIdentical($config->get('foo'), 'beer'); $config = config($dynamic_name); $this->assertIdentical($config->get('label'), 'Updated'); + + // Verify that appropriate module API hooks have been invoked. + $this->assertTrue(isset($GLOBALS['hook_config_test']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php index 7ec6d8e..d730554 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php @@ -26,12 +26,12 @@ class ConfigInstallTest extends WebTestBase { */ function testModuleInstallation() { $default_config = 'config_test.system'; - $default_thingie = 'config_test.dynamic.default'; + $default_configurable = 'config_test.dynamic.default'; // Verify that default module config does not exist before installation yet. $config = config($default_config); $this->assertIdentical($config->isNew(), TRUE); - $config = config($default_thingie); + $config = config($default_configurable); $this->assertIdentical($config->isNew(), TRUE); // Install the test module. @@ -40,11 +40,20 @@ class ConfigInstallTest extends WebTestBase { // Verify that default module config exists. $config = config($default_config); $this->assertIdentical($config->isNew(), FALSE); - $config = config($default_thingie); + $config = config($default_configurable); $this->assertIdentical($config->isNew(), FALSE); // Verify that configuration import callback was invoked for the dynamic - // thingie. + // configurable entity. $this->assertTrue($GLOBALS['hook_config_import']); + + // Verify that config_test API hooks were invoked for the dynamic default + // configurable entity. + $this->assertFalse(isset($GLOBALS['hook_config_test']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); } } diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc new file mode 100644 index 0000000..80f3381 --- /dev/null +++ b/core/modules/config/tests/config_test/config_test.hooks.inc @@ -0,0 +1,52 @@ +save(); + $config_test = entity_create('config_test', $new_config->get()); + $config_test->save(); return TRUE; } @@ -20,15 +23,26 @@ function config_test_config_import_create($name, $new_config, $old_config) { * Implements MODULE_config_import_change(). */ function config_test_config_import_change($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module - // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $new_config->save(); + // @todo Make this less ugly. + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + $config_test = entity_load('config_test', $id); + + $config_test->original = clone $config_test; + foreach ($old_config->get() as $property => $value) { + $config_test->original->$property = $value; + } + + foreach ($new_config->get() as $property => $value) { + $config_test->$property = $value; + } + + $config_test->save(); return TRUE; } @@ -36,15 +50,248 @@ function config_test_config_import_change($name, $new_config, $old_config) { * Implements MODULE_config_import_delete(). */ function config_test_config_import_delete($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module - // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $old_config->delete(); + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + config_test_delete($id); return TRUE; } +/** + * Implements hook_entity_info(). + */ +function config_test_entity_info() { + $types['config_test'] = array( + 'label' => 'Test configuration', + 'controller class' => 'Drupal\config\ConfigStorageController', + 'entity class' => 'Drupal\config_test\ConfigTest', + 'uri callback' => 'config_test_uri', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + return $types; +} + +/** + * Entity uri callback. + * + * @param Drupal\config_test\ConfigTest $config_test + * A ConfigTest entity. + */ +function config_test_uri(ConfigTest $config_test) { + return array( + 'path' => 'admin/structure/config_test/manage/' . $config_test->id(), + ); +} + +/** + * Implements hook_menu(). + */ +function config_test_menu() { + $items['admin/structure/config_test'] = array( + 'title' => 'Test configuration', + 'page callback' => 'config_test_list_page', + 'access callback' => TRUE, + ); + $items['admin/structure/config_test/add'] = array( + 'title' => 'Add test configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_form'), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_ACTION, + ); + $items['admin/structure/config_test/manage/%config_test'] = array( + 'title' => 'Edit test configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_form', 4), + 'access callback' => TRUE, + ); + $items['admin/structure/config_test/manage/%config_test/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/structure/config_test/manage/%config_test/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_delete_form', 4), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + ); + return $items; +} + +/** + * Loads a ConfigTest object. + * + * @param string $id + * The ID of the ConfigTest object to load. + */ +function config_test_load($id) { + return entity_load('config_test', $id); +} + +/** + * Saves a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * The ConfigTest object to save. + */ +function config_test_save(ConfigTest $config_test) { + return $config_test->save(); +} + +/** + * Deletes a ConfigTest object. + * + * @param string $id + * The ID of the ConfigTest object to delete. + */ +function config_test_delete($id) { + entity_delete_multiple('config_test', array($id)); +} + +/** + * Page callback; Lists available ConfigTest objects. + */ +function config_test_list_page() { + $entities = entity_load_multiple('config_test', FALSE); + uasort($entities, 'Drupal\config\ConfigurableBase::sort'); + + $rows = array(); + foreach ($entities as $config_test) { + $uri = $config_test->uri(); + $row = array(); + $row['name']['data'] = array( + '#type' => 'link', + '#title' => $config_test->label(), + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + $row['delete']['data'] = array( + '#type' => 'link', + '#title' => t('Delete'), + '#href' => $uri['path'] . '/delete', + '#options' => $uri['options'], + ); + $rows[] = $row; + } + $build = array( + '#theme' => 'table', + '#header' => array('Name', 'Operations'), + '#rows' => $rows, + '#empty' => format_string('No test configuration defined. Add some', array( + '@add-url' => url('admin/structure/config_test/add'), + )), + ); + return $build; +} + +/** + * Form constructor to add or edit a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * (optional) An existing ConfigTest object to edit. If omitted, the form + * creates a new ConfigTest. + */ +function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) { + // Standard procedure for handling the entity argument in entity forms, taking + // potential form caching and rebuilds properly into account. + // @see http://drupal.org/node/1499596 + if (!isset($form_state['config_test'])) { + if (!isset($config_test)) { + $config_test = entity_create('config_test', array()); + } + $form_state['config_test'] = $config_test; + } + else { + $config_test = $form_state['config_test']; + } + + $form['label'] = array( + '#type' => 'textfield', + '#title' => 'Label', + '#default_value' => $config_test->label(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $config_test->id(), + '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'config_test_load', + // @todo Update form_process_machine_name() to use 'label' by default. + 'source' => array('label'), + ), + ); + $form['style'] = array( + '#type' => 'select', + '#title' => 'Image style', + '#options' => array(), + '#default_value' => $config_test->get('style'), + '#access' => FALSE, + ); + if (module_exists('image')) { + $form['style']['#access'] = TRUE; + $form['style']['#options'] = image_style_options(); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Save'); + + return $form; +} + +/** + * Form submission handler for config_test_form(). + */ +function config_test_form_submit($form, &$form_state) { + form_state_values_clean($form_state); + + $config_test = $form_state['config_test']; + entity_form_submit_build_entity('config_test', $config_test, $form, $form_state); + + $status = $config_test->save(); + + if ($status == SAVED_UPDATED) { + drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $config_test->label()))); + } + else { + drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->label()))); + } + + $form_state['redirect'] = 'admin/structure/config_test'; +} + +/** + * Form constructor to delete a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * The ConfigTest object to delete. + */ +function config_test_delete_form($form, &$form_state, ConfigTest $config_test) { + $form_state['config_test'] = $config_test; + + $form['id'] = array('#type' => 'value', '#value' => $config_test->id()); + return confirm_form($form, + format_string('Are you sure you want to delete %label', array('%label' => $config_test->label())), + 'admin/structure/config_test', + NULL, + 'Delete' + ); +} + +/** + * Form submission handler for config_test_delete_form(). + */ +function config_test_delete_form_submit($form, &$form_state) { + $form_state['config_test']->delete(); + $form_state['redirect'] = 'admin/structure/config_test'; +} diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php new file mode 100644 index 0000000..104c11d --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php @@ -0,0 +1,34 @@ +addTag('translatable') - ->fields('c', array('cid', 'category', 'recipients', 'selected')) - ->orderBy('weight') - ->orderBy('category') - ->execute() - ->fetchAll(); + $categories = entity_load_multiple('contact_category', FALSE); + uasort($categories, 'Drupal\config\ConfigurableBase::sort'); + $default_category = config('contact.settings')->get('default_category'); // Loop through the categories and add them to the table. foreach ($categories as $category) { $rows[] = array( - check_plain($category->category), - check_plain($category->recipients), - ($category->selected ? t('Yes') : t('No')), - l(t('Edit'), 'admin/structure/contact/edit/' . $category->cid), - l(t('Delete'), 'admin/structure/contact/delete/' . $category->cid), + check_plain($category->label()), + check_plain(implode(', ', $category->recipients)), + ($default_category == $category->id() ? t('Yes') : t('No')), + l(t('Edit'), 'admin/structure/contact/manage/' . $category->id()), + l(t('Delete'), 'admin/structure/contact/manage/' . $category->id() . '/delete'), ); } @@ -57,68 +54,70 @@ function contact_category_list() { /** * Form constructor for the category edit form. * - * @param $category - * An array describing the category to be edited. May be empty for new - * categories. Recognized array keys are: - * - category: The name of the category. - * - recipients: A comma-separated list of recipients. - * - reply: (optional) The body of the auto-reply message. - * - weight: The weight of the category. - * - selected: Boolean indicating whether the category should be selected by - * default. - * - cid: The category ID for which the form is to be displayed. + * @param Drupal\contact\Category $category + * (optional) The contact category to be edited. * * @see contact_menu() * @see contact_category_edit_form_validate() * @see contact_category_edit_form_submit() * @ingroup forms */ -function contact_category_edit_form($form, &$form_state, array $category = array()) { - // If this is a new category, add the default values. - $category += array( - 'category' => '', - 'recipients' => '', - 'reply' => '', - 'weight' => 0, - 'selected' => 0, - 'cid' => NULL, - ); +function contact_category_edit_form($form, &$form_state, Category $category = NULL) { + // During initial form build, add the entity to the form state for use + // during form building and processing. During a rebuild, use what is in the + // form state. + if (!isset($form_state['contact_category'])) { + if (!isset($category)) { + $category = entity_create('contact_category', array()); + } + $form_state['contact_category'] = $category; + } + else { + $category = $form_state['contact_category']; + } - $form['category'] = array( + $default_category = config('contact.settings')->get('default_category'); + + $form['label'] = array( '#type' => 'textfield', - '#title' => t('Category'), + '#title' => t('Label'), '#maxlength' => 255, - '#default_value' => $category['category'], + '#default_value' => $category->label(), '#description' => t("Example: 'website feedback' or 'product information'."), '#required' => TRUE, ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $category->id(), + '#machine_name' => array( + 'exists' => 'contact_category_load', + 'source' => array('label'), + ), + '#disabled' => (bool) $category->id(), + ); $form['recipients'] = array( '#type' => 'textarea', '#title' => t('Recipients'), - '#default_value' => $category['recipients'], + '#default_value' => implode(', ', $category->recipients), '#description' => t("Example: 'webmaster@example.com' or 'sales@example.com,support@example.com' . To specify multiple recipients, separate each e-mail address with a comma."), '#required' => TRUE, ); $form['reply'] = array( '#type' => 'textarea', '#title' => t('Auto-reply'), - '#default_value' => $category['reply'], + '#default_value' => $category->reply, '#description' => t('Optional auto-reply. Leave empty if you do not want to send the user an auto-reply message.'), ); $form['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), - '#default_value' => $category['weight'], + '#default_value' => $category->weight, '#description' => t('When listing categories, those with lighter (smaller) weights get listed before categories with heavier (larger) weights. Categories with equal weights are sorted alphabetically.'), ); $form['selected'] = array( '#type' => 'checkbox', '#title' => t('Make this the default category.'), - '#default_value' => $category['selected'], - ); - $form['cid'] = array( - '#type' => 'value', - '#value' => $category['cid'], + '#default_value' => $default_category === $category->id(), ); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( @@ -138,24 +137,13 @@ function contact_category_edit_form_validate($form, &$form_state) { // Validate and each e-mail recipient. $recipients = explode(',', $form_state['values']['recipients']); - // When creating a new contact form, or renaming the category on an existing - // contact form, make sure that the given category is unique. - $category = $form_state['values']['category']; - $query = db_select('contact', 'c')->condition('c.category', $category, '='); - if (!empty($form_state['values']['cid'])) { - $query->condition('c.cid', $form_state['values']['cid'], '<>'); - } - if ($query->countQuery()->execute()->fetchField()) { - form_set_error('category', t('A contact form with category %category already exists.', array('%category' => $category))); - } - foreach ($recipients as &$recipient) { $recipient = trim($recipient); if (!valid_email_address($recipient)) { form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient))); } } - $form_state['values']['recipients'] = implode(',', $recipients); + $form_state['values']['recipients'] = $recipients; } /** @@ -164,48 +152,57 @@ function contact_category_edit_form_validate($form, &$form_state) { * @see contact_category_edit_form_validate() */ function contact_category_edit_form_submit($form, &$form_state) { + // Update the default category. + $contact_config = config('contact.settings'); if ($form_state['values']['selected']) { - // Unselect all other contact categories. - db_update('contact') - ->fields(array('selected' => '0')) - ->execute(); + $contact_config + ->set('default_category', $form_state['values']['id']) + ->save(); } - - if (empty($form_state['values']['cid'])) { - drupal_write_record('contact', $form_state['values']); - } - else { - drupal_write_record('contact', $form_state['values'], array('cid')); + // If it was the default category, empty out the setting. + elseif ($contact_config->get('default_category') == $form_state['values']['id']) { + $contact_config + ->clear('default_category') + ->save(); } - drupal_set_message(t('Category %category has been saved.', array('%category' => $form_state['values']['category']))); - watchdog('contact', 'Category %category has been saved.', array('%category' => $form_state['values']['category']), WATCHDOG_NOTICE, l(t('Edit'), 'admin/structure/contact/edit/' . $form_state['values']['cid'])); + // Remove the 'selected' value, which is not part of the Category. + unset($form_state['values']['selected']); + form_state_values_clean($form_state); + + $category = $form_state['contact_category']; + entity_form_submit_build_entity('contact_category', $category, $form, $form_state); + + $category->save(); + + drupal_set_message(t('Category %label has been saved.', array('%label' => $category->label()))); + watchdog('contact', 'Category %label has been saved.', array('%label' => $category->label()), WATCHDOG_NOTICE, l(t('Edit'), 'admin/structure/contact/manage/' . $category->id())); + $form_state['redirect'] = 'admin/structure/contact'; } /** * Form constructor for the contact category deletion form. * - * @param $contact - * Array describing the contact category to be deleted. See the documentation - * of contact_category_edit_form() for the recognized keys. + * @param Drupal\contact\Category $category + * The contact category to be deleted. * * @see contact_menu() * @see contact_category_delete_form_submit() */ -function contact_category_delete_form($form, &$form_state, array $contact) { - $form['contact'] = array( +function contact_category_delete_form($form, &$form_state, Category $category) { + $form_state['contact_category'] = $category; + $form['id'] = array( '#type' => 'value', - '#value' => $contact, + '#value' => $category->id(), ); return confirm_form( $form, - t('Are you sure you want to delete %category?', array('%category' => $contact['category'])), + t('Are you sure you want to delete %label?', array('%label' => $category->label())), 'admin/structure/contact', t('This action cannot be undone.'), - t('Delete'), - t('Cancel') + t('Delete') ); } @@ -213,14 +210,11 @@ function contact_category_delete_form($form, &$form_state, array $contact) { * Form submission handler for contact_category_delete_form(). */ function contact_category_delete_form_submit($form, &$form_state) { - $contact = $form['contact']['#value']; - - db_delete('contact') - ->condition('cid', $contact['cid']) - ->execute(); + $category = $form_state['contact_category']; + $category->delete(); - drupal_set_message(t('Category %category has been deleted.', array('%category' => $contact['category']))); - watchdog('contact', 'Category %category has been deleted.', array('%category' => $contact['category']), WATCHDOG_NOTICE); + drupal_set_message(t('Category %label has been deleted.', array('%label' => $category->label()))); + watchdog('contact', 'Category %label has been deleted.', array('%label' => $category->label()), WATCHDOG_NOTICE); $form_state['redirect'] = 'admin/structure/contact'; } diff --git a/core/modules/contact/contact.info b/core/modules/contact/contact.info index eff6d33..b04cd1d 100644 --- a/core/modules/contact/contact.info +++ b/core/modules/contact/contact.info @@ -4,3 +4,4 @@ package = Core version = VERSION core = 8.x configure = admin/structure/contact +dependencies[] = config diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install index f956242..ee83410 100644 --- a/core/modules/contact/contact.install +++ b/core/modules/contact/contact.install @@ -6,65 +6,6 @@ */ /** - * Implements hook_schema(). - */ -function contact_schema() { - $schema['contact'] = array( - 'description' => 'Contact form category settings.', - 'fields' => array( - 'cid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique category ID.', - ), - 'category' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Category name.', - 'translatable' => TRUE, - ), - 'recipients' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => 'Comma-separated list of recipient e-mail addresses.', - ), - 'reply' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => 'Text of the auto-reply message.', - ), - 'weight' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => "The category's weight.", - ), - 'selected' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - 'description' => 'Flag to indicate whether or not category is selected by default. (1 = Yes, 0 = No)', - ), - ), - 'primary key' => array('cid'), - 'unique keys' => array( - 'category' => array('category'), - ), - 'indexes' => array( - 'list' => array('weight', 'category'), - ), - ); - - return $schema; -} - -/** * Implements hook_install(). */ function contact_install() { @@ -72,18 +13,15 @@ function contact_install() { if (empty($site_mail)) { $site_mail = ini_get('sendmail_from'); } - // Insert a default contact category. - db_insert('contact') - ->fields(array( - 'category' => 'Website feedback', - 'recipients' => $site_mail, - 'selected' => 1, - 'reply' => '', - )) - ->execute(); + config('contact.category.feedback')->set('recipients', array($site_mail))->save(); } /** + * @addtogroup updates-7.x-to-8.x + * @{ + */ + +/** * Moves contact setting from variable to config. * * @ingroup config_upgrade @@ -95,3 +33,49 @@ function contact_update_8000() { 'contact_threshold_window' => 'flood.interval', )); } + +/** + * Migrate contact categories into configuration. + * + * @ingroup config_upgrade + */ +function contact_update_8001() { + $result = db_query('SELECT * FROM {contact}'); + foreach ($result as $category) { + // Generate machine readable names. + if ($category->category == 'Website feedback') { + // Use default id for unchanged category. + $category->id = 'feedback'; + } + else { + $category->id = drupal_strtolower($category->category); + $category->id = preg_replace('@[^a-z0-9]@', '_', $category->id); + } + // Save default category setting. + if ($category->selected) { + config('contact.settings') + ->set('default_category', $category->id) + ->save(); + } + // Save the config object. + config('contact.category.' . $category->id) + ->set('id', $category->id) + ->set('label', $category->category) + ->set('recipients', explode(',', $category->recipients)) + ->set('reply', $category->reply) + ->set('weight', $category->weight) + ->save(); + } +} + +/** + * Drop the {contact} table. + */ +function contact_update_8002() { + db_drop_table('contact'); +} + +/** + * @} End of "defgroup updates-7.x-to-8.x". + * The next series of updates should start at 9000. + */ diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module index 245c125..e176542 100644 --- a/core/modules/contact/contact.module +++ b/core/modules/contact/contact.module @@ -5,6 +5,8 @@ * Enables the use of personal and site-wide contact forms. */ +use Drupal\contact\Category; + /** * Implements hook_help(). */ @@ -71,15 +73,15 @@ function contact_menu() { 'weight' => 1, 'file' => 'contact.admin.inc', ); - $items['admin/structure/contact/edit/%contact'] = array( + $items['admin/structure/contact/manage/%contact_category'] = array( 'title' => 'Edit contact category', 'page callback' => 'drupal_get_form', 'page arguments' => array('contact_category_edit_form', 4), 'access arguments' => array('administer contact forms'), 'file' => 'contact.admin.inc', ); - $items['admin/structure/contact/delete/%contact'] = array( - 'title' => 'Delete contact', + $items['admin/structure/contact/manage/%contact_category/delete'] = array( + 'title' => 'Delete contact category', 'page callback' => 'drupal_get_form', 'page arguments' => array('contact_category_delete_form', 4), 'access arguments' => array('administer contact forms'), @@ -147,21 +149,97 @@ function _contact_personal_tab_access($account) { } /** + * Implements MODULE_config_import_create(). + */ +function contact_config_import_create($name, $new_config, $old_config) { + if (strpos($name, 'contact.category.') !== 0) { + return FALSE; + } + + $category = entity_create('contact_category', $new_config->get()); + $category->save(); + return TRUE; +} + +/** + * Implements MODULE_config_import_change(). + */ +function contact_config_import_change($name, $new_config, $old_config) { + if (strpos($name, 'contact.category.') !== 0) { + return FALSE; + } + + // @todo Make this less ugly. + $id = substr($name, strlen(Category::getConfigPrefix()) + 1); + $category = entity_load('contact_category', $id); + + $category->original = clone $category; + foreach ($old_config->get() as $property => $value) { + $category->original->$property = $value; + } + + foreach ($new_config->get() as $property => $value) { + $category->$property = $value; + } + + $category->save(); + return TRUE; +} + +/** + * Implements MODULE_config_import_delete(). + */ +function contact_config_import_delete($name, $new_config, $old_config) { + if (strpos($name, 'contact.category.') !== 0) { + return FALSE; + } + + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + entity_delete_multiple('contact_category', array($id)); + return TRUE; +} + +/** + * Implements hook_entity_info(). + */ +function contact_entity_info() { + $types['contact_category'] = array( + 'label' => 'Category', + 'controller class' => 'Drupal\config\ConfigStorageController', + 'entity class' => 'Drupal\contact\Category', + 'uri callback' => 'contact_category_uri', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + return $types; +} + +/** + * Entity uri callback. + * + * @param Drupal\contact\Category $category + * A Category entity. + */ +function contact_category_uri(Category $category) { + return array( + 'path' => 'admin/structure/contact/manage/' . $category->id(), + ); +} + +/** * Loads a contact category. * - * @param $cid - * The contact category ID. + * @param $id + * The ID of the contact category to load. * - * @return - * An array with the contact category's data. + * @return Drupal\contact\Category|false + * A Category object or FALSE if the requested $id does not exist. */ -function contact_load($cid) { - return db_select('contact', 'c') - ->addTag('translatable') - ->fields('c') - ->condition('cid', $cid) - ->execute() - ->fetchAssoc(); +function contact_category_load($id) { + return entity_load('contact_category', $id); } /** @@ -172,7 +250,7 @@ function contact_mail($key, &$message, $params) { $variables = array( '!site-name' => config('system.site')->get('name'), '!subject' => $params['subject'], - '!category' => isset($params['category']['category']) ? $params['category']['category'] : '', + '!category' => isset($params['category']) ? $params['category']->label() : '', '!form-url' => url(current_path(), array('absolute' => TRUE, 'language' => $language)), '!sender-name' => user_format_name($params['sender']), '!sender-url' => $params['sender']->uid ? url('user/' . $params['sender']->uid, array('absolute' => TRUE, 'language' => $language)) : $params['sender']->mail, @@ -188,7 +266,7 @@ function contact_mail($key, &$message, $params) { case 'page_autoreply': $message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->langcode)); - $message['body'][] = $params['category']['reply']; + $message['body'][] = $params['category']->reply; break; case 'user_mail': diff --git a/core/modules/contact/contact.pages.inc b/core/modules/contact/contact.pages.inc index 92d73bc..9e93f30 100644 --- a/core/modules/contact/contact.pages.inc +++ b/core/modules/contact/contact.pages.inc @@ -12,7 +12,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * Form constructor for the site-wide contact form. * * @see contact_menu() - * @see contact_site_form_validate() * @see contact_site_form_submit() * @ingroup forms */ @@ -29,14 +28,7 @@ function contact_site_form($form, &$form_state) { } // Get an array of the categories and the current default category. - $categories = db_select('contact', 'c') - ->addTag('translatable') - ->fields('c', array('cid', 'category')) - ->orderBy('weight') - ->orderBy('category') - ->execute() - ->fetchAllKeyed(); - $default_category = db_query("SELECT cid FROM {contact} WHERE selected = 1")->fetchField(); + $categories = entity_load_multiple('contact_category', FALSE); // If there are no categories, do not display the form. if (!$categories) { @@ -48,15 +40,18 @@ function contact_site_form($form, &$form_state) { } } + // Prepare array for select options. + uasort($categories, 'Drupal\config\ConfigurableBase::sort'); + $options = array(); + foreach ($categories as $category) { + $options[$category->id()] = $category->label(); + } + // If there is more than one category available and no default category has // been selected, prepend a default placeholder value. + $default_category = $config->get('default_category'); if (!$default_category) { - if (count($categories) > 1) { - $categories = array(0 => t('- Please choose -')) + $categories; - } - else { - $default_category = key($categories); - } + $default_category = !empty($categories) ? key($categories) : NULL; } if (!$user->uid) { @@ -103,11 +98,12 @@ function contact_site_form($form, &$form_state) { '#maxlength' => 255, '#required' => TRUE, ); - $form['cid'] = array( + $form['category'] = array( '#type' => 'select', '#title' => t('Category'), '#default_value' => $default_category, - '#options' => $categories, + '#options' => $options, + '#empty_value' => 0, '#required' => TRUE, '#access' => count($categories) > 1, ); @@ -133,17 +129,6 @@ function contact_site_form($form, &$form_state) { } /** - * Form validation handler for contact_site_form(). - * - * @see contact_site_form_submit() - */ -function contact_site_form_validate($form, &$form_state) { - if (!$form_state['values']['cid']) { - form_set_error('cid', t('You must select a valid category.')); - } -} - -/** * Form submission handler for contact_site_form(). * * @see contact_site_form_validate() @@ -156,7 +141,7 @@ function contact_site_form_submit($form, &$form_state) { $values['sender'] = $user; $values['sender']->name = $values['name']; $values['sender']->mail = $values['mail']; - $values['category'] = contact_load($values['cid']); + $values['category'] = contact_category_load($values['category']); // Save the anonymous user information to a cookie for reuse. if (!$user->uid) { @@ -165,7 +150,7 @@ function contact_site_form_submit($form, &$form_state) { } // Get the to and from e-mail addresses. - $to = $values['category']['recipients']; + $to = implode(', ', $values['category']->recipients); $from = $values['sender']->mail; // Send the e-mail to the recipients using the site default language. @@ -177,12 +162,12 @@ function contact_site_form_submit($form, &$form_state) { } // Send an auto-reply if necessary using the current language. - if ($values['category']['reply']) { + if ($values['category']->reply) { drupal_mail('contact', 'page_autoreply', $from, $language_interface, $values, $to); } flood_register_event('contact', config('contact.settings')->get('flood.interval')); - watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']['category'])); + watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']->label())); // Jump to home page rather than back to contact page to avoid // contradictory messages if flood control has been activated. diff --git a/core/modules/contact/lib/Drupal/contact/Category.php b/core/modules/contact/lib/Drupal/contact/Category.php new file mode 100644 index 0000000..dac5c33 --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Category.php @@ -0,0 +1,48 @@ +addCategory($this->randomName(16), $invalid_recipient, '', FALSE); + $this->addCategory($this->randomName(16), $this->randomName(16), $invalid_recipient, '', FALSE); $this->assertRaw(t('%recipient is an invalid e-mail address.', array('%recipient' => $invalid_recipient)), t('Caught invalid recipient (' . $invalid_recipient . ').')); } // Test validation of empty category and recipients fields. - $this->addCategory($category = '', '', '', TRUE); - $this->assertText(t('Category field is required.'), t('Caught empty category field')); + $this->addCategory('', '', '', '', TRUE); + $this->assertText(t('Label field is required.'), t('Caught empty category field')); $this->assertText(t('Recipients field is required.'), t('Caught empty recipients field.')); // Create first valid category. $recipients = array('simpletest@example.com', 'simpletest2@example.com', 'simpletest3@example.com'); - $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0])), '', TRUE); - $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.')); + $this->addCategory($id = drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0])), '', TRUE); + $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.')); // Make sure the newly created category is included in the list of categories. - $this->assertNoUniqueText($category, t('New category included in categories list.')); + $this->assertNoUniqueText($label, t('New category included in categories list.')); // Test update contact form category. - $categories = $this->getCategories(); - $category_id = $this->updateCategory($categories, $category = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE); - $category_array = db_query("SELECT category, recipients, reply, selected FROM {contact} WHERE cid = :cid", array(':cid' => $category_id))->fetchAssoc(); - $this->assertEqual($category_array['category'], $category); - $this->assertEqual($category_array['recipients'], $recipients_str); - $this->assertEqual($category_array['reply'], $reply); - $this->assertFalse($category_array['selected']); - $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.')); + $this->updateCategory($id, $label = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE); + $config = config('contact.category.' . $id)->get(); + $this->assertEqual($config['label'], $label); + $this->assertEqual($config['recipients'], array($recipients[0], $recipients[1])); + $this->assertEqual($config['reply'], $reply); + $this->assertNotEqual($id, config('contact.settings')->get('default_category')); + $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.')); // Ensure that the contact form is shown without a category selection input. user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form')); @@ -102,16 +101,16 @@ class ContactSitewideTest extends WebTestBase { $this->drupalLogin($admin_user); // Add more categories. - $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1])), '', FALSE); - $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.')); + $this->addCategory(drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0], $recipients[1])), '', FALSE); + $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.')); - $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1], $recipients[2])), '', FALSE); - $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.')); + $this->addCategory($name = drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0], $recipients[1], $recipients[2])), '', FALSE); + $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.')); // Try adding a category that already exists. - $this->addCategory($category, '', '', FALSE); - $this->assertNoRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category not saved.')); - $this->assertRaw(t('A contact form with category %category already exists.', array('%category' => $category)), t('Duplicate category error found.')); + $this->addCategory($name, $label, '', '', FALSE); + $this->assertNoRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category not saved.')); + $this->assertRaw(t('The machine-readable name is already in use. It must be unique.'), t('Duplicate category error found.')); // Clear flood table in preparation for flood test and allow other checks to complete. db_delete('flood')->execute(); @@ -130,36 +129,39 @@ class ContactSitewideTest extends WebTestBase { $this->assertResponse(200, t('Access granted to anonymous user with permission.')); // Submit contact form with invalid values. - $this->submitContact('', $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64)); + $categories = entity_load_multiple('contact_category', FALSE); + $id = key($categories); + + $this->submitContact('', $recipients[0], $this->randomName(16), $id, $this->randomName(64)); $this->assertText(t('Your name field is required.'), t('Name required.')); - $this->submitContact($this->randomName(16), '', $this->randomName(16), $categories[0], $this->randomName(64)); + $this->submitContact($this->randomName(16), '', $this->randomName(16), $id, $this->randomName(64)); $this->assertText(t('Your e-mail address field is required.'), t('E-mail required.')); - $this->submitContact($this->randomName(16), $invalid_recipients[0], $this->randomName(16), $categories[0], $this->randomName(64)); + $this->submitContact($this->randomName(16), $invalid_recipients[0], $this->randomName(16), $id, $this->randomName(64)); $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => 'invalid')), 'Valid e-mail required.'); - $this->submitContact($this->randomName(16), $recipients[0], '', $categories[0], $this->randomName(64)); + $this->submitContact($this->randomName(16), $recipients[0], '', $id, $this->randomName(64)); $this->assertText(t('Subject field is required.'), t('Subject required.')); - $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], ''); + $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $id, ''); $this->assertText(t('Message field is required.'), t('Message required.')); // Test contact form with no default category selected. - db_update('contact') - ->fields(array('selected' => 0)) - ->execute(); + config('contact.settings') + ->set('default_category', '') + ->save(); $this->drupalGet('contact'); - $this->assertRaw(t('- Please choose -'), t('Without selected categories the visitor is asked to chose a category.')); + $this->assertRaw(t('- Select -'), t('Without selected categories the visitor is asked to chose a category.')); // Submit contact form with invalid category id (cid 0). $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), 0, ''); - $this->assertText(t('You must select a valid category.'), t('Valid category required.')); + $this->assertText('Category field is required.'); // Submit contact form with correct values and check flood interval. for ($i = 0; $i < $flood_limit; $i++) { - $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64)); - $this->assertText(t('Your message has been sent.'), t('Message sent.')); + $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $id, $this->randomName(64)); + $this->assertText(t('Your message has been sent.')); } // Submit contact form one over limit. $this->drupalGet('contact'); @@ -182,9 +184,9 @@ class ContactSitewideTest extends WebTestBase { // Set up three categories, 2 with an auto-reply and one without. $foo_autoreply = $this->randomName(40); $bar_autoreply = $this->randomName(40); - $this->addCategory('foo', 'foo@example.com', $foo_autoreply, FALSE); - $this->addCategory('bar', 'bar@example.com', $bar_autoreply, FALSE); - $this->addCategory('no_autoreply', 'bar@example.com', '', FALSE); + $this->addCategory('foo', 'foo', 'foo@example.com', $foo_autoreply, FALSE); + $this->addCategory('bar', 'bar', 'bar@example.com', $bar_autoreply, FALSE); + $this->addCategory('no_autoreply', 'no_autoreply', 'bar@example.com', '', FALSE); // Log the current user out in order to test the name and e-mail fields. $this->drupalLogout(); @@ -193,34 +195,36 @@ class ContactSitewideTest extends WebTestBase { // Test the auto-reply for category 'foo'. $email = $this->randomName(32) . '@example.com'; $subject = $this->randomName(64); - $this->submitContact($this->randomName(16), $email, $subject, 2, $this->randomString(128)); + $this->submitContact($this->randomName(16), $email, $subject, 'foo', $this->randomString(128)); // We are testing the auto-reply, so there should be one e-mail going to the sender. $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'foo@example.com')); - $this->assertEqual(count($captured_emails), 1, t('Auto-reply e-mail was sent to the sender for category "foo".'), t('Contact')); - $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply), t('Auto-reply e-mail body is correct for category "foo".'), t('Contact')); + $this->assertEqual(count($captured_emails), 1); + $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply)); // Test the auto-reply for category 'bar'. $email = $this->randomName(32) . '@example.com'; - $this->submitContact($this->randomName(16), $email, $this->randomString(64), 3, $this->randomString(128)); + $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'bar', $this->randomString(128)); // Auto-reply for category 'bar' should result in one auto-reply e-mail to the sender. $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'bar@example.com')); - $this->assertEqual(count($captured_emails), 1, t('Auto-reply e-mail was sent to the sender for category "bar".'), t('Contact')); - $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply), t('Auto-reply e-mail body is correct for category "bar".'), t('Contact')); + $this->assertEqual(count($captured_emails), 1); + $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply)); // Verify that no auto-reply is sent when the auto-reply field is left blank. $email = $this->randomName(32) . '@example.com'; - $this->submitContact($this->randomName(16), $email, $this->randomString(64), 4, $this->randomString(128)); + $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'no_autoreply', $this->randomString(128)); $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'no_autoreply@example.com')); - $this->assertEqual(count($captured_emails), 0, t('No auto-reply e-mail was sent to the sender for category "no-autoreply".'), t('Contact')); + $this->assertEqual(count($captured_emails), 0); } /** * Adds a category. * - * @param string $category - * The category name. + * @param string $id + * The category machine name. + * @param string $label + * The category label. * @param string $recipients * The list of recipient e-mail addresses. * @param string $reply @@ -229,9 +233,10 @@ class ContactSitewideTest extends WebTestBase { * @param boolean $selected * Boolean indicating whether the category should be selected by default. */ - function addCategory($category, $recipients, $reply, $selected) { + function addCategory($id, $label, $recipients, $reply, $selected) { $edit = array(); - $edit['category'] = $category; + $edit['label'] = $label; + $edit['id'] = $id; $edit['recipients'] = $recipients; $edit['reply'] = $reply; $edit['selected'] = ($selected ? TRUE : FALSE); @@ -241,8 +246,10 @@ class ContactSitewideTest extends WebTestBase { /** * Updates a category. * - * @param string $category - * The category name. + * @param string $id + * The category machine name. + * @param string $label + * The category label. * @param string $recipients * The list of recipient e-mail addresses. * @param string $reply @@ -251,15 +258,13 @@ class ContactSitewideTest extends WebTestBase { * @param boolean $selected * Boolean indicating whether the category should be selected by default. */ - function updateCategory($categories, $category, $recipients, $reply, $selected) { - $category_id = $categories[array_rand($categories)]; + function updateCategory($id, $label, $recipients, $reply, $selected) { $edit = array(); - $edit['category'] = $category; + $edit['label'] = $label; $edit['recipients'] = $recipients; $edit['reply'] = $reply; $edit['selected'] = ($selected ? TRUE : FALSE); - $this->drupalPost('admin/structure/contact/edit/' . $category_id, $edit, t('Save')); - return ($category_id); + $this->drupalPost('admin/structure/contact/manage/' . $id, $edit, t('Save')); } /** @@ -271,17 +276,17 @@ class ContactSitewideTest extends WebTestBase { * The e-mail address of the sender. * @param string $subject * The subject of the message. - * @param integer $cid + * @param string $id * The category ID of the message. * @param string $message * The message body. */ - function submitContact($name, $mail, $subject, $cid, $message) { + function submitContact($name, $mail, $subject, $id, $message) { $edit = array(); $edit['name'] = $name; $edit['mail'] = $mail; $edit['subject'] = $subject; - $edit['cid'] = $cid; + $edit['category'] = $id; $edit['message'] = $message; $this->drupalPost('contact', $edit, t('Send message')); } @@ -290,22 +295,11 @@ class ContactSitewideTest extends WebTestBase { * Deletes all categories. */ function deleteCategories() { - $categories = $this->getCategories(); - foreach ($categories as $category) { - $category_name = db_query("SELECT category FROM {contact} WHERE cid = :cid", array(':cid' => $category))->fetchField(); - $this->drupalPost('admin/structure/contact/delete/' . $category, array(), t('Delete')); - $this->assertRaw(t('Category %category has been deleted.', array('%category' => $category_name)), t('Category deleted successfully.')); + $categories = entity_load_multiple('contact_category', FALSE); + foreach ($categories as $id => $category) { + $this->drupalPost('admin/structure/contact/manage/' . $id . '/delete', array(), t('Delete')); + $this->assertRaw(t('Category %label has been deleted.', array('%label' => $category->label())), t('Category deleted successfully.')); } } - /** - * Gets a list of all category IDs. - * - * @return array - * A list of the category IDs. - */ - function getCategories() { - $categories = db_query('SELECT cid FROM {contact}')->fetchCol(); - return $categories; - } } diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php index eeaeee8..e49ca95 100644 --- a/core/modules/entity/lib/Drupal/entity/Entity.php +++ b/core/modules/entity/lib/Drupal/entity/Entity.php @@ -43,7 +43,7 @@ class Entity implements EntityInterface { * * @var bool */ - public $isCurrentRevision = TRUE; + protected $isCurrentRevision = TRUE; /** * Constructs a new entity object. @@ -266,8 +266,12 @@ class Entity implements EntityInterface { /** * Implements Drupal\entity\EntityInterface::isCurrentRevision(). */ - public function isCurrentRevision() { - return $this->isCurrentRevision; + public function isCurrentRevision($new_value = NULL) { + $return = $this->isCurrentRevision; + if (isset($new_value)) { + $this->isCurrentRevision = (bool) $new_value; + } + return $return; } } diff --git a/core/modules/entity/lib/Drupal/entity/EntityInterface.php b/core/modules/entity/lib/Drupal/entity/EntityInterface.php index 699c424..0b89e12 100644 --- a/core/modules/entity/lib/Drupal/entity/EntityInterface.php +++ b/core/modules/entity/lib/Drupal/entity/EntityInterface.php @@ -200,9 +200,13 @@ interface EntityInterface { /** * Checks if this entity is the current revision. * + * @param bool $new_value + * (optional) A Boolean to (re)set the isCurrentRevision flag. + * * @return bool - * TRUE if the entity is the current revision, FALSE otherwise. + * TRUE if the entity is the current revision, FALSE otherwise. If + * $new_value was passed, the previous value is returned. */ - public function isCurrentRevision(); + public function isCurrentRevision($new_value = NULL); } diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index 0fa7b33..d082d1e 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -151,7 +151,7 @@ class NodeStorageController extends DatabaseStorageController { $entity->{$this->revisionKey} = $record->{$this->revisionKey}; // Mark this revision as the current one. - $entity->isCurrentRevision = TRUE; + $entity->isCurrentRevision(TRUE); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php index 8468185..8177165 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php @@ -45,6 +45,8 @@ class ModuleApiTest extends WebTestBase { // Try to install a new module. module_enable(array('contact')); $module_list[] = 'contact'; + // Contact module requires config. + $module_list[] = 'config'; sort($module_list); $this->assertModuleList($module_list, t('After adding a module'));