diff --git a/core/modules/path/lib/Drupal/path/Plugin/field/widget/NullWidget.php b/core/modules/path/lib/Drupal/path/Plugin/field/widget/NullWidget.php new file mode 100644 index 0000000..5f79f0e --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Plugin/field/widget/NullWidget.php @@ -0,0 +1,106 @@ +instance['settings']['pattern']['prefix'])) { + $prefix = $this->instance['settings']['pattern']['prefix'] . '/'; + } + else { + $prefix = ''; + } + $prefix_length = drupal_strlen($prefix); + + // @todo Consider to move this into path_field_load(). OTOH, that would + // (needlessly?) load URL aliases for every loaded entity. But then again, + // field data is cached, no? + $path = array(); + if (!$entity->isNew()) { + $uri = $entity->uri(); + $conditions = array( + 'source' => $uri['path'], + ); + if ($langcode != LANGUAGE_NOT_SPECIFIED) { + $conditions['langcode'] = $langcode; + } + if ($path = path_load($conditions)) { + // If there is an alias for this entity already, the configured prefix + // needs to be removed from it if it is identical, since the field is + // only supposed to store the actual user input. + if (drupal_substr($path['alias'], 0, $prefix_length) === $prefix) { + $path['value'] = drupal_substr($path['alias'], $prefix_length); + } + // If it is not, then the stored alias uses a different prefix than the + // one configured for this field instance, which can happen when + // manually saving an URL alias via Path module's administration pages, + // but also in case the field instance setting was not properly updated. + else { + $prefix = ''; + $path['value'] = $path['alias']; + } + } + else { + $path = $conditions; + } + } + $path += array( + 'pid' => NULL, + 'source' => NULL, + 'value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, + 'langcode' => $langcode, + ); + + $element += array( + '#access' => FALSE, + '#tree' => TRUE, + ); + $element['prefix'] = array( + '#type' => 'value', + '#value' => $prefix, + ); + $element['value'] = array( + '#type' => 'value', + '#default_value' => $path['value'], + ); + $element['pid'] = array( + '#type' => 'value', + '#value' => $path['pid'], + ); + return $element; + } + +} diff --git a/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php b/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php index 3e92e5a..71e7180 100644 --- a/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php +++ b/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php @@ -17,7 +17,7 @@ * @Plugin( * id = "path_default", * module = "path", - * label = @Translation("URL alias field"), + * label = @Translation("Permalink"), * field_types = { * "path" * } @@ -26,22 +26,6 @@ class PathWidget extends WidgetBase { /** - * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::settingsForm(). - */ -/* - public function settingsForm(array $form, array &$form_state) { - $element['size'] = array( - '#type' => 'number', - '#title' => t('Size of textfield'), - '#default_value' => $this->getSetting('size'), - '#required' => TRUE, - '#min' => 1, - ); - return $element; - } -*/ - - /** * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). */ public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { @@ -72,7 +56,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr // needs to be removed from it if it is identical, since the field is // only supposed to store the actual user input. if (drupal_substr($path['alias'], 0, $prefix_length) === $prefix) { - $path['alias'] = drupal_substr($path['alias'], $prefix_length); + $path['value'] = drupal_substr($path['alias'], $prefix_length); } // If it is not, then the stored alias uses a different prefix than the // one configured for this field instance, which can happen when @@ -80,6 +64,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr // but also in case the field instance setting was not properly updated. else { $prefix = ''; + $path['value'] = $path['alias']; } } else { @@ -89,7 +74,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr $path += array( 'pid' => NULL, 'source' => NULL, - 'alias' => isset($items[$delta]['alias']) ? $items[$delta]['alias'] : NULL, + 'value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, 'langcode' => $langcode, ); // @todo Entity-specific, dynamic token replacement for user-customizable @@ -107,25 +92,25 @@ public function formElement(array $items, $delta, array $element, $langcode, arr '#tree' => TRUE, '#element_validate' => array(array($this, 'validatePath')), ); + $element['pid'] = array( + '#type' => 'value', + '#value' => $path['pid'], + ); $element['prefix'] = array( '#type' => 'value', '#value' => $prefix, ); - $element['alias'] = array( + $element['value'] = array( '#type' => 'textfield', // @todo Use the field instance label ($element['#title']) here or always // identical default for consistency? Are fields able to provide default // labels for instances somehow? '#title' => t('Permalink'), '#field_prefix' => check_plain(url($prefix, array('absolute' => TRUE))), - '#default_value' => $path['alias'], + '#default_value' => $path['value'], '#required' => $element['#required'], '#maxlength' => 255 - drupal_strlen($prefix), ); - $element['pid'] = array( - '#type' => 'value', - '#value' => $path['pid'], - ); $element['source'] = array( '#type' => 'value', '#value' => $path['source'], @@ -141,10 +126,10 @@ public function formElement(array $items, $delta, array $element, $langcode, arr * Form element validation handler for PathWidget. */ public function validatePath(&$element, &$form_state, $form) { - if ($element['alias']['#value'] !== '') { + if ($element['value']['#value'] !== '') { // Trim the submitted value. - $alias = trim($element['alias']['#value']); - form_set_value($element['alias'], $alias, $form_state); + $element['value']['#value'] = trim($element['value']['#value']); + form_set_value($element['value'], $element['value']['#value'], $form_state); // Entity language needs special care. Since the language of the URL alias // depends on the entity language, and the entity language may be switched // right within the same form, we need to conditionally overload the @@ -157,7 +142,7 @@ public function validatePath(&$element, &$form_state, $form) { // Ensure that the submitted alias does not exist yet. $query = db_select('url_alias') - ->condition('alias', $element['prefix']['#value'] . $element['alias']['#value']) + ->condition('alias', $element['prefix']['#value'] . $element['value']['#value']) ->condition('langcode', $element['langcode']['#value']); if (!empty($element['source']['#value'])) { $query->condition('source', $element['source']['#value'], '<>'); @@ -165,7 +150,7 @@ public function validatePath(&$element, &$form_state, $form) { $query->addExpression('1'); $query->range(0, 1); if ($query->execute()->fetchField()) { - form_error($element['alias'], t('The alias is already in use.')); + form_error($element['value'], t('The alias is already in use.')); } } } diff --git a/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDTest.php b/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDTest.php new file mode 100644 index 0000000..5788700 --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDTest.php @@ -0,0 +1,155 @@ + 'Path field CRUD operations', + 'description' => 'Tests path field CRUD operations.', + 'group' => 'Path', + ); + } + + function setUp() { + parent::setUp(); + +// $this->enableModules(array('field', 'node')); +// $this->installSchema('system', 'url_alias'); + + $this->nodeType = (object) array( + 'type' => 'article', + 'name' => 'Article', + ); + node_type_save($this->nodeType); + + // Create a path field for the node type. + $this->field_name = drupal_strtolower($this->randomName()); + $this->langcode = LANGUAGE_NOT_SPECIFIED; + $this->prefix = 'test/prefix'; + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'path', + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => $this->nodeType->type, + 'settings' => array( + 'pattern' => array( + 'prefix' => $this->prefix, + 'auto' => FALSE, + ), + ), + 'widget' => array( + 'type' => 'path_default', + ), + ); + field_create_instance($this->instance); + } + + /** + * Tests a basic CRUD flow for path fields. + */ + function testBasicCRUD() { + // Create and save an entity with an alias. + $entity = entity_create('node', array( + 'type' => $this->nodeType->type, + 'title' => $this->randomName(), + )); + $edit = array( + 'value' => 'test-alias', + ); + $entity->{$this->field_name}[$this->langcode][0] = $edit; + $entity->save(); + $uri = $entity->uri(); + + // Verify that field data and the URL alias was stored. + $data = $entity->{$this->field_name}[$this->langcode][0]; + $this->assertTrue($data['pid']); + $this->assertIdentical($data['value'], $edit['value']); + $this->assertIdentical($data['alias'], $this->prefix . '/' . $edit['value']); + $path = path_load($data['pid']); + $this->assertIdentical($path['source'], $uri['path']); + $this->assertIdentical($path['alias'], $this->prefix . '/' . $edit['value']); + + // Edit the field value and update the entity. + $updated_value = 'updated-alias'; + $entity->{$this->field_name}[$this->langcode][0]['value'] = $updated_value; + $entity->save(); + + // Verify that field data and the URL alias was stored. + $data = $entity->{$this->field_name}[$this->langcode][0]; + $this->assertIdentical($data['pid'], $path['pid']); + $this->assertIdentical($data['value'], $updated_value); + $this->assertIdentical($data['alias'], $this->prefix . '/' . $updated_value); + $path = path_load($data['pid']); + $this->assertIdentical($path['source'], $uri['path']); + $this->assertIdentical($path['alias'], $this->prefix . '/' . $updated_value); + + // Verify that there is only one alias. + $count = db_query('SELECT COUNT(*) FROM {url_alias} WHERE source = :source', array( + ':source' => $uri['path'], + ))->fetchField(); + $this->assertEqual($count, 1); + + // Delete the entity. + $entity->delete(); + + // Verify that the alias no longer exists. + $path = path_load($data['pid']); + $this->assertFalse($path); + $count = db_query('SELECT COUNT(*) FROM {url_alias} WHERE source = :source', array( + ':source' => $uri['path'], + ))->fetchField(); + $this->assertEqual($count, 0); + } + + function xtestCRUDWithChangedPrefix() { + } + + /** + * Tests updating of field data after deletion of aliases via URL alias API. + */ + function testPathDelete() { + // Create and save an entity with an alias. + $entity = entity_create('node', array( + 'type' => $this->nodeType->type, + 'title' => $this->randomName(), + )); + $edit = array( + 'value' => 'test-alias', + ); + $entity->{$this->field_name}[$this->langcode][0] = $edit; + $entity->save(); + $uri = $entity->uri(); + + // Delete the alias. + path_delete(array('source' => $uri['path'])); + + // Reload the entity and verify that the alias no longer exists. + $entity = entity_load('node', $entity->id()); + $this->assertFalse($entity->{$this->field_name}); + } + +} diff --git a/core/modules/path/path.install b/core/modules/path/path.install index dc27735..a1f206d 100644 --- a/core/modules/path/path.install +++ b/core/modules/path/path.install @@ -21,7 +21,7 @@ function path_field_schema($field) { // revisions. Additionally, a change to the URL alias prefix in the field's // instance settings should potentially trigger a bulk-update of existing // aliases, using the same custom part but with a new prefix. - 'alias' => array( + 'value' => array( 'description' => 'The user-customizable part of the URL alias.', 'type' => 'varchar', 'length' => 255, diff --git a/core/modules/path/path.module b/core/modules/path/path.module index 2716293..0c3b316 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -47,8 +47,8 @@ function path_field_info() { 'instance_settings' => array( 'pattern' => array( 'prefix' => NULL, + 'auto' => FALSE, 'default' => NULL, - 'allow_custom' => TRUE, ), ), ); @@ -59,7 +59,7 @@ function path_field_info() { * Implements hook_field_is_empty(). */ function path_field_is_empty($item, $field) { - return !isset($item['alias']) || $item['alias'] === ''; + return !isset($item['value']) || $item['value'] === ''; } /** @@ -67,13 +67,16 @@ function path_field_is_empty($item, $field) { */ function path_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as &$item) { - $item['alias'] = trim($item['alias']); - if (!empty($item['alias'])) { + $item['value'] = trim($item['value']); + if (!empty($item['value'])) { $uri = $entity->uri(); $item['source'] = $uri['path']; $item['langcode'] = $langcode; if (!empty($instance['settings']['pattern']['prefix'])) { - $item['alias'] = $instance['settings']['pattern']['prefix'] . '/' . $item['alias']; + $item['alias'] = $instance['settings']['pattern']['prefix'] . '/' . $item['value']; + } + else { + $item['alias'] = $item['value']; } // path_save() populates $item['pid']. path_save($item); @@ -86,13 +89,13 @@ function path_field_insert($entity_type, $entity, $field, $instance, $langcode, */ function path_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as &$item) { - $item['alias'] = trim($item['alias']); + $item['value'] = trim($item['value']); // Delete old alias if it was erased. - if (!empty($item['pid']) && empty($item['alias'])) { + if (!empty($item['pid']) && empty($item['value'])) { path_delete($item['pid']); $item['pid'] = NULL; } - if (!empty($item['alias'])) { + if (!empty($item['value'])) { $uri = $entity->uri(); $item['source'] = $uri['path']; $item['langcode'] = $langcode; @@ -101,10 +104,13 @@ function path_field_update($entity_type, $entity, $field, $instance, $langcode, // @todo This looks wrong and will utterly fail when updating an entity // programmatically, but not sure how to address the possibility differently. if (isset($item['prefix'])) { - $item['alias'] = $item['prefix'] . $item['alias']; + $item['alias'] = $item['prefix'] . $item['value']; } elseif (!empty($instance['settings']['pattern']['prefix'])) { - $item['alias'] = $instance['settings']['pattern']['prefix'] . '/' . $item['alias']; + $item['alias'] = $instance['settings']['pattern']['prefix'] . '/' . $item['value']; + } + else { + $item['alias'] = $item['value']; } // path_save() populates $item['pid']. path_save($item); @@ -137,6 +143,7 @@ function path_field_instance_settings_form($field, $instance) { '#type' => 'fieldset', '#title' => t('URL alias pattern'), '#collapsible' => FALSE, + '#element_validate' => array('path_field_instance_settings_form_pattern_validate'), ); $form['pattern']['prefix'] = array( '#type' => 'textfield', @@ -147,24 +154,43 @@ function path_field_instance_settings_form($field, $instance) { '!example' => "'" . check_plain($instance['entity_type'] . '/' . $instance['bundle']) . "'", )), ); + $form['pattern']['auto'] = array( + '#type' => 'checkbox', + '#title' => t('Automatically create alias'), + '#default_value' => isset($settings['pattern']['auto']) ? $settings['pattern']['auto'] : NULL, + // @todo Add token support. + '#access' => FALSE, + ); $form['pattern']['default'] = array( '#type' => 'textfield', '#title' => t('Default alias'), '#default_value' => isset($settings['pattern']['default']) ? $settings['pattern']['default'] : NULL, '#maxlength' => 128, + // @todo Add token support. + '#access' => FALSE, '#description' => t('@todo Output some example tokens specific to the entity type/bundle.'), - ); - // @todo Drop this in favor of a generic, hypothetical field access system? - // Also, the widget is #access-restricted to user permissions already... - $form['pattern']['allow_custom'] = array( - '#type' => 'checkbox', - '#title' => t('Allow users to customize the URL alias'), - '#default_value' => isset($settings['pattern']['allow_custom']) ? $settings['pattern']['allow_custom'] : NULL, + '#states' => array( + 'visible' => array( + ':input[name*="pattern][auto"]' => array('checked' => TRUE), + ), + ), ); return $form; } /** + * #element_validate handler for 'pattern' in path_field_instance_settings_form(). + */ +function path_field_instance_settings_form_pattern_validate(&$element, &$form_state) { + // Ensure that the prefix does not contain a leading or trailing slash. + $element['prefix']['#value'] = trim($element['prefix']['#value'], '/'); + form_set_value($element['prefix'], $element['prefix']['#value'], $form_state); + + // @todo Validate that tokens in 'prefix' and 'default' exist. + // @todo Validate that 'default' contains at least one token. +} + +/** * Implements hook_permission(). */ function path_permission() { @@ -225,7 +251,60 @@ function path_menu() { */ function path_entity_delete(EntityInterface $entity) { // Delete all aliases associated with this entity. - path_delete(array('source' => $entity->uri())); + $uri = $entity->uri(); + path_delete(array('source' => $uri['path'])); +} + +/** + * Implements hook_path_delete(). + */ +function path_path_delete($path) { + // EntityFieldQuery does not support field queries across entity types, so + // iterate over all path field instances individually and update their field + // values accordingly. + // Please note that the entirety of the following code will update a *single* + // entity only, but Drupal core no longer provides a simple way for retrieving + // all entities that have a certain field value. + $entity_types = array(); + foreach (field_info_field_map() as $field_name => $info) { + if ($info['type'] == 'path') { + foreach ($info['bundles'] as $entity_type => $bundles) { + $entity_types[$entity_type][$field_name] = $bundles; + } + } + } + foreach ($entity_types as $entity_type => $field_info) { + $query = entity_query($entity_type); + $group = $query->orConditionGroup(); + foreach ($field_info as $field_name => $bundles) { + $group->condition($field_name . '.pid', $path['pid']); + } + $query->condition($group); + $ids = $query->execute(); + if (!$ids) { + continue; + } + + foreach (entity_load_multiple($entity_type, $ids) as $id => $entity) { + $changed = FALSE; + foreach ($field_info as $field_name => $bundles) { + // @todo Update for EntityNG, once core entities are converted. + if (isset($entity->{$field_name})) { + foreach ($entity->{$field_name} as $langcode => &$items) { + foreach ($items as $delta => $item) { + if (isset($item['pid']) && $item['pid'] == $path['pid']) { + unset($items[$delta]); + $changed = TRUE; + } + } + } + } + } + if ($changed) { + $entity->save(); + } + } + } } /**