diff --git a/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php b/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php
new file mode 100644
index 0000000..acca988
--- /dev/null
+++ b/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php
@@ -0,0 +1,459 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\link\Tests\LinkFieldTest.
+ */
+
+namespace Drupal\link\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests link field widgets and formatters.
+ */
+class LinkFieldTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('field_test', 'link');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Link field',
+      'description' => 'Tests link field widgets and formatters.',
+      'group' => 'Field types',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->web_user = $this->drupalCreateUser(array(
+      'access field_test content',
+      'administer field_test content',
+    ));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Tests link field URL validation.
+   */
+  function testURLValidation() {
+    // Create a field with settings to validate.
+    $this->field = array(
+      'field_name' => drupal_strtolower($this->randomName()),
+      'type' => 'link',
+    );
+    field_create_field($this->field);
+    $this->instance = array(
+      'field_name' => $this->field['field_name'],
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'settings' => array(
+        'title' => DRUPAL_DISABLED,
+      ),
+      'widget' => array(
+        'type' => 'link_default',
+      ),
+      'display' => array(
+        'full' => array(
+          'type' => 'link',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+    $langcode = LANGUAGE_NOT_SPECIFIED;
+
+    // Display creation form.
+    $this->drupalGet('test-entity/add/test_bundle');
+    $this->assertFieldByName("{$this->field['field_name']}[$langcode][0][url]", '', 'Link URL field is displayed');
+
+    // Verify that a valid URL can be submitted.
+    $value = 'http://www.example.com/';
+    $edit = array(
+      "{$this->field['field_name']}[$langcode][0][url]" => $value,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+    $id = $match[1];
+    $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)));
+    $this->assertRaw($value);
+
+    // Verify that invalid URLs cannot be submitted.
+    $wrong_entries = array(
+      // Missing protcol
+      'not-an-url',
+      // Invalid protocol
+      'invalid://not-a-valid-protocol',
+      // Missing host name
+      'http://',
+    );
+    $this->drupalGet('test-entity/add/test_bundle');
+    foreach ($wrong_entries as $invalid_value) {
+      $edit = array(
+        "{$this->field['field_name']}[$langcode][0][url]" => $invalid_value,
+      );
+      $this->drupalPost(NULL, $edit, t('Save'));
+      $this->assertText(t('The URL @url is not valid.', array('@url' => $invalid_value)));
+    }
+  }
+
+  /**
+   * Tests the title settings of a link field.
+   */
+  function testLinkTitle() {
+    // Create a field with settings to validate.
+    $this->field = array(
+      'field_name' => drupal_strtolower($this->randomName()),
+      'type' => 'link',
+    );
+    field_create_field($this->field);
+    $this->instance = array(
+      'field_name' => $this->field['field_name'],
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'settings' => array(
+        'title' => DRUPAL_OPTIONAL,
+      ),
+      'widget' => array(
+        'type' => 'link_default',
+      ),
+      'display' => array(
+        'full' => array(
+          'type' => 'link',
+          'label' => 'hidden',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+    $langcode = LANGUAGE_NOT_SPECIFIED;
+
+    // Verify that the title field works according to the field setting.
+    foreach (array(DRUPAL_DISABLED, DRUPAL_REQUIRED, DRUPAL_OPTIONAL) as $title_setting) {
+      // Update the title field setting.
+      $this->instance['settings']['title'] = $title_setting;
+      field_update_instance($this->instance);
+
+      // Display creation form.
+      $this->drupalGet('test-entity/add/test_bundle');
+      $this->assertFieldByName("{$this->field['field_name']}[$langcode][0][url]", '', 'URL field found.');
+
+      if ($title_setting === DRUPAL_DISABLED) {
+        $this->assertNoFieldByName("{$this->field['field_name']}[$langcode][0][title]", '', 'Title field not found.');
+      }
+      else {
+        $this->assertFieldByName("{$this->field['field_name']}[$langcode][0][title]", '', 'Title field found.');
+        if ($title_setting === DRUPAL_REQUIRED) {
+          // Verify that the title is required, if the URL is non-empty.
+          $edit = array(
+            "{$this->field['field_name']}[$langcode][0][url]" => 'http://www.example.com',
+          );
+          $this->drupalPost(NULL, $edit, t('Save'));
+          $this->assertText(t('!name field is required.', array('!name' => t('Title'))));
+
+          // Verify that the title is not required, if the URL is empty.
+          $edit = array(
+            "{$this->field['field_name']}[$langcode][0][url]" => '',
+          );
+          $this->drupalPost(NULL, $edit, t('Save'));
+          $this->assertNoText(t('!name field is required.', array('!name' => t('Title'))));
+
+          // Verify that a URL and title meets requirements.
+          $this->drupalGet('test-entity/add/test_bundle');
+          $edit = array(
+            "{$this->field['field_name']}[$langcode][0][url]" => 'http://www.example.com',
+            "{$this->field['field_name']}[$langcode][0][title]" => 'Example',
+          );
+          $this->drupalPost(NULL, $edit, t('Save'));
+          $this->assertNoText(t('!name field is required.', array('!name' => t('Title'))));
+        }
+      }
+    }
+
+    // Verify that a link without title is rendered using the URL as link text.
+    $value = 'http://www.example.com/';
+    $edit = array(
+      "{$this->field['field_name']}[$langcode][0][url]" => $value,
+      "{$this->field['field_name']}[$langcode][0][title]" => '',
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+    $id = $match[1];
+    $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)));
+
+    $this->renderTestEntity($id);
+    $expected_link = l($value, $value);
+    $this->assertRaw($expected_link);
+
+    // Verify that a link with title is rendered using the title as link text.
+    $title = $this->randomName();
+    $edit = array(
+      "{$this->field['field_name']}[$langcode][0][title]" => $title,
+    );
+    $this->drupalPost("test-entity/manage/$id/edit", $edit, t('Save'));
+    $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)));
+
+    $this->renderTestEntity($id);
+    $expected_link = l($title, $value);
+    $this->assertRaw($expected_link);
+  }
+
+  /**
+   * Tests the default 'link' formatter.
+   */
+  function testLinkFormatter() {
+    // Create a field with settings to validate.
+    $this->field = array(
+      'field_name' => drupal_strtolower($this->randomName()),
+      'type' => 'link',
+      'cardinality' => 2,
+    );
+    field_create_field($this->field);
+    $this->instance = array(
+      'field_name' => $this->field['field_name'],
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'settings' => array(
+        'title' => DRUPAL_OPTIONAL,
+      ),
+      'widget' => array(
+        'type' => 'link_default',
+      ),
+      'display' => array(
+        'full' => array(
+          'type' => 'link',
+          'label' => 'hidden',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+    $langcode = LANGUAGE_NOT_SPECIFIED;
+
+    // Create an entity with two link field values:
+    // - The first field item uses a URL only.
+    // - The second field item uses a URL and title.
+    // For consistency in assertion code below, the URL is assigned to the title
+    // variable for the first field.
+    $this->drupalGet('test-entity/add/test_bundle');
+    $url1 = 'http://www.example.com/content/articles/archive?author=John&year=2012#com';
+    $url2 = 'http://www.example.org/content/articles/archive?author=John&year=2012#org';
+    $title1 = $url1;
+    // Intentionally contains an ampersand that needs sanitization on output.
+    $title2 = 'A very long & strange example title that could break the nice layout of the site';
+    $edit = array(
+      "{$this->field['field_name']}[$langcode][0][url]" => $url1,
+      // Note that $title1 is not submitted.
+      "{$this->field['field_name']}[$langcode][0][title]" => '',
+      "{$this->field['field_name']}[$langcode][1][url]" => $url2,
+      "{$this->field['field_name']}[$langcode][1][title]" => $title2,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+    $id = $match[1];
+    $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)));
+
+    // Verify that the link is output according to the formatter settings.
+    // Not using generatePermutations(), since that leads to 32 cases, which
+    // would not test actual link field formatter functionality but rather
+    // theme_link() and options/attributes. Only 'url_plain' has a dependency on
+    // 'url_only', so we have a total of ~10 cases.
+    $options = array(
+      'trim_length' => array(NULL, 6),
+      'rel' => array(NULL, 'nofollow'),
+      'target' => array(NULL, '_blank'),
+      'url_only' => array(
+        array('url_only' => array(FALSE)),
+        array('url_only' => array(FALSE), 'url_plain' => TRUE),
+        array('url_only' => array(TRUE)),
+        array('url_only' => array(TRUE), 'url_plain' => TRUE),
+      ),
+    );
+    foreach ($options as $setting => $values) {
+      foreach ($values as $new_value) {
+        // Update the field formatter settings.
+        if (!is_array($new_value)) {
+          $this->instance['display']['full']['settings'] = array($setting => $new_value);
+        }
+        else {
+          $this->instance['display']['full']['settings'] = $new_value;
+        }
+        field_update_instance($this->instance);
+
+        $this->renderTestEntity($id);
+        switch ($setting) {
+          case 'trim_length':
+            $url = $url1;
+            $title = isset($new_value) ? truncate_utf8($title1, $new_value, FALSE, TRUE) : $title1;
+            $this->assertRaw('<a href="' . check_plain($url) . '">' . check_plain($title) . '</a>');
+
+            $url = $url2;
+            $title = isset($new_value) ? truncate_utf8($title2, $new_value, FALSE, TRUE) : $title2;
+            $this->assertRaw('<a href="' . check_plain($url) . '">' . check_plain($title) . '</a>');
+            break;
+
+          case 'rel':
+            $rel = isset($new_value) ? ' rel="' . $new_value . '"' : '';
+            $this->assertRaw('<a href="' . check_plain($url1) . '"' . $rel . '>' . check_plain($title1) . '</a>');
+            $this->assertRaw('<a href="' . check_plain($url2) . '"' . $rel . '>' . check_plain($title2) . '</a>');
+            break;
+
+          case 'target':
+            $target = isset($new_value) ? ' target="' . $new_value . '"' : '';
+            $this->assertRaw('<a href="' . check_plain($url1) . '"' . $target . '>' . check_plain($title1) . '</a>');
+            $this->assertRaw('<a href="' . check_plain($url2) . '"' . $target . '>' . check_plain($title2) . '</a>');
+            break;
+
+          case 'url_only':
+            // In this case, $new_value is an array.
+            if (!$new_value['url_only']) {
+              $this->assertRaw('<a href="' . check_plain($url1) . '">' . check_plain($title1) . '</a>');
+              $this->assertRaw('<a href="' . check_plain($url2) . '">' . check_plain($title2) . '</a>');
+            }
+            else {
+              if (empty($new_value['url_plain'])) {
+                $this->assertRaw('<a href="' . check_plain($url1) . '">' . check_plain($url1) . '</a>');
+                $this->assertRaw('<a href="' . check_plain($url2) . '">' . check_plain($url2) . '</a>');
+              }
+              else {
+                $this->assertNoRaw('<a href="' . check_plain($url1) . '">' . check_plain($url1) . '</a>');
+                $this->assertNoRaw('<a href="' . check_plain($url2) . '">' . check_plain($url2) . '</a>');
+                $this->assertRaw(check_plain($url1));
+                $this->assertRaw(check_plain($url2));
+              }
+            }
+            break;
+        }
+      }
+    }
+  }
+
+  /**
+   * Tests the 'link_separate' formatter.
+   *
+   * This test is mostly the same as testLinkFormatter(), but they cannot be
+   * merged, since they involve different configuration and output.
+   */
+  function testLinkSeparateFormatter() {
+    // Create a field with settings to validate.
+    $this->field = array(
+      'field_name' => drupal_strtolower($this->randomName()),
+      'type' => 'link',
+      'cardinality' => 2,
+    );
+    field_create_field($this->field);
+    $this->instance = array(
+      'field_name' => $this->field['field_name'],
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'settings' => array(
+        'title' => DRUPAL_OPTIONAL,
+      ),
+      'widget' => array(
+        'type' => 'link_default',
+      ),
+      'display' => array(
+        'full' => array(
+          'type' => 'link_separate',
+          'label' => 'hidden',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+    $langcode = LANGUAGE_NOT_SPECIFIED;
+
+    // Create an entity with two link field values:
+    // - The first field item uses a URL only.
+    // - The second field item uses a URL and title.
+    // For consistency in assertion code below, the URL is assigned to the title
+    // variable for the first field.
+    $this->drupalGet('test-entity/add/test_bundle');
+    $url1 = 'http://www.example.com/content/articles/archive?author=John&year=2012#com';
+    $url2 = 'http://www.example.org/content/articles/archive?author=John&year=2012#org';
+    // Intentionally contains an ampersand that needs sanitization on output.
+    $title2 = 'A very long & strange example title that could break the nice layout of the site';
+    $edit = array(
+      "{$this->field['field_name']}[$langcode][0][url]" => $url1,
+      "{$this->field['field_name']}[$langcode][1][url]" => $url2,
+      "{$this->field['field_name']}[$langcode][1][title]" => $title2,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+    $id = $match[1];
+    $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)));
+
+    // Verify that the link is output according to the formatter settings.
+    $options = array(
+      'trim_length' => array(NULL, 6),
+      'rel' => array(NULL, 'nofollow'),
+      'target' => array(NULL, '_blank'),
+    );
+    foreach ($options as $setting => $values) {
+      foreach ($values as $new_value) {
+        // Update the field formatter settings.
+        $this->instance['display']['full']['settings'] = array($setting => $new_value);
+        field_update_instance($this->instance);
+
+        $this->renderTestEntity($id);
+        switch ($setting) {
+          case 'trim_length':
+            $url = $url1;
+            $url_title = isset($new_value) ? truncate_utf8($url, $new_value, FALSE, TRUE) : $url;
+            $expected = '<div class="link-item">';
+            $expected .= '<div class="link-url"><a href="' . check_plain($url) . '">' . check_plain($url_title) . '</a></div>';
+            $expected .= '</div>';
+            $this->assertRaw($expected);
+
+            $url = $url2;
+            $url_title = isset($new_value) ? truncate_utf8($url, $new_value, FALSE, TRUE) : $url;
+            $title = isset($new_value) ? truncate_utf8($title2, $new_value, FALSE, TRUE) : $title2;
+            $expected = '<div class="link-item">';
+            $expected .= '<div class="link-title">' . check_plain($title) . '</div>';
+            $expected .= '<div class="link-url"><a href="' . check_plain($url) . '">' . check_plain($url_title) . '</a></div>';
+            $expected .= '</div>';
+            $this->assertRaw($expected);
+            break;
+
+          case 'rel':
+            $rel = isset($new_value) ? ' rel="' . $new_value . '"' : '';
+            $this->assertRaw('<div class="link-url"><a href="' . check_plain($url1) . '"' . $rel . '>' . check_plain($url1) . '</a></div>');
+            $this->assertRaw('<div class="link-url"><a href="' . check_plain($url2) . '"' . $rel . '>' . check_plain($url2) . '</a></div>');
+            break;
+
+          case 'target':
+            $target = isset($new_value) ? ' target="' . $new_value . '"' : '';
+            $this->assertRaw('<div class="link-url"><a href="' . check_plain($url1) . '"' . $target . '>' . check_plain($url1) . '</a></div>');
+            $this->assertRaw('<div class="link-url"><a href="' . check_plain($url2) . '"' . $target . '>' . check_plain($url2) . '</a></div>');
+            break;
+        }
+      }
+    }
+  }
+
+  /**
+   * Renders a test_entity and sets the output in the internal browser.
+   *
+   * @param int $id
+   *   The test_entity ID to render.
+   * @param string $view_mode
+   *   (optional) The view mode to use for rendering.
+   * @param bool $reset
+   *   (optional) Whether to reset the test_entity controller cache. Defaults to
+   *   TRUE to simplify testing.
+   */
+  protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
+    if ($reset) {
+      entity_get_controller('test_entity')->resetCache(array($id));
+    }
+    $entity = field_test_entity_test_load($id);
+    field_attach_prepare_view('test_entity', array($entity->id() => $entity), $view_mode);
+    $entity->content = field_attach_view('test_entity', $entity, $view_mode);
+
+    $output = drupal_render($entity->content);
+    $this->drupalSetContent($output);
+    $this->verbose($output);
+  }
+}
diff --git a/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldUITest.php b/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldUITest.php
new file mode 100644
index 0000000..2a29792
--- /dev/null
+++ b/core/modules/field/modules/link/lib/Drupal/link/Tests/LinkFieldUITest.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\link\Tests\LinkFieldUITest.
+ */
+
+namespace Drupal\link\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests link field UI functionality.
+ */
+class LinkFieldUITest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node', 'link', 'field_ui');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Link field UI',
+      'description' => 'Tests link field UI functionality.',
+      'group' => 'Field types',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->web_user = $this->drupalCreateUser(array('administer content types'));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Tests that link field UI functionality does not generate warnings.
+   */
+  function testFieldUI() {
+    // Add a content type.
+    $type = $this->drupalCreateContentType();
+    $type_path = 'admin/structure/types/manage/' . $type->type;
+
+    // Add a link field to the newly-created type.
+    $label = $this->randomName();
+    $field_name = drupal_strtolower($label);
+    $edit = array(
+      'fields[_add_new_field][label]' => $label,
+      'fields[_add_new_field][field_name]' => $field_name,
+      'fields[_add_new_field][type]' => 'link',
+      'fields[_add_new_field][widget_type]' => 'link_default',
+    );
+    $this->drupalPost("$type_path/fields", $edit, t('Save'));
+    // Proceed to the Edit (field instance settings) page.
+    $this->drupalPost(NULL, array(), t('Save field settings'));
+    // Proceed to the Manage fields overview page.
+    $this->drupalPost(NULL, array(), t('Save settings'));
+
+    // Load the formatter page to check that the settings summary does not
+    // generate warnings.
+    // @todo Mess with the formatter settings a bit here.
+    $this->drupalGet("$type_path/display");
+    $this->assertText(t('Link text trimmed to @limit characters', array('@limit' => 80)));
+  }
+
+}
diff --git a/core/modules/field/modules/link/link.info b/core/modules/field/modules/link/link.info
new file mode 100644
index 0000000..a43c222
--- /dev/null
+++ b/core/modules/field/modules/link/link.info
@@ -0,0 +1,6 @@
+name = Link
+description = Provides a simple link field type.
+core = 8.x
+package = Core
+version = VERSION
+dependencies[] = field
diff --git a/core/modules/field/modules/link/link.install b/core/modules/field/modules/link/link.install
new file mode 100644
index 0000000..3714d35
--- /dev/null
+++ b/core/modules/field/modules/link/link.install
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains install, update, and uninstall functionality for the Link module.
+ */
+
+/**
+ * Implements hook_field_schema().
+ */
+function link_field_schema($field) {
+  $schema['columns']['url'] = array(
+    'description' => 'The URL of the link.',
+    'type' => 'varchar',
+    'length' => 2048,
+    'not null' => FALSE,
+  );
+  $schema['columns']['title'] = array(
+    'description' => 'The link text.',
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => FALSE,
+  );
+  $schema['columns']['attributes'] = array(
+    'description' => 'Serialized array of attributes for the link.',
+    'type' => 'blob',
+    'size' => 'big',
+    'not null' => FALSE,
+    'serialize' => TRUE,
+  );
+  return $schema;
+}
diff --git a/core/modules/field/modules/link/link.module b/core/modules/field/modules/link/link.module
new file mode 100644
index 0000000..947c2e9
--- /dev/null
+++ b/core/modules/field/modules/link/link.module
@@ -0,0 +1,390 @@
+<?php
+
+/**
+ * @file
+ * Defines simple link field types.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function link_help($path, $arg) {
+  switch ($path) {
+    case 'admin/help#link':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Link module defines a simple link field type for the Field module. Links are external URLs, can have an optional title for each link, and they can be formatted when displayed. See the <a href="@field-help">Field module help page</a> for more information about fields.', array('@field-help' => url('admin/help/field'))) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_field_info().
+ */
+function link_field_info() {
+  $types['link'] = array(
+    'label' => t('Link'),
+    'description' => t('Stores a URL string, optional varchar title, and optional blob of attributes to assemble a link.'),
+    'instance_settings' => array(
+      'title' => DRUPAL_OPTIONAL,
+    ),
+    'default_widget' => 'link_default',
+    'default_formatter' => 'link',
+  );
+  return $types;
+}
+
+/**
+ * Implements hook_field_instance_settings_form().
+ */
+function link_field_instance_settings_form($field, $instance) {
+  $form['title'] = array(
+    '#type' => 'radios',
+    '#title' => t('Allow link title'),
+    '#default_value' => isset($instance['settings']['title']) ? $instance['settings']['title'] : DRUPAL_OPTIONAL,
+    '#options' => array(
+      DRUPAL_DISABLED => t('Disabled'),
+      DRUPAL_OPTIONAL => t('Optional'),
+      DRUPAL_REQUIRED => t('Required'),
+    ),
+  );
+  return $form;
+}
+
+/**
+ * Implements hook_field_load().
+ */
+function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
+  foreach ($entities as $id => $entity) {
+    foreach ($items[$id] as $delta => &$item) {
+      // Unserialize the attributes into an array. The value stored in the
+      // field data should either be NULL or a non-empty serialized array.
+      if (empty($item['attributes'])) {
+        $item['attributes'] = array();
+      }
+      else {
+        $item['attributes'] = unserialize($item['attributes']);
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function link_field_is_empty($item, $field) {
+  return !isset($item['url']) || $item['url'] === '';
+}
+
+/**
+ * Implements hook_field_presave().
+ */
+function link_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
+  foreach ($items as $delta => &$item) {
+    // Trim any spaces around the URL and title.
+    $item['url'] = trim($item['url']);
+    $item['title'] = trim($item['title']);
+
+    // Serialize the attributes array.
+    $item['attributes'] = !empty($item['attributes']) ? serialize($item['attributes']) : NULL;
+  }
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function link_field_widget_info() {
+  $widgets['link_default'] = array(
+    'label' => 'Link',
+    'field types' => array('link'),
+  );
+  return $widgets;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+  $settings = $instance['settings'];
+
+  $element['url'] = array(
+    '#type' => 'url',
+    '#title' => t('URL'),
+    '#default_value' => isset($items[$delta]['url']) ? $items[$delta]['url'] : NULL,
+    '#maxlength' => 2048,
+    '#required' => $element['#required'],
+  );
+  $element['title'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Title'),
+    '#default_value' => isset($items[$delta]['title']) ? $items[$delta]['title'] : NULL,
+    '#maxlength' => 255,
+    '#access' => $settings['title'] != DRUPAL_DISABLED,
+  );
+  // Post-process the title field to make it conditionally required if URL is
+  // non-empty. Omit the validation on the field edit form, since the field
+  // settings cannot be saved otherwise.
+  $is_field_edit_form = ($element['#entity'] === NULL);
+  if (!$is_field_edit_form && $settings['title'] == DRUPAL_REQUIRED) {
+    $element['#element_validate'][] = 'link_field_widget_validate_title';
+  }
+
+  // Exposing the attributes array in the widget is left for alternate and more
+  // advanced field widgets.
+  $element['attributes'] = array(
+    '#type' => 'value',
+    '#tree' => TRUE,
+    '#value' => !empty($items[$delta]['attributes']) ? $items[$delta]['attributes'] : array(),
+    '#attributes' => array('class' => array('link-field-widget-attributes')),
+  );
+
+  return $element;
+}
+
+/**
+ * Form element validation handler for link_field_widget_form().
+ *
+ * Conditionally requires the link title if a URL value was filled in.
+ */
+function link_field_widget_validate_title(&$element, &$form_state, $form) {
+  if ($element['url']['#value'] !== '' && $element['title']['#value'] === '') {
+    $element['title']['#required'] = TRUE;
+    form_error($element['title'], t('!name field is required.', array('!name' => $element['title']['#title'])));
+  }
+}
+
+/**
+ * Implements hook_field_prepare_view().
+ */
+function link_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
+  foreach ($entities as $id => $entity) {
+    foreach ($items[$id] as $delta => &$item) {
+      // Split out the link into the parts required for url(): path and options.
+      $parsed = drupal_parse_url($item['url']);
+      $item['path'] = $parsed['path'];
+      $item['options'] = array(
+        'query' => $parsed['query'],
+        'fragment' => $parsed['fragment'],
+        'attributes' => &$item['attributes'],
+      );
+    }
+  }
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function link_field_formatter_info() {
+  $formatters['link'] = array(
+    'label' => t('Link'),
+    'field types' => array('link'),
+    'settings' => array(
+      'trim_length' => 80,
+      'rel' => NULL,
+      'target' => NULL,
+      'url_only' => FALSE,
+      'url_plain' => FALSE,
+    ),
+  );
+  // @todo Merge into 'link' formatter once there is a #type like 'item' that
+  //   can render a compound label and content outside of a form context.
+  $formatters['link_separate'] = array(
+    'label' => t('Separate title and URL'),
+    'field types' => array('link'),
+    'settings' => array(
+      'trim_length' => 80,
+      'rel' => NULL,
+      'target' => NULL,
+    ),
+  );
+  return $formatters;
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function link_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+  $display = $instance['display'][$view_mode];
+  $settings = $display['settings'];
+
+  $element['trim_length'] = array(
+    '#type' => 'number',
+    '#title' => t('Trim link text length'),
+    '#field_suffix' => t('characters'),
+    '#default_value' => $settings['trim_length'],
+    '#min' => 1,
+    '#description' => t('Leave blank to allow unlimited link text lengths.'),
+  );
+  $element['url_only'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('URL only'),
+    '#default_value' => $settings['url_only'],
+    '#access' => $display['type'] == 'link',
+  );
+  $element['url_plain'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Show URL as plain text'),
+    '#default_value' => $settings['url_plain'],
+    '#access' => $display['type'] == 'link',
+    '#states' => array(
+      'visible' => array(
+        ':input[name*="url_only"]' => array('checked' => TRUE),
+      ),
+    ),
+  );
+  $element['rel'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Add rel="nofollow" to links'),
+    '#return_value' => 'nofollow',
+    '#default_value' => $settings['nofollow'],
+  );
+  $element['target'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Open link in new window'),
+    '#return_value' => '_blank',
+    '#default_value' => $settings['target'],
+  );
+
+  return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function link_field_formatter_settings_summary($field, $instance, $view_mode) {
+  $display = $instance['display'][$view_mode];
+  $settings = $display['settings'];
+
+  $summary = array();
+
+  if (!empty($settings['trim_length'])) {
+    $summary[] = t('Link text trimmed to @limit characters', array('@limit' => $settings['trim_length']));
+  }
+  else {
+    $summary[] = t('Link text not trimmed');
+  }
+  if (!empty($settings['url_only'])) {
+    if (!empty($settings['url_plain'])) {
+      $summary[] = t('Show URL only as plain-text');
+    }
+    else {
+      $summary[] = t('Show URL only');
+    }
+  }
+  if (!empty($settings['rel'])) {
+    $summary[] = t('Add rel="@rel"', array('@rel' => $settings['rel']));
+  }
+  if (!empty($settings['target'])) {
+    $summary[] = t('Open link in new window');
+  }
+
+  return implode('<br />', $summary);
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function link_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+  foreach ($entities as $id => $entity) {
+    $settings = $displays[$id]['settings'];
+
+    foreach ($items[$id] as $delta => &$item) {
+      // Add optional 'rel' attribute to link options.
+      if (!empty($settings['rel'])) {
+        $item['options']['attributes']['rel'] = $settings['rel'];
+      }
+      // Add optional 'target' attribute to link options.
+      if (!empty($settings['target'])) {
+        $item['options']['attributes']['target'] = $settings['target'];
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+  $element = array();
+  $settings = $display['settings'];
+
+  foreach ($items as $delta => $item) {
+    // By default use the full URL as the link title.
+    $link_title = $item['url'];
+
+    // If the title field value is available, use it for the link title.
+    if (empty($settings['url_only']) && !empty($item['title'])) {
+      // Unsanitizied token replacement here because $options['html'] is FALSE
+      // by default in theme_link().
+      $link_title = token_replace($item['title'], array($entity_type => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
+    }
+
+    // Trim the link title to the desired length.
+    if (!empty($settings['trim_length'])) {
+      $link_title = truncate_utf8($link_title, $settings['trim_length'], FALSE, TRUE);
+    }
+
+    if ($display['type'] == 'link') {
+      if (!empty($settings['url_only']) && !empty($settings['url_plain'])) {
+        $element[$delta] = array(
+          '#type' => 'markup',
+          '#markup' => check_plain($link_title),
+        );
+      }
+      else {
+        $element[$delta] = array(
+          '#type' => 'link',
+          '#title' => $link_title,
+          '#href' => $item['path'],
+          '#options' => $item['options'],
+        );
+      }
+    }
+    elseif ($display['type'] == 'link_separate') {
+      // The link_separate formatter has two titles; the link title (as in the
+      // field values) and the URL itself. If there is no title value,
+      // $link_title defaults to the URL, so it needs to be unset.
+      // The URL title may need to be trimmed as well.
+      if (empty($item['title'])) {
+        $link_title = NULL;
+      }
+      $url_title = $item['url'];
+      if (!empty($settings['trim_length'])) {
+        $url_title = truncate_utf8($item['url'], $settings['trim_length'], FALSE, TRUE);
+      }
+      $element[$delta] = array(
+        '#theme' => 'link_formatter_link_separate',
+        '#title' => $link_title,
+        '#url_title' => $url_title,
+        '#href' => $item['path'],
+        '#options' => $item['options'],
+      );
+    }
+  }
+  return $element;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function link_theme() {
+  return array(
+    'link_formatter_link_separate' => array(
+      'variables' => array('title' => NULL, 'url_title' => NULL, 'href' => NULL, 'options' => array()),
+    ),
+  );
+}
+
+/**
+ * Formats a link as separate title and URL elements.
+ */
+function theme_link_formatter_link_separate($vars) {
+  $output = '';
+  $output .= '<div class="link-item">';
+  if (!empty($vars['title'])) {
+    $output .= '<div class="link-title">' . check_plain($vars['title']) . '</div>';
+  }
+  $output .= '<div class="link-url">' . l($vars['url_title'], $vars['href'], $vars['options']) . '</div>';
+  $output .= '</div>';
+  return $output;
+}
