diff --git a/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php b/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php index 6117de9..de560f7 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php @@ -119,7 +119,7 @@ public function form(array $form, array &$form_state) { '#region_callback' => 'field_ui_display_overview_row_region', '#js_settings' => array( 'rowHandler' => 'field', - 'defaultFormatter' => $field_types[$field['type']]['default_formatter'], + 'defaultFormatter' => isset($field_types[$field['type']]['default_formatter']) ? $field_types[$field['type']]['default_formatter'] : 'hidden', ), 'human_name' => array( '#markup' => check_plain($instance['label']), 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 new file mode 100644 index 0000000..3a15504 --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php @@ -0,0 +1,144 @@ +isNew()) { + $uri = $entity->uri(); + $conditions = array( + 'source' => $uri['path'], + ); + if ($langcode != LANGUAGE_NOT_SPECIFIED) { + $conditions['langcode'] = $langcode; + } + if ($path = drupal_container()->get('path.crud')->load($conditions)) { + // The field is supposed to store the actual user input. + $path['value'] = $path['alias']; + } + else { + $path = $conditions; + } + } + $path += array( + 'pid' => NULL, + 'source' => NULL, + 'value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, + 'langcode' => $langcode, + ); + $language = language_load($langcode); + + $element += array( + '#type' => 'container', + '#attributes' => array( + 'class' => array('path-form'), + ), + '#attached' => array( + 'library' => array(array('path', 'drupal.path')), + ), + '#access' => user_access('create url aliases') || user_access('administer url aliases'), + '#tree' => TRUE, + '#element_validate' => array(array($this, 'validatePath')), + ); + $element['pid'] = array( + '#type' => 'value', + '#value' => $path['pid'], + ); + // Prepare the site's base URL as field prefix, taking the currently edited + // entity language into account. If the language has no path prefix, the + // site URL ends with a slash; if there is a prefix, there is no trailing + // slash; ensure there is one in all cases. + $field_prefix = trim(url('', array('absolute' => TRUE, 'language' => $language)), '/') . '/'; + $element['value'] = array( + '#type' => 'textfield', + '#title' => $element['#title'], + '#field_prefix' => check_plain($field_prefix), + '#default_value' => $path['value'], + '#required' => $element['#required'], + '#maxlength' => 255, + '#description' => $element['#description'], + ); + $element['source'] = array( + '#type' => 'value', + '#value' => $path['source'], + ); + $element['langcode'] = array( + '#type' => 'value', + '#value' => $path['langcode'], + ); + + // Integrate with advanced settings, if available. + // @todo Rename vertical tabs container to 'advanced'. + if (isset($form['additional_settings'])) { + $element['#type'] = 'details'; + $element['#group'] = 'additional_settings'; + $element['#title'] = t('URL path settings'); + $element['#collapsed'] = empty($path['pid']) && empty($path['value']); + } + + return $element; + } + + /** + * Form element validation handler for PathWidget. + */ + public function validatePath(&$element, &$form_state, $form) { + if ($element['value']['#value'] !== '') { + // Trim the submitted value. + $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 + // originally assigned URL alias language. + // @todo Remove this after stopping Locale module from abusing the content + // language system. + if (isset($form_state['values']['langcode'])) { + form_set_value($element['langcode'], $form_state['values']['langcode'], $form_state); + } + + // Ensure that the submitted alias does not exist yet. + $query = db_select('url_alias') + ->condition('alias', $element['value']['#value']) + ->condition('langcode', $element['langcode']['#value']); + if (!empty($element['source']['#value'])) { + $query->condition('source', $element['source']['#value'], '<>'); + } + $query->addExpression('1'); + $query->range(0, 1); + if ($query->execute()->fetchField()) { + form_error($element['value'], t('The alias is already in use.')); + } + } + } + +} diff --git a/core/modules/path/lib/Drupal/path/Tests/PathAliasTest.php b/core/modules/path/lib/Drupal/path/Tests/PathAliasTest.php index 23ff7f7..121857a 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathAliasTest.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathAliasTest.php @@ -12,13 +12,6 @@ */ class PathAliasTest extends PathTestBase { - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array('path'); - public static function getInfo() { return array( 'name' => 'Path alias functionality', @@ -140,23 +133,23 @@ function testNodeAlias() { // Create alias. $edit = array(); - $edit['path[alias]'] = $this->randomName(8); + $edit['path[und][0][value]'] = $this->randomName(8); $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['path[alias]']); + $this->drupalGet($edit['path[und][0][value]']); $this->assertText($node1->label(), 'Alias works.'); $this->assertResponse(200); // Change alias to one containing "exotic" characters. - $previous = $edit['path[alias]']; - $edit['path[alias]'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. + $previous = $edit['path[und][0][value]']; + $edit['path[und][0][value]'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['path[alias]']); + $this->drupalGet($edit['path[und][0][value]']); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); @@ -169,17 +162,17 @@ function testNodeAlias() { $node2 = $this->drupalCreateNode(); // Set alias to second test node. - // Leave $edit['path[alias]'] the same. + // Leave $edit['path[und][0][value]'] the same. $this->drupalPost('node/' . $node2->nid . '/edit', $edit, t('Save')); // Confirm that the alias didn't make a duplicate. $this->assertText(t('The alias is already in use.'), 'Attempt to moved alias was rejected.'); // Delete alias. - $this->drupalPost('node/' . $node1->nid . '/edit', array('path[alias]' => ''), t('Save')); + $this->drupalPost('node/' . $node1->nid . '/edit', array('path[und][0][value]' => ''), t('Save')); // Confirm that the alias no longer works. - $this->drupalGet($edit['path[alias]']); + $this->drupalGet($edit['path[und][0][value]']); $this->assertNoText($node1->label(), 'Alias was successfully deleted.'); $this->assertResponse(404); } @@ -204,13 +197,13 @@ function testDuplicateNodeAlias() { // Create one node with a random alias. $node_one = $this->drupalCreateNode(); $edit = array(); - $edit['path[alias]'] = $this->randomName(); + $edit['path[und][0][value]'] = $this->randomName(); $this->drupalPost('node/' . $node_one->nid . '/edit', $edit, t('Save')); // Now create another node and try to set the same alias. $node_two = $this->drupalCreateNode(); $this->drupalPost('node/' . $node_two->nid . '/edit', $edit, t('Save')); $this->assertText(t('The alias is already in use.')); - $this->assertFieldByXPath("//input[@name='path[alias]' and contains(@class, 'error')]", $edit['path[alias]'], 'Textfield exists and has the error class.'); + $this->assertFieldByXPath("//input[@name='path[und][0][value]' and contains(@class, 'error')]", $edit['path[und][0][value]'], 'Textfield exists and has the error class.'); } } 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..ac54ba4 --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDTest.php @@ -0,0 +1,144 @@ + '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' => 'page', + 'name' => 'Basic page', + ); + 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->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, + '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'], $edit['value']); + $path = $this->container->get('path.crud')->load(array('pid' => $data['pid'])); + $this->assertIdentical($path['source'], $uri['path']); + $this->assertIdentical($path['alias'], $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'], $updated_value); + $path = $this->container->get('path.crud')->load(array('pid' => $data['pid'])); + $this->assertIdentical($path['source'], $uri['path']); + $this->assertIdentical($path['alias'], $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 = $this->container->get('path.crud')->load(array('pid' => $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); + } + + /** + * 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. + $this->container->get('path.crud')->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/lib/Drupal/path/Tests/PathLanguageTest.php b/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php index 83be7b5..c6075f5 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php @@ -17,7 +17,7 @@ class PathLanguageTest extends PathTestBase { * * @var array */ - public static $modules = array('path', 'locale', 'translation'); + public static $modules = array('locale', 'translation_entity'); public static function getInfo() { return array( @@ -31,18 +31,26 @@ function setUp() { parent::setUp(); // Create and login user. - $this->web_user = $this->drupalCreateUser(array('edit any page content', 'create page content', 'administer url aliases', 'create url aliases', 'administer languages', 'translate all content', 'access administration pages', 'administer content types')); + $this->web_user = $this->drupalCreateUser(array('edit any page content', 'create page content', 'administer url aliases', 'create url aliases', 'administer languages', 'translate any entity', 'access administration pages', 'administer content types')); $this->drupalLogin($this->web_user); // Enable French language. $edit = array(); $edit['predefined_langcode'] = 'fr'; - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); // Enable URL language detection and selection. $edit = array('language_interface[enabled][language-url]' => 1); $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings')); + + // Enable entity/field translation for the body field. + $field = field_info_field('body'); + $field['translatable'] = TRUE; + field_update_field($field); + // Enable entity/field translation for the default path field. + $field = field_info_field('path'); + $field['translatable'] = TRUE; + field_update_field($field); } /** @@ -52,45 +60,44 @@ function testAliasTranslation() { // Set 'page' content type to enable translation. $edit = array( 'language_configuration[language_hidden]' => FALSE, + 'language_configuration[translation_entity]' => TRUE, ); $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.'); - variable_set('node_type_language_translation_enabled_page', TRUE); - $english_node = $this->drupalCreateNode(array('type' => 'page')); + $node = $this->drupalCreateNode(array('type' => 'page')); $english_alias = $this->randomName(); // Edit the node to set language and path. $edit = array(); $edit['langcode'] = 'en'; - $edit['path[alias]'] = $english_alias; - $this->drupalPost('node/' . $english_node->nid . '/edit', $edit, t('Save')); + $edit['path[und][0][value]'] = $english_alias; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); // Confirm that the alias works. $this->drupalGet($english_alias); - $this->assertText($english_node->label(), 'Alias works.'); + $this->assertText($node->label(), 'Alias works.'); // Translate the node into French. - $this->drupalGet('node/' . $english_node->nid . '/translate'); - $this->clickLink(t('add translation')); + $this->drupalGet('node/' . $node->nid . '/translations'); + $this->clickLink(t('add')); $edit = array(); - $langcode = LANGUAGE_NOT_SPECIFIED; - $edit["title"] = $this->randomName(); - $edit["body[$langcode][0][value]"] = $this->randomName(); + $edit['title'] = $this->randomName(); + $edit['body[fr][0][value]'] = $this->randomName(); $french_alias = $this->randomName(); - $edit['path[alias]'] = $french_alias; + $edit['path[fr][0][value]'] = $french_alias; $this->drupalPost(NULL, $edit, t('Save')); // Clear the path lookup cache. drupal_container()->get('path.alias_manager')->cacheClear(); // Ensure the node was created. - $french_node = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertTrue(($french_node), 'Node found in database.'); + $node = $this->drupalGetNodeByTitle($edit["title"]); + $this->assertTrue($node, 'Node found in database.'); // Confirm that the alias works. - $this->drupalGet('fr/' . $edit['path[alias]']); - $this->assertText($french_node->label(), 'Alias for French translation works.'); + $this->drupalGet('fr/' . $edit['path[fr][0][value]']); + $this->assertText($node->label(), 'Alias for French translation works.'); // Confirm that the alias is returned by url(). Languages are cached on // many levels, and we need to clear those caches. @@ -98,8 +105,8 @@ function testAliasTranslation() { drupal_static_reset('language_url_outbound_alter'); drupal_static_reset('language_url_rewrite_url'); $languages = language_list(); - $url = url('node/' . $french_node->nid, array('language' => $languages[$french_node->langcode])); - $this->assertTrue(strpos($url, $edit['path[alias]']), 'URL contains the path alias.'); + $url = url('node/' . $node->nid, array('language' => language_load('fr'))); + $this->assertTrue(strpos($url, $edit['path[fr][0][value]']), 'URL contains the path alias.'); // Confirm that the alias works even when changing language negotiation // options. Enable User language detection and selection over URL one. @@ -124,11 +131,11 @@ function testAliasTranslation() { // path alias for French matching the english alias. So the alias manager // needs to use the URL language to check whether the alias is valid. $this->drupalGet($english_alias); - $this->assertText($english_node->label(), 'Alias for English translation works.'); + $this->assertText($node->body['en'][0]['value'], 'Alias for English translation works.'); // Check that the French alias works. $this->drupalGet("fr/$french_alias"); - $this->assertText($french_node->label(), 'Alias for French translation works.'); + $this->assertText($node->body['fr'][0]['value'], 'Alias for French translation works.'); // Disable URL language negotiation. $edit = array('language_interface[enabled][language-url]' => FALSE); @@ -136,7 +143,7 @@ function testAliasTranslation() { // Check that the English alias still works. $this->drupalGet($english_alias); - $this->assertText($english_node->label(), 'Alias for English translation works.'); + $this->assertText($node->body['en'][0]['value'], 'Alias for English translation works.'); // Check that the French alias is not available. We check the unprefixed // alias because we disabled URL language negotiation above. In this @@ -148,17 +155,17 @@ function testAliasTranslation() { // The alias manager has an internal path lookup cache. Check to see that // it has the appropriate contents at this point. drupal_container()->get('path.alias_manager')->cacheClear(); - $french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, $french_node->langcode); - $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path works.'); + $french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, 'fr'); + $this->assertEqual($french_node_path, 'node/' . $node->nid, 'Normal path works.'); // Second call should return the same path. - $french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, $french_node->langcode); - $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path is the same.'); + $french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, 'fr'); + $this->assertEqual($french_node_path, 'node/' . $node->nid, 'Normal path is the same.'); // Confirm that the alias works. - $french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $french_node->nid, $french_node->langcode); + $french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $node->nid, 'fr'); $this->assertEqual($french_node_alias, $french_alias, 'Alias works.'); // Second call should return the same alias. - $french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $french_node->nid, $french_node->langcode); + $french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $node->nid, 'fr'); $this->assertEqual($french_node_alias, $french_alias, 'Alias is the same.'); } } diff --git a/core/modules/path/lib/Drupal/path/Tests/PathLanguageUiTest.php b/core/modules/path/lib/Drupal/path/Tests/PathLanguageUiTest.php index bd0d279..ab6a9bd 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathLanguageUiTest.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathLanguageUiTest.php @@ -17,7 +17,7 @@ class PathLanguageUiTest extends PathTestBase { * * @var array */ - public static $modules = array('path', 'locale'); + public static $modules = array('locale'); public static function getInfo() { return array( diff --git a/core/modules/path/lib/Drupal/path/Tests/PathTaxonomyTermTest.php b/core/modules/path/lib/Drupal/path/Tests/PathTaxonomyTermTest.php index 421870d..25bc7b2 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathTaxonomyTermTest.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathTaxonomyTermTest.php @@ -30,12 +30,13 @@ public static function getInfo() { function setUp() { parent::setUp(); - // Create a Tags vocabulary for the Article node type. + // Create a Tags vocabulary for the Basic page node type. $vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => t('Tags'), 'machine_name' => 'tags', )); $vocabulary->save(); + path_add_default_field_instance('taxonomy_term', 'tags'); // Create and login user. $web_user = $this->drupalCreateUser(array('administer url aliases', 'administer taxonomy', 'access administration pages')); @@ -52,35 +53,35 @@ function testTermAlias() { $edit = array(); $edit['name'] = $this->randomName(); $edit['description[value]'] = $description; - $edit['path[alias]'] = $this->randomName(); + $edit['path[und][0][value]'] = $this->randomName(); $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['path[alias]']); + $this->drupalGet($edit['path[und][0][value]']); $this->assertText($description, 'Term can be accessed on URL alias.'); // Change the term's URL alias. $tid = db_query("SELECT tid FROM {taxonomy_term_data} WHERE name = :name", array(':name' => $edit['name']))->fetchField(); $edit2 = array(); - $edit2['path[alias]'] = $this->randomName(); + $edit2['path[und][0][value]'] = $this->randomName(); $this->drupalPost('taxonomy/term/' . $tid . '/edit', $edit2, t('Save')); // Confirm that the changed alias works. - $this->drupalGet($edit2['path[alias]']); + $this->drupalGet($edit2['path[und][0][value]']); $this->assertText($description, 'Term can be accessed on changed URL alias.'); // Confirm that the old alias no longer works. - $this->drupalGet($edit['path[alias]']); + $this->drupalGet($edit['path[und][0][value]']); $this->assertNoText($description, 'Old URL alias has been removed after altering.'); $this->assertResponse(404, 'Old URL alias returns 404.'); // Remove the term's URL alias. $edit3 = array(); - $edit3['path[alias]'] = ''; + $edit3['path[und][0][value]'] = ''; $this->drupalPost('taxonomy/term/' . $tid . '/edit', $edit3, t('Save')); // Confirm that the alias no longer works. - $this->drupalGet($edit2['path[alias]']); + $this->drupalGet($edit2['path[und][0][value]']); $this->assertNoText($description, 'Old URL alias has been removed after altering.'); $this->assertResponse(404, 'Old URL alias returns 404.'); } diff --git a/core/modules/path/lib/Drupal/path/Tests/PathTestBase.php b/core/modules/path/lib/Drupal/path/Tests/PathTestBase.php index 97f3f81..9aafb56 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathTestBase.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathTestBase.php @@ -24,10 +24,10 @@ function setUp() { parent::setUp(); - // Create Basic page and Article node types. + // Create Basic page node type. if ($this->profile != 'standard') { $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); - $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + path_add_default_field_instance('node', 'page'); } } } diff --git a/core/modules/path/path.install b/core/modules/path/path.install new file mode 100644 index 0000000..9b70925 --- /dev/null +++ b/core/modules/path/path.install @@ -0,0 +1,39 @@ + array( + 'description' => 'Foreign Key: The path alias ID from {url_alias}.pid.', + 'type' => 'int', + 'not null' => FALSE, + ), + // This partially duplicates {url_alias}.alias, but contains the actual user + // input, which should be stored per field, to make it an inherent part of + // an entity's life-cycle; e.g., exposing changed values in field revisions. + 'value' => array( + 'description' => 'The user-customizable part of the URL alias.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ); + return array( + 'columns' => $columns, + 'foreign keys' => array( + 'path_url_alias' => array( + 'table' => 'url_alias', + 'columns' => array( + 'pid' => 'pid', + ), + ), + ), + ); +} diff --git a/core/modules/path/path.js b/core/modules/path/path.js index 7349d12..a568a3a 100644 --- a/core/modules/path/path.js +++ b/core/modules/path/path.js @@ -9,7 +9,7 @@ Drupal.behaviors.pathDetailsSummaries = { attach: function (context) { $(context).find('.path-form').drupalSetSummary(function (context) { - var path = $('.form-item-path-alias input').val(); + var path = $(context).find('input').val(); return path ? Drupal.t('Alias: @alias', { '@alias': path }) : diff --git a/core/modules/path/path.module b/core/modules/path/path.module index 63e5c6c..9b865b6 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -5,9 +5,7 @@ * Enables users to rename URLs. */ -use Drupal\node\Plugin\Core\Entity\Node; - -use Drupal\taxonomy\Plugin\Core\Entity\Term; +use Drupal\Core\Entity\EntityInterface; /** * Implements hook_help(). @@ -39,6 +37,154 @@ function path_help($path, $arg) { } /** + * Implements hook_field_info(). + */ +function path_field_info() { + $types['path'] = array( + 'label' => t('URL alias'), + 'description' => t('This field stores an URL alias in the database.'), + 'default_widget' => 'path_default', + ); + return $types; +} + +/** + * Implements hook_field_is_empty(). + */ +function path_field_is_empty($item, $field) { + // The primary condition always is whether there is an existing path alias ID, + // since Field API invokes this callback to determine whether any field values + // need to be deleted. Thus, if there has been a path alias ID, the field + // cannot be empty. For new field data, the field's emptiness depends on the + // item's value. + return empty($item['pid']) && (!isset($item['value']) || $item['value'] === ''); +} + +/** + * Implements hook_field_insert(). + */ +function path_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { + foreach ($items as &$item) { + $item['value'] = trim($item['value']); + if (!empty($item['value'])) { + $uri = $entity->uri(); + $item['source'] = $uri['path']; + $item['langcode'] = $langcode; + $item['alias'] = $item['value']; + $path = drupal_container()->get('path.crud')->save($item['source'], $item['alias'], $item['langcode']); + // Populate 'pid' column, so it is saved by the field storage. + $item['pid'] = $path['pid']; + } + } +} + +/** + * Implements hook_field_update(). + */ +function path_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { + $path_crud = drupal_container()->get('path.crud'); + foreach ($items as &$item) { + $item['value'] = trim($item['value']); + // Delete old alias if it was erased. + if ($item['value'] === '') { + if (!empty($item['pid'])) { + $path_crud->delete(array('pid' => $item['pid'])); + $item['pid'] = NULL; + $item['value'] = NULL; + } + } + else { + $uri = $entity->uri(); + // If we have a URL alias ID, try to load it. + if (!empty($item['pid'])) { + $conditions = array('pid' => $item['pid']); + } + // Otherwise, check whether there is URL alias for the entity URI. + else { + $conditions = array('source' => $uri['path'], 'langcode' => $langcode); + } + $path = $path_crud->load($conditions); + if ($path === FALSE) { + $path = $path_crud->save($uri['path'], $item['value'], $langcode); + } + else { + $path = $path_crud->save($uri['path'], $item['value'], $langcode, $path['pid']); + } + // Populate 'pid' column, so it is saved by the field storage. + $item['pid'] = $path['pid']; + // Provide all URL alias properties for subsequently invoked hooks. + $item['source'] = $uri['path']; + $item['langcode'] = $langcode; + $item['alias'] = $path['alias']; + } + } +} + +/** + * Implements hook_field_delete(). + */ +function path_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { + foreach ($items as $item) { + if (!empty($item['pid'])) { + drupal_container()->get('path.crud')->delete(array('pid' => $item['pid'])); + } + } +} + +/** + * Creates a default 'path' field that can be attached to any entity bundle. + * + * @return array + * The field information of the default path field. + * + * @see path_add_default_field_instance() + * @see path_install() + */ +function path_add_default_field() { + $field = field_info_field('path'); + if (empty($field)) { + $field = array( + 'field_name' => 'path', + 'type' => 'path', + ); + $field = field_create_field($field); + } + return $field; +} + +/** + * Adds the default path field to an entity bundle. + * + * @param string $entity_type + * The name of the entity type to add the field to. + * @param string $bundle + * The name of the bundle to add the field to. + * @param string $label + * (optional) The label to use for the field instance. Defaults to + * "URL alias". + * + * @return array + * The path field instance. + */ +function path_add_default_field_instance($entity_type, $bundle, $label = 'URL alias') { + $field = path_add_default_field(); + $instance = field_info_instance($entity_type, $field['field_name'], $bundle); + if (empty($instance)) { + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'label' => $label, + 'widget' => array( + 'type' => 'path_default', + ), + ); + $instance = field_create_instance($instance); + } + return $instance; +} + +/** * Implements hook_permission(). */ function path_permission() { @@ -95,225 +241,71 @@ function path_menu() { } /** - * Implements hook_form_BASE_FORM_ID_alter() for node_form(). - * - * @see path_form_element_validate() + * Implements hook_entity_delete(). */ -function path_form_node_form_alter(&$form, $form_state) { - $node = $form_state['controller']->getEntity($form_state); - $path = array(); - if (!empty($node->nid)) { - $conditions = array('source' => 'node/' . $node->nid); - if ($node->langcode != LANGUAGE_NOT_SPECIFIED) { - $conditions['langcode'] = $node->langcode; - } - $path = drupal_container()->get('path.crud')->load($conditions); - if ($path === FALSE) { - $path = array(); - } - } - $path += array( - 'pid' => NULL, - 'source' => isset($node->nid) ? 'node/' . $node->nid : NULL, - 'alias' => '', - 'langcode' => isset($node->langcode) ? $node->langcode : LANGUAGE_NOT_SPECIFIED, - ); - - $form['path'] = array( - '#type' => 'details', - '#title' => t('URL path settings'), - '#collapsible' => TRUE, - '#collapsed' => empty($path['alias']), - '#group' => 'additional_settings', - '#attributes' => array( - 'class' => array('path-form'), - ), - '#attached' => array( - 'library' => array(array('path', 'drupal.path')), - ), - '#access' => user_access('create url aliases') || user_access('administer url aliases'), - '#weight' => 30, - '#tree' => TRUE, - '#element_validate' => array('path_form_element_validate'), - ); - $form['path']['alias'] = array( - '#type' => 'textfield', - '#title' => t('URL alias'), - '#default_value' => $path['alias'], - '#maxlength' => 255, - '#description' => t('The alternative URL for this content. Use a relative path without a trailing slash. For example, enter "about" for the about page.'), - ); - $form['path']['pid'] = array('#type' => 'value', '#value' => $path['pid']); - $form['path']['source'] = array('#type' => 'value', '#value' => $path['source']); - $form['path']['langcode'] = array('#type' => 'value', '#value' => $path['langcode']); -} - -/** - * Form element validation handler for URL alias form element. - * - * @see path_form_node_form_alter() - */ -function path_form_element_validate($element, &$form_state, $complete_form) { - if (!empty($form_state['values']['path']['alias'])) { - // Trim the submitted value. - $alias = trim($form_state['values']['path']['alias']); - form_set_value($element['alias'], $alias, $form_state); - // Node language needs special care. Since the language of the URL alias - // depends on the node language, and the node language can be switched - // right within the same form, we need to conditionally overload the - // originally assigned URL alias language. - // @todo Remove this after converting Path module to a field, and, after - // stopping Locale module from abusing the content language system. - if (isset($form_state['values']['langcode'])) { - form_set_value($element['langcode'], $form_state['values']['langcode'], $form_state); - } - - $path = $form_state['values']['path']; - - // Ensure that the submitted alias does not exist yet. - $query = db_select('url_alias') - ->condition('alias', $path['alias']) - ->condition('langcode', $path['langcode']); - if (!empty($path['source'])) { - $query->condition('source', $path['source'], '<>'); - } - $query->addExpression('1'); - $query->range(0, 1); - if ($query->execute()->fetchField()) { - form_error($element, t('The alias is already in use.')); - } - } -} - -/** - * Implements hook_node_insert(). - */ -function path_node_insert(Node $node) { - if (isset($node->path)) { - $alias = trim($node->path['alias']); - // Only save a non-empty alias. - if (!empty($alias)) { - // Ensure fields for programmatic executions. - $source = 'node/' . $node->nid; - $langcode = isset($node->langcode) ? $node->langcode : LANGUAGE_NOT_SPECIFIED; - drupal_container()->get('path.crud')->save($source, $alias, $langcode); - } - } +function path_entity_delete(EntityInterface $entity) { + // Delete all aliases associated with this entity. + $uri = $entity->uri(); + drupal_container()->get('path.crud')->delete(array('source' => $uri['path'])); } /** - * Implements hook_node_update(). + * Implements hook_path_delete(). */ -function path_node_update(Node $node) { - if (isset($node->path)) { - $path = $node->path; - $alias = trim($path['alias']); - // Delete old alias if user erased it. - if (!empty($path['pid']) && empty($path['alias'])) { - drupal_container()->get('path.crud')->delete(array('pid' => $path['pid'])); - } - // Only save a non-empty alias. - if (!empty($path['alias'])) { - // Ensure fields for programmatic executions. - $source = 'node/' . $node->nid; - $langcode = isset($node->langcode) ? $node->langcode : LANGUAGE_NOT_SPECIFIED; - drupal_container()->get('path.crud')->save($source, $alias, $langcode, $path['pid']); +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; + } } } -} - -/** - * Implements hook_node_predelete(). - */ -function path_node_predelete(Node $node) { - // Delete all aliases associated with this node. - drupal_container()->get('path.crud')->delete(array('source' => 'node/' . $node->nid)); -} + foreach ($entity_types as $entity_type => $field_info) { + $query = entity_query($entity_type); + // Disable entity access checks, since field values need to be updated + // regardless of the permissions of the current user (if any). + $query->accessCheck(FALSE); -/** - * Implements hook_form_FORM_ID_alter() for taxonomy_term_form(). - */ -function path_form_taxonomy_term_form_alter(&$form, $form_state) { - // Make sure this does not show up on the delete confirmation form. - if (empty($form_state['confirm_delete'])) { - $term = $form_state['controller']->getEntity($form_state); - $path = (isset($term->tid) ? drupal_container()->get('path.crud')->load(array('source' => 'taxonomy/term/' . $term->tid)) : array()); - if ($path === FALSE) { - $path = array(); + $group = $query->orConditionGroup(); + foreach ($field_info as $field_name => $bundles) { + $group->condition($field_name . '.pid', $path['pid']); } - $path += array( - 'pid' => NULL, - 'source' => isset($term->tid) ? 'taxonomy/term/' . $term->tid : NULL, - 'alias' => '', - 'langcode' => LANGUAGE_NOT_SPECIFIED, - ); - $form['path'] = array( - '#access' => user_access('create url aliases') || user_access('administer url aliases'), - '#tree' => TRUE, - '#element_validate' => array('path_form_element_validate'), - ); - $form['path']['alias'] = array( - '#type' => 'textfield', - '#title' => t('URL alias'), - '#default_value' => $path['alias'], - '#maxlength' => 255, - '#weight' => 0, - '#description' => t("Optionally specify an alternative URL by which this term can be accessed. Use a relative path and don't add a trailing slash or the URL alias won't work."), - ); - $form['path']['pid'] = array('#type' => 'value', '#value' => $path['pid']); - $form['path']['source'] = array('#type' => 'value', '#value' => $path['source']); - $form['path']['langcode'] = array('#type' => 'value', '#value' => $path['langcode']); - } -} - -/** - * Implements hook_taxonomy_term_insert(). - */ -function path_taxonomy_term_insert(Term $term) { - if (isset($term->path)) { - $path = $term->path; - $path['alias'] = trim($path['alias']); - // Only save a non-empty alias. - if (!empty($path['alias'])) { - // Ensure fields for programmatic executions. - $path['source'] = 'taxonomy/term/' . $term->tid; - $path['langcode'] = LANGUAGE_NOT_SPECIFIED; - drupal_container()->get('path.crud')->save($path['source'], $path['alias'], $path['langcode']); + $query->condition($group); + $ids = $query->execute(); + if (!$ids) { + continue; } - } -} -/** - * Implements hook_taxonomy_term_update(). - */ -function path_taxonomy_term_update(Term $term) { - if (isset($term->path)) { - $path = $term->path; - $path['alias'] = trim($path['alias']); - // Delete old alias if user erased it. - if (!empty($path['pid']) && empty($path['alias'])) { - drupal_container()->get('path.crud')->delete(array('pid' => $path['pid'])); - } - // Only save a non-empty alias. - if (!empty($path['alias'])) { - $pid = (!empty($path['pid']) ? $path['pid'] : NULL); - // Ensure fields for programmatic executions. - $path['source'] = 'taxonomy/term/' . $term->tid; - $path['langcode'] = LANGUAGE_NOT_SPECIFIED; - drupal_container()->get('path.crud')->save($path['source'], $path['alias'], $path['langcode'], $pid); + 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(); + } } } } /** - * Implements hook_taxonomy_term_delete(). - */ -function path_taxonomy_term_delete(Term $term) { - // Delete all aliases associated with this term. - drupal_container()->get('path.crud')->delete(array('source' => 'taxonomy/term/' . $term->tid)); -} - -/** * Implements hook_library_info(). */ function path_library_info() { diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index e2d65f0..218e83a 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -216,6 +216,7 @@ function standard_install() { $type = node_type_set_defaults($type); node_type_save($type); node_add_body_field($type); + path_add_default_field_instance('node', $type->type); } // Insert default pre-defined RDF mapping into the database. @@ -268,6 +269,7 @@ function standard_install() { 'help' => $help, )); taxonomy_vocabulary_save($vocabulary); + path_add_default_field_instance('taxonomy_term', 'tags'); $field = array( 'field_name' => 'field_' . $vocabulary->machine_name,