diff --git a/apps/contacts/ajax/addproperty.php b/apps/contacts/ajax/addproperty.php
index b6b5dc7c416446eb333bfc32329b461a78d5750b..e1a31292837fbc01a196dffc4c9a474d103ca546 100644
--- a/apps/contacts/ajax/addproperty.php
+++ b/apps/contacts/ajax/addproperty.php
@@ -27,14 +27,16 @@ require_once('../../../lib/base.php');
 OC_JSON::checkLoggedIn();
 OC_JSON::checkAppEnabled('contacts');
 
-$id = $_POST['id'];
-$vcard = OC_Contacts_App::getContactVCard( $id );
+$id = isset($_POST['id'])?$_POST['id']:null;
+$name = isset($_POST['name'])?$_POST['name']:null;
+$value = isset($_POST['value'])?$_POST['value']:null;
+$parameters = isset($_POST['parameters'])?$_POST['parameters']:array();
+
+$vcard = OC_Contacts_App::getContactVCard($id);
 
-$name = $_POST['name'];
-$value = $_POST['value'];
 if(!is_array($value)){
 	$value = trim($value);
-	if(!$value && in_array($name, array('TEL', 'EMAIL', 'ORG', 'BDAY', 'NICKNAME'))) {
+	if(!$value && in_array($name, array('TEL', 'EMAIL', 'ORG', 'BDAY', 'NICKNAME', 'NOTE'))) {
 		OC_JSON::error(array('data' => array('message' => OC_Contacts_App::$l10n->t('Cannot add empty property.'))));
 		exit();
 	}
@@ -51,7 +53,6 @@ if(!is_array($value)){
 		exit();
 	}
 }
-$parameters = isset($_POST['parameters']) ? $_POST['parameters'] : array();
 
 // Prevent setting a duplicate entry
 $current = $vcard->select($name);
@@ -82,7 +83,9 @@ switch($name) {
 		}
 	case 'N':
 	case 'ORG':
+	case 'NOTE':
 	case 'NICKNAME':
+		// TODO: Escape commas and semicolons.
 		break;
 	case 'EMAIL':
 		$value = strtolower($value);
diff --git a/apps/contacts/ajax/categories/add.php b/apps/contacts/ajax/categories/add.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b6c262978b871ea346c38aa86c5c2c572116f56
--- /dev/null
+++ b/apps/contacts/ajax/categories/add.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Copyright (c) 2012 Thomas Tanghus <thomas@tanghus.net>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+require_once('../../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+OC_JSON::checkAppEnabled('contacts');
+
+function bailOut($msg) {
+	OC_JSON::error(array('data' => array('message' => $msg)));
+	OC_Log::write('contacts','ajax/categories/add.php: '.$msg, OC_Log::DEBUG);
+	exit();
+}
+function debug($msg) {
+	OC_Log::write('contacts','ajax/categories/add.php: '.$msg, OC_Log::DEBUG);
+}
+
+$category = isset($_GET['category'])?strip_tags($_GET['category']):null;
+
+if(is_null($category)) {
+	bailOut(OC_Contacts_App::$l10n->t('No category to add?'));
+}
+
+debug(print_r($category, true));
+
+$categories = new OC_VCategories('contacts');
+if($categories->hasCategory($category)) {
+	bailOut(OC_Contacts_App::$l10n->t('This category already exists: '.$category));
+} else {
+	$categories->add($category, true);
+}
+
+OC_JSON::success(array('data' => array('categories'=>$categories->categories())));
+
+?>
diff --git a/apps/contacts/ajax/categories/checksumfor.php b/apps/contacts/ajax/categories/checksumfor.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff535866bf0de06b7145151ec0141ac99457ba79
--- /dev/null
+++ b/apps/contacts/ajax/categories/checksumfor.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Copyright (c) 2012 Thomas Tanghus <thomas@tanghus.net>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+require_once('../../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+OC_JSON::checkAppEnabled('contacts');
+
+$id = isset($_GET['id'])?$_GET['id']:null;
+if(is_null($id)) {
+	OC_JSON::error(array('data' => array('message' => OC_Contacts_App::$l10n->t('No ID provided'))));
+	exit();
+}
+$vcard = OC_Contacts_App::getContactVCard( $id );
+foreach($vcard->children as $property){
+	//OC_Log::write('contacts','ajax/categories/checksumfor.php: '.$property->name, OC_Log::DEBUG);
+	if($property->name == 'CATEGORIES') {
+		$checksum = md5($property->serialize());
+		OC_JSON::success(array('data' => array('checksum'=>$checksum)));
+		exit();
+	}
+}
+OC_JSON::error(array('data' => array('message' => OC_Contacts_App::$l10n->t('Error setting checksum.'))));
+?>
diff --git a/apps/contacts/ajax/categories/delete.php b/apps/contacts/ajax/categories/delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ba5aa16068eced43d40eec07c245b5ba84bd898
--- /dev/null
+++ b/apps/contacts/ajax/categories/delete.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Copyright (c) 2012 Thomas Tanghus <thomas@tanghus.net>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+require_once('../../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+OC_JSON::checkAppEnabled('contacts');
+
+foreach ($_POST as $key=>$element) {
+	debug('_POST: '.$key.'=>'.print_r($element, true));
+}
+
+function bailOut($msg) {
+	OC_JSON::error(array('data' => array('message' => $msg)));
+	OC_Log::write('contacts','ajax/categories/delete.php: '.$msg, OC_Log::DEBUG);
+	exit();
+}
+function debug($msg) {
+	OC_Log::write('contacts','ajax/categories/delete.php: '.$msg, OC_Log::DEBUG);
+}
+
+$categories = isset($_POST['categories'])?$_POST['categories']:null;
+
+if(is_null($categories)) {
+	bailOut(OC_Contacts_App::$l10n->t('No categories selected for deletion.'));
+}
+
+debug(print_r($categories, true));
+
+$addressbooks = OC_Contacts_Addressbook::all(OC_User::getUser());
+if(count($addressbooks) == 0) {
+	bailOut(OC_Contacts_App::$l10n->t('No address books found.'));
+}
+$addressbookids = array();
+foreach($addressbooks as $addressbook) {
+	$addressbookids[] = $addressbook['id'];
+} 
+$contacts = OC_Contacts_VCard::all($addressbookids);
+if(count($contacts) == 0) {
+	bailOut(OC_Contacts_App::$l10n->t('No contacts found.'));
+}
+
+$cards = array();
+foreach($contacts as $contact) {
+	$cards[] = array($contact['id'], $contact['carddata']);
+} 
+
+debug('Before delete: '.print_r($categories, true));
+
+$catman = new OC_VCategories('contacts');
+$catman->delete($categories, $cards);
+debug('After delete: '.print_r($catman->categories(), true));
+OC_Contacts_VCard::updateDataByID($cards);
+OC_JSON::success(array('data' => array('categories'=>$catman->categories())));
+
+?>
diff --git a/apps/contacts/ajax/categories/edit.php b/apps/contacts/ajax/categories/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ecc3540b115ac97174593ee826a1023e5339f01
--- /dev/null
+++ b/apps/contacts/ajax/categories/edit.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Copyright (c) 2012 Thomas Tanghus <thomas@tanghus.net>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+require_once('../../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+OC_JSON::checkAppEnabled('contacts');
+function bailOut($msg) {
+	OC_JSON::error(array('data' => array('message' => $msg)));
+	OC_Log::write('contacts','ajax/categories/edit.php: '.$msg, OC_Log::DEBUG);
+	exit();
+}
+function debug($msg) {
+	OC_Log::write('contacts','ajax/categories/edit.php: '.$msg, OC_Log::DEBUG);
+}
+
+$tmpl = new OC_TEMPLATE("contacts", "part.edit_categories_dialog");
+
+$categories = OC_Contacts_App::$categories->categories();
+debug(print_r($categories, true));
+$tmpl->assign('categories',$categories);
+$tmpl->printpage();
+
+?>
diff --git a/apps/contacts/ajax/categories/list.php b/apps/contacts/ajax/categories/list.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b41b7bfa952cfc703a2d977958752f44ca01446
--- /dev/null
+++ b/apps/contacts/ajax/categories/list.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Copyright (c) 2012 Thomas Tanghus <thomas@tanghus.net>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+require_once('../../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+OC_JSON::checkAppEnabled('contacts');
+
+$categories = OC_Contacts_App::$categories->categories();
+
+OC_JSON::success(array('data' => array('categories'=>$categories)));
+
+?>
diff --git a/apps/contacts/ajax/contactdetails.php b/apps/contacts/ajax/contactdetails.php
index f35fd595c5667d9e6547a51ea7de46f0860a5ae2..03895c862aab3ed195a885a962e6b54506e96737 100644
--- a/apps/contacts/ajax/contactdetails.php
+++ b/apps/contacts/ajax/contactdetails.php
@@ -71,5 +71,5 @@ if(isset($details['PHOTO'])) {
 	$details['PHOTO'] = false;
 }
 $details['id'] = $id;
-
+OC_Contacts_App::setLastModifiedHeader($vcard);
 OC_JSON::success(array('data' => $details));
diff --git a/apps/contacts/ajax/saveproperty.php b/apps/contacts/ajax/saveproperty.php
index db209fedfc702b94050736341dc8895855fdf5a6..0769791ea3654962e75e7419d43225e5ee633110 100644
--- a/apps/contacts/ajax/saveproperty.php
+++ b/apps/contacts/ajax/saveproperty.php
@@ -36,7 +36,7 @@ function debug($msg) {
 	OC_Log::write('contacts','ajax/saveproperty.php: '.$msg, OC_Log::DEBUG);
 }
 foreach ($_POST as $key=>$element) {
-	debug('_POST: '.$key.'=>'.$element);
+	debug('_POST: '.$key.'=>'.print_r($element, true));
 }
 
 $id = isset($_POST['id'])?$_POST['id']:null;
@@ -51,12 +51,8 @@ $checksum = isset($_POST['checksum'])?$_POST['checksum']:null;
 // 	}
 // }
 
-if(is_array($value)){
-	$value = array_map('strip_tags', $value);
-	ksort($value); // NOTE: Important, otherwise the compound value will be set in the order the fields appear in the form!
-	$value = OC_VObject::escapeSemicolons($value);
-} else {
-	$value = trim(strip_tags($value));
+if(!$name) {
+	bailOut(OC_Contacts_App::$l10n->t('element name is not set.'));
 }
 if(!$id) {
 	bailOut(OC_Contacts_App::$l10n->t('id is not set.'));
@@ -64,14 +60,22 @@ if(!$id) {
 if(!$checksum) {
 	bailOut(OC_Contacts_App::$l10n->t('checksum is not set.'));
 }
-if(!$name) {
-	bailOut(OC_Contacts_App::$l10n->t('element name is not set.'));
+if(is_array($value)){
+	$value = array_map('strip_tags', $value);
+	ksort($value); // NOTE: Important, otherwise the compound value will be set in the order the fields appear in the form!
+	if($name == 'CATEGORIES') {
+		$value = OC_Contacts_VCard::escapeDelimiters($value, ',');
+	} else {
+		$value = OC_Contacts_VCard::escapeDelimiters($value, ';');
+	}
+} else {
+	$value = trim(strip_tags($value));
 }
 
 $vcard = OC_Contacts_App::getContactVCard( $id );
 $line = OC_Contacts_App::getPropertyLineByChecksum($vcard, $checksum);
 if(is_null($line)) {
-	bailOut(OC_Contacts_App::$l10n->t('Information about vCard is incorrect. Please reload the page.'.$checksum.' "'.$line.'"'));
+	bailOut(OC_Contacts_App::$l10n->t('Information about vCard is incorrect. Please reload the page: ').$checksum);
 }
 $element = $vcard->children[$line]->name;
 
@@ -91,7 +95,9 @@ switch($element) {
 		}
 	case 'N':
 	case 'ORG':
+	case 'NOTE':
 	case 'NICKNAME':
+	case 'CATEGORIES':
 		debug('Setting string:'.$name.' '.$value);
 		$vcard->setString($name, $value);
 		break;
@@ -123,12 +129,8 @@ $checksum = md5($vcard->children[$line]->serialize());
 debug('New checksum: '.$checksum);
 
 if(!OC_Contacts_VCard::edit($id,$vcard)) {
-	OC_JSON::error(array('data' => array('message' => OC_Contacts_App::$l10n->t('Error updating contact property.'))));
-	OC_Log::write('contacts','ajax/setproperty.php: Error updating contact property: '.$value, OC_Log::ERROR);
+	bailOut(OC_Contacts_App::$l10n->t('Error updating contact property.'));
 	exit();
 }
 
-//$adr_types = OC_Contacts_App::getTypesOfProperty('ADR');
-//$phone_types = OC_Contacts_App::getTypesOfProperty('TEL');
-
 OC_JSON::success(array('data' => array( 'line' => $line, 'checksum' => $checksum, 'oldchecksum' => $_POST['checksum'] )));
diff --git a/apps/contacts/css/contacts.css b/apps/contacts/css/contacts.css
index b24ec438f247c68498477d2f2463a92c0e5d5c16..a6f7d9316f8e77d32edacd7c2dc99ed50d23cef2 100644
--- a/apps/contacts/css/contacts.css
+++ b/apps/contacts/css/contacts.css
@@ -20,7 +20,8 @@
 #firstrun { width: 100%; position: absolute; top: 5em; left: 0; text-align: center; font-weight:bold; font-size:1.5em; color:#777; }
 #firstrun #selections { font-size:0.8em; margin: 2em auto auto auto; clear: both; }
 
-#card input[type="text"].contacts_property,input[type="email"].contacts_property { width: 16em; }
+#card input[type="text"].contacts_property,input[type="email"].contacts_property { width: 14em; }
+.categories { float: left; width: 16em; }
 #card input[type="text"],input[type="email"],input[type="tel"],input[type="date"], select { background-color: #f8f8f8; border: 0 !important; -webkit-appearance:none !important; -moz-appearance:none  !important; -webkit-box-sizing:none !important; -moz-box-sizing:none !important; box-sizing:none !important; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; -moz-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; float: left; }
 #card input[type="text"]:hover, input[type="text"]:focus, input[type="text"]:active,input[type="email"]:hover,input[type="tel"]:hover,input[type="date"]:hover,input[type="date"],input[type="date"]:hover,input[type="date"]:active,input[type="date"]:active,input[type="date"]:active,input[type="email"]:active,input[type="tel"]:active, select:hover, select:focus, select:active { border: 0 !important; -webkit-appearance:textfield; -moz-appearance:textfield; -webkit-box-sizing:content-box; -moz-box-sizing:content-box; box-sizing:content-box; background:#fff; color:#333; border:1px solid #ddd; -moz-box-shadow:0 1px 1px #fff, 0 2px 0 #bbb inset; -webkit-box-shadow:0 1px 1px #fff, 0 1px 0 #bbb inset; box-shadow:0 1px 1px #fff, 0 1px 0 #bbb inset; -moz-border-radius:.5em; -webkit-border-radius:.5em; border-radius:.5em; outline:none; float: left; }
 input[type="text"]:invalid,input[type="email"]:invalid,input[type="tel"]:invalid,input[type="date"]:invalid { background-color: #ffc0c0 !important; }
@@ -68,7 +69,7 @@ dl.form
 	/*background-color: yellow;*/
 }
 
-.loading { background: url('../../../core/img/loading.gif') no-repeat center !important;}
+.loading { background: url('../../../core/img/loading.gif') no-repeat center !important; /*cursor: progress; */ cursor: wait; }
 
 /*.add { cursor: pointer;  width: 25px; height: 25px; margin: 0px; float: right; position:relative; content: "\+"; font-weight: bold; color: #666; font-size: large; bottom: 0px; right: 0px; clear: both; text-align: center; vertical-align: bottom; display: none; }*/
 
@@ -185,4 +186,12 @@ input[type="checkbox"] { width: 20px; height: 20px; vertical-align: bottom; }
 .propertylist li > input[type="checkbox"],input[type="radio"] { float: left; clear: left; width: 20px; height: 20px; vertical-align: middle; }
 .propertylist li > select { float: left; max-width: 8em; }
 .typelist { float: left; max-width: 10em; } /* for multiselect */
-.addresslist { clear: both; }
\ No newline at end of file
+.addresslist { clear: both; }
+
+#categoryform .scrollarea { position: absolute; left: 10px; top: 10px; right: 10px; bottom: 50px; overflow: auto; border:1px solid #ddd; background: #f8f8f8; }
+#categoryform .bottombuttons { position: absolute; bottom: 10px;}
+#categoryform .bottombuttons * { float: left;}
+/*#categorylist { border:1px solid #ddd;}*/
+#categorylist li { background:#f8f8f8; padding:.3em .8em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; -webkit-transition:background-color 500ms; -moz-transition:background-color 500ms; -o-transition:background-color 500ms; transition:background-color 500ms; }
+#categorylist li:hover, li:active { background:#eee; }
+#category_addinput { width: 10em; }
diff --git a/apps/contacts/index.php b/apps/contacts/index.php
index 0a21ddd04b6ac2ae70ec5ea40db81696ae623d95..48b2b8b4f019ac9709ad28fd32b279c2814b36e6 100644
--- a/apps/contacts/index.php
+++ b/apps/contacts/index.php
@@ -34,6 +34,7 @@ if(!is_null($id)) {
 }
 $property_types = OC_Contacts_App::getAddPropertyOptions();
 $phone_types = OC_Contacts_App::getTypesOfProperty('TEL');
+$categories = OC_Contacts_App::$categories->categories();
 
 $upload_max_filesize = OC_Helper::computerFileSize(ini_get('upload_max_filesize'));
 $post_max_size = OC_Helper::computerFileSize(ini_get('post_max_size'));
@@ -48,6 +49,7 @@ OC_Util::addScript('contacts','contacts');
 OC_Util::addScript('contacts','jquery.combobox');
 OC_Util::addScript('contacts','jquery.inview');
 OC_Util::addScript('contacts','jquery.Jcrop');
+OC_Util::addScript('contacts','jquery.multi-autocomplete');
 OC_Util::addStyle('','jquery.multiselect');
 //OC_Util::addStyle('contacts','styles');
 OC_Util::addStyle('contacts','jquery.combobox');
@@ -58,7 +60,9 @@ $tmpl = new OC_Template( "contacts", "index", "user" );
 $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize);
 $tmpl->assign('uploadMaxHumanFilesize', OC_Helper::humanFileSize($maxUploadFilesize));
 $tmpl->assign('property_types',$property_types);
+$tmpl->assign('categories',OC_Contacts_App::getCategories());
 $tmpl->assign('phone_types',$phone_types);
+$tmpl->assign('categories',$categories);
 $tmpl->assign('addressbooks', $addressbooks);
 $tmpl->assign('contacts', $contacts);
 $tmpl->assign('details', $details );
diff --git a/apps/contacts/js/contacts.js b/apps/contacts/js/contacts.js
index 4ede2ff433643cf54b6dc9badd5361f4ae518d89..ccedf1d303252ef6d1a708938063898a5c836e07 100644
--- a/apps/contacts/js/contacts.js
+++ b/apps/contacts/js/contacts.js
@@ -10,17 +10,115 @@ String.prototype.strip_tags = function(){
 	return stripped;
 };
 
+Categories={
+	edit:function(){
+		console.log('Categories.edit');
+		$('body').append('<div id="category_dialog"></div>');
+		$('#category_dialog').load(OC.filePath('contacts', 'ajax', 'categories/edit.php'), function(response, status, xhr){
+			try {
+				var response = jQuery.parseJSON(response);
+				console.log('status: ' + status + ', response: ' + response + ', response.status:' + response.status);
+				if(response.status == 'error'){
+					OC.dialogs.alert(response.data.message, 'Error');
+				} else {
+					OC.dialogs.alert(response, 'Error');
+				}
+			} catch(e) {
+				$('#edit_categories_dialog').dialog({
+						modal: true,
+						height: 350, minHeight:200, width: 250, minWidth: 200,
+						buttons: {
+							'Delete':function() {
+								Categories.delete();
+							},
+							'Rescan':function() {
+								Categories.rescan();
+							}
+						},
+						close : function(event, ui) {
+							//alert('close');
+							$(this).dialog('destroy').remove();
+							$('#category_dialog').remove();
+						},
+						open : function(event, ui) {
+							$('#category_addinput').live('input',function(){
+								if($(this).val().length > 0) {
+									$('#category_addbutton').removeAttr('disabled');
+								}
+							});
+							$('#categoryform').submit(function() {
+								Categories.add($('#category_addinput').val());
+								$('#category_addinput').val('');
+								$('#category_addbutton').attr('disabled', 'disabled');
+								return false;
+							});
+							$('#category_addbutton').live('click',function(e){
+								e.preventDefault();
+								if($('#category_addinput').val().length > 0) {
+									Categories.add($('#category_addinput').val());
+									$('#category_addinput').val('');
+								}
+							});
+						}
+				});
+			}
+		});
+	},
+	delete:function(){
+		var categories = $('#categorylist').find('input[type="checkbox"]').serialize();
+		console.log('Categories.delete: ' + categories);
+		$.post(OC.filePath('contacts', 'ajax', 'categories/delete.php'),categories,function(jsondata){
+			if(jsondata.status == 'success'){
+				Categories._update(jsondata.data.categories);
+			} else {
+				OC.dialogs.alert(jsondata.data.message, 'Error');
+			}
+		});
+	},
+	add:function(category){
+		console.log('Categories.add ' + category);
+		$.getJSON(OC.filePath('contacts', 'ajax', 'categories/add.php'),{'category':category},function(jsondata){
+			if(jsondata.status == 'success'){
+				Categories._update(jsondata.data.categories);
+			} else {
+				OC.dialogs.alert(jsondata.data.message, 'Error');
+			}
+		});
+		return false;
+	},
+	rescan:function(){
+		console.log('Categories.rescan');
+		$.getJSON(OC.filePath('contacts', 'ajax', 'categories/rescan.php'),{},function(jsondata){
+			if(jsondata.status == 'success'){
+				Categories._update(jsondata.data.categories);
+			} else {
+				OC.dialogs.alert(jsondata.data.message, 'Error');
+			}
+		});
+	},
+	_update:function(categories){
+		var categorylist = $('#categorylist');
+		categorylist.find('li').remove();
+		for(var category in categories) {
+			var item = '<li><input type="checkbox" name="categories" value="' + categories[category] + '" />' + categories[category] + '</li>';
+			$(item).appendTo(categorylist);
+		}
+		if(Categories.changed != undefined) {
+			Categories.changed(categories);
+		}
+	}
+}
 
 Contacts={
 	UI:{
 		notImplemented:function() {
-			Contacts.UI.messageBox(t('contacts', 'Not implemented'), t('contacts', 'Sorry, this functionality has not been implemented yet'));
+			OC.dialogs.alert(t('contacts', 'Sorry, this functionality has not been implemented yet'), t('contacts', 'Not implemented'));
 		},
 		searchOSM:function(obj) {
 			var adr = Contacts.UI.propertyContainerFor(obj).find('.adr').val();
 			console.log('adr 1: ' + adr);
 			if(adr == undefined) {
-				Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', 'Couldn\'t get a valid address.'));
+				OC.dialogs.alert(t('contacts', 'Couldn\'t get a valid address.'), t('contacts', 'Error'));
 				return;
 			}
 			// FIXME: I suck at regexp. /Tanghus
@@ -53,7 +151,7 @@ Contacts={
 		mailTo:function(obj) {
 			var adr = Contacts.UI.propertyContainerFor($(obj)).find('input[type="email"]').val().trim();
 			if(adr == '') {
-				Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', 'Please enter an email address.'));
+				OC.dialogs.alert(t('contacts', 'Please enter an email address.'), t('contacts', 'Error'));
 				return;
 			}
 			window.location.href='mailto:' + adr;
@@ -116,34 +214,6 @@ Contacts={
 			$('#carddav_url').show();
 			$('#carddav_url_close').show();
 		},
-		messageBox:function(title, msg) {
-			if(msg.toLowerCase().indexOf('auth') != -1) {
-				// fugly hack, I know
-				alert(msg);
-			}
-			if($('#messagebox').dialog('isOpen') == true){
-				// NOTE: Do we ever get here?
-				$('#messagebox').dialog('moveToTop');
-			}else{
-				$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'messagebox.php'), function(){
-					$('#messagebox').dialog(
-						{
-							autoOpen: true,
-							title: title,
-							buttons: [{
-										text: "Ok",
-										click: function() { $(this).dialog("close"); }
-									}],
-							close: function(event, ui) {
-								$(this).dialog('destroy').remove();
-							},
-							open: function(event, ui) {
-								$('#messagebox_msg').html(msg);
-							}
-					});
-				});
-			};
-		},
 		loadListHandlers:function() {
 			//$('.add,.delete').hide();
 			$('.globe,.mail,.delete,.edit').tipsy();
@@ -182,6 +252,12 @@ Contacts={
 			$('#bday').datepicker({
 						dateFormat : 'dd-mm-yy'
 			});
+			$('#categories_value').find('select').multiselect({
+										noneSelectedText: t('contacts', 'Select categories'),
+										header: false,
+										selectedList: 6,
+										classes: 'categories'
+									});
 			// Style phone types
 			$('#phonelist').find('select.contacts_property').multiselect({
 													noneSelectedText: t('contacts', 'Select type'),
@@ -223,6 +299,7 @@ Contacts={
 					click: function() { $(this).dialog('close'); }
 				}
 			] );
+			//$('#categories').multiple_autocomplete({source: categories});
 			Contacts.UI.loadListHandlers();
 		},
 		Card:{
@@ -252,12 +329,12 @@ Contacts={
 									if(jsondata.status == 'success'){
 										Contacts.UI.Card.loadContact(jsondata.data);
 									} else{
-										Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+										OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 									}
 								});
 							}
 						} else{
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						}
 					});
 				}
@@ -269,7 +346,7 @@ Contacts={
 							$('#rightcontent').data('id','');
 							$('#rightcontent').html(jsondata.data.page);
 						} else {
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						}
 					});
 				}
@@ -309,7 +386,7 @@ Contacts={
 								
 							}
 							else{
-								Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+								OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 								//alert(jsondata.data.message);
 							}
 						});
@@ -318,7 +395,7 @@ Contacts={
 						// TODO: Add to contacts list.
 					}
 					else{
-						Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						//alert(jsondata.data.message);
 					}
 				});
