diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/RowPluginTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/RowPluginTest.php
new file mode 100644
index 0000000..6a523ae
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/Views/RowPluginTest.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\Views\RowPluginTest.
+ */
+
+namespace Drupal\node\Tests\Views;
+
+/**
+ * Tests the node row plugin.
+ *
+ * @see \Drupal\node\Plugin\views\row\NodeRow
+ */
+class RowPluginTest extends NodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node', 'comment');
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = array('test_node_row_plugin');
+
+  /**
+   * Contains all comments keyed by node used by the test.
+   *
+   * @var array
+   */
+  protected $comments;
+
+  /**
+   * Contains all nodes used by this test.
+   *
+   * @var array
+   */
+  protected $nodes;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node: Row plugin',
+      'description' => 'Tests the node row plugin.',
+      'group' => 'Views Modules',
+    );
+  }
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalCreateContentType(array('type' => 'article'));
+
+    // Create two nodes, with 5 comments on all of them.
+    for ($i = 0; $i < 2; $i++) {
+      $this->nodes[] = $this->drupalCreateNode(
+        array(
+          'type' => 'article',
+          'body' => array(
+            LANGUAGE_NOT_SPECIFIED => array(
+              array(
+                'value' => $this->randomName(42),
+                'format' => filter_default_format(),
+                'summary' => $this->randomName(),
+              ),
+            ),
+          ),
+        )
+      );
+    }
+
+    foreach ($this->nodes as $node) {
+      for ($i = 0; $i < 5; $i++) {
+        $this->comments[$node->id()][] = $this->drupalCreateComment(array('nid' => $node->id()));
+      }
+    }
+  }
+
+  /**
+   * Helper function to create a random comment.
+   *
+   * @param array $settings
+   *   (optional) An associative array of settings for the comment, as used in
+   *   entity_create().
+   *
+   * @return \Drupal\comment\Plugin\Core\Entity\Comment
+   *   Returns the created and saved comment.
+   */
+  public function drupalCreateComment(array $settings = array()) {
+    $node = node_load($settings['nid']);
+    $settings += array(
+      'subject' => $this->randomName(),
+      'node_type' => "comment_node_{$node->bundle()}",
+      'comment_body' => $this->randomName(40),
+    );
+
+    $comment = entity_create('comment', $settings);
+    $comment->save();
+    return $comment;
+  }
+
+  /**
+   * Tests the node row plugin.
+   */
+  public function testRowPlugin() {
+    $view = views_get_view('test_node_row_plugin');
+    $view->initDisplay();
+    $view->setDisplay('page');
+    $view->initStyle();
+    $view->style_plugin->row_plugin->options['view_mode'] = 'full';
+
+    // Test with view_mode full.
+    $output = $view->preview();
+    foreach ($this->nodes as $node) {
+      $body = $node->body;
+      $teaser = $body[LANGUAGE_NOT_SPECIFIED][0]['summary'];
+      $full = $body[LANGUAGE_NOT_SPECIFIED][0]['value'];
+      $this->assertFalse(strpos($output, $teaser) !== FALSE, 'Make sure the teaser appears in the output of the view.');
+      $this->assertTrue(strpos($output, $full) !== FALSE, 'Make sure the full text appears in the output of the view.');
+    }
+
+    // Test with teasers.
+    $view->style_plugin->row_plugin->options['view_mode'] = 'teaser';
+    $output = $view->preview();
+    foreach ($this->nodes as $node) {
+      $body = $node->body;
+      $teaser = $body[LANGUAGE_NOT_SPECIFIED][0]['summary'];
+      $full = $body[LANGUAGE_NOT_SPECIFIED][0]['value'];
+      $this->assertTrue(strpos($output, $teaser) !== FALSE, 'Make sure the teaser appears in the output of the view.');
+      $this->assertFalse(strpos($output, $full) !== FALSE, 'Make sure the full text does not appears in the output of the view if teaser is set as viewmode.');
+    }
+
+    // Test with links disabled.
+    $view->style_plugin->row_plugin->options['links'] = FALSE;
+    $output = $view->preview();
+    $this->drupalSetContent($output);
+    foreach ($this->nodes as $node) {
+      $this->assertFalse($this->xpath('//li[contains(@class, :class)]/a[contains(@href, :href)]', array(':class' => 'node-readmore', ':href' => "node/{$node->id()}")), 'Make sure no readmore link appears.');
+    }
+
+    // Test with links enabled.
+    $view->style_plugin->row_plugin->options['links'] = TRUE;
+    $output = $view->preview();
+    $this->drupalSetContent($output);
+    foreach ($this->nodes as $node) {
+      $this->assertTrue($this->xpath('//li[contains(@class, :class)]/a[contains(@href, :href)]', array(':class' => 'node-readmore', ':href' => "node/{$node->id()}")), 'Make sure no readmore link appears.');
+    }
+
+    // Test with comments enabled.
+    $view->style_plugin->row_plugin->options['comments'] = TRUE;
+    $output = $view->preview();
+    foreach ($this->nodes as $node) {
+      foreach ($this->comments[$node->id()] as $comment) {
+        $this->assertTrue(strpos($output, $comment->comment_body->value) !== FALSE, 'Make sure the comment appears in the output.');
+      }
+    }
+
+    // Test with comments disabled.
+    $view->style_plugin->row_plugin->options['comments'] = FALSE;
+    $output = $view->preview();
+    foreach ($this->nodes as $node) {
+      foreach ($this->comments[$node->id()] as $comment) {
+        $this->assertFalse(strpos($output, $comment->comment_body->value) !== FALSE, 'Make sure the comment does not appears in the output when the comments option disabled.');
+      }
+    }
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_row_plugin.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_row_plugin.yml
new file mode 100644
index 0000000..f50f972
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_row_plugin.yml
@@ -0,0 +1,56 @@
+base_field: nid
+base_table: node
+core: 8
+description: ''
+status: '1'
+display:
+  default:
+    display_options:
+      access:
+        type: perm
+      cache:
+        type: none
+      exposed_form:
+        type: basic
+      filters:
+        status:
+          expose:
+            operator: '0'
+          field: status
+          group: '1'
+          id: status
+          table: node
+          value: '1'
+          plugin_id: boolean
+      pager:
+        options:
+          items_per_page: '10'
+        type: full
+      query:
+        type: views_query
+      row:
+        options:
+          build_mode: teaser
+          comments: '0'
+          links: '1'
+        type: node
+      sorts: {  }
+      style:
+        type: default
+      title: test_node_row_plugin
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: {  }
+  page:
+    display_options:
+      path: test-node-row-plugin
+    display_plugin: page
+    display_title: Page
+    id: page
+    position: {  }
+human_name: test_node_row_plugin
+langcode: und
+module: views
+id: test_node_row_plugin
+tag: default
diff --git a/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/Profile.php b/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/Profile.php
new file mode 100644
index 0000000..5ec5991
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/Profile.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Plugin\Core\Entity\Profile.
+ */
+
+namespace Drupal\profile2\Plugin\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\Entity;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Defines the profile entity class.
+ *
+ * @Plugin(
+ *   id = "profile2",
+ *   label = @Translation("Profile"),
+ *   module = "profile2",
+ *   controller_class = "Drupal\profile2\ProfileStorageController",
+ *   render_controller_class = "Drupal\Core\Entity\EntityRenderController",
+ *   access_controller_class = "Drupal\profile2\ProfileAccessController",
+ *   form_controller_class = {
+ *     "default" = "Drupal\profile2\ProfileFormController"
+ *   },
+ *   base_table = "profile",
+ *   uri_callback = "profile2_profile_uri",
+ *   fieldable = TRUE,
+ *   entity_keys = {
+ *     "id" = "pid",
+ *     "uuid" = "uuid",
+ *     "bundle" = "type",
+ *     "label" = "label"
+ *   },
+ *   bundle_keys = {
+ *     "bundle" = "id"
+ *   },
+ *   view_modes = {
+ *     "account" = {
+ *       "label" = "User account",
+ *       "custom_settings" = FALSE
+ *     }
+ *   }
+ * )
+ */
+class Profile extends Entity implements ContentEntityInterface {
+
+  /**
+   * The profile id.
+   *
+   * @var integer
+   */
+  public $pid;
+
+  /**
+   * The profile UUID.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The name of the profile type.
+   *
+   * @var string
+   */
+  public $type;
+
+  /**
+   * The profile label.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The user id of the profile owner.
+   *
+   * @var integer
+   */
+  public $uid;
+
+  /**
+   * The Unix timestamp when the profile was created.
+   *
+   * @var integer
+   */
+  public $created;
+
+  /**
+   * The Unix timestamp when the profile was most recently saved.
+   *
+   * @var integer
+   */
+  public $changed;
+
+  /**
+   * Overrides Entity::id().
+   */
+  public function id() {
+    return isset($this->pid) ? $this->pid : NULL;
+  }
+
+  /**
+   * Overrides Entity::bundle().
+   */
+  public function bundle() {
+    return $this->type;
+  }
+
+  /**
+   * Overrides Entity::label().
+   */
+  public function label($langcode = NULL) {
+    // If this profile has a custom label, use it. Otherwise, use the label of
+    // the profile type.
+    if (isset($this->label) && $this->label !== '') {
+      return $this->label;
+    }
+    else {
+      return entity_load('profile2_type', $this->type)->label($langcode);
+    }
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/ProfileType.php b/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/ProfileType.php
new file mode 100644
index 0000000..a671e42
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Plugin/Core/Entity/ProfileType.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Plugin\Core\Entity\ProfileType.
+ */
+
+namespace Drupal\profile2\Plugin\Core\Entity;
+
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+
+/**
+ * Defines the profile type entity class.
+ *
+ * @Plugin(
+ *   id = "profile2_type",
+ *   label = @Translation("Profile type"),
+ *   module = "profile2",
+ *   controller_class = "Drupal\profile2\ProfileTypeStorageController",
+ *   list_controller_class = "Drupal\profile2\ProfileTypeListController",
+ *   form_controller_class = {
+ *     "default" = "Drupal\profile2\ProfileTypeFormController"
+ *   },
+ *   config_prefix = "profile2.type",
+ *   uri_callback = "profile2_profile_type_uri",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   }
+ * )
+ */
+class ProfileType extends ConfigEntityBase {
+
+  /**
+   * The primary identifier of the profile type.
+   *
+   * @var integer
+   */
+  public $id;
+
+  /**
+   * The universally unique identifier of the profile type.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The human-readable name of the profile type.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * Whether the profile type is shown during registration.
+   *
+   * @var boolean
+   */
+  public $registration = FALSE;
+
+  /**
+   * The weight of the profile type compared to others.
+   *
+   * @var integer
+   */
+  public $weight = 0;
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileAccessController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileAccessController.php
new file mode 100644
index 0000000..82a6132
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileAccessController.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileAccessController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityAccessControllerInterface;
+use Drupal\user\Plugin\Core\Entity\User;
+
+/**
+ * Access controller for profiles.
+ */
+class ProfileAccessController implements EntityAccessControllerInterface {
+
+  /**
+   * Static cache for access checks.
+   *
+   * @var array
+   */
+  protected $accessCache = array();
+
+  /**
+   * Implements EntityAccessControllerInterface::viewAccess().
+   */
+  public function viewAccess(EntityInterface $profile, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    return $this->access($profile, 'view', $langcode, $account);
+  }
+
+  /**
+   * Implements EntityAccessControllerInterface::createAccess().
+   */
+  public function createAccess(EntityInterface $profile, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // Create and update operations are folded into edit access for profiles.
+    return $this->access($profile, 'edit', $langcode, $account);
+  }
+
+  /**
+   * Implements EntityAccessControllerInterface::updateAccess().
+   */
+  public function updateAccess(EntityInterface $profile, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // Create and update operations are folded into edit access for profiles.
+    return $this->access($profile, 'edit', $langcode, $account);
+  }
+
+  /**
+   * Implements EntityAccessControllerInterface::deleteAccess().
+   */
+  public function deleteAccess(EntityInterface $profile, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    return $this->access($profile, 'delete', $langcode, $account);
+  }
+
+  /**
+   * Determines whether the given user has access to a profile.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $profile
+   *   A profile to check access for.
+   * @param string $operation
+   *   The operation being performed. One of 'view', 'create', 'update', or
+   *   'delete'.
+   * @param string $langcode
+   *   The language code for which to check access.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user to check for. Omit to check access for the global
+   *   user.
+   *
+   * @return bool
+   *   TRUE if access is allowed, FALSE otherwise.
+   *
+   * @see hook_profile2_access()
+   * @see profile2_profile2_access()
+   */
+  protected function access(EntityInterface $profile, $operation, $langcode, User $account = NULL) {
+    if (!isset($account)) {
+      $account = entity_load('user', $GLOBALS['user']->uid);
+    }
+    // Check for the bypass access permission first. No need to cache this,
+    // since user_access() is cached already.
+    if (user_access('bypass profile access', $account)) {
+      return TRUE;
+    }
+    $uid = $account->id();
+    // For existing profiles, check access for the particular profile ID. When
+    // creating a new profile, check access for the profile's bundle.
+    $pid = $profile->id() ?: $profile->bundle();
+
+    if (isset($this->accessCache[$uid][$operation][$pid][$langcode])) {
+      return $this->accessCache[$uid][$operation][$pid][$langcode];
+    }
+
+    $access = NULL;
+    // Ask modules to grant or deny access.
+    foreach (module_implements('profile2_access', $operation, $profile, $account) as $module) {
+      $return = module_invoke($module, 'profile2_access', $operation, $profile, $account);
+      // If a module denies access, there is no point in asking further.
+      if ($return === FALSE) {
+        $access = FALSE;
+        break;
+      }
+      // A module may grant access, but others may still deny.
+      if ($return === TRUE) {
+        $access = TRUE;
+      }
+    }
+    // Final access is only TRUE if any module explicitly returned TRUE. If at
+    // least one returned FALSE, $access will be FALSE. If no module returned
+    // anything, $access will be NULL, which means access is denied.
+    // @see hook_profile2_access()
+    $this->accessCache[$uid][$operation][$pid][$langcode] = ($access === TRUE);
+
+    return $this->accessCache[$uid][$operation][$pid][$langcode];
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileFormController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileFormController.php
new file mode 100644
index 0000000..2f1f11d
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileFormController.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileFormController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for profile forms.
+ */
+class ProfileFormController extends EntityFormController {
+
+  /**
+   * Overrides EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $element['delete']['#access'] = $this->getEntity($form_state)->access('delete');
+    return $element;
+  }
+
+  /**
+   * Overrides EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $profile = $this->getEntity($form_state);
+    $profile->save();
+
+    if ($GLOBALS['user']->uid == $profile->uid) {
+      drupal_set_message(t('Your profile has been saved.'));
+    }
+    else {
+      drupal_set_message(t("%name's profile has been updated.", array('%name' => user_format_name(user_load($profile->uid)))));
+    }
+  }
+
+  /**
+   * Overrides EntityFormController::delete().
+   */
+  public function delete(array $form, array &$form_state) {
+    $profile = $this->getEntity($form_state);
+    // Redirect to the deletion confirmation form.
+    $form_state['redirect'] = 'user/' . $profile->uid . '/edit/' . $profile->bundle() . '/delete';
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileStorageController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileStorageController.php
new file mode 100644
index 0000000..ceb7559
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileStorageController.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileStorageController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Entity\DatabaseStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Controller class for profile types.
+ */
+class ProfileStorageController extends DatabaseStorageController {
+
+  /**
+   * Overrides DatabaseStorageController::create().
+   */
+  public function create(array $values) {
+    $entity = parent::create($values);
+
+    // Set the created time to now.
+    if (!$entity->created) {
+      $entity->created = REQUEST_TIME;
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Overrides DatabaseStorageController::preSave().
+   */
+  protected function preSave(EntityInterface $entity) {
+    // Before saving the profile set the 'changed' timestamp.
+    $entity->changed = REQUEST_TIME;
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileTypeFormController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeFormController.php
new file mode 100644
index 0000000..0bdf5dd
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeFormController.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileTypeFormController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for profile type forms.
+ */
+class ProfileTypeFormController extends EntityFormController {
+
+  /**
+   * Overrides EntityFormController::form().
+   */
+  function form(array $form, array &$form_state, EntityInterface $type) {
+    $form['label'] = array(
+      '#title' => t('Label'),
+      '#type' => 'textfield',
+      '#default_value' => $type->label(),
+      '#description' => t('The human-readable name of this profile type.'),
+      '#required' => TRUE,
+      '#size' => 30,
+    );
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $type->id(),
+      '#maxlength' => 32,
+      '#machine_name' => array(
+        'exists' => 'profile2_type_load',
+      ),
+    );
+    $form['registration'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Show during user account registration.'),
+      '#default_value' => $type->get('registration'),
+    );
+    return $form;
+  }
+
+  /**
+   * Overrides EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $type = $this->getEntity($form_state);
+    $status = $type->save();
+
+    if ($status == SAVED_UPDATED) {
+      drupal_set_message(t('%label profile type has been updated.', array('%label' => $type->label())));
+    }
+    else {
+      drupal_set_message(t('%label profile type has been created.', array('%label' => $type->label())));
+    }
+    $form_state['redirect'] = 'admin/people/profiles';
+  }
+
+  /**
+   * Overrides EntityFormController::delete().
+   */
+  public function delete(array $form, array &$form_state) {
+    $type = $this->getEntity($form_state);
+    $form_state['redirect'] = 'admin/people/profiles/manage/' . $type->id() . '/delete';
+  }
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileTypeListController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeListController.php
new file mode 100644
index 0000000..d21f713
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeListController.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileTypeListController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Config\Entity\ConfigEntityListController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * List controller for profile types.
+ */
+class ProfileTypeListController extends ConfigEntityListController {
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityListController::getOperations().
+   */
+  public function getOperations(EntityInterface $entity) {
+    $operations = parent::getOperations($entity);
+    if (module_exists('field_ui')) {
+      $uri = $entity->uri();
+      $operations['manage-fields'] = array(
+        'title' => t('Manage fields'),
+        'href' => $uri['path'] . '/fields',
+        'options' => $uri['options'],
+        'weight' => 11,
+      );
+      $operations['manage-display'] = array(
+        'title' => t('Manage display'),
+        'href' => $uri['path'] . '/display',
+        'options' => $uri['options'],
+        'weight' => 12,
+      );
+    }
+    return $operations;
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/ProfileTypeStorageController.php b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeStorageController.php
new file mode 100644
index 0000000..fddc533
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/ProfileTypeStorageController.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\ProfileTypeController.
+ */
+
+namespace Drupal\profile2;
+
+use Drupal\Core\Config\Entity\ConfigStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Controller class for profile types.
+ */
+class ProfileTypeStorageController extends ConfigStorageController {
+
+  /**
+   * Overrides ConfigStorageController::postSave().
+   */
+  protected function postSave(EntityInterface $entity, $update) {
+    parent::postSave($entity, $update);
+
+    if (!$update) {
+      field_attach_create_bundle('profile2', $entity->id());
+    }
+    elseif ($entity->original->id() != $entity->id()) {
+      field_attach_rename_bundle('profile2', $entity->original->id(), $entity->id());
+    }
+  }
+
+  /**
+   * Overrides ConfigStorageController::preDelete().
+   */
+  protected function preDelete($entities) {
+    parent::preDelete($entities);
+
+    // Delete all profiles of this type.
+    if ($profiles = entity_load_multiple_by_properties('profile2', array('type' => array_keys($entities)))) {
+      entity_get_controller('profile2')->delete($profiles);
+    }
+  }
+
+  /**
+   * Overrides ConfigStorageController::postDelete().
+   */
+  protected function postDelete($entities) {
+    parent::postDelete($entities);
+
+    foreach ($entities as $entity) {
+      field_attach_delete_bundle('profile2', $entity->id());
+    }
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAccessTest.php b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAccessTest.php
new file mode 100644
index 0000000..4263afe
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAccessTest.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Tests\ProfileAccessTest.
+ */
+
+namespace Drupal\profile2\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests profile access handling.
+ */
+class ProfileAccessTest extends WebTestBase {
+
+  public static $modules = array('profile2', 'text');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Profile access',
+      'description' => 'Tests profile access handling.',
+      'group' => 'Profile2',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->type = entity_create('profile2_type', array(
+      'id' => 'test',
+      'label' => 'Test profile',
+    ));
+    $this->type->save();
+    $id = $this->type->id();
+
+    $this->field = array(
+      'field_name' => 'profile_fullname',
+      'type' => 'text',
+      'cardinality' => 1,
+      'translatable' => FALSE,
+    );
+    $this->field = field_create_field($this->field);
+    $this->instance = array(
+      'entity_type' => 'profile2',
+      'field_name' => $this->field['field_name'],
+      'bundle' => $this->type->id(),
+      'label' => 'Full name',
+      'widget' => array(
+        'type' => 'text_textfield',
+      ),
+    );
+    $this->instance = field_create_instance($this->instance);
+    $this->display = entity_get_display('profile2', 'test', 'default')
+      ->setComponent($this->field['field_name'], array(
+        'type' => 'text_default',
+      ));
+    $this->display->save();
+
+    $this->checkPermissions(array(), TRUE);
+
+    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access user profiles'));
+    $this->admin_user = $this->drupalCreateUser(array(
+      'administer profile types',
+      "view any $id profile",
+      "edit any $id profile",
+      "delete any $id profile",
+    ));
+  }
+
+  /**
+   * Tests administrative-only profiles.
+   */
+  function testAdminOnlyProfiles() {
+    $id = $this->type->id();
+    $field_name = $this->field['field_name'];
+
+    // Create a test user account.
+    $web_user = $this->drupalCreateUser(array('access user profiles'));
+    $uid = $web_user->id();
+    $value = $this->randomName();
+
+    // Administratively enter profile field values for the new account.
+    $this->drupalLogin($this->admin_user);
+    $edit = array(
+      "{$field_name}[und][0][value]" => $value,
+    );
+    $this->drupalPost("user/$uid/edit/$id", $edit, t('Save'));
+
+    // Verify that the administrator can see the profile.
+    $this->drupalGet("user/$uid");
+    $this->assertText($this->type->label());
+    $this->assertText($value);
+
+    // Verify that the user can not access or edit the profile.
+    $this->drupalLogin($web_user);
+    $this->drupalGet("user/$uid");
+    $this->assertNoText($this->type->label());
+    $this->assertNoText($value);
+    $this->drupalGet("user/$uid/edit/$id");
+    $this->assertResponse(403);
+
+    // Allow users to edit own profiles.
+    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array("edit own $id profile"));
+
+    // Verify that the user is able to edit the own profile.
+    $value = $this->randomName();
+    $edit = array(
+      "{$field_name}[und][0][value]" => $value,
+    );
+    $this->drupalPost("user/$uid/edit/$id", $edit, t('Save'));
+    $this->assertText(t('Your profile has been saved.'));
+
+    // Verify that the own profile is still not visible on the account page.
+    $this->drupalGet("user/$uid");
+    $this->assertNoText($this->type->label());
+    $this->assertNoText($value);
+
+    // Allow users to view own profiles.
+    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array("view own $id profile"));
+
+    // Verify that the own profile is visible on the account page.
+    $this->drupalGet("user/$uid");
+    $this->assertText($this->type->label());
+    $this->assertText($value);
+
+    // Allow users to delete own profiles.
+    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array("delete own $id profile"));
+
+    // Verify that the user can delete the own profile.
+    $this->drupalPost("user/$uid/edit/$id", array(), t('Delete'));
+    $this->drupalPost(NULL, array(), t('Delete'));
+    $this->assertRaw(t('Your %label profile has been deleted.', array('%label' => $this->type->label())));
+    $this->assertUrl("user/$uid");
+
+    // Verify that the profile is gone.
+    $this->drupalGet("user/$uid");
+    $this->assertNoText($this->type->label());
+    $this->assertNoText($value);
+    $this->drupalGet("user/$uid/edit/$id");
+    $this->assertNoText($value);
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAttachTest.php b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAttachTest.php
new file mode 100644
index 0000000..34edb72
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileAttachTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Tests\ProfileAttachTest.
+ */
+
+namespace Drupal\profile2\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests attaching of profile entity forms to other forms.
+ */
+class ProfileAttachTest extends WebTestBase {
+
+  public static $modules = array('profile2', 'text');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Profile form attachment',
+      'description' => 'Tests attaching of profile entity forms to other forms.',
+      'group' => 'Profile2',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->type = entity_create('profile2_type', array(
+      'id' => 'test',
+      'label' => 'Test profile',
+      'weight' => 0,
+      'registration' => TRUE,
+    ));
+    $this->type->save();
+
+    $this->field = array(
+      'field_name' => 'profile_fullname',
+      'type' => 'text',
+      'cardinality' => 1,
+      'translatable' => FALSE,
+    );
+    $this->field = field_create_field($this->field);
+    $this->instance = array(
+      'entity_type' => 'profile2',
+      'field_name' => $this->field['field_name'],
+      'bundle' => $this->type->id(),
+      'label' => 'Full name',
+      'required' => TRUE,
+      'widget' => array(
+        'type' => 'text_textfield',
+      ),
+    );
+    $this->instance = field_create_instance($this->instance);
+    $this->display = entity_get_display('profile2', 'test', 'default')
+      ->setComponent($this->field['field_name'], array(
+        'type' => 'text_default',
+      ));
+    $this->display->save();
+
+    $this->checkPermissions(array(), TRUE);
+  }
+
+  /**
+   * Test user registration integration.
+   */
+  function testUserRegisterForm() {
+    $id = $this->type->id();
+    $field_name = $this->field['field_name'];
+
+    // Allow registration without administrative approval and log in user
+    // directly after registering.
+    config('user.settings')
+      ->set('register', USER_REGISTER_VISITORS)
+      ->set('verify_mail', 0)
+      ->save();
+    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('view own test profile'));
+
+    // Verify that the additional profile field is attached and required.
+    $name = $this->randomName();
+    $pass_raw = $this->randomName();
+    $edit = array(
+      'name' => $name,
+      'mail' => $this->randomName() . '@example.com',
+      'pass[pass1]' => $pass_raw,
+      'pass[pass2]' => $pass_raw,
+    );
+    $this->drupalPost('user/register', $edit, t('Create new account'));
+    $this->assertRaw(t('@name field is required.', array('@name' => $this->instance['label'])));
+
+    // Verify that we can register.
+    $edit["profile[$id][$field_name][und][0][value]"] = $this->randomName();
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertText(t('Registration successful. You are now logged in.'));
+
+    $new_user = user_load_by_name($name);
+    $this->assertTrue($new_user->status, 'New account is active after registration.');
+
+    // Verify that a new profile was created for the new user ID.
+    $profiles = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $new_user->id(),
+      'type' => $this->type->id(),
+    ));
+    $profile = reset($profiles);
+    $this->assertEqual($profile->{$field_name}[LANGUAGE_NOT_SPECIFIED][0]['value'], $edit["profile[$id][$field_name][und][0][value]"], 'Field value found in loaded profile.');
+
+    // Verify that the profile field value appears on the user account page.
+    $this->drupalGet('user');
+    $this->assertText($edit["profile[$id][$field_name][und][0][value]"], 'Field value found on user account page.');
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileCRUDTest.php b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileCRUDTest.php
new file mode 100644
index 0000000..5f31e61
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileCRUDTest.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Tests\ProfileCRUDTest.
+ */
+
+namespace Drupal\profile2\Tests;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+
+/**
+ * Tests basic CRUD functionality of profiles.
+ */
+class ProfileCRUDTest extends DrupalUnitTestBase {
+
+  public static $modules = array('system', 'field', 'field_sql_storage', 'user', 'profile2');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Profile CRUD operations',
+      'description' => 'Tests basic CRUD functionality of profiles.',
+      'group' => 'Profile2',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    $this->installSchema('system', 'url_alias');
+    $this->installSchema('system', 'sequences');
+    $this->enableModules(array('field', 'user', 'profile2'));
+  }
+
+  /**
+   * Tests CRUD operations.
+   */
+  function testCRUD() {
+    $types_data = array(
+      0 => array('label' => $this->randomName()),
+      1 => array('label' => $this->randomName()),
+    );
+    foreach ($types_data as $id => $values) {
+      $types[$id] = entity_create('profile2_type', array('id' => $id) + $values);
+      $types[$id]->save();
+    }
+    $this->user1 = entity_create('user', array(
+      'name' => $this->randomName(),
+      'mail' => $this->randomName() . '@example.com',
+    ));
+    $this->user1->save();
+    $this->user2 = entity_create('user', array(
+      'name' => $this->randomName(),
+      'mail' => $this->randomName() . '@example.com',
+    ));
+    $this->user2->save();
+
+    // Create a new profile.
+    $profile = entity_create('profile2', $expected = array(
+      'type' => $types[0]->id(),
+      'uid' => $this->user1->id(),
+    ));
+    $this->assertIdentical($profile->id(), NULL);
+    $this->assertTrue($profile->uuid());
+    $this->assertIdentical($profile->type, $expected['type']);
+    $this->assertIdentical($profile->label(), $types[0]->label());
+    $this->assertIdentical($profile->uid, $this->user1->id());
+    $this->assertIdentical($profile->created, REQUEST_TIME);
+    $this->assertIdentical($profile->changed, NULL);
+
+    // Save the profile.
+    $status = $profile->save();
+    $this->assertIdentical($status, SAVED_NEW);
+    $this->assertTrue($profile->id());
+    $this->assertIdentical($profile->changed, REQUEST_TIME);
+
+    // List profiles for the user and verify that the new profile appears.
+    $list = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $this->user1->uid,
+    ));
+    $this->assertEqual($list, array(
+      $profile->id() => $profile,
+    ));
+
+    // Reload and update the profile.
+    $profile = entity_load('profile2', $profile->id());
+    $profile->changed -= 1000;
+    $original = clone $profile;
+    $status = $profile->save();
+    $this->assertIdentical($status, SAVED_UPDATED);
+    $this->assertIdentical($profile->id(), $original->id());
+    $this->assertEqual($profile->created, REQUEST_TIME);
+    $this->assertEqual($original->changed, REQUEST_TIME - 1000);
+    $this->assertEqual($profile->changed, REQUEST_TIME);
+
+    // Create a second profile.
+    $user1_profile1 = $profile;
+    $profile = entity_create('profile2', array(
+      'type' => $types[1]->id(),
+      'uid' => $this->user1->id(),
+    ));
+    $status = $profile->save();
+    $this->assertIdentical($status, SAVED_NEW);
+    $user1_profile2 = $profile;
+
+    // List profiles for the user and verify that both profiles appear.
+    $list = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $this->user1->uid,
+    ));
+    $this->assertEqual($list, array(
+      $user1_profile1->id() => $user1_profile1,
+      $user1_profile2->id() => $user1_profile2,
+    ));
+
+    // Delete the second profile and verify that the first still exists.
+    $user1_profile2->delete();
+    $this->assertFalse(entity_load('profile2', $user1_profile2->id()));
+    $list = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $this->user1->uid,
+    ));
+    $this->assertEqual($list, array(
+      $user1_profile1->id() => $user1_profile1,
+    ));
+
+    // Create a new second profile.
+    $user1_profile2 = entity_create('profile2', array(
+      'type' => $types[1]->id(),
+      'uid' => $this->user1->id(),
+    ));
+    $status = $user1_profile2->save();
+    $this->assertIdentical($status, SAVED_NEW);
+
+    // Create a profile for the second user.
+    $user2_profile1 = entity_create('profile2', array(
+      'type' => $types[0]->id(),
+      'uid' => $this->user2->id(),
+    ));
+    $status = $user2_profile1->save();
+    $this->assertIdentical($status, SAVED_NEW);
+
+    // Delete the first user and verify that all of its profiles are deleted.
+    $this->user1->delete();
+    $this->assertFalse(entity_load('user', $this->user1->id()));
+    $list = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $this->user1->uid,
+    ));
+    $this->assertEqual($list, array());
+
+    // List profiles for the second user and verify that they still exist.
+    $list = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $this->user2->uid,
+    ));
+    $this->assertEqual($list, array(
+      $user2_profile1->id() => $user2_profile1,
+    ));
+
+    // @todo Rename a profile type; verify that existing profiles are updated.
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileFieldAccessTest.php b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileFieldAccessTest.php
new file mode 100644
index 0000000..acc7638
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileFieldAccessTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Tests\ProfileFieldAccessTest.
+ */
+
+namespace Drupal\profile2\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests profile field access functionality.
+ */
+class ProfileFieldAccessTest extends WebTestBase {
+
+  public static $modules = array('profile2', 'text', 'field_ui');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Field access',
+      'description' => 'Tests profile field access functionality.',
+      'group' => 'Profile2',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->type = entity_create('profile2_type', array(
+      'id' => 'personal',
+      'label' => 'Personal data',
+      'weight' => 0,
+      'registration' => TRUE,
+    ));
+    $this->type->save();
+
+    $this->checkPermissions(array(), TRUE);
+    $this->admin_user = $this->drupalCreateUser(array(
+      'access user profiles',
+      'administer profile types',
+      'administer profile2 fields',
+      'administer profile2 display',
+      'bypass profile access',
+    ));
+    $user_permissions = array(
+      'access user profiles',
+      'edit own personal profile',
+      'view any personal profile',
+    );
+    $this->web_user = $this->drupalCreateUser($user_permissions);
+    $this->other_user = $this->drupalCreateUser($user_permissions);
+  }
+
+  /**
+   * Tests private profile field access.
+   */
+  function testPrivateField() {
+    $id = $this->type->id();
+
+    $this->drupalLogin($this->admin_user);
+
+    // Create a private profile field.
+    $edit = array(
+      'fields[_add_new_field][label]' => 'Secret',
+      'fields[_add_new_field][field_name]' => 'secret',
+      'fields[_add_new_field][type]' => 'text',
+      'fields[_add_new_field][widget_type]' => 'text_textfield',
+    );
+    $this->drupalPost("admin/people/profiles/manage/$id/fields", $edit, t('Save'));
+
+    $edit = array(
+      'field[settings][profile2_private]' => 1,
+    );
+    $this->drupalPost(NULL, $edit, t('Save field settings'));
+
+    $this->drupalPost(NULL, array(), t('Save settings'));
+
+    // Fill in a field value.
+    $this->drupalLogin($this->web_user);
+    $uid = $this->web_user->id();
+    $secret = $this->randomName();
+    $edit = array(
+      'field_secret[und][0][value]' => $secret,
+    );
+    $this->drupalPost("user/$uid/edit/$id", $edit, t('Save'));
+
+    // Verify that the private field value appears for the profile owner.
+    $this->drupalGet("user/$uid");
+    $this->assertText($secret);
+
+    // Verify that the private field value appears for the administrator.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet("user/$uid");
+    $this->assertText($secret);
+
+    // Verify that the private field value does not appear for other users.
+    $this->drupalLogin($this->other_user);
+    $this->drupalGet("user/$uid");
+    $this->assertNoText($secret);
+  }
+
+}
diff --git a/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileTypeCRUDTest.php b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileTypeCRUDTest.php
new file mode 100644
index 0000000..55c54db
--- /dev/null
+++ b/core/modules/profile2/lib/Drupal/profile2/Tests/ProfileTypeCRUDTest.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\profile2\Tests\ProfileTypeCRUDTest.
+ */
+
+namespace Drupal\profile2\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests basic CRUD functionality of profile types.
+ */
+class ProfileTypeCRUDTest extends WebTestBase {
+
+  public static $modules = array('profile2', 'field_ui', 'text');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Profile type CRUD operations',
+      'description' => 'Tests basic CRUD functionality of profile types.',
+      'group' => 'Profile2',
+    );
+  }
+
+  /**
+   * Tests CRUD operations for profile types through the UI.
+   */
+  function testCRUDUI() {
+    $this->drupalLogin($this->root_user);
+
+    // Create a new profile type.
+    $this->drupalGet('admin/people/profiles');
+    $this->clickLink(t('Add profile type'));
+    $this->assertUrl('admin/people/profiles/add');
+    $id = drupal_strtolower($this->randomName());
+    $label = $this->randomString();
+    $edit = array(
+      'id' => $id,
+      'label' => $label,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/people/profiles');
+    $this->assertRaw(t('%label profile type has been created.', array('%label' => $label)));
+    $this->assertLinkByHref("admin/people/profiles/manage/$id/edit");
+    $this->assertLinkByHref("admin/people/profiles/manage/$id/fields");
+    $this->assertLinkByHref("admin/people/profiles/manage/$id/display");
+    $this->assertLinkByHref("admin/people/profiles/manage/$id/delete");
+
+    // Edit the new profile type.
+    $this->drupalGet("admin/people/profiles/manage/$id/edit");
+    $this->assertRaw(t('Edit %label profile type', array('%label' => $label)));
+    $edit = array(
+      'registration' => 1,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/people/profiles');
+    $this->assertRaw(t('%label profile type has been updated.', array('%label' => $label)));
+
+    // Add a field to the profile type.
+    $this->drupalGet("admin/people/profiles/manage/$id/fields");
+    $field_name = drupal_strtolower($this->randomName());
+    $field_label = $this->randomString();
+    $edit = array(
+      'fields[_add_new_field][label]' => $field_name,
+      'fields[_add_new_field][field_name]' => $field_name,
+      'fields[_add_new_field][type]' => 'text',
+      'fields[_add_new_field][widget_type]' => 'text_textfield',
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->drupalPost(NULL, array(), t('Save field settings'));
+    $this->drupalPost(NULL, array(), t('Save settings'));
+    $this->assertUrl("admin/people/profiles/manage/$id/fields");
+
+    // Rename the profile type ID.
+    $this->drupalGet("admin/people/profiles/manage/$id/edit");
+    $new_id = drupal_strtolower($this->randomName());
+    $edit = array(
+      'id' => $new_id,
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/people/profiles');
+    $this->assertRaw(t('%label profile type has been updated.', array('%label' => $label)));
+    $this->assertLinkByHref("admin/people/profiles/manage/$new_id/edit");
+    $this->assertNoLinkByHref("admin/people/profiles/manage/$id/edit");
+    $id = $new_id;
+
+    // Verify that the field is still associated with it.
+    $this->drupalGet("admin/people/profiles/manage/$id/fields");
+    // @todo D8 core: This assertion fails for an unknown reason. Database
+    //   contains the right values, so field_attach_rename_bundle() works
+    //   correctly. The pre-existing field does not appear on the Manage
+    //   fields page of the renamed bundle. Not even flushing all caches
+    //   helps. Can be reproduced manually.
+    //$this->assertText(check_plain($field_label));
+  }
+
+}
diff --git a/core/modules/profile2/profile2.admin.inc b/core/modules/profile2/profile2.admin.inc
new file mode 100644
index 0000000..be4f156
--- /dev/null
+++ b/core/modules/profile2/profile2.admin.inc
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Profile type editing UI.
+ */
+
+use Drupal\profile2\Plugin\Core\Entity\ProfileType;
+
+/**
+ * Page callback; Lists available ProfileType objects.
+ */
+function profile2_type_list_page() {
+  $controller = entity_list_controller('profile2_type');
+  return $controller->render();
+}
+
+/**
+ * Page callback: Presents the form for creating a profile type.
+ *
+ * @return array
+ *   A form array as expected by drupal_render().
+ */
+function profile2_type_add() {
+  $type = entity_create('profile2_type', array());
+  return entity_get_form($type);
+}
+
+/**
+ * Page callback: Presents the form for editing a profile type.
+ *
+ * @param Drupal\profile2\Plugin\Core\Entity\ProfileType $type
+ *   The profile type to edit.
+ *
+ * @return array
+ *   A form array as expected by drupal_render().
+ */
+function profile2_type_edit(ProfileType $type) {
+  drupal_set_title(t('Edit %label profile type', array('%label' => $type->label())), PASS_THROUGH);
+  return entity_get_form($type);
+}
+
+/**
+ * Form constructor to delete a ProfileType object.
+ *
+ * @param Drupal\profile2\Plugin\Core\Entity\ProfileType $type
+ *   The ProfileType object to delete.
+ */
+function profile2_type_delete_form($form, &$form_state, ProfileType $type) {
+  $form_state['profile2_type'] = $type;
+
+  $form['id'] = array('#type' => 'value', '#value' => $type->id());
+  return confirm_form($form,
+    t('Are you sure you want to delete %label and all of its associated profiles?', array('%label' => $type->label())),
+    'admin/people/profiles',
+    NULL,
+    t('Delete')
+  );
+}
+
+/**
+ * Form submission handler for profile2_type_delete_form().
+ */
+function profile2_type_delete_form_submit($form, &$form_state) {
+  $form_state['profile2_type']->delete();
+  drupal_set_message(t('The profile type %label has been deleted.', array(
+    '%label' => $form_state['profile2_type']->label(),
+  )));
+  $form_state['redirect'] = 'admin/people/profiles';
+}
diff --git a/core/modules/profile2/profile2.api.php b/core/modules/profile2/profile2.api.php
new file mode 100644
index 0000000..d1cbaf6
--- /dev/null
+++ b/core/modules/profile2/profile2.api.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by Profile2 module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Control access to a user profile.
+ *
+ * Modules may implement this hook to control whether a user has access to
+ * perform a certain operation on a profile.
+ *
+ * @param string $op
+ *   The operation being performed. One of 'view', 'edit' (being the same as
+ *   'create' or 'update'), or 'delete'.
+ * @param Drupal\profile2\Plugin\Core\Entity\Profile $profile
+ *   A profile to check access for.
+ * @param Drupal\user\Plugin\Core\Entity\User $account
+ *   The user performing the operation; the currently logged in user by default.
+ *
+ * @return bool
+ *   Either a Boolean or NULL:
+ *   - FALSE to explicitly deny access. If a module denies access, no other
+ *     module is able to grant access and access is denied.
+ *   - TRUE to grant access. Access is only granted if at least one module
+ *     grants access and no module denies access.
+ *   - NULL or nothing to not affect the operation. If no module explicitly
+ *     grants access, access is denied.
+ */
+function hook_profile2_access($op, Drupal\profile2\Plugin\Core\Entity\Profile $profile, Drupal\user\Plugin\Core\Entity\User $account) {
+  // Explicitly deny access for a 'secret' profile type.
+  if ($profile->type == 'secret' && !user_access('custom permission')) {
+    return FALSE;
+  }
+  // For profiles other than the default profile grant access.
+  if ($profile->type != 'main' && user_access('custom permission')) {
+    return TRUE;
+  }
+  // In other cases do not alter access.
+}
+
+/**
+ * @}
+ */
diff --git a/core/modules/profile2/profile2.info b/core/modules/profile2/profile2.info
new file mode 100644
index 0000000..046076d
--- /dev/null
+++ b/core/modules/profile2/profile2.info
@@ -0,0 +1,6 @@
+name = Profile2
+description = Provides configurable user profiles.
+core = 8.x
+configure = admin/people/profiles
+dependencies[] = user
+dependencies[] = field
diff --git a/core/modules/profile2/profile2.install b/core/modules/profile2/profile2.install
new file mode 100644
index 0000000..9a7a3a6
--- /dev/null
+++ b/core/modules/profile2/profile2.install
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the profile module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function profile2_schema() {
+  $schema['profile'] = array(
+    'description' => 'Stores profile items.',
+    'fields' => array(
+      'pid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique profile item ID.',
+      ),
+      'uuid' => array(
+        'description' => 'Unique Key: Universally unique identifier for this entity.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+      ),
+      'type' => array(
+        'description' => 'The profile type ID of this profile.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+        'default' => NULL,
+        'description' => "The {users}.uid of the associated user.",
+      ),
+      'langcode' => array(
+        'description' => 'The {language}.langcode of this profile.',
+        'type' => 'varchar',
+        'length' => 12,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'label' => array(
+        'description' => 'A human-readable label for this profile.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the profile was created.',
+        'type' => 'int',
+        'not null' => FALSE,
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the profile was most recently saved.',
+        'type' => 'int',
+        'not null' => FALSE,
+      ),
+    ),
+    'indexes' => array(
+      'uid' => array('uid'),
+      'type' => array('type'),
+    ),
+    'foreign keys' => array(
+      'uid' => array(
+        'table' => 'users',
+        'columns' => array('uid' => 'uid'),
+      ),
+    ),
+    'primary key' => array('pid'),
+    'unique keys' => array(
+      'uuid' => array('uuid'),
+    ),
+  );
+  return $schema;
+}
+
diff --git a/core/modules/profile2/profile2.module b/core/modules/profile2/profile2.module
new file mode 100644
index 0000000..7d52e32
--- /dev/null
+++ b/core/modules/profile2/profile2.module
@@ -0,0 +1,552 @@
+<?php
+
+/**
+ * @file
+ * Support for configurable user profiles.
+ */
+
+use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
+use Drupal\profile2\Plugin\Core\Entity\Profile;
+use Drupal\profile2\Plugin\Core\Entity\ProfileType;
+use Drupal\user\Plugin\Core\Entity\User;
+
+/**
+ * Implements hook_entity_bundle_info().
+ */
+function profile2_entity_bundle_info() {
+  $info = array();
+  $config_names = config_get_storage_names_with_prefix('profile2.type.');
+  foreach ($config_names as $config_name) {
+    $config = config($config_name);
+    $id = $config->get('id');
+    $label = $config->get('label');
+    $info['profile2'][$id] = array(
+      'label' => $label,
+      'admin' => array(
+        'path' => 'admin/people/profiles/manage/%profile2_type',
+        'real path' => 'admin/people/profiles/manage/' . $id,
+        'bundle argument' => 4,
+        'access arguments' => array('administer profile types'),
+      ),
+    );
+  }
+  return $info;
+}
+
+/**
+ * Entity URI callback for profiles.
+ *
+ * @param Drupal\profile2\Plugin\Core\Entity\Profile $profile
+ *   A profile entity.
+ */
+function profile2_profile_uri(Profile $profile) {
+  $uri = entity_load('user', $profile->uid)->uri();
+  $uri['options']['fragment'] = 'profile-' . $profile->bundle();
+  return $uri;
+}
+
+/**
+ * Entity URI callback for profile types.
+ *
+ * @param Drupal\profile2\Plugin\Core\Entity\ProfileType $profile_type
+ *   A profile type entity.
+ */
+function profile2_profile_type_uri(ProfileType $profile_type) {
+  return array(
+    'path' => 'admin/people/profiles/manage/' . $profile_type->id(),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function profile2_menu() {
+  $items['admin/people/profiles'] = array(
+    'title' => 'Profile types',
+    'description' => 'Manage profile types, including fields.',
+    'page callback' => 'profile2_type_list_page',
+    'access arguments' => array('administer profile types'),
+    'type' => MENU_LOCAL_TASK,
+    // @todo User module: Apply custom/higher weights to Permissions and Roles.
+    'weight' => -1,
+    'file' => 'profile2.admin.inc',
+  );
+  $items['admin/people/profiles/add'] = array(
+    'title' => 'Add profile type',
+    'page callback' => 'profile2_type_add',
+    'access arguments' => array('administer profile types'),
+    'type' => MENU_LOCAL_ACTION,
+    'file' => 'profile2.admin.inc',
+  );
+  $items['admin/people/profiles/manage/%profile2_type'] = array(
+    'title' => 'Edit profile type',
+    'title callback' => 'entity_page_label',
+    'title arguments' => array(4),
+    'page callback' => 'profile2_type_edit',
+    'page arguments' => array(4),
+    'access arguments' => array('administer profile types'),
+    'file' => 'profile2.admin.inc',
+  );
+  $items['admin/people/profiles/manage/%profile2_type/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/people/profiles/manage/%profile2_type/delete'] = array(
+    'title' => 'Delete',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('profile2_type_delete_form', 4),
+    'access arguments' => array('administer profile types'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'profile2.admin.inc',
+  );
+
+  // @todo Move into User module.
+  $items['user/%user/edit/account'] = array(
+    'title' => 'Account',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['user/%user/edit/%profile2_menu_arg'] = array(
+    // @see http://drupal.org/node/1863502
+    'load arguments' => array(1, 'edit'),
+    'load arguments' => array('%map', 'edit'),
+    'title' => 'Edit profile',
+    'title callback' => 'entity_page_label',
+    'title arguments' => array(3),
+    'access callback' => 'profile2_access',
+    'access arguments' => array(3, 'edit'),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array(3),
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['user/%user/edit/%profile2_menu_arg/delete'] = array(
+    // @see http://drupal.org/node/1863502
+    'load arguments' => array(1, 'delete'),
+    'load arguments' => array('%map', 'delete'),
+    'title' => 'Delete profile',
+    'access callback' => 'profile2_access',
+    'access arguments' => array(3, 'delete'),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('profile2_delete_confirm_form', 3),
+    'type' => MENU_VISIBLE_IN_BREADCRUMB,
+    'file' => 'profile2.pages.inc',
+  );
+  return $items;
+}
+
+/**
+ * Menu argument callback; Loads a profile of a certain type for a given user.
+ *
+ * @param string $type_id
+ *   The profile type ID to load.
+ * @param Drupal\user\Plugin\Core\Entity\User $account
+ *   The user account for which to load the profile.
+ * @param string $op
+ *   (optional) The operation to perform. If 'edit' and if there is no profile
+ *   for the user account yet, a new profile entity will be created on the fly.
+ *
+ * @return Drupal\profile2\Plugin\Core\Entity\Profile|false
+ *   The profile of type $type_id of the user account, or FALSE.
+ */
+// @see http://drupal.org/node/1863502
+//function profile2_menu_arg_load($type_id, User $account, $op = '') {
+function profile2_menu_arg_load($type_id, $map, $op = '') {
+  if (is_array($map)) {
+    if (!isset($map[1]) || !($map[1] instanceof User)) {
+      return FALSE;
+    }
+    $account = $map[1];
+  }
+  else {
+    $account = $map;
+  }
+  if ($type_id === '' || !$account->id()) {
+    return FALSE;
+  }
+  $profiles = entity_load_multiple_by_properties('profile2', array(
+    'uid' => $account->id(),
+    'type' => $type_id,
+  ));
+  $profile = reset($profiles);
+  if ($op == 'edit' && !$profile) {
+    $profile = entity_create('profile2', array(
+      'type' => $type_id,
+      'uid' => $account->id(),
+    ));
+  }
+  return $profile;
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function profile2_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+  if ($root_path === 'user/%/edit' || $root_path === 'user/%/edit/%') {
+    $tabs = &$data['tabs'][1]['output'];
+    // Determine the currently selected tab, if any.
+    $selected_index = -1;
+    $selected_id = '';
+    foreach ($tabs as $index => &$tab) {
+      if (isset($tab['#link']['path']) && $tab['#link']['path'] == 'user/%/edit/%') {
+        $selected_index = $index;
+        $selected_id = $router_item['original_map'][3];
+      }
+    }
+    // Expand the dynamic %profile_menu argument into a tab for each type.
+    $types = entity_load_multiple('profile2_type');
+    foreach ($types as $type) {
+      // If the current page is the active tab registered in hook_menu(), then
+      // the menu router item with the dynamic argument will be exposed already.
+      // We must not duplicate that tab, but in order to ensure that all of our
+      // tabs appear in a consistent order when switching between tabs, we need
+      // to re-inject it.
+      if ($type->id() === $selected_id) {
+        $tabs[$selected_index]['#link']['title'] = $type->label();
+        $tabs[] = $tabs[$selected_index];
+        unset($tabs[$selected_index]);
+        continue;
+      }
+      $tabs[] = array(
+        '#theme' => 'menu_local_task',
+        '#link' => array(
+          'title' => $type->label(),
+          'href' => 'user/' . $router_item['original_map'][1] . '/edit/' . $type->id(),
+          'localized_options' => array('html' => FALSE),
+        ),
+      );
+    }
+    if ($types) {
+      $data['tabs'][1]['count']++;
+    }
+  }
+}
+
+/**
+ * Menu argument loader; Load a profile type by string.
+ *
+ * @param string $id
+ *   The machine-readable name of a profile type to load.
+ *
+ * @return Drupal\profile2\Plugin\Core\Entity\ProfileType|false
+ *   A profile type array or FALSE if $type does not exist.
+ */
+function profile2_type_load($id) {
+  return entity_load('profile2_type', $id);
+}
+
+/**
+ * Implements hook_permission().
+ */
+function profile2_permission() {
+  $permissions = array(
+    'administer profile types' => array(
+      'title' => t('Administer profile types'),
+      'restrict access' => TRUE,
+    ),
+    'bypass profile access' => array(
+      'title' => t('Bypass profile access'),
+      'description' => t('View and edit all user profiles, including private field values.'),
+      'restrict access' => TRUE,
+    ),
+  );
+  // Generate per profile type permissions.
+  foreach (entity_load_multiple('profile2_type') as $type) {
+    $type_id = $type->id();
+    $permissions += array(
+      "view own $type_id profile" => array(
+        'title' => t('%type: View own profile', array('%type' => $type->label())),
+      ),
+      "view any $type_id profile" => array(
+        'title' => t('%type: View any profile', array('%type' => $type->label())),
+      ),
+      "edit own $type_id profile" => array(
+        'title' => t('%type: Edit own profile', array('%type' => $type->label())),
+      ),
+      "edit any $type_id profile" => array(
+        'title' => t('%type: Edit any profile', array('%type' => $type->label())),
+      ),
+      "delete own $type_id profile" => array(
+        'title' => t('%type: Delete own profile', array('%type' => $type->label())),
+      ),
+      "delete any $type_id profile" => array(
+        'title' => t('%type: Delete any profile', array('%type' => $type->label())),
+      ),
+    );
+  }
+  return $permissions;
+}
+
+/**
+ * Implements hook_user_predelete().
+ */
+function profile2_user_predelete(User $account) {
+  if ($profiles = entity_load_multiple_by_properties('profile2', array('uid' => $account->id()))) {
+    entity_get_controller('profile2')->delete($profiles);
+  }
+}
+
+/**
+ * Implements hook_user_view().
+ */
+function profile2_user_view(User $account, EntityDisplay $display, $view_mode, $langcode) {
+  // Only attach profiles for the full account view.
+  if ($view_mode != 'full') {
+    return;
+  }
+  foreach (entity_load_multiple('profile2_type') as $id => $type) {
+    $profiles = entity_load_multiple_by_properties('profile2', array(
+      'uid' => $account->id(),
+      'type' => $id,
+    ));
+    foreach ($profiles as $profile) {
+      if ($profile->access('view')) {
+        $build = entity_render_controller('profile2')->view($profile, 'account');
+        $build += array(
+          '#prefix' => '<a id="profile-' . $profile->bundle() . '"></a>',
+        );
+        $account->content['profile'][$id][$profile->id()] = $build;
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for user_register_form().
+ */
+function profile2_form_user_register_form_alter(&$form, &$form_state) {
+  foreach (entity_load_multiple('profile2_type') as $id => $type) {
+    if ($type->get('registration')) {
+      if (empty($form_state['profiles'][$id])) {
+        $form_state['profiles'][$id] = entity_create('profile2', array(
+          'type' => $id,
+        ));
+      }
+      profile2_attach_form($form, $form_state);
+
+      // Wrap each profile form in a fieldset.
+      $form['profile'][$id] += array(
+        '#type' => 'fieldset',
+        '#title' => check_plain($type->label()),
+      );
+    }
+  }
+  if (!empty($form_state['profiles'])) {
+    $form['#validate'][] = 'profile2_user_form_validate';
+    $form['actions']['submit']['#submit'][] = 'profile2_user_form_submit';
+  }
+}
+
+/**
+ * Attaches the profile forms of the profiles set in $form_state['profiles'].
+ *
+ * Modules may alter the profile2 entity form regardless to which form it is
+ * attached by making use of hook_form_profile2_form_alter().
+ *
+ * @param $form
+ *   The form to which to attach the profile2 form. For each profile the form
+ *   is added to @code $form['profile_' . $profile->type] @endcode. This helper
+ *   also adds in a validation and a submit handler caring for the attached
+ *   profile forms.
+ *
+ * @see profile2_user_form_validate()
+ * @see profile2_user_form_submit()
+ */
+function profile2_attach_form(&$form, &$form_state) {
+  foreach ($form_state['profiles'] as $bundle => $profile) {
+    $form['profile'][$bundle]['#tree'] = TRUE;
+    $form['profile'][$bundle]['#parents'] = array('profile', $bundle);
+
+    field_attach_form($profile, $form['profile'][$bundle], $form_state);
+
+    if (count(field_info_instances('profile2', $bundle)) == 0) {
+      $form['profile'][$bundle]['message'] = array(
+        '#access' => user_access('administer profile types'),
+        '#markup' => t('No fields have been associated with this profile type. Go to the <a href="!url">Profile types</a> page to add some fields.', array('!url' => url('admin/people/profiles'))),
+      );
+    }
+
+    // Provide a central place for modules to alter the profile forms, but
+    // skip that in case the caller cares about invoking the hooks.
+    if (!isset($form_state['profile2_skip_hook'])) {
+      $hooks = array();
+      $hooks[] = 'form_' . $bundle . '_profile2_form';
+      $hooks[] = 'form_profile2_form';
+      drupal_alter($hooks, $form['profile'][$bundle], $form_state);
+    }
+  }
+}
+
+/**
+ * Validation handler for the profile form.
+ *
+ * @see profile2_attach_form()
+ */
+function profile2_user_form_validate(&$form, &$form_state) {
+  foreach ($form_state['profiles'] as $bundle => $profile) {
+    if (isset($form_state['values']['profile'][$bundle])) {
+      // @see entity_form_field_validate()
+      $pseudo_entity = entity_create('profile2', array_merge($form_state['values']['profile'][$bundle], array(
+        'type' => $bundle,
+      )));
+      field_attach_form_validate($pseudo_entity, $form['profile'][$bundle], $form_state);
+    }
+  }
+}
+
+/**
+ * Submit handler that builds and saves all profiles in the form.
+ *
+ * @see profile2_attach_form()
+ */
+function profile2_user_form_submit(&$form, &$form_state) {
+  profile2_form_submit_build_profile($form, $form_state);
+
+  foreach ($form_state['profiles'] as $bundle => $profile) {
+    // During registration set the uid field of the newly created user.
+    if (empty($profile->uid) && isset($form_state['user']->uid)) {
+      $profile->uid = $form_state['user']->uid;
+    }
+    $profile->save();
+  }
+}
+
+/**
+ * Submit builder. Extracts the form values and updates the profile entities.
+ *
+ * @see profile2_attach_form()
+ */
+function profile2_form_submit_build_profile(&$form, &$form_state) {
+  foreach ($form_state['profiles'] as $bundle => $profile) {
+    // @see entity_form_submit_build_entity()
+    if (isset($form['profile'][$bundle]['#entity_builders'])) {
+      foreach ($form['profile'][$bundle]['#entity_builders'] as $function) {
+        $function('profile2', $profile, $form['profile'][$bundle], $form_state);
+      }
+    }
+    field_attach_extract_form_values($profile, $form['profile'][$bundle], $form_state);
+  }
+}
+
+/**
+ * Helper function for checking profile access.
+ */
+function profile2_access(Profile $profile, $op, User $account = NULL) {
+  if ($op == 'edit') {
+    $op = ($profile->isNew() ? 'create' : 'update');
+  }
+  return $profile->access($op, $account);
+}
+
+/**
+ * Implements hook_profile2_access().
+ */
+function profile2_profile2_access($op, Profile $profile, User $account) {
+  if (user_access("$op any $profile->type profile", $account)) {
+    return TRUE;
+  }
+  if (isset($profile->uid) && $profile->uid == $account->uid && user_access("$op own $profile->type profile", $account)) {
+    return TRUE;
+  }
+  // Do not explicitly deny access so others may still grant access.
+}
+
+/**
+ * Implements hook_theme().
+ */
+function profile2_theme() {
+  return array(
+    'profile2' => array(
+      'render element' => 'elements',
+      'template' => 'profile2',
+    ),
+  );
+}
+
+/**
+ * Processes variables for profile2.tpl.php.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An array of elements to display in view mode.
+ *   - profile: The profile object.
+ *   - view_mode: View mode; e.g., 'full', 'account'...
+ *
+ * @see profile2.tpl.php
+ */
+function template_preprocess_profile2(&$variables) {
+  $variables['view_mode'] = $variables['elements']['#view_mode'];
+  $variables['profile'] = $variables['elements']['#profile2'];
+  $profile = $variables['profile'];
+
+  $variables['name'] = theme('username', array(
+    'account' => $profile,
+    'link_attributes' => array('rel' => 'author'),
+  ));
+
+  $uri = $profile->uri();
+  $variables['url'] = url($uri['path'], $uri['options']);
+  $variables['title'] = check_plain($profile->label());
+  $variables['page'] = $variables['view_mode'] == 'full';
+  $variables['type'] = $profile->bundle();
+
+  // Helpful $content variable for templates.
+  $variables += array('content' => array());
+  foreach (element_children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+
+  // Make the field variables available with the appropriate language.
+  field_attach_preprocess($profile, $variables['content'], $variables);
+
+  // Add article ARIA role.
+  $variables['attributes']['role'] = 'article';
+
+  // Gather template classes.
+  $variables['attributes']['class'][] = 'profile';
+  $variables['attributes']['class'][] = drupal_html_class('profile-' . $profile->bundle());
+  if ($variables['view_mode']) {
+    $variables['attributes']['class'][] = drupal_html_class('view-mode-' . $variables['view_mode']);
+  }
+  if (isset($variables['preview'])) {
+    $variables['attributes']['class'][] = 'preview';
+  }
+
+  $variables['theme_hook_suggestions'][] = 'profile2__' . $profile->bundle();
+  $variables['theme_hook_suggestions'][] = 'profile2__' . $profile->uid;
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Adds a checkbox for controlling field view access to fields added to
+ * profiles.
+ */
+function profile2_form_field_ui_field_settings_form_alter(&$form, &$form_state) {
+  // Only add the field setting if this field is attached to a profile entity
+  // bundle.
+  if (isset($form['#field']['bundles']['profile2'])) {
+    $form['field']['settings']['profile2_private'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Private field'),
+      '#default_value' => !empty($form['#field']['settings']['profile2_private']),
+      // Only expose the setting when editing the settings for a field attached
+      // to a profile entity bundle. For other entity types, we just ensure that
+      // the existing value is retained.
+      '#access' => $form['#entity_type'] == 'profile2',
+      '#description' => t('Only show the field content to the profile owner and administrators.'),
+    );
+  }
+}
+
+/**
+ * Implements hook_field_access().
+ */
+function profile2_field_access($op, $field, $entity_type, $profile, $account) {
+  if ($entity_type == 'profile2' && $op == 'view' && !empty($field['settings']['profile2_private']) && !user_access('bypass profile access', $account)) {
+    // Deny view access, if someone else views a private profile field.
+    if ($account->uid != $profile->uid) {
+      return FALSE;
+    }
+  }
+}
diff --git a/core/modules/profile2/profile2.pages.inc b/core/modules/profile2/profile2.pages.inc
new file mode 100644
index 0000000..1df2ba6
--- /dev/null
+++ b/core/modules/profile2/profile2.pages.inc
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Page and form callbacks for profiles.
+ */
+
+use Drupal\profile2\Plugin\Core\Entity\Profile;
+
+/**
+ * Form constructor to confirm deletion of a profile.
+ *
+ * @param Drupal\profile2\Plugin\Core\Entity\Profile $profile
+ *   The profile to delete.
+ *
+ * @return array
+ *   A form render array suitable for drupal_render().
+ *
+ * @see profile2_delete_confirm_form_submit()
+ */
+function profile2_delete_confirm_form(array $form, array &$form_state, Profile $profile) {
+  $form_state['profile'] = $profile;
+  $form_state['account'] = entity_load('user', $profile->uid);
+  $form['pid'] = array('#type' => 'value', '#value' => $profile->id());
+
+  if ($GLOBALS['user']->uid == $profile->uid) {
+    $confirm_question = t('Are you sure you want to delete your %label profile?', array(
+      '%label' => $profile->label(),
+    ));
+  }
+  else {
+    $confirm_question = t("Are you sure you want to delete %name's %label profile?", array(
+      '%name' => user_format_name($form_state['account']),
+      '%label' => $profile->label(),
+    ));
+  }
+  return confirm_form($form, $confirm_question, $profile->uri(), NULL, t('Delete'));
+}
+
+/**
+ * Form submission handler for profile2_delete_confirm_form().
+ */
+function profile2_delete_confirm_form_submit(array $form, array &$form_state) {
+  $form_state['profile']->delete();
+
+  if ($GLOBALS['user']->uid == $form_state['profile']->uid) {
+    $message = t('Your %label profile has been deleted.', array(
+      '%label' => $form_state['profile']->label(),
+    ));
+  }
+  else {
+    $message = t("%name's %label profile has been deleted.", array(
+      '%name' => user_format_name($form_state['account']),
+      '%label' => $form_state['profile']->label(),
+    ));
+  }
+  drupal_set_message($message);
+
+  // Redirect to the user page.
+  $uri = $form_state['account']->uri();
+  $form_state['redirect'] = array($uri['path'], $uri['options']);
+}
diff --git a/core/modules/profile2/templates/profile2.tpl.php b/core/modules/profile2/templates/profile2.tpl.php
new file mode 100644
index 0000000..52c1d25
--- /dev/null
+++ b/core/modules/profile2/templates/profile2.tpl.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation to display a profile.
+ *
+ * Available variables:
+ * - $title: The (sanitized) profile type label.
+ * - $content: An array of profile items. Use render($content) to print them
+ *   all, or print a subset such as render($content['field_example']). Use
+ *   hide($content['field_example']) to temporarily suppress the printing of a
+ *   given element.
+ * - $url: Direct URL of the current profile.
+ * - $page: TRUE if this is the main view page $url points too.
+ * - $attributes: An instance of Attributes class that can be manipulated as an
+ *    array and printed as a string.
+ * - $title_prefix (array): An array containing additional output populated by
+ *   modules, intended to be displayed in front of the main title tag that
+ *   appears in the template.
+ * - $title_suffix (array): An array containing additional output populated by
+ *   modules, intended to be displayed after the main title tag that appears in
+ *   the template.
+ *
+ * Other variables:
+ * - $profile: Full profile entity. Contains data that may not be safe.
+ *
+ * @see template_preprocess()
+ * @see template_process()
+ *
+ * @ingroup themeable
+ */
+?>
+<article class="<?php print $attributes['class']; ?> clearfix"<?php print $attributes; ?>>
+
+  <?php print render($title_prefix); ?>
+  <?php if (!$page): ?>
+    <h2<?php print $title_attributes; ?>><a href="<?php print $url; ?>" rel="bookmark"><?php print $title; ?></a></h2>
+  <?php endif; ?>
+  <?php print render($title_suffix); ?>
+
+  <div class="content"<?php print $content_attributes; ?>>
+    <?php
+      print render($content);
+    ?>
+  </div>
+
+</article>
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php
index 93685aa..4eda92f 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\views\Tests;
 
+use DOMDocument;
 use Drupal\simpletest\WebTestBase;
 
 /**
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 8dc5f5a..b4398f0 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -234,6 +234,7 @@ function views_plugin_list() {
  * node portion of the theme registry.
  */
 function views_preprocess_node(&$vars) {
+  module_load_include('inc', 'node', 'node.views');
   // The 'view' attribute of the node is added in views_preprocess_node()
   if (!empty($vars['node']->view) && $vars['node']->view->storage->id()) {
     $vars['view'] = $vars['node']->view;
@@ -250,7 +251,7 @@ function views_preprocess_node(&$vars) {
   }
 
   // Allow to alter comments and links based on the settings in the row plugin.
-  if (!empty($vars['view']->style_plugin->row_plugin) && get_class($vars['view']->style_plugin->row_plugin) == 'views_plugin_row_node_view') {
+  if (!empty($vars['view']->style_plugin->row_plugin) && $vars['view']->style_plugin->row_plugin->getPluginId() == 'node') {
     node_row_node_view_preprocess_node($vars);
   }
 }
