diff --git a/core/lib/Drupal/Core/Path/Path.php b/core/lib/Drupal/Core/Path/Path.php index a7fd55a..ed5aa31 100644 --- a/core/lib/Drupal/Core/Path/Path.php +++ b/core/lib/Drupal/Core/Path/Path.php @@ -130,7 +130,9 @@ public function load($conditions) { * An array of criteria. */ public function delete($conditions) { - $path = $this->load($conditions); + if (!$path = $this->load($conditions)) { + return; + } $query = $this->connection->delete('url_alias'); foreach ($conditions as $field => $value) { $query->condition($field, $value); diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterPluginManager.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterPluginManager.php index c935c44..41d7d97 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterPluginManager.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterPluginManager.php @@ -76,16 +76,27 @@ public function getInstance(array $options) { $configuration = $this->prepareConfiguration($field['type'], $configuration); } - $plugin_id = $configuration['type']; + if (isset($configuration['type'])) { + $plugin_id = $configuration['type']; - // Switch back to default formatter if either: - // - $type_info doesn't exist (the widget type is unknown), - // - the field type is not allowed for the widget. - $definition = $this->getDefinition($configuration['type']); - if (!isset($definition['class']) || !in_array($field['type'], $definition['field_types'])) { - // Grab the default widget for the field type. - $field_type_definition = field_info_field_types($field['type']); - $plugin_id = $field_type_definition['default_formatter']; + // Switch back to default formatter if either: + // - $type_info doesn't exist (the formatter type is unknown), + // - the field type is not allowed for the formatter. + $definition = $this->getDefinition($configuration['type']); + if (!isset($definition['class']) || !in_array($field['type'], $definition['field_types'])) { + // Grab the default formatter for the field type. + $field_type_definition = field_info_field_types($field['type']); + if (!empty($field_type_definition['default_formatter'])) { + $plugin_id = $field_type_definition['default_formatter']; + } + } + } + + // If no formatter is configured for this field instance and the field type + // does not define a default_formatter, then this field does not have any + // associated formatters (and is not meant to be output). + if (empty($plugin_id)) { + return; } $configuration += array( @@ -115,11 +126,14 @@ public function prepareConfiguration($field_type, array $configuration) { // If no formatter is specified, use the default formatter. if (!isset($configuration['type'])) { $field_type = field_info_field_types($field_type); - $configuration['type'] = $field_type['default_formatter']; + if (!empty($field_type['default_formatter'])) { + $configuration['type'] = $field_type['default_formatter']; + } } // Fill in default settings values for the formatter. - $configuration['settings'] += field_info_formatter_settings($configuration['type']); - + if (isset($configuration['type'])) { + $configuration['settings'] += field_info_formatter_settings($configuration['type']); + } return $configuration; } 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 12e6011..c579a24 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/DisplayOverview.php @@ -107,7 +107,7 @@ public function build(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']), @@ -153,7 +153,7 @@ public function build(array $form, array &$form_state) { '#title' => t('Formatter for @title', array('@title' => $instance['label'])), '#title_display' => 'invisible', '#options' => $formatter_options, - '#default_value' => $display_options ? $display_options['type'] : 'hidden', + '#default_value' => isset($display_options['type']) ? $display_options['type'] : 'hidden', '#parents' => array('fields', $name, 'type'), '#attributes' => array('class' => array('field-formatter-type')), ), @@ -170,7 +170,7 @@ public function build(array $form, array &$form_state) { } // Get the corresponding formatter object. - if ($display_options && $display_options['type'] != 'hidden') { + if (isset($display_options['type']) && $display_options['type'] != 'hidden') { $formatter = drupal_container()->get('plugin.manager.field.formatter')->getInstance(array( 'instance' => $instance, 'view_mode' => $this->view_mode, 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..dcd03bb --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Plugin/field/widget/PathWidget.php @@ -0,0 +1,147 @@ +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, + '#path' => $path, + '#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'], + ); + // Remove the #description from $element, as it would be duplicated into the + // container otherwise. + unset($element['#description']); + + $element['source'] = array( + '#type' => 'value', + '#value' => $path['source'], + ); + $element['langcode'] = array( + '#type' => 'value', + '#value' => $path['langcode'], + ); + + // Integrate with advanced settings, if available. + if (isset($form['advanced'])) { + $element['#type'] = 'details'; + $element['#group'] = 'advanced'; + $element['#title'] = t('URL settings'); + } + + 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..c79c1c1 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', @@ -137,26 +130,27 @@ function testAdminAlias() { function testNodeAlias() { // Create test node. $node1 = $this->drupalCreateNode(); + $lang_code = LANGUAGE_NOT_SPECIFIED; // Create alias. $edit = array(); - $edit['path[alias]'] = $this->randomName(8); + $edit["path[$lang_code][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[$lang_code][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[$lang_code][0][value]"]; + $edit["path[$lang_code][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[$lang_code][0][value]"]); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); @@ -169,17 +163,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[$lang_code][0][value]"]); $this->assertNoText($node1->label(), 'Alias was successfully deleted.'); $this->assertResponse(404); } @@ -204,13 +198,14 @@ function testDuplicateNodeAlias() { // Create one node with a random alias. $node_one = $this->drupalCreateNode(); $edit = array(); - $edit['path[alias]'] = $this->randomName(); + $lang_code = LANGUAGE_NOT_SPECIFIED; + $edit["path[$lang_code][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[$lang_code][0][value]' and contains(@class, 'error')]", $edit["path[$lang_code][0][value]"], 'Textfield exists and has the error class.'); } } diff --git a/core/modules/path/lib/Drupal/path/Tests/PathCustomUITest.php b/core/modules/path/lib/Drupal/path/Tests/PathCustomUITest.php new file mode 100644 index 0000000..6013a88 --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Tests/PathCustomUITest.php @@ -0,0 +1,87 @@ + 'Custom URL alias fields', + 'description' => 'Tests custom URL alias fields.', + 'group' => 'Path', + ); + } + + /** + * Tests alias functionality through the admin interfaces. + */ + function testCustomUserAlias() { + $label = 'My URL alias'; + $field_name = 'url_alias'; + $langcode = LANGUAGE_NOT_SPECIFIED; + $edit_field_key = 'field_' . $field_name . '[' . $langcode . '][0][value]'; + + // Add a URL alias field to user accounts. + $this->drupalLogin($this->root_user); + $edit = array( + 'fields[_add_new_field][label]' => $label, + 'fields[_add_new_field][field_name]' => $field_name, + 'fields[_add_new_field][type]' => 'path', + 'fields[_add_new_field][widget_type]' => 'path_default', + ); + $this->drupalPost('admin/config/people/accounts/fields', $edit, t('Save')); + + // Verify that it appears as hidden in the display settings. + $this->drupalGet('admin/config/people/accounts/display'); + $this->assertFieldByName('fields[field_url_alias][type]', 'hidden'); + + // Create a user account and add a URL alias for it. + $account = $this->drupalCreateUser(); + $alias = 'members/foo'; + $edit = array( + $edit_field_key => $alias, + ); + $this->drupalPost("user/$account->uid/edit", $edit, t('Save')); + $this->assertText($label); + + // Verify that the URL alias works. + $this->drupalGet($alias); + $this->assertUrl($alias); + $this->assertResponse(200); + $this->drupalGet("user/$account->uid"); + $this->assertUrl("user/$account->uid"); + $this->assertResponse(200); + + // Remove the URL alias and verify that it was removed. + $edit = array( + $edit_field_key => '', + ); + $this->drupalPost("user/$account->uid/edit", $edit, t('Save')); + $this->drupalGet($alias); + $this->assertUrl($alias); + $this->assertResponse(404); + $this->drupalGet("user/$account->uid"); + $this->assertUrl("user/$account->uid"); + $this->assertResponse(200); + + // Verify that the user is not able to edit the URL alias field. + $this->drupalLogin($account); + $this->drupalGet("user/$account->uid/edit"); + $this->assertNoText($label); + } +} diff --git a/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDUnitTest.php b/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDUnitTest.php new file mode 100644 index 0000000..4f29248 --- /dev/null +++ b/core/modules/path/lib/Drupal/path/Tests/PathFieldCRUDUnitTest.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 455a3f6..93512e2 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( @@ -30,19 +30,26 @@ public static function getInfo() { 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->drupalLogin($this->web_user); + // Permissions do not matter for this test. + $this->drupalLogin($this->root_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')); + + // @todo Rewrite entity_translation module to provide proper APIs. + $edit = array( + 'entity_types[node]' => 'node', + 'settings[node][page][translatable]' => TRUE, + 'settings[node][page][fields][body]' => 'body', + 'settings[node][page][fields][path]' => 'path', + ); + $this->drupalPost('admin/config/regional/content-language', $edit, t('Save')); } /** @@ -55,42 +62,41 @@ function testAliasTranslation() { ); $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')); + $lang_code = LANGUAGE_NOT_SPECIFIED; + $edit["path[$lang_code][0][value]"] = $english_alias; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save and keep published')); // 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; - $this->drupalPost(NULL, $edit, t('Save')); + $edit['path[fr][0][value]'] = $french_alias; + $this->drupalPost(NULL, $edit, t('Save and keep published')); // 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 +104,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. @@ -113,7 +119,7 @@ function testAliasTranslation() { // Change user language preference. $edit = array('preferred_langcode' => 'fr'); - $this->drupalPost("user/{$this->web_user->uid}/edit", $edit, t('Save')); + $this->drupalPost("user/{$this->root_user->uid}/edit", $edit, t('Save')); // Check that the English alias works. In this situation French is the // current UI and content language, while URL language is English (since we @@ -124,11 +130,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 +142,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 +154,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 2122b9a..a0912e6 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'), 'vid' => '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,36 +53,36 @@ function testTermAlias() { $edit = array( 'name' => $this->randomName(), 'description[value]' => $description, - 'path[alias]' => $this->randomName(), + 'path[und][0][value]' => $this->randomName(), ); $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->id() . '/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..eae337d --- /dev/null +++ b/core/modules/path/path.install @@ -0,0 +1,38 @@ + array( + 'pid' => 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, + ), + ), + '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 7d3fadd..a4f1f53 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(). @@ -20,8 +18,10 @@ function path_help($path, $arg) { $output .= '

' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module Pathauto. For more information, see the online handbook entry for the Path module.', array('@path' => 'http://drupal.org/documentation/modules/path', '@pathauto' => 'http://drupal.org/project/pathauto')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; + $output .= '
' . t('Enabling aliases') . '
'; + $output .= '
' . t('A URL alias field can be added to content types and other entity bundles that support fields (such as taxonomy vocabularies and user accounts). This field allows users to create or edit a URL alias when they are creating or editing content.') . '
'; $output .= '
' . t('Creating aliases') . '
'; - $output .= '
' . t('Users with sufficient permissions can create aliases under the URL path settings section when they create or edit content. Some examples of aliases are: ', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-path')))); + $output .= '
' . t('Users with sufficient permissions can create aliases under the URL settings section when they create or edit content. Some examples of aliases are: ', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-path')))); $output .= '
'; @@ -39,6 +39,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(EntityInterface $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(EntityInterface $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(EntityInterface $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,224 +243,71 @@ function path_menu() { } /** - * Implements hook_form_BASE_FORM_ID_alter() for node_form(). - * - * @see path_form_element_validate() - */ -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'), - '#collapsed' => empty($path['alias']), - '#group' => 'advanced', - '#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() + * Implements hook_entity_delete(). */ -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.')); - } - } +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_insert(). + * Implements hook_path_delete(). */ -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_path_delete(array $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); + // 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_node_update(). - */ -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'])); + $group = $query->orConditionGroup(); + foreach ($field_info as $field_name => $bundles) { + $group->condition($field_name . '.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']); + $query->condition($group); + $ids = $query->execute(); + if (!$ids) { + continue; } - } -} -/** - * 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)); -} - -/** - * 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(); + 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(); + } } - $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']); - } - } -} - -/** - * 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); - } - } -} - -/** - * 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() { @@ -331,3 +326,15 @@ function path_library_info() { return $libraries; } + +/** + * Implements hook_form_FORM_ID_alter() for field_ui_field_settings_form(). + */ +function path_form_field_ui_field_settings_form_alter(&$form, $form_state) { + if ($form['#field']['type'] == 'path') { + // We only support having one path at the entity so it doesn't make sense + // to let the site builder choose anything else. + $form['field']['container']['cardinality']['#options'] = drupal_map_assoc(array(1)); + $form['field']['container']['#access'] = FALSE; + } +} diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index 85c818c..c3fef83 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -50,6 +50,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. @@ -102,6 +103,7 @@ function standard_install() { 'help' => $help, )); taxonomy_vocabulary_save($vocabulary); + path_add_default_field_instance('taxonomy_term', 'tags'); $field = array( 'field_name' => 'field_' . $vocabulary->id(),