@@ -344,13 +421,13 @@ Contacts={
 									$('#rightcontent').html(jsondata.data.page);
 								}
 								else{
-									Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+									OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 								}
 							});
 						}
 					}
 					else{
-						Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						//alert(jsondata.data.message);
 					}
 				});
@@ -361,7 +438,7 @@ Contacts={
 				this.data = jsondata;
 				this.id = this.data.id;
 				$('#rightcontent').data('id',this.id);
-				//console.log('loaded: ' + this.data.FN[0]['value']);
+				console.log('loaded: ' + this.data.FN[0]['value']);
 				this.populateNameFields();
 				this.loadCategories();
 				this.loadPhoto();
@@ -369,9 +446,19 @@ Contacts={
 				this.loadPhones();
 				this.loadAddresses();
 				this.loadSingleProperties();
+				// TODO: load NOTE ;-)
+				if(this.data.NOTE) {
+					$('#note').data('checksum', this.data.NOTE[0]['checksum']);
+					$('#note').find('textarea').val(this.data.NOTE[0]['value']);
+					$('#note').show();
+				} else {
+					$('#note').data('checksum', '');
+					$('#note').find('textarea').val('');
+					$('#note').hide();
+				}
 			},
 			loadSingleProperties:function() {
-				var props = ['BDAY', 'NICKNAME', 'ORG'];
+				var props = ['BDAY', 'NICKNAME', 'ORG']; //, 'CATEGORIES'];
 				// Clear all elements
 				$('#ident .propertycontainer').each(function(){
 					if(props.indexOf($(this).data('element')) > -1) {
@@ -407,6 +494,12 @@ Contacts={
 								$('#contact_identity').find('#org_label').show();
 								$('#contact_identity').find('#org_value').show();
 								break;
+							/*case 'CATEGORIES':
+								$('#contact_identity').find('#categories').val(value);
+								$('#contact_identity').find('#categories_value').data('checksum', checksum);
+								$('#contact_identity').find('#categories_label').show();
+								$('#contact_identity').find('#categories_value').show();
+								break;*/
 						}
 					} else {
 						$('#contacts_propertymenu a[data-type="'+props[prop]+'"]').parent().show();
@@ -460,36 +553,84 @@ Contacts={
 				$('#contact_identity').find('*[data-element="FN"]').data('checksum', this.data.FN[0]['checksum']);
 				$('#contact_identity').show();
 			},
-			loadCategories:function(){
+			hasCategory:function(category) {
 				if(this.data.CATEGORIES) {
-					//
+					for(var c in this.data.CATEGORIES[0]['value']) {
+						var cat = this.data.CATEGORIES[0]['value'][c];
+						//console.log('hasCategory: ' + cat + ' === ' + category + '?');
+						if(typeof cat === 'string' && (cat.toUpperCase() === category.toUpperCase())) {
+							//console.log('Yes');
+							return true;
+						}
+					}
 				}
+				return false;
+			},
+			categoriesChanged:function(categories) { // Categories added/deleted.
+				console.log('categoriesChanged for ' + Contacts.UI.Card.id + ' : ' + categories);
+				var categorylist = $('#categories_value').find('select');
+				categorylist.find('option').remove();
+				for(var category in categories) {
+					console.log('categoriesChanged: ' + categories[category]);
+					var selected = Contacts.UI.Card.hasCategory(categories[category]) ? ' selected="selected"' : '';
+					var item = '<option value="' + categories[category] + '"' + selected + '>' + categories[category] + '</option>';
+					$(item).appendTo(categorylist);
+				}
+				$('#categories_value').find('select').multiselect('refresh');
+				$.getJSON(OC.filePath('contacts', 'ajax', 'categories/checksumfor.php'),{'id':Contacts.UI.Card.id},function(jsondata){
+					if(jsondata.status == 'success'){
+						console.log('Setting checksum: ' + jsondata.data.checksum);
+						$('#categories_value').data('checksum', jsondata.data.checksum);
+					} else {
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
+					}
+				});
+			},
+			loadCategories:function(){ // On loading contact.
+				var categories = $('#categories_value').find('select');
+				if(this.data.CATEGORIES) {
+					$('#categories_value').data('checksum', this.data.CATEGORIES[0]['checksum']);
+				} else {
+					$('#categories_value').data('checksum', '');
+				}
+				categories.find('option').each(function(){ 
+					if(Contacts.UI.Card.hasCategory($(this).val())) {
+						$(this).attr('selected', 'selected');
+					} else {
+						$(this).removeAttr('selected');
+					}
+				});
+				categories.multiselect('refresh');
 			},
 			editNew:function(){ // add a new contact
 				this.id = ''; this.fn = ''; this.fullname = ''; this.givname = ''; this.famname = ''; this.addname = ''; this.honpre = ''; this.honsuf = '';
-				$.getJSON('ajax/newcontact.php',{},function(jsondata){
+				$.getJSON(OC.filePath('contacts', 'ajax', 'newcontact.php'),{},function(jsondata){
 					if(jsondata.status == 'success'){
 						id = '';
 						$('#rightcontent').data('id','');
 						$('#rightcontent').html(jsondata.data.page);
 						Contacts.UI.Card.editName();
-					}
-					else{
-						Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+					} else {
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						//alert(jsondata.data.message);
 					}
 				});
 			},
 			savePropertyInternal:function(name, fields, oldchecksum, checksum){
 				// TODO: Add functionality for new fields.
-				//console.log('savePropertyInternal: ' + name + ', checksum: ' + checksum);
-				//console.log('savePropertyInternal: ' + this.data[name]);
+				console.log('savePropertyInternal: ' + name + ', fields: ' + fields + 'checksum: ' + checksum);
+				console.log('savePropertyInternal: ' + this.data[name]);
+				var multivalue = ['CATEGORIES'];
 				var params = {};
-				var value = undefined;
+				var value = multivalue.indexOf(name) != -1 ? new Array() : undefined;
 				jQuery.each(fields, function(i, field){
 					//.substring(11,'parameters[TYPE][]'.indexOf(']'))
 					if(field.name.substring(0, 5) === 'value') {
-						value = field.value;
+						if(multivalue.indexOf(name) != -1) {
+							value.push(field.value);
+						} else {
+							value = field.value;
+						}
 					} else if(field.name.substring(0, 10) === 'parameters') {
 						var p = field.name.substring(11,'parameters[TYPE][]'.indexOf(']'));
 						if(!(p in params)) {
@@ -509,11 +650,11 @@ Contacts={
 			saveProperty:function(obj){
 				// I couldn't get the selector to filter on 'contacts_property' so I filter by hand here :-/
 				if(!$(obj).hasClass('contacts_property')) {
-					//console.log('Filtering out object.' + obj);
+					console.log('Filtering out object.' + obj);
 					return false;
 				}
 				if($(obj).hasClass('nonempty') && $(obj).val().trim() == '') {
-					Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', 'This property has to be non-empty.'));
+					OC.dialogs.alert(t('contacts', 'This property has to be non-empty.'), t('contacts', 'Error'));
 					return false;
 				}
 				container = $(obj).parents('.propertycontainer').first(); // get the parent holding the metadata.
@@ -532,32 +673,38 @@ Contacts={
 				if(checksum != undefined && checksum != '') { // save
 					q = q + '&checksum=' + checksum;
 					console.log('Saving: ' + q);
+					$(obj).attr('disabled', 'disabled');
 					$.post('ajax/saveproperty.php',q,function(jsondata){
 						if(jsondata.status == 'success'){
 							container.data('checksum', jsondata.data.checksum);
 							Contacts.UI.Card.savePropertyInternal(name, fields, checksum, jsondata.data.checksum);
 							Contacts.UI.loading(container, false);
+							$(obj).removeAttr('disabled');
 							return true;
 						}
 						else{
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 							Contacts.UI.loading(container, false);
+							$(obj).removeAttr('disabled');
 							return false;
 						}
 					},'json');
 				} else { // add
 					console.log('Adding: ' + q);
+					$(obj).attr('disabled', 'disabled');
 					$.post('ajax/addproperty.php',q,function(jsondata){
 						if(jsondata.status == 'success'){
 							container.data('checksum', jsondata.data.checksum);
 							// TODO: savePropertyInternal doesn't know about new fields
 							//Contacts.UI.Card.savePropertyInternal(name, fields, checksum, jsondata.data.checksum);
 							Contacts.UI.loading(container, false);
+							$(obj).removeAttr('disabled');
 							return true;
 						}
 						else{
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 							Contacts.UI.loading(container, false);
+							$(obj).removeAttr('disabled');
 							return false;
 						}
 					},'json');
@@ -568,10 +715,14 @@ Contacts={
 				console.log('addProperty:' + type);
 				switch (type) {
 					case 'PHOTO':
-						this.loadPhoto();
+						this.loadPhoto(true);
 						$('#file_upload_form').show();
 						$('#contacts_propertymenu a[data-type="'+type+'"]').parent().hide();
 						break;
+					case 'NOTE':
+						$('#note').show();
+						$('#contacts_propertymenu a[data-type="'+type+'"]').parent().hide();
+						break;
 					case 'EMAIL':
 						if($('#emaillist>li').length == 1) {
 							$('#emails').show();
@@ -596,6 +747,7 @@ Contacts={
 					case 'NICKNAME':
 					case 'ORG':
 					case 'BDAY':
+					case 'CATEGORIES':
 						$('dl dt[data-element="'+type+'"],dd[data-element="'+type+'"]').show();
 						$('#contacts_propertymenu a[data-type="'+type+'"]').parent().hide();
 						break;
@@ -619,13 +771,13 @@ Contacts={
 								$('#contacts_propertymenu a[data-type="'+proptype+'"]').parent().show();
 								Contacts.UI.loading(obj, false);
 							} else {
-								Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', '\'deleteProperty\' called without type argument. Please report at bugs.owncloud.org'));
+								OC.dialogs.alert(t('contacts', '\'deleteProperty\' called without type argument. Please report at bugs.owncloud.org'), t('contacts', 'Error'));
 								Contacts.UI.loading(obj, false);
 							}
 						}
 						else{
 							Contacts.UI.loading(obj, false);
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						}
 					});
 				} else { // Property hasn't been saved so there's nothing to delete.
@@ -640,7 +792,7 @@ Contacts={
 						$('#contacts_propertymenu a[data-type="'+proptype+'"]').parent().show();
 						Contacts.UI.loading(obj, false);
 					} else {
-						Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', '\'deleteProperty\' called without type argument. Please report at bugs.owncloud.org'));
+						OC.dialogs.alert(t('contacts', '\'deleteProperty\' called without type argument. Please report at bugs.owncloud.org'), t('contacts', 'Error'));
 					}
 				}
 			},
@@ -650,8 +802,9 @@ Contacts={
 				if($('#edit_name_dialog').dialog('isOpen') == true){
 					$('#edit_name_dialog').dialog('moveToTop');
 				}else{ // TODO: If id=='' call addcontact.php (or whatever name) instead and reload view with id.
-					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'editname.php')+'?id='+this.id, function(){
-						$('#edit_name_dialog' ).dialog({
+					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'editname.php')+'?id='+this.id, function(jsondata){
+						if(jsondata.status != 'error'){
+							$('#edit_name_dialog' ).dialog({
 								modal: (isnew && true || false),
 								closeOnEscape: (isnew == '' && false || true),
 								title:  (isnew && t('contacts', 'Add contact') || t('contacts', 'Edit name')),
@@ -670,7 +823,10 @@ Contacts={
 								open : function(event, ui) {
 									// load 'N' property - maybe :-P
 								}*/
-						});
+							});
+						} else {
+							alert(jsondata.data.message);
+						}
 					});
 				}
 			},
@@ -695,7 +851,14 @@ Contacts={
 
 				$('#fn_select option').remove();
 				//$('#fn_select').combobox('value', this.fn);
-				var names = [this.fullname, this.givname + ' ' + this.famname, this.famname + ' ' + this.givname, this.famname + ', ' + this.givname];
+				var tmp = [this.fullname, this.givname + ' ' + this.famname, this.famname + ' ' + this.givname, this.famname + ', ' + this.givname];
+				var names = new Array();
+				for(var name in tmp) {
+					console.log('idx: ' + names.indexOf(tmp[name]));
+					if(names.indexOf(tmp[name]) == -1) {
+						names.push(tmp[name]);
+					}
+				}
 				$.each(names, function(key, value) {
 					$('#fn_select')
 						.append($('<option></option>')
@@ -774,8 +937,9 @@ Contacts={
 				if($('#edit_address_dialog').dialog('isOpen') == true){
 					$('#edit_address_dialog').dialog('moveToTop');
 				}else{
-					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'editaddress.php')+q, function(){
-						$('#edit_address_dialog' ).dialog({
+					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'editaddress.php')+q, function(jsondata){
+						if(jsondata.status != 'error'){
+							$('#edit_address_dialog' ).dialog({
 								/*modal: true,*/
 								height: 'auto', width: 'auto',
 								buttons: {
@@ -806,7 +970,10 @@ Contacts={
 								open : function(event, ui) {
 									// load 'ADR' property - maybe :-P
 								}*/
-						});
+							});
+						} else {
+							alert(jsondata.data.message);
+						}
 					});
 				}
 			},
@@ -846,7 +1013,7 @@ Contacts={
 			},
 			uploadPhoto:function(filelist) {
 				if(!filelist) {
-					Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts','No files selected for upload.'));
+					OC.dialogs.alert(t('contacts','No files selected for upload.'), t('contacts', 'Error'));
 					return;
 				}
 				//var file = filelist.item(0);
@@ -855,7 +1022,7 @@ Contacts={
 				var form = $('#file_upload_form');
 				var totalSize=0;
 				if(file.size > $('#max_upload').val()){
-					Contacts.UI.messageBox(t('Upload too large'), t('contacts','The file you are trying to upload exceed the maximum size for file uploads on this server.'));
+					OC.dialogs.alert(t('contacts','The file you are trying to upload exceed the maximum size for file uploads on this server.'), t('contacts', 'Error'));
 					return;
 				} else {
 					target.load(function(){
@@ -864,21 +1031,21 @@ Contacts={
 							Contacts.UI.Card.editPhoto(response.data.id, response.data.tmp);
 							//alert('File: ' + file.tmp + ' ' + file.name + ' ' + file.mime);
 						}else{
-							Contacts.UI.messageBox(t('contacts', 'Error'), response.data.message);
+							OC.dialogs.alert(response.data.message, t('contacts', 'Error'));
 						}
 					});
 					form.submit();
 				}
 			},
-			loadPhoto:function(){
-				if(this.data.PHOTO) {
+			loadPhoto:function(force){
+				if(this.data.PHOTO||force==true) {
 					$.getJSON('ajax/loadphoto.php',{'id':this.id},function(jsondata){
 						if(jsondata.status == 'success'){
 							//alert(jsondata.data.page);
 							$('#contacts_details_photo_wrapper').html(jsondata.data.page);
 						}
 						else{
-							Contacts.UI.messageBox(jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						}
 					});
 					$('#file_upload_form').show();
@@ -897,7 +1064,7 @@ Contacts={
 						$('#edit_photo_dialog_img').html(jsondata.data.page);
 					}
 					else{
-						Contacts.UI.messageBox(jsondata.data.message);
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 					}
 				});
 				if($('#edit_photo_dialog').dialog('isOpen') == true){
@@ -916,7 +1083,7 @@ Contacts={
 						// load cropped photo.
 						$('#contacts_details_photo_wrapper').html(response.data.page);
 					}else{
-						Contacts.UI.messageBox(t('contacts','Error'), response.data.message);
+						OC.dialogs.alert(response.data.message, t('contacts', 'Error'));
 					}
 				});
 				$('#contacts [data-id="'+this.id+'"]').find('a').css('background','url(thumbnail.php?id='+this.id+'&refresh=1'+Math.random()+') no-repeat');
@@ -1009,13 +1176,17 @@ Contacts={
 				if($('#chooseaddressbook_dialog').dialog('isOpen') == true){
 					$('#chooseaddressbook_dialog').dialog('moveToTop');
 				}else{
-					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'chooseaddressbook.php'), function(){
-						$('#chooseaddressbook_dialog').dialog({
-							width : 600,
-							close : function(event, ui) {
-								$(this).dialog('destroy').remove();
-							}
-						});
+					$('#dialog_holder').load(OC.filePath('contacts', 'ajax', 'chooseaddressbook.php'), function(jsondata){
+						if(jsondata.status != 'error'){
+							$('#chooseaddressbook_dialog').dialog({
+								width : 600,
+								close : function(event, ui) {
+									$(this).dialog('destroy').remove();
+								}
+							});
+						} else {
+							alert(jsondata.data.message);
+						}
 					});
 				}
 			},
@@ -1052,7 +1223,7 @@ Contacts={
 							Contacts.UI.Contacts.update();
 							Contacts.UI.Addressbooks.overview();
 						} else {
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 							//alert('Error: ' + data.message);
 						}
 					  });
@@ -1067,7 +1238,7 @@ Contacts={
 				var description = $("#description_"+bookid).val();
 				
 				if(displayname.length == 0) {
-					Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', 'Displayname cannot be empty.'));
+					OC.dialogs.alert(t('contacts', 'Displayname cannot be empty.'), t('contacts', 'Error'));
 					return false;
 				}
 				var url;
@@ -1082,7 +1253,7 @@ Contacts={
 							$(button).closest('tr').prev().html(jsondata.page).show().next().remove();
 							Contacts.UI.Contacts.update();
 						} else {
-							Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+							OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						}
 					});
 			},
@@ -1102,7 +1273,7 @@ Contacts={
 						Contacts.UI.Card.update();
 					}
 					else{
-						Contacts.UI.messageBox(t('contacts', 'Error'),jsondata.data.message);
+						OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 						//alert(jsondata.data.message);
 					}
 				});
@@ -1124,6 +1295,7 @@ Contacts={
 $(document).ready(function(){
 
 	Contacts.UI.loadHandlers();
+	Categories.changed = Contacts.UI.Card.categoriesChanged;
 
 	/**
 	 * Show the Addressbook chooser
@@ -1151,7 +1323,7 @@ $(document).ready(function(){
 				Contacts.UI.Card.loadContact(jsondata.data);
 			}
 			else{
-				Contacts.UI.messageBox(t('contacts', 'Error'), jsondata.data.message);
+				OC.dialogs.alert(jsondata.data.message, t('contacts', 'Error'));
 				//alert(jsondata.data.message);
 			}
 		});
@@ -1192,7 +1364,8 @@ $(document).ready(function(){
 	
 	// NOTE: For some reason the selector doesn't work when I select by '.contacts_property' too...
 	// I do the filtering in the event handler instead.
-	$('input[type="text"],input[type="checkbox"],input[type="email"],input[type="tel"],input[type="date"], select').live('change', function(){
+	//$('input[type="text"],input[type="checkbox"],input[type="email"],input[type="tel"],input[type="date"], select').live('change', function(){
+	$('.contacts_property').live('change', function(){
 		Contacts.UI.Card.saveProperty(this);
 	});
 
@@ -1249,17 +1422,17 @@ $(document).ready(function(){
 		var file = files[0];
 		console.log('size: '+file.size);
 		if(file.size > $('#max_upload').val()){
-			Contacts.UI.messageBox(t('contacts','Upload too large'), t('contacts','The file you are trying to upload exceed the maximum size for file uploads on this server.'));
+			OC.dialogs.alert(t('contacts','The file you are trying to upload exceed the maximum size for file uploads on this server.'), t('contacts','Upload too large'));
 			return;
 		}
 		if (file.type.indexOf("image") != 0) {
-			Contacts.UI.messageBox(t('contacts','Wrong file type'), t('contacts','Only image files can be used as profile picture.'));
+			OC.dialogs.alert(t('contacts','Only image files can be used as profile picture.'), t('contacts','Wrong file type'));
 			return;
 		}
 		var xhr = new XMLHttpRequest();
 
 		if (!xhr.upload) {
-			Contacts.UI.messageBox(t('contacts', 'Error'), t('contacts', 'Your browser doesn\'t support AJAX upload. Please click on the profile picture to select a photo to upload.'))
+			OC.dialogs.alert(t('contacts', 'Your browser doesn\'t support AJAX upload. Please click on the profile picture to select a photo to upload.'), t('contacts', 'Error'))
 		}
 		fileUpload = xhr.upload,
 		xhr.onreadystatechange = function() {
@@ -1269,11 +1442,11 @@ $(document).ready(function(){
 					if(xhr.status == 200) {
 						Contacts.UI.Card.editPhoto(response.data.id, response.data.tmp);
 					} else {
-						Contacts.UI.messageBox(t('contacts', 'Error'), xhr.status + ': ' + xhr.responseText);
+						OC.dialogs.alert(xhr.status + ': ' + xhr.responseText, t('contacts', 'Error'));
 					}
 				} else {
 					//alert(xhr.responseText);
-					Contacts.UI.messageBox(t('contacts', 'Error'), response.data.message);
+					OC.dialogs.alert(response.data.message, t('contacts', 'Error'));
 				}
 				// stop loading indicator
 				//$('#contacts_details_photo_progress').hide();
@@ -1301,8 +1474,19 @@ $(document).ready(function(){
 		xhr.send(file);
 	}
 
+	$('body').live('click',function(e){
+		if(!$(e.target).is('#contacts_propertymenu_button')) {
+			$('#contacts_propertymenu').hide();
+		}
+	});
 	$('#contacts_propertymenu_button').live('click',function(){
-		$('#contacts_propertymenu').is(':hidden') && $('#contacts_propertymenu').slideDown() || $('#contacts_propertymenu').slideUp();
+		var menu = $('#contacts_propertymenu');
+		if(menu.is(':hidden')) {
+			menu.show();
+			menu.find('ul').focus();
+		} else {
+			menu.hide();
+		}
 	});
 	$('#contacts_propertymenu a').live('click',function(){
 		Contacts.UI.Card.addProperty(this);
diff --git a/apps/contacts/js/jquery.multi-autocomplete.js b/apps/contacts/js/jquery.multi-autocomplete.js
new file mode 100644
index 0000000000000000000000000000000000000000..4be8d901c96076c02e84989bbe3bebc30246db78
--- /dev/null
+++ b/apps/contacts/js/jquery.multi-autocomplete.js
@@ -0,0 +1,47 @@
+/**
+ * Inspired by http://jqueryui.com/demos/autocomplete/#multiple
+ */
+
+(function( $ ) {
+	$.widget('ui.multiple_autocomplete', {
+		_create: function() {
+			function split( val ) {
+				return val.split( /,\s*/ );
+			}
+			function extractLast( term ) {
+				return split( term ).pop();
+			}
+			//console.log('_create: ' + this.options['id']);
+			var self = this;
+			this.element.bind( "keydown", function( event ) {
+				if ( event.keyCode === $.ui.keyCode.TAB &&
+						$( this ).data( "autocomplete" ).menu.active ) {
+					event.preventDefault();
+				}
+			})
+			.autocomplete({
+				minLength: 0,
+				source: function( request, response ) {
+					// delegate back to autocomplete, but extract the last term
+					response( $.ui.autocomplete.filter(
+						self.options.source, extractLast( request.term ) ) );
+				},
+				focus: function() {
+					// prevent value inserted on focus
+					return false;
+				},
+				select: function( event, ui ) {
+					var terms = split( this.value );
+					// remove the current input
+					terms.pop();
+					// add the selected item
+					terms.push( ui.item.value );
+					// add placeholder to get the comma-and-space at the end
+					terms.push( "" );
+					this.value = terms.join( ", " );
+					return false;
+				}
+			});
+		},
+	});
+})( jQuery );
diff --git a/apps/contacts/lib/app.php b/apps/contacts/lib/app.php
index e8c3087c8a2f97fdec13fffb6dde4a0b290603d5..cc33c733007247cb5d1448b2984f694b50e851cd 100644
--- a/apps/contacts/lib/app.php
+++ b/apps/contacts/lib/app.php
@@ -10,8 +10,10 @@
  * This class manages our app actions
  */
 OC_Contacts_App::$l10n = new OC_L10N('contacts');
+OC_Contacts_App::$categories = new OC_VCategories('contacts');
 class OC_Contacts_App {
 	public static $l10n;
+	public static $categories;
 
 	/**
 	* Render templates/part.details to json output
@@ -153,6 +155,10 @@ class OC_Contacts_App {
 		}
 	}
 
+	public static function getCategories() {
+		return self::$categories->categories();
+	}
+
 	public static function setLastModifiedHeader($contact) {
 		$rev = $contact->getAsString('REV');
 		if ($rev) {
diff --git a/apps/contacts/lib/vcard.php b/apps/contacts/lib/vcard.php
index eade8859e76c8c63aafefc1b694bbf08bcc2efdb..a7e1817d7c8302ae69e3f1c5afa43596d7552d8f 100644
--- a/apps/contacts/lib/vcard.php
+++ b/apps/contacts/lib/vcard.php
@@ -4,6 +4,7 @@
  *
  * @author Jakob Sack
  * @copyright 2011 Jakob Sack mail@jakobsack.de
+ * @copyright 2012 Thomas Tanghus <thomas@tanghus.net>
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -233,6 +234,8 @@ class OC_Contacts_VCard{
 			return null;
 		};
 
+		OC_Contacts_App::$categories->loadFromVObject($card);
+
 		self::updateValuesFromAdd($card);
 
 		$fn = $card->getAsString('FN');
@@ -267,6 +270,29 @@ class OC_Contacts_VCard{
 		return self::add($id, $data, $uri);
 	}
 
+	/**
+	 * @brief Mass updates an array of cards
+	 * @param array $objects  An array of [id, carddata].
+	 */
+	public static function updateDataByID($objects){
+		$stmt = OC_DB::prepare( 'UPDATE *PREFIX*contacts_cards SET carddata = ?, lastmodified = ? WHERE id = ?' );
+		$now = new DateTime;
+		foreach($objects as $object) {
+			$vcard = OC_VObject::parse($object[1]);
+			if(!is_null($vcard)){
+				$vcard->setString('REV', $now->format(DateTime::W3C));
+				$data = $vcard->serialize();
+				try {
+					$result = $stmt->execute(array($data,time(),$object[0]));
+					//OC_Log::write('contacts','OC_Contacts_VCard::updateDataByID, id: '.$object[0].': '.$object[1],OC_Log::DEBUG);
+				} catch(Exception $e) {
+					OC_Log::write('contacts','OC_Contacts_VCard::updateDataByID:, exception: '.$e->getMessage(),OC_Log::DEBUG);
+					OC_Log::write('contacts','OC_Contacts_VCard::updateDataByID, id: '.$object[0],OC_Log::DEBUG);
+				}
+			}
+		}
+	}
+
 	/**
 	 * @brief edits a card
 	 * @param integer $id id of card
@@ -280,6 +306,8 @@ class OC_Contacts_VCard{
 			return false;
 		}
 
+		OC_Contacts_App::$categories->loadFromVObject($card);
+
 		$fn = $card->getAsString('FN');
 		if (empty($fn)) {
 			$fn = null;
@@ -338,6 +366,43 @@ class OC_Contacts_VCard{
 		return true;
 	}
 
+	/**
+	 * @brief Escapes delimiters from an array and returns a string.
+	 * @param array $value
+	 * @param char $delimiter
+	 * @return string
+	 */
+	public static function escapeDelimiters($value, $delimiter=';') {
+		foreach($value as &$i ) {
+			$i = implode("\\$delimiter", explode($delimiter, $i));
+		}
+		return implode($delimiter, $value);
+	}
+
+
+	/**
+	 * @brief Creates an array out of a multivalue property
+	 * @param string $value
+	 * @param char $delimiter
+	 * @return array
+	 */
+	public static function unescapeDelimiters($value, $delimiter=';') {
+		$array = explode($delimiter,$value);
+		for($i=0;$i<count($array);$i++) {
+			if(substr($array[$i],-1,1)=="\\") {
+				if(isset($array[$i+1])) {
+					$array[$i] = substr($array[$i],0,count($array[$i])-2).$delimiter.$array[$i+1];
+					unset($array[$i+1]);
+				} else {
+					$array[$i] = substr($array[$i],0,count($array[$i])-2).$delimiter;
+				}
+				$i = $i - 1;
+			}
+		}
+		$array = array_map('trim', $array);
+		return $array;
+	}
+
 	/**
 	 * @brief Data structure of vCard
 	 * @param object $property
@@ -376,7 +441,9 @@ class OC_Contacts_VCard{
 		$value = $property->value;
 		//$value = htmlspecialchars($value);
 		if($property->name == 'ADR' || $property->name == 'N'){
-			$value = OC_VObject::unescapeSemicolons($value);
+			$value = self::unescapeDelimiters($value);
+		} elseif($property->name == 'CATEGORIES') {
+			$value = self::unescapeDelimiters($value, ',');
 		}
 		$temp = array(
 			'name' => $property->name,
diff --git a/apps/contacts/templates/index.php b/apps/contacts/templates/index.php
index e81597f23d6df00680360e38b9c6b78d8b625173..efd797e25cb3d8d23bc3496c50ac58fdaa4fcdd6 100644
--- a/apps/contacts/templates/index.php
+++ b/apps/contacts/templates/index.php
@@ -1,5 +1,6 @@
 <script type='text/javascript'>
 	var totalurl = '<?php echo OC_Helper::linkToAbsolute('contacts', 'carddav.php'); ?>/addressbooks';
+	var categories = <?php echo json_encode($_['categories']); ?>;
 </script>
 <div id="controls">
 	<form>
diff --git a/apps/contacts/templates/part.contact.php b/apps/contacts/templates/part.contact.php
index 5be20964f4bb153f10c3d053ca34db60c2826025..67838238e2b3a9d02903b5b73e72529472410121 100644
--- a/apps/contacts/templates/part.contact.php
+++ b/apps/contacts/templates/part.contact.php
@@ -13,6 +13,8 @@ $id = isset($_['id']) ? $_['id'] : '';
 		<li><a data-type="TEL"><?php echo $l->t('Phone'); ?></a></li>
 		<li><a data-type="EMAIL"><?php echo $l->t('Email'); ?></a></li>
 		<li><a data-type="ADR"><?php echo $l->t('Address'); ?></a></li>
+		<li><a data-type="NOTE"><?php echo $l->t('Note'); ?></a></li>
+		<li><a data-type="CATEGORIES"><?php echo $l->t('Categories'); ?></a></li>
 	</ul>
 	</div>
 	<img  onclick="Contacts.UI.Card.export();" class="svg action" id="contacts_downloadcard" src="<?php echo image_path('', 'actions/download.svg'); ?>" title="<?php echo $l->t('Download contact');?>" />
@@ -45,7 +47,7 @@ $id = isset($_['id']) ? $_['id'] : '';
 		<dt><label for="fn"><?php echo $l->t('Display name'); ?></label></dt>
 		<dd class="propertycontainer" data-element="FN">
 		<select id="fn_select" title="<?php echo $l->t('Format custom, Short name, Full name, Reverse or Reverse with comma'); ?>" style="width:16em;">
-		</select><a id="edit_name" class="edit" title="<?php echo $l->t('Edit name details'); ?>"></a>
+		</select><a id="edit_name" class="action edit" title="<?php echo $l->t('Edit name details'); ?>"></a>
 		</dd>
 		<dt style="display:none;" id="org_label" data-element="ORG"><label for="org"><?php echo $l->t('Organization'); ?></label></dt>
 		<dd style="display:none;" class="propertycontainer" id="org_value" data-element="ORG"><input id="org"  required="required" name="value[ORG]" type="text" class="contacts_property" style="width:16em;" name="value" value="" placeholder="<?php echo $l->t('Organization'); ?>" /><a class="delete" onclick="$(this).tipsy('hide');Contacts.UI.Card.deleteProperty(this, 'single');" title="<?php echo $l->t('Delete'); ?>"></a></dd>
@@ -53,8 +55,21 @@ $id = isset($_['id']) ? $_['id'] : '';
 		<dd style="display:none;" class="propertycontainer" id="nickname_value" data-element="NICKNAME"><input id="nickname" required="required" name="value[NICKNAME]" type="text" class="contacts_property" style="width:16em;" name="value" value="" placeholder="<?php echo $l->t('Enter nickname'); ?>" /><a class="delete" onclick="$(this).tipsy('hide');Contacts.UI.Card.deleteProperty(this, 'single');" title="<?php echo $l->t('Delete'); ?>"></a></dd>
 		<dt style="display:none;" id="bday_label" data-element="BDAY"><label for="bday"><?php echo $l->t('Birthday'); ?></label></dt>
 		<dd style="display:none;" class="propertycontainer" id="bday_value" data-element="BDAY"><input id="bday"  required="required" name="value" type="text" class="contacts_property" value="" placeholder="<?php echo $l->t('dd-mm-yyyy'); ?>" /><a class="delete" onclick="$(this).tipsy('hide');Contacts.UI.Card.deleteProperty(this, 'single');" title="<?php echo $l->t('Delete'); ?>"></a></dd>
+		<dt id="categories_label" data-element="CATEGORIES"><label for="categories"><?php echo $l->t('Categories'); ?></label></dt>
+		<dd class="propertycontainer" id="categories_value" data-element="CATEGORIES">
+			<select class="contacts_property" multiple="multiple" id="categories" name="value[]">
+				<?php echo html_select_options($_['categories'], array(), array('combine'=>true)) ?>
+			</select>
+			<a class="action edit" onclick="$(this).tipsy('hide');Categories.edit();" title="<?php echo $l->t('Edit categories'); ?>"></a>
+		</dd>
+		<!-- dt style="display:none;" id="categories_label" data-element="CATEGORIES"><label for="categories"><?php echo $l->t('Categories'); ?></label></dt>
+		<dd style="display:none;" class="propertycontainer" id="categories_value" data-element="CATEGORIES"><input id="categories"  required="required" name="value[CATEGORIES]" type="text" class="contacts_property" style="width:16em;" name="value" value="" placeholder="<?php echo $l->t('Categories'); ?>" /><a class="delete" onclick="$(this).tipsy('hide');Contacts.UI.Card.deleteProperty(this, 'single');" title="<?php echo $l->t('Delete'); ?>"></a><a class="action edit" onclick="$(this).tipsy('hide');Categories.edit();" title="<?php echo $l->t('Edit categories'); ?>"></a></dd -->
 	</dl>
 	</fieldset>
+	<fieldset id="note" class="formfloat propertycontainer" style="display:none;" data-element="NOTE">
+	<legend><?php echo $l->t('Note'); ?></legend>
+	<textarea class="contacts_property note" name="value"></textarea>
+	</fieldset>
 	</form>
 	</div>
 
diff --git a/lib/vcategories.php b/lib/vcategories.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a31a5268d1d37a609828077471d24ebfd9cb846
--- /dev/null
+++ b/lib/vcategories.php
@@ -0,0 +1,209 @@
+<?php
+/**
+* ownCloud
+*
+* @author Thomas Tanghus
+* @copyright 2012 Thomas Tanghus <thomas@tanghus.net>
+* @copyright 2012 Bart Visscher bartv@thisnet.nl
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+
+/**
+ * Class for easy access to categories in VCARD, VEVENT, VTODO and VJOURNAL.
+ * A Category can be e.g. 'Family', 'Work', 'Chore', 'Special Occation' or
+ * anything else that is either parsed from a vobject or that the user chooses
+ * to add.
+ * Category names are not case-sensitive, but will be saved with the case they
+ * are entered in. If a user already has a category 'family' for an app, and
+ * tries to add a category named 'Family' it will be silently ignored.
+ * NOTE: There is a limitation in that the the configvalue field in the
+ * preferences table is a varchar(255).
+ */
+class OC_VCategories {
+	const PREF_CATEGORIES_LABEL = 'extra_categories';
+	/**
+	 * Categories
+	 */
+	private $categories = array();
+
+	private $app = null;
+	private $user = null;
+
+	/**
+	* @brief Constructor.
+	* @param $app The application identifier e.g. 'contacts' or 'calendar'.
+	* @param $user The user whos data the object will operate on. This
+	*   parameter should normally be omitted but to make an app able to
+	*   update categories for all users it is made possible to provide it.
+	* @param $defcategories An array of default categories to be used if none is stored.
+	* NOTE: Not implemented.
+	*/
+	public function __construct($app, $user=null, $defcategories=null) {
+		$this->app = $app;
+		$this->user = is_null($user) ? OC_User::getUser() : $user;
+		$categories = trim(OC_Preferences::getValue($this->user, $app, self::PREF_CATEGORIES_LABEL, ''));
+		$this->categories = $categories != '' ? unserialize($categories) : array();
+	}
+
+	/**
+	* @brief Get the categories for a specific user.
+	* @returns array containing the categories as strings.
+	*/
+	public function categories() {
+		OC_Log::write('core','OC_VCategories::categories: '.print_r($this->categories, true), OC_Log::DEBUG);
+		return $this->categories;
+	}
+
+	/**
+	* @brief Checks whether a category is already saved.
+	* @param $name The name to check for.
+	* @returns bool
+	*/
+	public function hasCategory($name) {
+		return $this->in_arrayi($name, $this->categories);
+	}
+
+	/**
+	* @brief Add a new category name.
+	* @param $names A string with a name or an array of strings containing
+	* the name(s) of the categor(y|ies) to add.
+	* @param $sync bool When true, save the categories
+	* @returns bool Returns false on error.
+	*/
+	public function add($names, $sync=false) {
+		if(!is_array($names)) {
+			$names = array($names);
+		}
+		$names = array_map('trim', $names);
+		$newones = array();
+		foreach($names as $name) {
+			if(($this->in_arrayi($name, $this->categories) == false) && $name != '') {
+				$newones[] = $name;
+			}
+		}
+		if(count($newones) > 0) {
+			$this->categories = array_merge($this->categories, $newones);
+			natcasesort($this->categories); // Dunno if this is necessary
+			if($sync === true) {
+				$this->save();
+			}
+		}
+		return true;
+	}
+
+	/**
+	* @brief Extracts categories from a vobject and add the ones not already present.
+	* @param $vobject The instance of OC_VObject to load the categories from.
+	*/
+	public function loadFromVObject($vobject, $sync=false) {
+		$this->add($vobject->getAsArray('CATEGORIES'), $sync);
+	}
+
+	/**
+	* @brief Reset saved categories and rescan supplied vobjects for categories.
+	* @param $objects An array of vobjects (as text).
+	* To get the object array, do something like:
+	*	// For Addressbook:
+	*	$categories = new OC_VCategories('contacts');
+	*	$stmt = OC_DB::prepare( 'SELECT carddata FROM *PREFIX*contacts_cards' );
+	*	$result = $stmt->execute();
+	*	$objects = array();
+	*	if(!is_null($result)) {
+	*		while( $row = $result->fetchRow()){
+	*			$objects[] = $row['carddata'];
+	*		}
+	*	}
+	* 	$categories->rescan($objects);
+	*/
+	public function rescan($objects, $sync=true) {
+		$this->categories = array();
+		foreach($objects as $object) {
+			//OC_Log::write('core','OC_VCategories::rescan: '.substr($object, 0, 100).'(...)', OC_Log::DEBUG);
+			$vobject = OC_VObject::parse($object);
+			if(!is_null($vobject)) {
+				$this->loadFromVObject($vobject, $sync);
+			} else {
+				OC_Log::write('core','OC_VCategories::rescan, unable to parse. ID: '.$value[0].', '.substr($value[1], 0, 50).'(...)', OC_Log::DEBUG);
+			}
+		}
+		$this->save();
+	}
+
+	/**
+	 * @brief Save the list with categories
+	 */
+	private function save() {
+		$escaped_categories = serialize($this->categories);
+		OC_Log::write('core','OC_VCategories::save: '.print_r($this->categories, true), OC_Log::DEBUG);
+		OC_Preferences::setValue($this->user, $this->app, self::PREF_CATEGORIES_LABEL, $escaped_categories);
+	}
+
+	/**
+	* @brief Delete categories from the db and from all the vobject supplied
+	* @param $names An array of categories to delete
+	* @param $objects An array of arrays with [id,vobject] (as text) pairs suitable for updating the apps object table.
+	*/
+	public function delete($names, array &$objects) {
+		if(!is_array($names)) {
+			$names = array($names);
+		}
+		OC_Log::write('core','OC_VCategories::delete, before: '.print_r($this->categories, true), OC_Log::DEBUG);
+		foreach($names as $name) {
+			OC_Log::write('core','OC_VCategories::delete: '.$name, OC_Log::DEBUG);
+			if($this->hasCategory($name)) {
+				OC_Log::write('core','OC_VCategories::delete: '.$name.' got it', OC_Log::DEBUG);
+				unset($this->categories[$this->array_searchi($name, $this->categories)]);
+			}
+		}
+		$this->save();
+		OC_Log::write('core','OC_VCategories::delete, after: '.print_r($this->categories, true), OC_Log::DEBUG);
+		foreach($objects as $key=>&$value) {
+			$vobject = OC_VObject::parse($value[1]);
+			if(!is_null($vobject)){
+				$categories = $vobject->getAsArray('CATEGORIES');
+				//OC_Log::write('core','OC_VCategories::delete, before: '.$key.': '.print_r($categories, true), OC_Log::DEBUG);
+				foreach($names as $name) {
+					$idx = $this->array_searchi($name, $categories);
+					OC_Log::write('core','OC_VCategories::delete, loop: '.$name.', '.print_r($idx, true), OC_Log::DEBUG);
+					if($idx !== false) {
+						OC_Log::write('core','OC_VCategories::delete, unsetting: '.$categories[$this->array_searchi($name, $categories)], OC_Log::DEBUG);
+						unset($categories[$this->array_searchi($name, $categories)]);
+						//unset($categories[$idx]);
+					}
+				}
+				OC_Log::write('core','OC_VCategories::delete, after: '.$key.': '.print_r($categories, true), OC_Log::DEBUG);
+				$vobject->setString('CATEGORIES', implode(',', $categories));
+				$value[1] = $vobject->serialize();
+				$objects[$key] = $value;
+			} else {
+				OC_Log::write('core','OC_VCategories::delete, unable to parse. ID: '.$value[0].', '.substr($value[1], 0, 50).'(...)', OC_Log::DEBUG);
+			}
+		}
+	}
+
+	// case-insensitive in_array
+	private function in_arrayi($needle, $haystack) {
+		return in_array(strtolower($needle), array_map('strtolower', $haystack));
+	}
+
+	// case-insensitive array_search
+	private function array_searchi($needle, $haystack) {
+		return array_search(strtolower($needle),array_map('strtolower',$haystack)); 
+	}
+
+}
+?>