Index: wp-includes/functions.php
===================================================================
--- wp-includes/functions.php	(revision 3022)
+++ wp-includes/functions.php	(working copy)
@@ -1188,6 +1188,7 @@
 }
 
 function get_posts($args) {
+	// support for multiple categories and negations added: 9-nov-05 drac
 	global $wpdb;
 	parse_str($args, $r);
 	if ( !isset($r['numberposts']) )
@@ -1200,16 +1201,67 @@
 		$r['orderby'] = 'post_date';
 	if ( !isset($r['order']) )
 		$r['order'] = 'DESC';
+	
+	$negation_char = '^';
+	$multiple_categories_present = false;
+	if( is_array($r['category']) ) {
+		$multiple_categories_present = true;
+	}
 
 	$now = current_time('mysql');
 
-	$posts = $wpdb->get_results(
-		"SELECT DISTINCT * FROM $wpdb->posts " .
+	$sql = "SELECT DISTINCT * FROM $wpdb->posts " .
 		( empty( $r['category'] ) ? "" : ", $wpdb->post2cat " ) .
-		" WHERE post_date <= '$now' AND (post_status = 'publish') ".
-		( empty( $r['category'] ) ? "" : "AND $wpdb->posts.ID = $wpdb->post2cat.post_id AND $wpdb->post2cat.category_id = " . $r['category']. " " ) .
-		" GROUP BY $wpdb->posts.ID ORDER BY " . $r['orderby'] . " " . $r['order'] . " LIMIT " . $r['offset'] . ',' . $r['numberposts'] );
+		" WHERE post_date <= '$now' AND (post_status = 'publish') ";
 
+	if ( false == $multiple_categories_present ) {	// check for a single category first
+
+		if ( strpos($r['category'], $negation_char) !== false ) {		// category is a negation. note: strpos requires a bool comparison operator
+			$r['category'] = strtr($r['category'], $negation_char, " ");
+			$sql .= ( empty( $r['category'] ) ? "" : "AND $wpdb->posts.ID = $wpdb->post2cat.post_id AND $wpdb->post2cat.category_id != " . $r['category']. " " );
+		}
+		else {													
+			$sql .= ( empty( $r['category'] ) ? "" : "AND $wpdb->posts.ID = $wpdb->post2cat.post_id AND $wpdb->post2cat.category_id = " . $r['category']. " " );
+		}
+	}
+
+	else {			// we have multiple categories to select from. yay
+		$included_categories = array();
+		$negated_categories = array();						
+		$have_categories = false;							// are any categories actually sent through to us ? this boolean will prevent unnecessary SQL being generated
+
+		foreach ( $r['category'] as $category_param_element ) {	// multiple category params are possible, so iterate through all of them
+			$cat_tokens = split(' ', $category_param_element);	
+			foreach ( $cat_tokens as $token ) {				
+				if ( strpos($token, $negation_char) !== false ) { // you are a category to be negated from display.	n.b: strpos wants a bool comparison operator
+					$token = strtr($token, $negation_char, " ");
+					array_push($negated_categories, $token);					
+				}
+				else {
+					array_push($included_categories, $token);					
+				}
+			}
+			if( ( !$have_categories ) && count ( $cat_tokens > 0 ) ) { 
+				$have_categories = true; 
+			}
+		}
+		
+		if ( $have_categories ) { // there is at least one category to be included or excluded
+			$sql .= "AND $wpdb->posts.ID = $wpdb->post2cat.post_id ";
+		}
+
+		if ( 0 != count ($included_categories)  )  { 
+			$sql .=  "AND $wpdb->post2cat.category_id in ( " . implode(",", $included_categories) . " ) " ;
+		}
+		if ( 0 != count($negated_categories) )  {
+			$sql .=  "AND $wpdb->post2cat.category_id not in ( " . implode(",", $negated_categories) . " ) ";
+		}
+	}
+
+	$sql .= " GROUP BY $wpdb->posts.ID ORDER BY " . $r['orderby'] . " " . $r['order'] . " LIMIT " . $r['offset'] . ',' . $r['numberposts'] ;
+	
+	$posts = $wpdb->get_results( $sql );
+
 	update_post_caches($posts);
 
 	return $posts;
