diff --git a/database/updates.inc b/database/updates.inc
index 6bc5a5741666b859f5180458c5faa8e6196fc260..799e57f4df48c68a9323dcb8fe03cd2c61127b19 100644
--- a/database/updates.inc
+++ b/database/updates.inc
@@ -789,17 +789,42 @@ function update_79() {
 
 function update_80() {
 
+  // Add a 'created' field to the users table:
+  $ret[] = update_sql('ALTER TABLE {users} ADD created INT(11) NOT NULL');
+  $ret[] = update_sql('ALTER TABLE {users} CHANGE timestamp changed INT(11) NOT NULL');
 
+  // Add some indices to speed up the update process:
+  $ret[] = update_sql('ALTER TABLE {comments} ADD index (timestamp)');
+  $ret[] = update_sql('ALTER TABLE {node} ADD index (created)');
 
-  if ($GLOBALS["db_type"] == "mysql") {
-    // Add a 'created' field to the users table:
-    $ret[] = update_sql('ALTER TABLE {users} ADD created INT(11) NOT NULL');
-    $ret[] = update_sql('ALTER TABLE {users} CHANGE timestamp changed INT(11) NOT NULL');
+  // Assign everyone a created timestamp to begin with:
+  $ret[] = update_sql("UPDATE {users} SET created = changed WHERE created = ''");
 
+  // Print a status message:"
+  print '<p>Note: this might take a while ...</p>';
 
+  // Try updating the user records using the comment table:
+  $result = db_query('SELECT DISTINCT(u.uid) FROM {comments} c LEFT JOIN {users} u ON c.uid = u.uid WHERE c.timestamp < u.created');
+  while ($account = db_fetch_object($result)) {
+    // Retrieve the proper timestamp:
+    $timestamp = db_result(db_query('SELECT MIN(timestamp) FROM {comments} WHERE uid = %d', $account->uid));
+
+    // Update this user record as well as older records with an older timestamp:
+    db_query('UPDATE {users} SET created = %d WHERE created > %d AND uid <= %d', $timestamp, $timestamp, $account->uid);
+  }
 
-    // Add profile module related tables:
-    $ret[] = update_sql("CREATE TABLE {profile_fields} (
+  // Try updating the user records using the node table:
+  $result = db_query('SELECT DISTINCT(u.uid) FROM {node} n LEFT JOIN {users} u ON n.uid = u.uid WHERE n.created < u.created');
+  while ($account = db_fetch_object($result)) {
+    // Retrieve the proper timestamp:
+    $timestamp = db_result(db_query('SELECT MIN(created) FROM {node} WHERE uid = %d', $account->uid));
+
+    // Update this user record as well as older records with an older timestamp:
+    db_query('UPDATE {users} SET created = %d WHERE created > %d AND uid <= %d', $timestamp, $timestamp, $account->uid);
+  }
+
+  // Add profile module related tables:
+  $ret[] = update_sql("CREATE TABLE {profile_fields} (
     fid int(10) NOT NULL auto_increment,
     title varchar(255) default NULL,
     name varchar(128) default NULL,
@@ -814,149 +839,80 @@ function update_80() {
     PRIMARY KEY (fid)
    );");
 
-    $ret[] = update_sql("CREATE TABLE {profile_values} (
+  $ret[] = update_sql("CREATE TABLE {profile_values} (
     fid int(11) unsigned default '0',
     uid int(11) unsigned default '0',
     value text,
     KEY uid (uid),
     KEY fid (fid)
    );");
-    // Add some indices to speed up the update process:
-    $ret[] = update_sql('ALTER TABLE {comments} ADD index (timestamp)');
-    $ret[] = update_sql('ALTER TABLE {node} ADD index (created)');
-    // Assign everyone a created timestamp to begin with:
-    $ret[] = update_sql("UPDATE {users} SET created = changed WHERE created = ''");
-
-   }
-  else {// pgsql
-
-    $ret[] = update_sql('ALTER TABLE {users} RENAME timestamp TO changed');
-    $ret[] = update_sql('ALTER TABLE {users} ADD created integer');
-    $ret[] = update_sql('UPDATE {users} SET created = 0');
-    $ret[] = update_sql('ALTER TABLE {users} ALTER COLUMN created SET NOT NULL');
-    $ret[] = update_sql('ALTER TABLE {users} ALTER COLUMN created SET DEFAULT 0');
-    // Add profile module related tables:
-    $ret[] = update_sql("CREATE TABLE {profile_fields} (
-    fid serial,
-    title varchar(255) default NULL,
-    name varchar(128) default NULL,
-    explanation TEXT default NULL,
-    category varchar(255) default NULL,
-    type varchar(128) default NULL,
-    weight smallint DEFAULT '0' NOT NULL,
-    overview smallint DEFAULT '0' NOT NULL,
-    options text,
-    UNIQUE (name),
-    PRIMARY KEY (fid)
-  );");
-    $ret[] = update_sql("CREATE INDEX profile_fields_category ON {profile_fields} (category);");
-    $ret[] = update_sql("CREATE TABLE {profile_values} (
-    fid integer default '0',
-    uid integer default '0',
-    value text
-  );");
-    $ret[] = update_sql("CREATE INDEX profile_values_uid ON profile_values (uid);");
-    $ret[] = update_sql("CREATE INDEX profile_values_fid ON profile_values (fid);");
-
-  // Add some indices to speed up the update process:
-    $ret[] = update_sql('CREATE INDEX comments_timestamp ON {comments} (timestamp)');
-    $ret[] = update_sql('CREATE INDEX node_created on {node} (created)');
-    // Assign everyone a created timestamp to begin with:
-    $ret[] = update_sql("UPDATE {users} SET created = changed WHERE created = 0");
 
-  }
-
-
-  // Print a status message:"
-  print '<p>Note: this might take a while ...</p>';
-
-  // Try updating the user records using the comment table:
-  $result = db_query('SELECT DISTINCT(u.uid) FROM {comments} c LEFT JOIN {users} u ON c.uid = u.uid WHERE c.timestamp < u.created');
+  // Migrate the old profile data to the new scheme:
+  $fields = array(
+    array("Name", "realname", "textfield", NULL, 0),
+    array("Address", "address", "textfield", NULL, 0),
+    array("City", "city", "textfield", NULL, 0),
+    array("State, province or region", "state", "textfield", NULL, 0),
+    array("Zip or postal code", "zip", "textfield", NULL, 0),
+    array("Country", "country", "textfield", NULL, 1),
+    array("Gender", "gender", "selection", "male\nfemale", 1),
+    array("Job title", "job", "textfield", NULL, 0),
+    array("ICQ messenger ID", "icq", "textfield", NULL, 0),
+    array("MSN messenger ID", "msn", "textfield", NULL, 0),
+    array("Yahoo messenger ID", "yahoo", "textfield", NULL, 0),
+    array("AIM messenger ID", "aim", "textfield", NULL, 0),
+    array("URL of homepage", "homepage", "url", NULL, 1),
+    array("Biography", "biography", "textarea", NULL, 0),
+    array("Interests", "interests", "textarea", NULL, 0),
+    array("Public key", "publickey", "textarea", NULL, 0),
+    array("Birthday", "birthday", "date", NULL, 0)
+  );
+
+  // Remove existing data (debug mode):
+  db_query('DELETE FROM {profile_fields}');
+  db_query('DELETE FROM {profile_values}');
+
+  foreach ($fields as $field) {
+    db_query("INSERT INTO {profile_fields} (title, name, type, category, options, overview) VALUES ('%s', '%s', '%s', 'Personal information', '%s', %d)", $field[0], $field[1], $field[2], $field[3], $field[4]);
+  }
+  db_query("ALTER TABLE {users} ADD picture varchar(255) NOT NULL DEFAULT ''");
+
+  $result = db_query("SELECT uid FROM {users}");
   while ($account = db_fetch_object($result)) {
-    // Retrieve the proper timestamp:
-    $timestamp = db_result(db_query('SELECT MIN(timestamp) FROM {comments} WHERE uid = %d', $account->uid));
-    // Try updating the user records using the node table:
-    $result = db_query('SELECT DISTINCT(u.uid) FROM {node} n LEFT JOIN {users} u ON n.uid = u.uid WHERE n.created < u.created');
-    while ($account = db_fetch_object($result)) {
-      // Retrieve the proper timestamp:
-      $timestamp = db_result(db_query('SELECT MIN(created) FROM {node} WHERE uid = %d', $account->uid));
-
-      // Update this user record as well as older records with an older timestamp:
-      db_query('UPDATE {users} SET created = %d WHERE created > %d AND uid <= %d', $timestamp, $timestamp, $account->uid);
-    }
-
-      // Update this user record as well as older records with an older timestamp:
-      db_query('UPDATE {users} SET created = %d WHERE created > %d AND uid <= %d', $timestamp, $timestamp, $account->uid);
-    }
-
-
-   // Migrate the old profile data to the new scheme:
-    $fields = array(
-        array("Name", "realname", "textfield", NULL, 0),
-        array("Address", "address", "textfield", NULL, 0),
-        array("City", "city", "textfield", NULL, 0),
-        array("State, province or region", "state", "textfield", NULL, 0),
-        array("Zip or postal code", "zip", "textfield", NULL, 0),
-        array("Country", "country", "textfield", NULL, 1),
-        array("Gender", "gender", "selection", "male\nfemale", 1),
-        array("Job title", "job", "textfield", NULL, 0),
-        array("ICQ messenger ID", "icq", "textfield", NULL, 0),
-        array("MSN messenger ID", "msn", "textfield", NULL, 0),
-        array("Yahoo messenger ID", "yahoo", "textfield", NULL, 0),
-        array("AIM messenger ID", "aim", "textfield", NULL, 0),
-        array("URL of homepage", "homepage", "url", NULL, 1),
-        array("Biography", "biography", "textarea", NULL, 0),
-        array("Interests", "interests", "textarea", NULL, 0),
-        array("Public key", "publickey", "textarea", NULL, 0)
-        );
-
-    // Remove existing data (debug mode):
-    db_query('DELETE FROM {profile_fields}');
-    db_query('DELETE FROM {profile_values}');
+    // Load the user record:
+    $account = user_load(array('uid' => $account->uid));
+    $edit = array();
 
+    // Modify the user record:
     foreach ($fields as $field) {
-      db_query("INSERT INTO {profile_fields} (title, name, type, category, options, overview) VALUES ('%s', '%s', '%s', 'Personal information', '%s', %d)", $field[0], $field[1], $field[2], $field[3], $field[4]);
+      $old = "profile_". $field[1];
+      $new = $field[1];
+      if ($account->$old) {
+        $edit[$new] = $account->$old;
+      }
+      unset($account->$old);
     }
-    if ($GLOBALS["db_type"] == "mysql") {
 
-      db_query("ALTER TABLE {users} ADD picture varchar(255) NOT NULL DEFAULT ''");
+    // Birthday format change:
+    if ($edit['birthday']) {
+      $edit['birthday'] = array('day' => $edit['birthday'], 'month' => $account->profile_birthmonth, 'year' => $account->profile_birthyear);
+      unset($account->profile_birthmonth);
+      unset($account->profile_birthyear);
     }
-    else {
-      db_query("ALTER TABLE {users} ADD picture varchar(255)");
-      db_query("UPDATE {users} SET picture = ''");
-      db_query("ALTER TABLE {users} ALTER COLUMN picture SET NOT NULL");
-      db_query("ALTER TABLE {users} ALTER COLUMN picture SET DEFAULT ''");
-    }
-
-    $result = db_query("SELECT uid FROM {users} WHERE uid > 0");
-    while ($account = db_fetch_object($result)) {
-      // Load the user record:
-      $account = user_load(array('uid' => $account->uid));
-      $edit = array();
-
-      // Modify the user record:
-      foreach ($fields as $field) {
-        $old = "profile_". $field[1];
-        $new = $field[1];
-        if ($account->$old) {
-          $edit[$new] = $account->$old;
-        }
-        unset($account->$old);
-      }
-
-      // Gender specific changes:
-      if ($edit['gender'] == 'f') $edit['gender'] = 'female';
-      if ($edit['gender'] == 'm') $edit['gender'] = 'male';
 
-      // Avatar specific changes:
-      if ($account->profile_avatar) {
-        $edit['picture'] = $account->profile_avatar;
-      }
-      unset($account->profile_avatar);
+    // Gender specific changes:
+    if ($edit['gender'] == 'f') $edit['gender'] = 'female';
+    if ($edit['gender'] == 'm') $edit['gender'] = 'male';
 
-      // Save the update record:
-      user_save($account, $edit, 'Personal information');
+    // Avatar specific changes:
+    if ($account->profile_avatar) {
+      $edit['picture'] = $account->profile_avatar;
     }
+    unset($account->profile_avatar);
+
+    // Save the update record:
+    user_save($account, $edit);
+  }
 
   return $ret;
 }
diff --git a/modules/profile.module b/modules/profile.module
index 1ad8288c7e477b8e66a5d503e4ea19cd61900e0b..802daca6cbd1af5ab1026105d23e408eff83b1a1 100644
--- a/modules/profile.module
+++ b/modules/profile.module
@@ -1,8 +1,6 @@
 <?php
 // $Id$
 
-// TODO: add a 'date' field so we can migrate the birthday information.
-
 /**
  * Flags to define the visibility of a profile field.
  */
@@ -56,12 +54,18 @@ function profile_menu() {
  */
 function profile_browse() {
 
-  $name = strip_tags(arg(1));
-  $value = strip_tags(arg(2));
+  $name = arg(1);
+  $value = arg(2);
 
-  $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page FROM {profile_fields} WHERE name = '%s'", $name));
+  $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page, visibility FROM {profile_fields} WHERE name = '%s'", $name));
 
   if ($name && $field->fid) {
+    // Do not allow browsing of private fields by non-admins
+    if (!user_access('administer users') && $field->visibility == PROFILE_PRIVATE) {
+       drupal_access_denied();
+       return;
+    }
+
     // Compile a list of fields to show
     $fields = array();
     $result = db_query('SELECT name, title, type FROM {profile_fields} WHERE fid != %d AND visibility = %d', $field->fid, PROFILE_PUBLIC_LISTINGS);
@@ -80,6 +84,9 @@ function profile_browse() {
       case 'list':
         $query = "v.value LIKE '%%". check_query($value) ."%%'";
         break;
+      default:
+        drupal_not_found();
+        return;
     }
 
     // Extract the affected users:
@@ -127,17 +134,20 @@ function profile_browse() {
 }
 
 function profile_load_profile(&$user) {
-  $result = db_query('SELECT f.name, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
+  $result = db_query('SELECT f.name, f.type, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
   while ($field = db_fetch_object($result)) {
     if (empty($user->{$field->name})) {
-      $user->{$field->name} = $field->value;
+      $user->{$field->name} = _profile_field_serialize($field->type) ? unserialize($field->value) : $field->value;
     }
   }
 }
 
 function profile_save_profile(&$edit, &$user, $category) {
-  $result = db_query("SELECT fid, name FROM {profile_fields} WHERE LOWER(category) = '%s'", strtolower($category));
+  $result = db_query("SELECT fid, name, type FROM {profile_fields} WHERE LOWER(category) = '%s'", strtolower($category));
   while ($field = db_fetch_object($result)) {
+    if (_profile_field_serialize($field->type)) {
+       $edit[$field->name] = serialize($edit[$field->name]);
+    }
     db_query("DELETE FROM {profile_values} WHERE fid = %d AND uid = %d", $field->fid, $user->uid);
     db_query("INSERT INTO {profile_values} (fid, uid, value) VALUES (%d, %d, '%s')", $field->fid, $user->uid, $edit[$field->name]);
     unset($edit[$field->name], $user->{$field->name});
@@ -145,23 +155,38 @@ function profile_save_profile(&$edit, &$user, $category) {
 }
 
 function profile_view_field($user, $field) {
+  // Only allow browsing of private fields for admins
+  $browse = user_access('administer users') || $field->visibility != PROFILE_PRIVATE;
+
   if ($value = $user->{$field->name}) {
     switch ($field->type) {
       case 'textfield':
+        return drupal_specialchars($value);
       case 'textarea':
         return check_output($value);
       case 'selection':
-        return l($value, "profile/$field->name/". drupal_specialchars($value, ENT_QUOTES));
+        return $browse ? l(drupal_specialchars($value), "profile/$field->name/". check_url($value)) : drupal_specialchars($value);
       case 'checkbox':
-        return l($field->title, "profile/$field->name");
+        return $browse ? l(drupal_specialchars($field->title), "profile/$field->name") : drupal_specialchars($field->title);
       case 'url':
-        return '<a href="'. check_url(strip_tags($value)) .'">'. strip_tags($value) .'</a>';
+        return '<a href="'. check_url($value) .'">'. drupal_specialchars($value) .'</a>';
+      case 'date':
+        list($format) = explode(' - ', variable_get('date_format_short', 'm/d/Y - H:i'), 2);
+        // Note: we avoid PHP's date() because it does not handle dates before
+        // 1970 on Windows. This would make the date field useless for e.g.
+        // birthdays.
+        $replace = array('d' => sprintf('%02d', $value['day']),
+                         'j' => $value['day'],
+                         'm' => sprintf('%02d', $value['month']),
+                         'M' => _profile_map_month($value['month']),
+                         'Y' => $value['year']);
+        return strtr($format, $replace);
       case 'list':
         $values = split("[,\n\r]", $value);
         $fields = array();
         foreach ($values as $value) {
-          if ($value = trim(strip_tags($value))) {
-            $fields[] = l($value, "profile/$field->name/". drupal_specialchars($value, ENT_QUOTES));
+          if ($value = trim($value)) {
+            $fields[] = $browse ? l(drupal_specialchars($value), "profile/$field->name/". check_url($value)) : drupal_specialchars($value);
           }
         }
         return implode(', ', $fields);
@@ -173,15 +198,19 @@ function profile_view_profile($user) {
 
   profile_load_profile($user);
 
-  $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_PRIVATE);
+  // Show private fields to administrators and people viewing their own account.
+  if (user_access('administer users') || $GLOBALS['user']->uid == $user->uid) {
+    $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
+  }
+  else {
+    $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_PRIVATE);
+  }
+
   while ($field = db_fetch_object($result)) {
     if ($value = profile_view_field($user, $field)) {
-      if ($field->type == 'checkbox') {
-        $fields[$field->category] .= "<p>$value</p>";
-      }
-      else {
-        $fields[$field->category] .= form_item($field->title, check_output($value));
-      }
+      $description = ($field->visibility == PROFILE_PRIVATE) ? t('The content of this field is private and only visible to yourself.') : '';
+      $title = ($field->type != 'checkbox') ? $field->title : '';
+      $fields[$field->category] .= form_item($title, $value, $description);
     }
   }
 
@@ -232,6 +261,9 @@ function profile_form_profile($edit, $user, $category) {
 
         $output .= form_select($field->title, $field->name, $edit[$field->name], $options, _profile_form_explanation($field), 0, 0, $field->required);
         break;
+      case 'date':
+        $output .= _profile_date_field($field, $edit);
+        break;
     }
   }
 
@@ -240,6 +272,54 @@ function profile_form_profile($edit, $user, $category) {
   }
 }
 
+/**
+ * Helper function: output a date selector
+ */
+function _profile_date_field($field, $edit) {
+  // Default to current date
+  if (!isset($edit[$field->name])) {
+    $edit[$field->name] = array('day' => format_date(time(), 'custom', 'j'),
+                                'month' => format_date(time(), 'custom', 'n'),
+                                'year' => format_date(time(), 'custom', 'Y'));
+  }
+
+  // Determine the order of day, month, year in the site's chosen date format.
+  $format = variable_get('date_format_short', 'm/d/Y');
+  $sort = array();
+  $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j'));
+  $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
+  $sort['year'] = strpos($format, 'Y');
+  asort($sort);
+  $order = array_keys($sort);
+
+  // Output multi-selector for date
+  $output = '<div class="container-inline">';
+  foreach ($order as $type) {
+    switch ($type) {
+      case 'day':
+        $options = drupal_map_assoc(range(1, 31));
+        break;
+      case 'month':
+        $options = drupal_map_assoc(range(1, 12), '_profile_map_month');
+        break;
+      case 'year':
+        $options = drupal_map_assoc(range(1900, 2050));
+        break;
+    }
+    $output .= form_select('', $field->name .']['. $type, $edit[$field->name][$type], $options, '', 0, 0);
+  }
+  $output .= '</div>';
+
+  return form_item($field->title, $output, _profile_form_explanation($field), NULL, $field->required);
+}
+
+/**
+ * Helper function for usage with drupal_map_assoc
+ */
+function _profile_map_month($month) {
+  return format_date(gmmktime(0, 0, 0, $month, 2, 1970), 'custom', 'M', 0);
+}
+
 function profile_validate_profile($edit, $category) {
   $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = '%s' ORDER BY weight", strtolower($category));
 
@@ -316,9 +396,6 @@ function profile_validate_form($edit) {
  * Menu callback; adds a new field to all user profiles.
  */
 function profile_admin_add($type) {
-  $type = _profile_field_types($type);
-
-
   if ($_POST['op']) {
     $data = $_POST['edit'];
 
@@ -326,17 +403,17 @@ function profile_admin_add($type) {
     profile_validate_form($data);
 
     if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE title = '%s'", $data['title']))) {
-      form_set_error('title', t('the specified title is already in use.'));
+      form_set_error('title', t('The specified title is already in use.'));
     }
 
     if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE name = '%s'", $data['name']))) {
-      form_set_error('name', t('the specified name is already in use.'));
+      form_set_error('name', t('The specified name is already in use.'));
     }
 
     if (!form_get_errors()) {
       db_query("INSERT INTO {profile_fields} (title, name, explanation, category, type, weight, required, visibility, options, page) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s')", $data['title'], $data['name'], $data['explanation'], $data['category'], $type, $data['weight'], $data['required'], $data['visibility'], $data['options'], $data['page']);
 
-      drupal_set_message(t('the field has been created.'));
+      drupal_set_message(t('The field has been created.'));
       drupal_goto('admin/user/configure/profile');
     }
   }
@@ -344,7 +421,7 @@ function profile_admin_add($type) {
     $data = array('name' => 'profile_');
   }
 
-  print theme('page', _profile_field_form($type, $data), t('Add new %type', array('%type' => $type)));
+  print theme('page', _profile_field_form($type, $data), t('Add new %type', array('%type' => _profile_field_types($type))));
 }
 
 /**
@@ -361,7 +438,7 @@ function profile_admin_edit($fid) {
     if (!form_get_errors()) {
       db_query("UPDATE {profile_fields} SET title = '%s', name = '%s', explanation = '%s', category = '%s', weight = %d, required = %d, visibility = %d, options = '%s', page = '%s' WHERE fid = %d", $data['title'], $data['name'], $data['explanation'], $data['category'], $data['weight'], $data['required'], $data['visibility'], $data['options'], $data['page'], $fid);
 
-      drupal_set_message(t('the field has been updated.'));
+      drupal_set_message(t('The field has been updated.'));
       drupal_goto('admin/user/configure/profile');
     }
   }
@@ -377,7 +454,7 @@ function profile_admin_edit($fid) {
  */
 function profile_admin_delete($fid) {
   db_query('DELETE FROM {profile_fields} WHERE fid = %d', $fid);
-  drupal_set_message(t('the field has been deleted.'));
+  drupal_set_message(t('The field has been deleted.'));
   drupal_goto('admin/user/configure/profile');
 }
 
@@ -413,17 +490,21 @@ function _profile_field_form($type, $edit = array()) {
 function profile_admin_overview() {
 
   $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
+  $rows = array();
   while ($field = db_fetch_object($result)) {
-    $rows[] = array($field->title, $field->name, $field->type, $field->category, l(t('edit'), "admin/user/configure/profile/edit/$field->fid"), l(t('delete'), "admin/user/configure/profile/delete/$field->fid"));
+    $rows[] = array($field->title, $field->name, _profile_field_types($field->type), $field->category, l(t('edit'), "admin/user/configure/profile/edit/$field->fid"), l(t('delete'), "admin/user/configure/profile/delete/$field->fid"));
+  }
+  if (count($rows) == 0) {
+    $rows[] = array(array('data' => t('No fields defined.'), 'colspan' => '6'));
   }
 
   $header = array(t('title'), t('name'), t('type'), t('category'), array('data' => t('operations'), 'colspan' => '2'));
 
   $output  = theme('table', $header, $rows);
-  $output .= '<h2>'. t('Create new field') .'</h2>';
+  $output .= '<h2>'. t('Add new field') .'</h2>';
   $output .= '<ul>';
   foreach (_profile_field_types() as $key => $value) {
-    $output .= '<li>'. l(t('Add new %type', array('%type' => $value)), "admin/user/configure/profile/add/$key") .'</li>';
+    $output .= '<li>'. l($value, "admin/user/configure/profile/add/$key") .'</li>';
   }
   $output .= '</ul>';
 
@@ -448,8 +529,18 @@ function theme_profile_profile($user, $fields = array()) {
 }
 
 function _profile_field_types($type = NULL) {
-  $types = array('textfield', 'textarea', 'checkbox', 'selection', 'list', 'url');
+  $types = array('textfield' => t('single-line textfield'),
+                 'textarea' => t('multi-line textfield'),
+                 'checkbox' => t('checkbox'),
+                 'selection' => t('list selection'),
+                 'list' => t('freeform list'),
+                 'url' => t('URL'),
+                 'date' => t('date'));
   return isset($type) ? $types[$type] : $types;
 }
 
+function _profile_field_serialize($type = NULL) {
+  return $type == 'date';
+}
+
 ?>
diff --git a/modules/profile/profile.module b/modules/profile/profile.module
index 1ad8288c7e477b8e66a5d503e4ea19cd61900e0b..802daca6cbd1af5ab1026105d23e408eff83b1a1 100644
--- a/modules/profile/profile.module
+++ b/modules/profile/profile.module
@@ -1,8 +1,6 @@
 <?php
 // $Id$
 
-// TODO: add a 'date' field so we can migrate the birthday information.
-
 /**
  * Flags to define the visibility of a profile field.
  */
@@ -56,12 +54,18 @@ function profile_menu() {
  */
 function profile_browse() {
 
-  $name = strip_tags(arg(1));
-  $value = strip_tags(arg(2));
+  $name = arg(1);
+  $value = arg(2);
 
-  $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page FROM {profile_fields} WHERE name = '%s'", $name));
+  $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page, visibility FROM {profile_fields} WHERE name = '%s'", $name));
 
   if ($name && $field->fid) {
+    // Do not allow browsing of private fields by non-admins
+    if (!user_access('administer users') && $field->visibility == PROFILE_PRIVATE) {
+       drupal_access_denied();
+       return;
+    }
+
     // Compile a list of fields to show
     $fields = array();
     $result = db_query('SELECT name, title, type FROM {profile_fields} WHERE fid != %d AND visibility = %d', $field->fid, PROFILE_PUBLIC_LISTINGS);
@@ -80,6 +84,9 @@ function profile_browse() {
       case 'list':
         $query = "v.value LIKE '%%". check_query($value) ."%%'";
         break;
+      default:
+        drupal_not_found();
+        return;
     }
 
     // Extract the affected users:
@@ -127,17 +134,20 @@ function profile_browse() {
 }
 
 function profile_load_profile(&$user) {
-  $result = db_query('SELECT f.name, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
+  $result = db_query('SELECT f.name, f.type, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
   while ($field = db_fetch_object($result)) {
     if (empty($user->{$field->name})) {
-      $user->{$field->name} = $field->value;
+      $user->{$field->name} = _profile_field_serialize($field->type) ? unserialize($field->value) : $field->value;
     }
   }
 }
 
 function profile_save_profile(&$edit, &$user, $category) {
-  $result = db_query("SELECT fid, name FROM {profile_fields} WHERE LOWER(category) = '%s'", strtolower($category));
+  $result = db_query("SELECT fid, name, type FROM {profile_fields} WHERE LOWER(category) = '%s'", strtolower($category));
   while ($field = db_fetch_object($result)) {
+    if (_profile_field_serialize($field->type)) {
+       $edit[$field->name] = serialize($edit[$field->name]);
+    }
     db_query("DELETE FROM {profile_values} WHERE fid = %d AND uid = %d", $field->fid, $user->uid);
     db_query("INSERT INTO {profile_values} (fid, uid, value) VALUES (%d, %d, '%s')", $field->fid, $user->uid, $edit[$field->name]);
     unset($edit[$field->name], $user->{$field->name});
@@ -145,23 +155,38 @@ function profile_save_profile(&$edit, &$user, $category) {
 }
 
 function profile_view_field($user, $field) {
+  // Only allow browsing of private fields for admins
+  $browse = user_access('administer users') || $field->visibility != PROFILE_PRIVATE;
+
   if ($value = $user->{$field->name}) {
     switch ($field->type) {
       case 'textfield':
+        return drupal_specialchars($value);
       case 'textarea':
         return check_output($value);
       case 'selection':
-        return l($value, "profile/$field->name/". drupal_specialchars($value, ENT_QUOTES));
+        return $browse ? l(drupal_specialchars($value), "profile/$field->name/". check_url($value)) : drupal_specialchars($value);
       case 'checkbox':
-        return l($field->title, "profile/$field->name");
+        return $browse ? l(drupal_specialchars($field->title), "profile/$field->name") : drupal_specialchars($field->title);
       case 'url':
-        return '<a href="'. check_url(strip_tags($value)) .'">'. strip_tags($value) .'</a>';
+        return '<a href="'. check_url($value) .'">'. drupal_specialchars($value) .'</a>';
+      case 'date':
+        list($format) = explode(' - ', variable_get('date_format_short', 'm/d/Y - H:i'), 2);
+        // Note: we avoid PHP's date() because it does not handle dates before
+        // 1970 on Windows. This would make the date field useless for e.g.
+        // birthdays.
+        $replace = array('d' => sprintf('%02d', $value['day']),
+                         'j' => $value['day'],
+                         'm' => sprintf('%02d', $value['month']),
+                         'M' => _profile_map_month($value['month']),
+                         'Y' => $value['year']);
+        return strtr($format, $replace);
       case 'list':
         $values = split("[,\n\r]", $value);
         $fields = array();
         foreach ($values as $value) {
-          if ($value = trim(strip_tags($value))) {
-            $fields[] = l($value, "profile/$field->name/". drupal_specialchars($value, ENT_QUOTES));
+          if ($value = trim($value)) {
+            $fields[] = $browse ? l(drupal_specialchars($value), "profile/$field->name/". check_url($value)) : drupal_specialchars($value);
           }
         }
         return implode(', ', $fields);
@@ -173,15 +198,19 @@ function profile_view_profile($user) {
 
   profile_load_profile($user);
 
-  $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_PRIVATE);
+  // Show private fields to administrators and people viewing their own account.
+  if (user_access('administer users') || $GLOBALS['user']->uid == $user->uid) {
+    $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
+  }
+  else {
+    $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_PRIVATE);
+  }
+
   while ($field = db_fetch_object($result)) {
     if ($value = profile_view_field($user, $field)) {
-      if ($field->type == 'checkbox') {
-        $fields[$field->category] .= "<p>$value</p>";
-      }
-      else {
-        $fields[$field->category] .= form_item($field->title, check_output($value));
-      }
+      $description = ($field->visibility == PROFILE_PRIVATE) ? t('The content of this field is private and only visible to yourself.') : '';
+      $title = ($field->type != 'checkbox') ? $field->title : '';
+      $fields[$field->category] .= form_item($title, $value, $description);
     }
   }
 
@@ -232,6 +261,9 @@ function profile_form_profile($edit, $user, $category) {
 
         $output .= form_select($field->title, $field->name, $edit[$field->name], $options, _profile_form_explanation($field), 0, 0, $field->required);
         break;
+      case 'date':
+        $output .= _profile_date_field($field, $edit);
+        break;
     }
   }
 
@@ -240,6 +272,54 @@ function profile_form_profile($edit, $user, $category) {
   }
 }
 
+/**
+ * Helper function: output a date selector
+ */
+function _profile_date_field($field, $edit) {
+  // Default to current date
+  if (!isset($edit[$field->name])) {
+    $edit[$field->name] = array('day' => format_date(time(), 'custom', 'j'),
+                                'month' => format_date(time(), 'custom', 'n'),
+                                'year' => format_date(time(), 'custom', 'Y'));
+  }
+
+  // Determine the order of day, month, year in the site's chosen date format.
+  $format = variable_get('date_format_short', 'm/d/Y');
+  $sort = array();
+  $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j'));
+  $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
+  $sort['year'] = strpos($format, 'Y');
+  asort($sort);
+  $order = array_keys($sort);
+
+  // Output multi-selector for date
+  $output = '<div class="container-inline">';
+  foreach ($order as $type) {
+    switch ($type) {
+      case 'day':
+        $options = drupal_map_assoc(range(1, 31));
+        break;
+      case 'month':
+        $options = drupal_map_assoc(range(1, 12), '_profile_map_month');
+        break;
+      case 'year':
+        $options = drupal_map_assoc(range(1900, 2050));
+        break;
+    }
+    $output .= form_select('', $field->name .']['. $type, $edit[$field->name][$type], $options, '', 0, 0);
+  }
+  $output .= '</div>';
+
+  return form_item($field->title, $output, _profile_form_explanation($field), NULL, $field->required);
+}
+
+/**
+ * Helper function for usage with drupal_map_assoc
+ */
+function _profile_map_month($month) {
+  return format_date(gmmktime(0, 0, 0, $month, 2, 1970), 'custom', 'M', 0);
+}
+
 function profile_validate_profile($edit, $category) {
   $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = '%s' ORDER BY weight", strtolower($category));
 
@@ -316,9 +396,6 @@ function profile_validate_form($edit) {
  * Menu callback; adds a new field to all user profiles.
  */
 function profile_admin_add($type) {
-  $type = _profile_field_types($type);
-
-
   if ($_POST['op']) {
     $data = $_POST['edit'];
 
@@ -326,17 +403,17 @@ function profile_admin_add($type) {
     profile_validate_form($data);
 
     if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE title = '%s'", $data['title']))) {
-      form_set_error('title', t('the specified title is already in use.'));
+      form_set_error('title', t('The specified title is already in use.'));
     }
 
     if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE name = '%s'", $data['name']))) {
-      form_set_error('name', t('the specified name is already in use.'));
+      form_set_error('name', t('The specified name is already in use.'));
     }
 
     if (!form_get_errors()) {
       db_query("INSERT INTO {profile_fields} (title, name, explanation, category, type, weight, required, visibility, options, page) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s')", $data['title'], $data['name'], $data['explanation'], $data['category'], $type, $data['weight'], $data['required'], $data['visibility'], $data['options'], $data['page']);
 
-      drupal_set_message(t('the field has been created.'));
+      drupal_set_message(t('The field has been created.'));
       drupal_goto('admin/user/configure/profile');
     }
   }
@@ -344,7 +421,7 @@ function profile_admin_add($type) {
     $data = array('name' => 'profile_');
   }
 
-  print theme('page', _profile_field_form($type, $data), t('Add new %type', array('%type' => $type)));
+  print theme('page', _profile_field_form($type, $data), t('Add new %type', array('%type' => _profile_field_types($type))));
 }
 
 /**
@@ -361,7 +438,7 @@ function profile_admin_edit($fid) {
     if (!form_get_errors()) {
       db_query("UPDATE {profile_fields} SET title = '%s', name = '%s', explanation = '%s', category = '%s', weight = %d, required = %d, visibility = %d, options = '%s', page = '%s' WHERE fid = %d", $data['title'], $data['name'], $data['explanation'], $data['category'], $data['weight'], $data['required'], $data['visibility'], $data['options'], $data['page'], $fid);
 
-      drupal_set_message(t('the field has been updated.'));
+      drupal_set_message(t('The field has been updated.'));
       drupal_goto('admin/user/configure/profile');
     }
   }
@@ -377,7 +454,7 @@ function profile_admin_edit($fid) {
  */
 function profile_admin_delete($fid) {
   db_query('DELETE FROM {profile_fields} WHERE fid = %d', $fid);
-  drupal_set_message(t('the field has been deleted.'));
+  drupal_set_message(t('The field has been deleted.'));
   drupal_goto('admin/user/configure/profile');
 }
 
@@ -413,17 +490,21 @@ function _profile_field_form($type, $edit = array()) {
 function profile_admin_overview() {
 
   $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
+  $rows = array();
   while ($field = db_fetch_object($result)) {
-    $rows[] = array($field->title, $field->name, $field->type, $field->category, l(t('edit'), "admin/user/configure/profile/edit/$field->fid"), l(t('delete'), "admin/user/configure/profile/delete/$field->fid"));
+    $rows[] = array($field->title, $field->name, _profile_field_types($field->type), $field->category, l(t('edit'), "admin/user/configure/profile/edit/$field->fid"), l(t('delete'), "admin/user/configure/profile/delete/$field->fid"));
+  }
+  if (count($rows) == 0) {
+    $rows[] = array(array('data' => t('No fields defined.'), 'colspan' => '6'));
   }
 
   $header = array(t('title'), t('name'), t('type'), t('category'), array('data' => t('operations'), 'colspan' => '2'));
 
   $output  = theme('table', $header, $rows);
-  $output .= '<h2>'. t('Create new field') .'</h2>';
+  $output .= '<h2>'. t('Add new field') .'</h2>';
   $output .= '<ul>';
   foreach (_profile_field_types() as $key => $value) {
-    $output .= '<li>'. l(t('Add new %type', array('%type' => $value)), "admin/user/configure/profile/add/$key") .'</li>';
+    $output .= '<li>'. l($value, "admin/user/configure/profile/add/$key") .'</li>';
   }
   $output .= '</ul>';
 
@@ -448,8 +529,18 @@ function theme_profile_profile($user, $fields = array()) {
 }
 
 function _profile_field_types($type = NULL) {
-  $types = array('textfield', 'textarea', 'checkbox', 'selection', 'list', 'url');
+  $types = array('textfield' => t('single-line textfield'),
+                 'textarea' => t('multi-line textfield'),
+                 'checkbox' => t('checkbox'),
+                 'selection' => t('list selection'),
+                 'list' => t('freeform list'),
+                 'url' => t('URL'),
+                 'date' => t('date'));
   return isset($type) ? $types[$type] : $types;
 }
 
+function _profile_field_serialize($type = NULL) {
+  return $type == 'date';
+}
+
 ?>