Index: wp-includes/post-template.php
===================================================================
--- wp-includes/post-template.php	(revision 18444)
+++ wp-includes/post-template.php	(working copy)
@@ -737,7 +737,7 @@
 		echo "<ul class='post-meta'>\n";
 		foreach ( (array) $keys as $key ) {
 			$keyt = trim($key);
-			if ( '_' == $keyt[0] )
+			if ( is_hidden_meta( $keyt ) )
 				continue;
 			$values = array_map('trim', get_post_custom_values($key));
 			$value = implode($values,', ');
Index: wp-includes/class-wp-xmlrpc-server.php
===================================================================
--- wp-includes/class-wp-xmlrpc-server.php	(revision 18444)
+++ wp-includes/class-wp-xmlrpc-server.php	(working copy)
@@ -234,9 +234,8 @@
 
 		foreach ( (array) has_meta($post_id) as $meta ) {
 			// Don't expose protected fields.
-			if ( strpos($meta['meta_key'], '_wp_') === 0 ) {
+			if ( ! current_user_can( 'edit_post_meta', $post_id , $meta['meta_key'] ) )
 				continue;
-			}
 
 			$custom_fields[] = array(
 				"id"    => $meta['meta_id'],
@@ -262,19 +261,18 @@
 		foreach ( (array) $fields as $meta ) {
 			if ( isset($meta['id']) ) {
 				$meta['id'] = (int) $meta['id'];
-
+				$pmeta = get_metadata_by_mid( 'post', $meta['id'] );
 				if ( isset($meta['key']) ) {
-					update_meta($meta['id'], $meta['key'], $meta['value']);
+					if ( $meta['key'] != $pmeta->meta_key )
+						continue;
+					if ( current_user_can( 'edit_post_meta', $post_id, $meta['key'] ) )
+						update_meta($meta['id'], $meta['key'], $meta['value']);
+				} elseif ( current_user_can( 'delete_post_meta', $post_id, $pmeta->meta_key ) ) {
+						delete_meta($meta['id']);
 				}
-				else {
-					delete_meta($meta['id']);
-				}
+			} elseif ( current_user_can( 'add_post_meta', $post_id, $meta['key'] ) ) {
+					add_post_meta( $post_id, $meta['key'], $meta['value'] );
 			}
-			else {
-				$_POST['metakeyinput'] = $meta['key'];
-				$_POST['metavalue'] = $meta['value'];
-				add_meta($post_id);
-			}
 		}
 	}
 
Index: wp-includes/capabilities.php
===================================================================
--- wp-includes/capabilities.php	(revision 18444)
+++ wp-includes/capabilities.php	(working copy)
@@ -951,6 +951,23 @@
 		else
 			$caps[] = $post_type->cap->read_private_posts;
 		break;
+	case 'edit_post_meta':
+	case 'delete_post_meta':
+	case 'add_post_meta':
+		$post = get_post( $args[0] );
+		$post_type_object = get_post_type_object( $post->post_type );
+		$caps = map_meta_cap( $post_type_object->cap->edit_post, $user_id, $post->ID );	
+
+		$meta_key = isset( $args[ 1 ] ) ? $args[ 1 ] : false; 
+			
+		if ( $meta_key && has_filter( "auth_post_meta_{$meta_key}" ) ) {
+			$allowed = apply_filters( "auth_post_meta_{$meta_key}", false, $meta_key, $post->ID, $user_id, $cap, $caps );
+			if ( ! $allowed )
+				$caps[] = $cap;
+		} elseif ( $meta_key && is_protected_meta( $meta_key, 'post' ) ) {
+			$caps[] = $cap;
+		}
+		break;
 	case 'edit_comment':
 		$comment = get_comment( $args[0] );
 		$post = get_post( $comment->comment_post_ID );
Index: wp-includes/meta.php
===================================================================
--- wp-includes/meta.php	(revision 18444)
+++ wp-includes/meta.php	(working copy)
@@ -26,7 +26,7 @@
  * @param bool $unique Optional, default is false.  Whether the specified metadata key should be
  * 		unique for the object.  If true, and the object already has a value for the specified
  * 		metadata key, no change will be made
- * @return bool True on successful update, false on failure.
+ * @return bool The meta ID on successful update, false on failure.
  */
 function add_metadata($meta_type, $object_id, $meta_key, $meta_value, $unique = false) {
 	if ( !$meta_type || !$meta_key )
@@ -49,7 +49,7 @@
 
 	$check = apply_filters( "add_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $unique );
 	if ( null !== $check )
-		return (bool) $check;
+		return $check;
 
 	if ( $unique && $wpdb->get_var( $wpdb->prepare(
 		"SELECT COUNT(*) FROM $table WHERE meta_key = %s AND $column = %d",
@@ -61,20 +61,25 @@
 
 	do_action( "add_{$meta_type}_meta", $object_id, $meta_key, $_meta_value );
 
-	$wpdb->insert( $table, array(
+	$result = $wpdb->insert( $table, array(
 		$column => $object_id,
 		'meta_key' => $meta_key,
 		'meta_value' => $meta_value
 	) );
 
+	if ( ! $result )
+		return false;
+
+	$mid = (int) $wpdb->insert_id;
+
 	wp_cache_delete($object_id, $meta_type . '_meta');
 	// users cache stores usermeta that must be cleared.
 	if ( 'user' == $meta_type )
 		clean_user_cache($object_id);
 
-	do_action( "added_{$meta_type}_meta", $wpdb->insert_id, $object_id, $meta_key, $_meta_value );
+	do_action( "added_{$meta_type}_meta", $mid, $object_id, $meta_key, $_meta_value );
 
-	return true;
+	return $mid;
 }
 
 /**
@@ -146,6 +151,7 @@
 	do_action( "update_{$meta_type}_meta", $meta_id, $object_id, $meta_key, $_meta_value );
 
 	$wpdb->update( $table, $data, $where );
+
 	wp_cache_delete($object_id, $meta_type . '_meta');
 	// users cache stores usermeta that must be cleared.
 	if ( 'user' == $meta_type )
@@ -282,6 +288,40 @@
 }
 
 /**
+ * Get meta data by meta ID
+ *
+ * @since 3.3.0
+ *
+ * @param string $meta_type Type of object metadata is for (e.g., comment, post, or user)
+ * @param int $meta_id ID for a specific meta row
+ * @return object Meta object or false.
+ */
+function get_metadata_by_mid( $meta_type, $meta_id ) {
+	global $wpdb;
+
+	if ( ! $meta_type )
+		return false;
+
+	if ( !$meta_id = absint( $meta_id ) )
+		return false;
+
+	if ( ! $table = _get_meta_table($meta_type) )
+		return false;
+
+	$id_column = ( 'user' == $meta_type ) ? 'umeta_id' : 'meta_id';
+
+	$meta = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE $id_column = %d", $meta_id ) );
+
+	if ( empty( $meta ) )
+		return false;
+
+	if ( isset( $meta->meta_value ) )
+		$meta->meta_value = maybe_unserialize( $meta->meta_value );
+
+	return $meta;
+}
+
+/**
  * Update the metadata cache for the specified objects.
  *
  * @since 2.9.0
@@ -588,12 +628,26 @@
  * @return bool True if the key is protected, false otherwise.
  */
 function is_protected_meta( $meta_key, $meta_type = null ) {
-	$protected = (  '_' == $meta_key[0] );
+	$protected = ( '_' == $meta_key[0] );
 
 	return apply_filters( 'is_protected_meta', $protected, $meta_key, $meta_type );
 }
 
 /**
+ * Determine whether a meta key is hidden
+ *
+ * @since 3.2.0
+ *
+ * @param string $meta_key Meta key
+ * @return bool True if the key is hidden, false otherwise.
+ */
+function is_hidden_meta( $meta_key, $meta_type = null ) {
+	$hidden = ( '_' == $meta_key[0] );
+
+	return apply_filters( 'is_hidden_meta', $hidden, $meta_key, $meta_type );
+}
+
+/**
  * Sanitize meta value
  *
  * @since 3.1.3
@@ -603,8 +657,38 @@
  * @param string $meta_type Type of meta
  * @return mixed Sanitized $meta_value
  */
-function sanitize_meta( $meta_key, $meta_value, $meta_type = null ) {
-	return apply_filters( 'sanitize_meta', $meta_value, $meta_key, $meta_type );
+function sanitize_meta( $meta_key, $meta_value, $meta_type ) {
+	return apply_filters( "sanitize_{$meta_type}_meta_{$meta_key}", $meta_value, $meta_key, $meta_type );
 }
 
+/**
+ * Register meta key
+ * 
+ * @since 3.3.0
+ *
+ * @param string $meta_key Meta key
+ * @param string $meta_type Type of meta
+ * @param string|array $sanitize_callback A function or method to call when sanitizing the value of $meta_key.
+ * @param string|array $auth_callback Optional. A function or method to call when performing edit_post_meta, add_post_meta, and delete_post_meta capability checks.
+ * @param array $args Arguments
+ */
+function register_meta( $meta_key, $meta_type, $sanitize_callback, $auth_callback = null ) {
+	global $_wp_meta;
+
+	$args = (object) $args;
+
+	if ( is_callable( $sanitize_callback ) )
+		add_filter( "sanitize_{$meta_type}_meta_{$meta_key}", $sanitize_callback, 10, 3 );
+
+	if ( empty( $auth_callback ) ) {
+		if ( is_protected_meta( $meta_key, $meta_type ) )
+			$auth_callback = '__return_false';
+		else
+			$auth_callback = '__return_true';
+	}
+
+	if ( is_callable( $auth_callback ) )
+		add_filter( "auth_{$meta_type}_meta_{$meta_key}", $auth_callback, 10, 6 );
+}
+
 ?>
Index: wp-admin/admin-ajax.php
===================================================================
--- wp-admin/admin-ajax.php	(revision 18444)
+++ wp-admin/admin-ajax.php	(working copy)
@@ -393,10 +393,10 @@
 	break;
 case 'delete-meta' :
 	check_ajax_referer( "delete-meta_$id" );
-	if ( !$meta = get_post_meta_by_id( $id ) )
+	if ( !$meta = get_metadata_by_mid( 'post', $id ) )
 		die('1');
 
-	if ( !current_user_can( 'edit_post', $meta->post_id ) || is_protected_meta( $meta->meta_key ) )
+	if ( !current_user_can( 'delete_post_meta',  $meta->post_id, $meta->meta_key ) || is_hidden_meta( $meta->meta_key ) )
 		die('-1');
 	if ( delete_meta( $meta->meta_id ) )
 		die('1');
@@ -849,7 +849,7 @@
 			die(__('Please provide a custom field value.'));
 		}
 
-		$meta = get_post_meta_by_id( $mid );
+		$meta = get_metadata_by_mid( 'post', $mid );
 		$pid = (int) $meta->post_id;
 		$meta = get_object_vars( $meta );
 		$x = new WP_Ajax_Response( array(
@@ -869,10 +869,8 @@
 			die(__('Please provide a custom field value.'));
 		if ( !$meta = get_post_meta_by_id( $mid ) )
 			die('0'); // if meta doesn't exist
-		if ( !current_user_can( 'edit_post', $meta->post_id ) )
+		if ( is_hidden_meta( $meta->meta_key ) || !current_user_can( 'edit_post_meta', $meta->post_id, $meta->meta_key ) )
 			die('-1');
-		if ( is_protected_meta( $meta->meta_key ) )
-			die('-1');
 		if ( $meta->meta_value != stripslashes($value) || $meta->meta_key != stripslashes($key) ) {
 			if ( !$u = update_meta( $mid, $key, $value ) )
 				die('0'); // We know meta exists; we also know it's unchanged (or DB error, in which case there are bigger problems).
Index: wp-admin/includes/post.php
===================================================================
--- wp-admin/includes/post.php	(revision 18444)
+++ wp-admin/includes/post.php	(working copy)
@@ -210,7 +210,7 @@
 				continue;
 			if ( $meta->post_id != $post_ID )
 				continue;
-			if ( is_protected_meta( $value['key'] ) )
+			if ( is_hidden_meta( $value['key'] ) || ! current_user_can( 'edit_post_meta', $post_ID, $value['key'] ) )
 				continue;
 			update_meta( $key, $value['key'], $value['value'] );
 		}
@@ -222,7 +222,7 @@
 				continue;
 			if ( $meta->post_id != $post_ID )
 				continue;
-			if ( is_protected_meta( $meta->meta_key ) )
+			if ( is_hidden_meta( $meta->meta_key ) || ! current_user_can( 'delete_post_meta', $post_ID, $meta->meta_key ) )
 				continue;
 			delete_meta( $key );
 		}
@@ -671,7 +671,7 @@
 	if ( is_string($metavalue) )
 		$metavalue = trim( $metavalue );
 
-	if ( ('0' === $metavalue || !empty ( $metavalue ) ) && ((('#NONE#' != $metakeyselect) && !empty ( $metakeyselect) ) || !empty ( $metakeyinput) ) ) {
+	if ( ('0' === $metavalue || ! empty ( $metavalue ) ) && ((('#NONE#' != $metakeyselect) && !empty ( $metakeyselect) ) || !empty ( $metakeyinput) ) ) {
 		// We have a key/value pair. If both the select and the
 		// input for the key have data, the input takes precedence:
 
@@ -681,16 +681,12 @@
 		if ( $metakeyinput)
 			$metakey = $metakeyinput; // default
 
-		if ( is_protected_meta( $metakey ) )
+		if ( is_hidden_meta( $metakey ) || ! current_user_can( 'add_post_meta', $post_ID, $metakey ) )
 			return false;
 
-		wp_cache_delete($post_ID, 'post_meta');
-		$wpdb->insert( $wpdb->postmeta, array( 'post_id' => $post_ID, 'meta_key' => $metakey, 'meta_value' => $metavalue ) );
-		$meta_id = $wpdb->insert_id;
-		do_action( 'added_postmeta', $meta_id, $post_ID, $metakey, $metavalue );
-
-		return $meta_id;
+		return add_post_meta($post_ID, $metakey, $metavalue);
 	}
+
 	return false;
 } // add_meta
 
@@ -771,7 +767,6 @@
 	return $wpdb->get_results( $wpdb->prepare("SELECT meta_key, meta_value, meta_id, post_id
 			FROM $wpdb->postmeta WHERE post_id = %d
 			ORDER BY meta_key,meta_id", $postid), ARRAY_A );
-
 }
 
 /**
@@ -789,7 +784,7 @@
 
 	$meta_key = stripslashes($meta_key);
 
-	if ( is_protected_meta( $meta_key ) )
+	if ( is_protected_meta( $meta_key, 'post' ) )
 		return false;
 
 	if ( '' === trim( $meta_value ) )
Index: wp-admin/includes/meta-boxes.php
===================================================================
--- wp-admin/includes/meta-boxes.php	(revision 18444)
+++ wp-admin/includes/meta-boxes.php	(working copy)
@@ -425,7 +425,11 @@
 <div id="ajax-response"></div>
 <?php
 $metadata = has_meta($post->ID);
-list_meta($metadata);
+foreach ( $metadata as $key => $value ) {
+	if ( is_hidden_meta( $metadata[ $key ][ 'meta_key' ] ) || ! current_user_can( 'edit_post_meta', $post->ID, $metadata[ $key ][ 'meta_key' ] ) )
+		unset( $metadata[ $key ] );
+}
+list_meta( $metadata );
 meta_form(); ?>
 </div>
 <p><?php _e('Custom fields can be used to add extra metadata to a post that you can <a href="http://codex.wordpress.org/Using_Custom_Fields" target="_blank">use in your theme</a>.'); ?></p>
Index: wp-admin/includes/template.php
===================================================================
--- wp-admin/includes/template.php	(revision 18444)
+++ wp-admin/includes/template.php	(working copy)
@@ -466,7 +466,7 @@
 function _list_meta_row( $entry, &$count ) {
 	static $update_nonce = false;
 
-	if ( is_protected_meta( $entry['meta_key'] ) )
+	if ( is_hidden_meta( $entry['meta_key'] ) || is_protected_meta( $entry['meta_key'], 'post' ) )
 		return;
 
 	if ( !$update_nonce )
@@ -478,8 +478,6 @@
 		$style = 'alternate';
 	else
 		$style = '';
-	if ('_' == $entry['meta_key'] { 0 } )
-		$style .= ' hidden';
 
 	if ( is_serialized( $entry['meta_value'] ) ) {
 		if ( is_serialized_string( $entry['meta_value'] ) ) {
