View Issue Details

IDProjectCategoryView StatusLast Update
0008274mantisbttaggingpublic2019-09-09 03:53
Reporterjreese Assigned Tojreese  
PrioritynormalSeverityfeatureReproducibilityN/A
Status closedResolutionfixed 
Product Version1.1.0a4 
Target Version1.1.0rc1Fixed in Version1.1.0rc1 
Summary0008274: Implement Keyword Tagging Features
Description

From the requirements page:

"One of the biggest movements in the modern web communities is the push towards ‘tagging’ articles and topics with short descriptive keywords that represent the content and can be used to find items with the same subject. Mantis currently can only classify issues by project and category, which can be limiting, and only allows one classification per issue. By implementing tagging, it would allow users to attach multiple keywords to each report, which could be new tags typed in manually or existing tags selected from a list, either through AJAX auto-completion, or a popup window of some fashion.

"Because of the ‘metadata’ nature of tagging, it would not be primary method of classification for issues (which would still be left to categories), and could allow for lower permission requirements for tagging reports, such as allowing any registered reporters to add tags at any time. It could also allow for potential custom auto-tagging features, where certain tags could be automatically attached to any bug containing special characteristics, like a report with version control checkins."

Additional Information

Requirements page: http://www.mantisbt.org/wiki/doku.php/mantisbt:tagging_requirements

TagsNo tags attached.

Relationships

has duplicate 0006381 closedgiallu Add tagging-feature for issues (multiple categories for one issue) 
related to 0026119 closeddregad Add $g_tag_create_threshold to Workflow Thresholds in the GUI 

Activities

2007-08-17 17:57

 

mantis-tagging-2007-08-17.patch (52,788 bytes)   
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/admin/schema.php mantis-tagging/admin/schema.php
--- mantis-cvs/admin/schema.php	2007-08-07 10:54:56.000000000 -0400
+++ mantis-tagging/admin/schema.php	2007-08-17 15:19:22.000000000 -0400
@@ -330,6 +330,21 @@
 $upgrade[] = Array('CreateIndexSQL',Array('idx_diskfile',config_get('mantis_bug_file_table'),'diskfile'));
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_user_print_pref_table' ), "print_pref C(64) NOTNULL" ) );
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_bug_history_table' ), "field_name C(64) NOTNULL" ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_tag_table' ), "
+	id				I		UNSIGNED NOTNULL PRIMARY AUTOINCREMENT,
+	user_id			I		UNSIGNED NOTNULL DEFAULT '0',
+	name			C(100)	NOTNULL DEFAULT \" '' \",
+	description		XL		NOTNULL,
+	date_created	T		NOTNULL DEFAULT '1970-01-01 00:00:01',
+	date_updated	T		NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
+$upgrade[] = Array('CreateIndexSQL', Array( 'idx_tag_name', config_get( 'mantis_tag_table' ), 'name', Array( 'UNIQUE' ) ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' ), "
+	bug_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	tag_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	user_id			I	UNSIGNED NOTNULL DEFAULT '0',
+	date_attached	T	NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 # Release marker: 1.1.0a4
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/bug_view_advanced_page.php mantis-tagging/bug_view_advanced_page.php
--- mantis-cvs/bug_view_advanced_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_advanced_page.php	2007-08-17 11:29:28.000000000 -0400
@@ -20,6 +20,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 
 	$f_bug_id		= gpc_get_int( 'bug_id' );
 	$f_history		= gpc_get_bool( 'history', config_get( 'history_default_visible' ) );
@@ -464,13 +465,35 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
 	<td colspan="6"></td>
 </tr>
 
-
 <!-- Custom Fields -->
 <?php
 	$t_custom_fields_found = false;
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/bug_view_page.php mantis-tagging/bug_view_page.php
--- mantis-cvs/bug_view_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_page.php	2007-08-17 15:36:02.000000000 -0400
@@ -22,6 +22,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 ?>
 <?php
 	$f_bug_id	= gpc_get_int( 'bug_id' );
@@ -341,6 +342,29 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/config_defaults_inc.php mantis-tagging/config_defaults_inc.php
--- mantis-cvs/config_defaults_inc.php	2007-08-16 13:23:56.000000000 -0400
+++ mantis-tagging/config_defaults_inc.php	2007-08-16 13:48:44.000000000 -0400
@@ -1331,6 +1331,7 @@
 	$g_mantis_bug_monitor_table				= '%db_table_prefix%_bug_monitor%db_table_suffix%';
 	$g_mantis_bug_relationship_table		= '%db_table_prefix%_bug_relationship%db_table_suffix%';
 	$g_mantis_bug_table						= '%db_table_prefix%_bug%db_table_suffix%';
+	$g_mantis_bug_tag_table					= '%db_table_prefix%_bug_tag%db_table_suffix%';
 	$g_mantis_bug_text_table				= '%db_table_prefix%_bug_text%db_table_suffix%';
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
@@ -1340,6 +1341,7 @@
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
 	$g_mantis_project_user_list_table		= '%db_table_prefix%_project_user_list%db_table_suffix%';
 	$g_mantis_project_version_table			= '%db_table_prefix%_project_version%db_table_suffix%';
+	$g_mantis_tag_table						= '%db_table_prefix%_tag%db_table_suffix%';
 	$g_mantis_user_table					= '%db_table_prefix%_user%db_table_suffix%';
 	$g_mantis_user_profile_table			= '%db_table_prefix%_user_profile%db_table_suffix%';
 	$g_mantis_user_pref_table				= '%db_table_prefix%_user_pref%db_table_suffix%';
@@ -1835,6 +1837,34 @@
 	$g_recently_visited_count = 5;
 
 	#####################
+	# Bug Tagging
+	#####################
+
+	# String that will separate tags as entered for input
+	$g_tag_separator = ',';
+
+	# Access level required to view tags attached to a bug
+	$g_tag_view_threshold = VIEWER;
+
+	# Access level required to attach tags to a bug
+	$g_tag_attach_threshold = REPORTER;
+
+	# Access level required to detach tags from a bug
+	$g_tag_detach_threshold = DEVELOPER;
+
+	# Access level required to detach tags attached by the same user
+	$g_tag_detach_own_threshold = REPORTER;
+
+	# Access level required to create new tags
+	$g_tag_create_threshold = REPORTER;
+
+	# Access level required to edit tag names and descriptions
+	$g_tag_edit_threshold = DEVELOPER;
+
+	# Access level required to edit descriptions by the creating user
+	$g_tag_edit_own_threshold = REPORTER;
+
+	#####################
 	# Time tracking
 	#####################
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/constant_inc.php mantis-tagging/core/constant_inc.php
--- mantis-cvs/core/constant_inc.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/constant_inc.php	2007-08-16 13:48:43.000000000 -0400
@@ -153,6 +153,9 @@
 	define( 'CHECKIN',				22 );
 	define( 'BUG_REPLACE_RELATIONSHIP', 		23 );
 	define( 'BUG_PAID_SPONSORSHIP', 		24 );
+	define( 'TAG_ATTACHED', 				25 );
+	define( 'TAG_DETACHED', 				26 );
+	define( 'TAG_RENAMED', 					27 );
 
 	# bug relationship constants
 	define( 'BUG_DUPLICATE',	0 );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/filter_api.php mantis-tagging/core/filter_api.php
--- mantis-cvs/core/filter_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/filter_api.php	2007-08-17 14:31:51.000000000 -0400
@@ -56,6 +56,7 @@
 	define( 'FILTER_PROPERTY_FILTER_BY_DATE', 'do_filter_by_date' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_PROPERTY_TAG_STRING', 'tag_string' );
 
 	###########################################################################
 	# Filter Query Parameter Names
@@ -96,6 +97,7 @@
 	define( 'FILTER_SEARCH_FILTER_BY_DATE', 'filter_by_date' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_SEARCH_TAG_STRING', 'tag_string' );
 
 	# Checks the supplied value to see if it is an ANY value.
 	# $p_field_value - The value to check.
@@ -306,6 +308,10 @@
 			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_OS_BUILD, $p_custom_filter[FILTER_PROPERTY_OS_BUILD] );
 		}
 
+		if ( !filter_str_field_is_any( $p_custom_filter[FILTER_PROPERTY_TAG_STRING] ) ) {
+			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_TAG_STRING, $p_custom_filter[FILTER_PROPERTY_TAG_STRING] );
+		}
+
 		if ( isset( $p_custom_filter['custom_fields'] ) ) {
 			foreach( $p_custom_filter['custom_fields'] as $t_custom_field_id => $t_custom_field_values ) {
 				if ( !filter_str_field_is_any( $t_custom_field_values ) ) {
@@ -1057,6 +1063,59 @@
 			array_push( $t_where_clauses, '('. implode( ' OR ', $t_clauses ) .')' );
 		}
 
+		# tags
+		$c_tag_string = trim( $t_filter['tag_string'] );
+		if ( !is_blank( $c_tag_string ) ) {
+			require_once( $t_core_path . 'tag_api.php' );
+			$t_tags = tag_parse_filters( $c_tag_string );
+
+			if ( !count( $t_tags ) ) { break; }
+
+			$t_tags_all = array();
+			$t_tags_any = array();
+			$t_tags_none = array();
+
+			foreach( $t_tags as $t_tag_row ) {
+				switch ( $t_tag_row['filter'] ) {
+					case 1:
+						$t_tags_all[] = $t_tag_row;
+						break;
+					case 0:
+						$t_tags_any[] = $t_tag_row;
+						break;
+					case -1:
+						$t_tags_none[] = $t_tag_row;
+						break;
+				}
+			}
+
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			if ( count( $t_tags_all ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_all as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE $t_bug_tag_table.tag_id = $t_tag_row[id] )" );
+				}
+				array_push( $t_where_clauses, '('. implode( ' AND ', $t_clauses ) .')' );
+			}
+			
+			if ( count( $t_tags_any ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_any as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+			
+			if ( count( $t_tags_none ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_none as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id NOT IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+		}
+
 		# custom field filters
 		if( ON == config_get( 'filter_by_custom_fields' ) ) {
 			# custom field filtering
@@ -2291,6 +2350,7 @@
 				<a href="<?php PRINT $t_filters_url . 'os_build'; ?>" id="os_build_filter"><?php echo lang_get( 'os_version' ) ?>:</a>
 			</td>
 			<td class="small-caption" valign="top" colspan="5">
+				<a href="<?php PRINT $t_filters_url . 'tag_string'; ?>" id="tag_string_filter"><?php echo lang_get( 'tags' ) ?>:</a>
 			</td>
 			<?php if ( $t_filter_cols > 8 ) {
 				echo '<td class="small-caption" valign="top" colspan="' . ( $t_filter_cols - 8 ) . '">&nbsp;</td>';
@@ -2312,7 +2372,12 @@
 					print_multivalue_field( FILTER_PROPERTY_OS_BUILD, $t_filter[FILTER_PROPERTY_OS_BUILD] );
 				?>
 			</td>
-			<td class="small-caption" colspan="5">
+			<td class="small-caption" valign="top" id="tag_string_filter_target" colspan="5">
+				<?php PRINT $t_filter['tag_string'] ?>
+				<input type="hidden" name="tag_string" value="<?php echo $t_filter['tag_string'] ?>"/>
+				<?php
+						//print_tag_input();
+				?>
 			</td>
 		</tr>
 		<?php
@@ -3025,6 +3090,9 @@
 		if ( !isset( $p_filter_arr['target_version'] ) ) {
 			$p_filter_arr['target_version'] = META_FILTER_ANY;
 		}
+		if ( !isset( $p_filter_arr['tag_string'] ) ) {
+			$p_filter_arr['tag_string'] = gpc_get_string( 'tag_string', '' );
+		}
 
 		$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 		$f_custom_fields_data 	= array();
@@ -3531,6 +3599,17 @@
 
 	}
 
+	function print_filter_tag_string() {
+		global $t_filter;
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $t_filter['tag_string'] ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+	}
+
 	function print_filter_custom_field($p_field_id){
 		global $t_filter, $t_accessible_custom_fields_names, $t_accessible_custom_fields_types, $t_accessible_custom_fields_values, $t_accessible_custom_fields_ids, $t_select_modifier;
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/history_api.php mantis-tagging/core/history_api.php
--- mantis-cvs/core/history_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/history_api.php	2007-08-17 15:41:19.000000000 -0400
@@ -165,6 +165,15 @@
 				}
 			}
 
+			// tags
+			if ( $v_type == TAG_ATTACHED ||
+				$v_type == TAG_DETACHED ||
+				$v_type == TAG_RENAMED ) {
+				if ( !access_has_global_level( config_get( 'tag_view_threshold' ) ) ) {
+					continue;
+				}
+			}
+
 			$raw_history[$j]['date']	= db_unixtimestamp( $v_date_modified );
 			$raw_history[$j]['userid']	= $v_user_id;
 
@@ -390,6 +399,16 @@
 				case CHECKIN:
 					$t_note = lang_get( 'checkin' );
 					break;
+				case TAG_ATTACHED:
+					$t_note = lang_get( 'tag_history_attached' ) .': '. $p_old_value;
+					break;
+				case TAG_DETACHED:
+					$t_note = lang_get( 'tag_history_detached' ) .': '. $p_old_value;
+					break;
+				case TAG_RENAMED:
+					$t_note = lang_get( 'tag_history_renamed' );
+					$t_change = $p_old_value . ' => ' . $p_new_value;
+					break;
 			}
 		}
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/html_api.php mantis-tagging/core/html_api.php
--- mantis-cvs/core/html_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/html_api.php	2007-08-17 11:56:56.000000000 -0400
@@ -1218,4 +1218,24 @@
 
 		echo '</tr></table>';
 	}
+
+	function html_button_tag_update( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+			|| ( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' )
+				&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) )
+		{
+			html_button( 'tag_update_page.php', lang_get( 'tag_update_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_button_tag_delete( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+			html_button( 'tag_delete.php', lang_get( 'tag_delete_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_buttons_tag_view_page( $p_tag_id ) {
+		html_button_tag_update( $p_tag_id );
+		html_button_tag_delete( $p_tag_id );
+	}
 ?>
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/print_api.php mantis-tagging/core/print_api.php
--- mantis-cvs/core/print_api.php	2007-08-14 10:12:41.000000000 -0400
+++ mantis-tagging/core/print_api.php	2007-08-17 11:02:32.000000000 -0400
@@ -11,6 +11,7 @@
 
 	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
 
+	require_once( $t_core_dir . 'ajax_api.php' );
 	require_once( $t_core_dir . 'current_user_api.php' );
 	require_once( $t_core_dir . 'string_api.php' );
 	require_once( $t_core_dir . 'prepare_api.php' );
@@ -255,6 +256,54 @@
 			PRINT "<option value=\"$t_duplicate_id\">".$t_duplicate_id."</option>";
 		}
 	}
+
+	function print_tag_attach_form( $p_bug_id, $p_string="" ) {
+		?>
+		<small><?php echo sprintf( lang_get( 'tag_separate_by' ), config_get('tag_separator') ) ?></small> 
+		<form method="post" action="tag_attach.php">
+		<input type="hidden" name="bug_id" value="<?php echo $p_bug_id ?>" />
+		<?php
+			print_tag_input( $p_bug_id, $p_string );
+		?>
+		<input type="submit" value="<?php echo lang_get( 'tag_attach' ) ?>" class="button" />
+		</form>
+		<?php
+		return true;
+	}
+
+	function print_tag_input( $p_bug_id = 0, $p_string="" ) {
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $p_string ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+
+		return true;
+	}
+
+	function print_tag_option_list( $p_bug_id = 0 ) {
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id, name FROM $t_tag_table ";
+		if ( 0 != $p_bug_id ) {
+			$c_bug_id = db_prepare_int( $p_bug_id );
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			$query .= "	WHERE id NOT IN ( 
+						SELECT tag_id FROM $t_bug_tag_table WHERE bug_id='$c_bug_id' ) ";
+		}
+
+		$query .= " ORDER BY name ASC ";
+		$result = db_query( $query );
+
+		echo '<option value="0">',lang_get( 'tag_existing' ),'</option>';
+		while ( $row = db_fetch_array( $result ) ) {
+			echo '<option value="',$row['id'],'" onclick="tag_string_append(\'',$row['name'],'\')">',$row['name'],'</option>';
+		}
+	}
+
 	# --------------------
 	# Get current headlines and id  prefix with v_
 	function print_news_item_option_list() {
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/tag_api.php mantis-tagging/core/tag_api.php
--- mantis-cvs/core/tag_api.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/core/tag_api.php	2007-08-17 17:01:13.000000000 -0400
@@ -0,0 +1,474 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: tag_api.php,v 1.3 2007/04/20 08:28:23 vboctor Exp $
+	# --------------------------------------------------------
+
+	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
+
+	require_once( $t_core_dir . 'bug_api.php' );
+	require_once( $t_core_dir . 'history_api.php' );
+	
+	### Tag API ###
+
+	function tag_exists( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table WHERE id='$c_tag_id'";
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) > 0;
+	}
+
+	function tag_ensure_exists( $p_tag_id ) {
+		if ( !tag_exists( $p_tag_id ) ) {
+			error_parameters( $p_tag_id );
+			trigger_error( ERROR_TAG_NOT_FOUND, ERROR );
+		}
+	}
+
+	function tag_is_unique( $p_name ) {
+		$c_name = trim( db_prepare_string( $p_name ) );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id FROM $t_tag_table WHERE name like '$c_name'";
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) == 0;
+	}
+
+	function tag_ensure_unique( $p_name ) {
+		if ( !tag_is_unique( $p_name ) ) {
+			trigger_error( ERROR_TAG_DUPLICATE, ERROR );
+		}
+	}
+
+	# Name must start with letter/number and consist of letters, numbers, hyphen, underscore, period, or spaces
+	function tag_name_is_valid( $p_name, $p_prefix="", &$p_matches=null ) {
+		$t_pattern = "/^$p_prefix([a-zA-Z0-9][a-zA-Z0-9-_. ]*)$/";
+		return preg_match( $t_pattern, $p_name, $p_matches );
+	}
+
+	function tag_ensure_name_is_valid( $p_name ) {
+		if ( !tag_name_is_valid( $p_name ) ) {
+			trigger_error( ERROR_TAG_NAME_INVALID, ERROR );
+		}
+	}
+
+	function tag_cmp_name( $p_tag1, $p_tag2 ) {
+		return strcasecmp( $p_tag1['name'], $p_tag2['name'] );
+	}
+
+	function tag_parse_string( $p_string ) {
+		$t_tags = array();
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( "" == trim( $t_name ) ) { continue; }
+			
+			$t_tag_row = tag_get_by_name( $t_name );
+			if ( $t_tag_row !== false ) {
+				$t_tags[] = $t_tag_row;
+			} else {
+				if ( tag_name_is_valid( $t_name ) ) {
+					$t_id = -1;
+				} else {
+					$t_id = -2;
+				}
+				$t_tags[] = array( 'id' => $t_id, 'name' => $t_name );
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	function tag_parse_filters( $p_string ) {
+		$t_tags = array();
+		$t_prefix = "[+-]{0,1}";
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( "" == trim( $t_name ) || !tag_name_is_valid( $t_name, $t_prefix ) ) { continue; }
+			
+			$t_matches = array();
+			if ( tag_name_is_valid( $t_name, $t_prefix, $t_matches ) ) {
+				$t_tag_row = tag_get_by_name( $t_matches[1] );
+				if ( $t_tag_row !== false ) {
+					$t_filter = substr( $t_name, 0, 1 );
+
+					if ( "+" == $t_filter ) {
+						$t_tag_row['filter'] = 1;
+					} elseif ( "-" == $t_filter ) {
+						$t_tag_row['filter'] = -1;
+					} else {
+						$t_tag_row['filter'] = 0;
+					}
+
+					$t_tags[] = $t_tag_row;
+				}
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	# CRUD
+
+	function tag_get( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+
+		$c_tag_id		= db_prepare_int( $p_tag_id );
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	function tag_get_by_name( $p_name ) {
+		$c_name 		= db_prepare_string( $p_name );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE name LIKE '$c_name'";
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	function tag_get_field( $p_tag_id, $p_field_name ) {
+		$row = tag_get( $p_tag_id );
+
+		if ( isset( $row[$p_field_name] ) ) {
+			return $row[$p_field_name];
+		} else {
+			error_parameters( $p_field_name );
+			trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
+			return '';
+		}
+	}
+
+	function tag_create( $p_name, $p_user_id, $p_description='' ) {
+		
+		tag_ensure_name_is_valid( $p_name );
+		tag_ensure_unique( $p_name );
+
+		$c_name			= trim( db_prepare_string( $p_name ) );
+		$c_description	= db_prepare_string( $p_description );
+		$c_user_id		= db_prepare_int( $p_user_id );
+		$c_date_created	= db_now();
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "INSERT INTO $t_tag_table
+				( user_id, 	
+				  name, 
+				  description, 
+				  date_created, 
+				  date_updated 
+				)
+				VALUES
+				( '$c_user_id', 
+				  '$c_name', 
+				  '$c_description', 
+				  ".$c_date_created.", 
+				  ".$c_date_created."
+				)";
+
+		db_query( $query );
+		return db_insert_id( $t_tag_table );
+	}
+
+	function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description ) {
+		tag_ensure_exists( $p_tag_id );
+		user_ensure_exists( $p_user_id );
+		
+		tag_ensure_name_is_valid( $p_name );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+
+		$t_rename = false;
+		if ( strtolower($p_name) != strtolower($t_tag_name) ) {
+			tag_ensure_unique( $p_name );
+			$t_rename = true;
+		}
+		
+		$c_tag_id		= trim( db_prepare_int( $p_tag_id ) );
+		$c_user_id		= db_prepare_string( $p_user_id );
+		$c_name			= db_prepare_string( $p_name );
+		$c_description	= db_prepare_string( $p_description );
+		$c_date_updated	= db_now();
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "UPDATE $t_tag_table
+					SET user_id='$c_user_id',
+						name='$c_name',
+						description='$c_description',
+						date_updated=".$c_date_updated."
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		if ( $t_rename ) {
+			$t_bugs = tag_get_bugs_attached( $p_tag_id );
+
+			foreach ( $t_bugs as $t_bug_id ) {
+				history_log_event_special( $t_bug_id, TAG_RENAMED, $t_tag_name, $c_name );
+			}
+		}
+
+		return true;
+	}
+
+	function tag_delete( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+		
+		$t_bugs = tag_get_bugs_attached( $p_tag_id );
+		foreach ( $t_bugs as $t_bug_id ) {
+			tag_bug_detach( $p_tag_id, $t_bug_id );
+		}
+		
+		$c_tag_id			= db_prepare_int( $p_tag_id );
+
+		$t_tag_table		= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table	= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		return true;
+	}
+	
+	# Associative
+
+	function tag_bug_is_attached( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+		return ( db_num_rows( $result ) > 0 );
+	}
+
+	function tag_bug_get_row( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		if ( db_num_rows( $result ) == 0 ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+		return db_fetch_array( $result );
+	}
+
+	function tag_bug_get_attached( $p_bug_id ) {
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT t.*, b.user_id as user_attached, b.date_attached
+					FROM $t_tag_table as t
+					LEFT JOIN $t_bug_tag_table as b
+						on t.id=b.tag_id
+					WHERE b.bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		$rows = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$rows[] = $row;
+		}
+		
+		usort( $rows, "tag_cmp_name" );
+		return $rows;
+	}
+
+	function tag_get_bugs_attached( $p_tag_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT bug_id FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		$bugs = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$bugs[] = $row['bug_id'];
+		}
+
+		return $bugs;
+	}
+
+	function tag_bug_attach( $p_tag_id, $p_bug_id, $p_user_id ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+		user_ensure_exists( $p_user_id );
+		
+		if ( tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_ALREADY_ATTACHED, ERROR );
+		}
+
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+		$c_user_id	 	= db_prepare_int( $p_user_id );
+		$c_date_attached= db_now();
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "INSERT INTO $t_bug_tag_table
+					( tag_id,
+					  bug_id,
+					  user_id,
+					  date_attached
+					)
+					VALUES
+					( '$c_tag_id',
+					  '$c_bug_id',
+					  '$c_user_id',
+					  ".$c_date_attached."
+					)";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_ATTACHED, $t_tag_name );
+
+		return true;
+	}
+
+	function tag_bug_detach( $p_tag_id, $p_bug_id ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+
+		if ( !tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_bug_tag_table 
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_DETACHED, $t_tag_name );
+
+		return true;
+	}
+
+	# Display
+
+	function tag_display_link( $p_tag_row, $p_bug_id=0 ) {
+		if ( auth_get_current_user_id() == $p_tag_row[user_attached] ) {
+			$t_detach = config_get( 'tag_detach_own_threshold' );
+		} else {
+			$t_detach = config_get( 'tag_detach_threshold' );
+		}
+		
+		$t_name = string_display_line( $p_tag_row['name'] );
+		$t_description = string_display_line( $p_tag_row['description'] );
+		
+		echo "<a href='tag_view_page.php?tag_id=$p_tag_row[id]' title='$t_description'>$t_name</a>";
+		
+		if ( access_has_global_level($t_detach) ) {
+			$t_tooltip = sprintf( lang_get( 'tag_detach' ), $t_name );
+			echo " [<a href='tag_detach.php?bug_id=$p_bug_id&tag_id=$p_tag_row[id]' title='$t_tooltip'>x</a>]";
+		}
+		
+		return true;
+	}
+
+	function tag_display_attached( $p_bug_id ) {
+		$t_tag_rows = tag_bug_get_attached( $p_bug_id );
+
+		if ( count( $t_tag_rows ) == 0 ) {
+			echo lang_get( 'tag_none_attached' );
+		} else {
+			$i = 0;
+			foreach ( $t_tag_rows as $t_tag ) {
+				echo ( $i > 0 ? config_get('tag_separator')." " : "" );
+				tag_display_link( $t_tag, $p_bug_id );
+				$i++;
+			}
+		}
+
+		return true;
+	}
+
+	# Statistics
+
+	function tag_stats_attached( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT bug_id FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		return db_num_rows( $result );
+	}
+
+	function tag_stats_related( $p_tag_id, $p_limit=5 ) {
+		$t_tag_table = config_get( 'mantis_tag_table' );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+		$c_tag_id = db_prepare_int( $p_tag_id );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id != $c_tag_id
+						AND bug_id IN ( SELECT bug_id FROM $t_bug_tag_table
+									WHERE tag_id=$c_tag_id ) ";
+		$result = db_query( $query );
+
+		$t_tag_counts = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$t_tag_counts[$row['tag_id']]++;
+		}
+
+		asort( $t_tag_counts );
+
+		$t_tags = array();
+		$i = 1;
+		foreach ( $t_tag_counts as $t_tag_id => $t_count ) {
+			$t_tag_row = tag_get($t_tag_id);
+			$t_tag_row['count'] = $t_count;
+			$t_tags[] = $t_tag_row;
+			$i++;
+			if ( $i > $p_limit ) { break; }
+		}
+
+		return $t_tags;
+	}
Binary files mantis-cvs/core/.tag_api.php.swp and mantis-tagging/core/.tag_api.php.swp differ
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/xmlhttprequest_api.php mantis-tagging/core/xmlhttprequest_api.php
--- mantis-cvs/core/xmlhttprequest_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/xmlhttprequest_api.php	2007-08-16 13:48:44.000000000 -0400
@@ -31,6 +31,15 @@
 		echo '</select>';
 	}
 
+	function xmlhttprequest_user_combobox() {
+		$f_user_id = gpc_get_int( 'user_id' );
+		$f_user_access = gpc_get_int( 'access_level' );
+		
+		echo '<select name="user_id">';
+		print_user_option_list( $f_user_id, ALL_PROJECTS, $f_user_access );
+		echo '</select>';
+	}
+
 	# ---------------
 	# Echos a serialized list of platforms starting with the prefix specified in the $_POST
 	function xmlhttprequest_platform_get_with_prefix() {
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/javascript/common.js mantis-tagging/javascript/common.js
--- mantis-cvs/javascript/common.js	2007-08-14 10:12:43.000000000 -0400
+++ mantis-tagging/javascript/common.js	2007-08-16 14:57:40.000000000 -0400
@@ -163,3 +163,16 @@
   setDisplay( idTag, (document.getElementById(idTag).style.display == 'none')?1:0 );
 }
 
+/* Tag functionality */
+function tag_string_append( p_string ) {
+	t_tag_separator = document.getElementById('tag_separator').value;
+	t_tag_string = document.getElementById('tag_string');
+	t_tag_select = document.getElementById('tag_select');
+	if ( t_tag_string.value != '' ) {
+		t_tag_string.value = t_tag_string.value + t_tag_separator + p_string;
+	} else {
+		t_tag_string.value = t_tag_string.value + p_string;
+	}
+	t_tag_select.selectedIndex=0;
+}
+
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/lang/strings_english.txt mantis-tagging/lang/strings_english.txt
--- mantis-cvs/lang/strings_english.txt	2007-08-16 13:24:00.000000000 -0400
+++ mantis-tagging/lang/strings_english.txt	2007-08-17 16:59:16.000000000 -0400
@@ -275,6 +275,11 @@
 $MANTIS_ERROR[ERROR_USER_CHANGE_LAST_ADMIN] = 'You cannot change the access level of the only ADMINISTRATOR in the system.';
 $MANTIS_ERROR[ERROR_PAGE_REDIRECTION] = 'Page redirection error, ensure that there are no spaces outside the PHP block (&lt;?php ?&gt;) in config_inc.php or custom_*.php files.';
 $MANTIS_ERROR[ERROR_TWITTER_NO_CURL_EXT] = 'Twitter integration requires PHP CURL extension which is not installed.';
+$MANTIS_ERROR[ERROR_TAG_NOT_FOUND] = 'Could not find a tag with that name.';
+$MANTIS_ERROR[ERROR_TAG_DUPLICATE] = 'A tag already exists with that name.';
+$MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
+$MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
+$MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -1347,6 +1352,38 @@
 # wiki related strings
 $s_wiki = 'Wiki';
 
+# Tagging
+$s_tags = 'Tags';
+$s_tag_details = 'Tag Details: %s';
+$s_tag_id = 'Tag ID';
+$s_tag_name = 'Name';
+$s_tag_creator = 'Creator';
+$s_tag_created = 'Date Created';
+$s_tag_updated = 'Last Updated';
+$s_tag_description = 'Tag Description';
+$s_tag_statistics = 'Usage Statistics';
+$s_tag_update = 'Update Tag: %s';
+$s_tag_update_return = 'Back to Tag';
+$s_tag_update_button = 'Update Tag';
+$s_tag_delete_button = 'Delete Tag';
+$s_tag_delete_message = 'Are you sure you wish to delete this tag?';
+$s_tag_existing = 'Existing tags';
+$s_tag_none_attached = 'No tags attached.';
+$s_tag_attach = 'Attach';
+$s_tag_attach_long = 'Attach Tags';
+$s_tag_attach_failed = 'Tag attachment failed.';
+$s_tag_detach = 'Detach %s';
+$s_tag_separate_by = "(Separate by '%s')";
+$s_tag_invalid_name = 'Invalid tag name.';
+$s_tag_create_denied = 'Create permission denied.';
+$s_tag_filter_default = 'Attached Issues (%s)';
+$s_tag_history_attached = 'Tag Attached';
+$s_tag_history_detached = 'Tag Detached';
+$s_tag_history_renamed = 'Tag Renamed';
+$s_tag_related = 'Related Tags';
+$s_tag_related_issues = 'Shared Issues';
+$s_tag_stats_attached = 'Issues attached: %s';
+
 # Time Tracking
 $s_time_tracking_billing_link = 'Billing';
 $s_time_tracking = 'Time tracking';
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_attach.php mantis-tagging/tag_attach.php
--- mantis-cvs/tag_attach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_attach.php	2007-08-17 11:30:39.000000000 -0400
@@ -0,0 +1,103 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_bug_id = gpc_get_int( 'bug_id' );
+	$t_user_id = auth_get_current_user_id();
+
+	access_ensure_global_level( config_get( 'tag_attach_threshold' ) );
+
+	$t_tags = tag_parse_string( gpc_get_string( 'tag_string' ) );
+	$t_can_create = access_has_global_level( config_get( 'tag_create_threshold' ) );
+	
+	$t_tags_create = array();
+	$t_tags_attach = array();
+	$t_tags_failed = array();
+
+	foreach ( $t_tags as $t_tag_row ) {
+		if ( -1 == $t_tag_row['id'] ) {
+			if ( $t_can_create ) {
+				$t_tags_create[] = $t_tag_row;
+			} else {
+				$t_tags_failed[] = $t_tag_row;
+			}
+		} elseif ( -2 == $t_tag_row['id'] ) {
+			$t_tags_failed[] = $t_tag_row;
+		} else {
+			$t_tags_attach[] = $t_tag_row;
+		}
+	}
+
+	if ( count( $t_tags_failed ) > 0 ) {
+		html_page_top1( lang_get( 'tag_attach_long' ).' '.bug_format_summary( $f_bug_id, SUMMARY_CAPTION ) );
+		html_page_top2();
+?>
+<br/>
+<table class="width75" align="center">
+	<tr class="row-category">
+	<td colspan="2"><?php echo lang_get( 'tag_attach_failed' ) ?></td>
+	</tr>
+	<tr class="spacer"><td colspan="2"></td></tr>
+<?php		
+		$t_tag_string = "";
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+
+		foreach( $t_tags_failed as $t_tag_row ) {
+			echo '<tr ',helper_alternate_class(),'>';
+			if ( -1 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_invalid_name' ),'</td>';
+			} elseif ( -2 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_create_denied' ),'</td>';
+			}
+			echo '<td>',$t_tag_row['name'],'</td></tr>';
+			
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+?>
+	<tr class="spacer"><td colspan="2"></td></tr>
+	<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td>
+<?php
+		print_tag_input( $f_bug_id, $t_tag_string );
+?>	
+	</td>
+	</tr>
+</table>
+<?php
+		html_page_bottom1(__FILE__);
+	} else {
+		foreach( $t_tags_create as $t_tag_row ) {
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id );
+			$t_tags_attach[] = $t_tag_row;
+		}
+
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( ! tag_bug_is_attached( $t_tag_row['id'], $f_bug_id ) ) {
+				tag_bug_attach( $t_tag_row['id'], $f_bug_id, $t_user_id );
+			}
+		}
+
+		print_successful_redirect_to_bug( $f_bug_id );
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_delete.php mantis-tagging/tag_delete.php
--- mantis-cvs/tag_delete.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_delete.php	2007-08-17 13:34:13.000000000 -0400
@@ -0,0 +1,27 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_edit_threshold' ) );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	helper_ensure_confirmed( lang_get( 'tag_delete_message' ), lang_get( 'tag_delete_button' ) );
+
+	tag_delete( $f_tag_id );
+	
+	print_successful_redirect( config_get( 'default_home_page' ) );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_detach.php mantis-tagging/tag_detach.php
--- mantis-cvs/tag_detach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_detach.php	2007-08-17 11:34:04.000000000 -0400
@@ -0,0 +1,33 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$f_bug_id = gpc_get_int( 'bug_id' );
+
+	$t_tag_row = tag_get( $f_tag_id );
+	$t_tag_bug_row = tag_bug_get_row( $f_tag_id, $f_bug_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_detach_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_bug_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_detach_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	tag_bug_detach( $f_tag_id, $f_bug_id );
+	
+	print_successful_redirect_to_bug( $f_bug_id );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_update_page.php mantis-tagging/tag_update_page.php
--- mantis-cvs/tag_update_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update_page.php	2007-08-17 13:37:45.000000000 -0400
@@ -0,0 +1,105 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'ajax_api.php' );
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+	
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+	
+	html_page_top1( sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<form method="post" action="tag_update.php">
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) ?>
+		<input type="hidden" name="tag_id" value="<?php echo $f_tag_id ?>"/>
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'tag_view_page.php?tag_id='.$f_tag_id, lang_get( 'tag_update_return' ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><input type="text" <?php echo helper_get_tab_index() ?> name="name" value="<?php echo $t_tag_row['name'] ?>"/></td>
+	<td><?php
+			if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+				if ( ON == config_get( 'use_javascript' ) ) {
+					$t_username = prepare_user_name( $t_tag_row['user_id'] );
+					echo ajax_click_to_edit( $t_username, 'user_id', 'entrypoint=user_combobox&user_id=' . $t_tag_row['user_id'] . '&access_level=' . config_get( 'tag_create_threshold' ) );
+				} else {
+					echo '<select ', helper_get_tab_index(), ' name="user_id">';
+					print_user_option_list( $t_tag_row['user_id'], ALL_PROJECTS, config_get( 'tag_create_threshold' ) );
+					echo '</select>';
+				}
+			} else {
+				echo user_get_name($t_tag_row['user_id']);
+			}
+		?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4">
+		<textarea name="description" <?php echo helper_get_tab_index() ?> cols="80" rows="6"><?php echo $t_tag_row['description'] ?></textarea>
+	</td>
+</tr>
+
+<!-- Submit Button -->
+<tr>
+	<td class="center" colspan="6">
+		<input <?php echo helper_get_tab_index() ?> type="submit" class="button" value="<?php echo lang_get( 'tag_update_button' ) ?>" />
+	</td>
+</tr>
+
+</table>
+</form>
+
+<?php
+	html_page_bottom1( __FILE__ );
+?>
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_update.php mantis-tagging/tag_update.php
--- mantis-cvs/tag_update.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update.php	2007-08-16 13:48:44.000000000 -0400
@@ -0,0 +1,55 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+		$f_new_user_id = gpc_get_int( 'user_id', $t_tag_row['user_id'] );
+	} else {
+		$f_new_user_id = $t_tag_row['user_id'];
+	}
+
+	$f_new_name = gpc_get_string( 'name', $t_tag_row['name'] );
+	$f_new_description = gpc_get_string( 'description', $t_tag_row['description'] );
+
+	$t_update = false;
+
+	if ( $t_tag_row['user_id'] != $f_new_user_id ) {
+		user_ensure_exists( $f_new_user_id );
+		$t_update = true;
+	}
+
+	if ( 	$t_tag_row['name'] != $f_new_name ||
+			$t_tag_row['description'] != $f_new_description ) {
+
+		$t_update = true;
+	}
+
+	tag_update( $f_tag_id, $f_new_name, $f_new_user_id, $f_new_description );
+		
+	$t_url = 'tag_view_page.php?tag_id='.$f_tag_id;
+	print_successful_redirect( $t_url );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_view_page.php mantis-tagging/tag_view_page.php
--- mantis-cvs/tag_view_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_view_page.php	2007-08-17 17:00:45.000000000 -0400
@@ -0,0 +1,107 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id:  $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+	
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_view_threshold' ) );
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	$t_name = string_display_line( $t_tag_row['name'] );
+	$t_description = string_display( $t_tag_row['description'] );
+
+	html_page_top1( sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) ?>
+
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'search.php?hide_status_id=90&tag_string='.urlencode($t_tag_row['name']), sprintf( lang_get( 'tag_filter_default' ), tag_stats_attached( $f_tag_id ) ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><?php echo $t_name ?></td>
+	<td><?php echo user_get_name($t_tag_row['user_id']) ?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4"><?php echo $t_description ?></td>
+</tr>
+
+<!-- Statistics -->
+<?php
+	$t_tags_related = tag_stats_related( $f_tag_id );
+	if ( count( $t_tags_related ) ) { 
+		echo '<tr ',helper_alternate_class(),'>';
+		echo '<td class="category" rowspan="',count( $t_tags_related ),'">',lang_get( 'tag_related' ),'</td>';
+		
+		$i = 0;
+		foreach( $t_tags_related as $t_tag ) {
+			$t_name = string_display_line( $t_tag['name'] );
+			$t_description = string_display_line( $t_tag['description'] );
+			$t_count = $t_tag['count'];
+
+			echo ( $i > 0 ? '<tr '.helper_alternate_class().'>' : '' );
+			echo "<td><a href='tag_view_page.php?tag_id=$t_tag[id]' title='$t_description'>$t_name</a></td>\n";
+			echo '<td colspan="3">';
+			print_bracket_link( "search.php?hide_status_is=90&tag_string=+$t_tag_row[name]".config_get('tag_separator')."+$t_name", lang_get( 'tag_related_issues' ) );
+			echo '</a></td></tr>';
+			
+			$i++;
+		}
+	}
+?>
+
+<!-- Buttons -->
+<tr>
+	<td colspan="5">
+		<?php html_buttons_tag_view_page( $f_tag_id ); ?>
+	</td>
+</tr>
+
+</table>
+<?php
+	html_page_bottom1( __FILE__ );
+?>
Binary files mantis-cvs/.tag_view_page.php.swp and mantis-tagging/.tag_view_page.php.swp differ
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/view_all_set.php mantis-tagging/view_all_set.php
--- mantis-cvs/view_all_set.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/view_all_set.php	2007-08-16 16:27:27.000000000 -0400
@@ -203,6 +203,8 @@
 	$f_do_filter_by_date	= gpc_get_bool( 'do_filter_by_date' );
 	$f_view_state			= gpc_get_int( 'view_state', META_FILTER_ANY );
 
+	$f_tag_string			= gpc_get_string( 'tag_string', '' );
+
 	$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 	$f_custom_fields_data 	= array();
 	if ( is_array( $t_custom_fields ) && ( sizeof( $t_custom_fields ) > 0 ) ) {
@@ -414,6 +416,7 @@
 				$t_setting_arr['platform'] = $f_platform;
 				$t_setting_arr['os'] = $f_os;
 				$t_setting_arr['os_build'] = $f_os_build;
+				$t_setting_arr['tag_string'] = $f_tag_string;
 				break;
 		# Set the sort order and direction
 		case '2':
mantis-tagging-2007-08-17.patch (52,788 bytes)   
jreese

jreese

2007-08-17 18:16

reporter   ~0015448

Patch 'mantis-tagging-2007-08-17.patch' was created from CVS head on Friday, Aug 17, 2007.

Test site is available at http://leetcode.homeip.net/mantis-tagging

thraxisp

thraxisp

2007-08-17 22:47

reporter   ~0015450

Comments on the patch

  • subqueries aren't supported in mySQL 4.0 and earlier (we need to check with Victor to see if support for older versions is needed).
  • SQL insert/alter for numeric data doesn't need to be quoted. I think that this is problematic in Oracle and other databases
  • you can use 'COUNT(*)' in the function 'tag_stats_attached' and similar functions. It is faster.
  • in tag_update, you can manually change the creator of the tag. I would have thought that the creator would be the last user who editted the tag.
jreese

jreese

2007-08-17 23:47

reporter   ~0015451

  • Assuming we choose to support 4.1 and newer (as discussed on the dev mailing list), this should not be a problem.
  • I didn't add any inserts or alters for the tagging, are you talking about the default values in my create entries? If so, I'm simply replicating the way it was done in other parts of the schema file.
  • Already done in my local version, thank you.
  • As discussed in the requirements and mailing list, the creator is the original creator, although I added the ability to edit that only by those with edit access for all tags. This can easily be changed if you'd rather not be able to change the creator.

2007-08-20 09:57

 

mantis-tagging-2007-08-20.patch (55,957 bytes)   
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/admin/schema.php mantis-tagging/admin/schema.php
--- mantis-cvs/admin/schema.php	2007-08-07 10:54:56.000000000 -0400
+++ mantis-tagging/admin/schema.php	2007-08-17 15:19:22.000000000 -0400
@@ -330,6 +330,21 @@
 $upgrade[] = Array('CreateIndexSQL',Array('idx_diskfile',config_get('mantis_bug_file_table'),'diskfile'));
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_user_print_pref_table' ), "print_pref C(64) NOTNULL" ) );
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_bug_history_table' ), "field_name C(64) NOTNULL" ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_tag_table' ), "
+	id				I		UNSIGNED NOTNULL PRIMARY AUTOINCREMENT,
+	user_id			I		UNSIGNED NOTNULL DEFAULT '0',
+	name			C(100)	NOTNULL DEFAULT \" '' \",
+	description		XL		NOTNULL,
+	date_created	T		NOTNULL DEFAULT '1970-01-01 00:00:01',
+	date_updated	T		NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
+$upgrade[] = Array('CreateIndexSQL', Array( 'idx_tag_name', config_get( 'mantis_tag_table' ), 'name', Array( 'UNIQUE' ) ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' ), "
+	bug_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	tag_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	user_id			I	UNSIGNED NOTNULL DEFAULT '0',
+	date_attached	T	NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 # Release marker: 1.1.0a4
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/bug_view_advanced_page.php mantis-tagging/bug_view_advanced_page.php
--- mantis-cvs/bug_view_advanced_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_advanced_page.php	2007-08-17 11:29:28.000000000 -0400
@@ -20,6 +20,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 
 	$f_bug_id		= gpc_get_int( 'bug_id' );
 	$f_history		= gpc_get_bool( 'history', config_get( 'history_default_visible' ) );
@@ -464,13 +465,35 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
 	<td colspan="6"></td>
 </tr>
 
-
 <!-- Custom Fields -->
 <?php
 	$t_custom_fields_found = false;
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/bug_view_page.php mantis-tagging/bug_view_page.php
--- mantis-cvs/bug_view_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_page.php	2007-08-17 15:36:02.000000000 -0400
@@ -22,6 +22,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 ?>
 <?php
 	$f_bug_id	= gpc_get_int( 'bug_id' );
@@ -341,6 +342,29 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/config_defaults_inc.php mantis-tagging/config_defaults_inc.php
--- mantis-cvs/config_defaults_inc.php	2007-08-16 13:23:56.000000000 -0400
+++ mantis-tagging/config_defaults_inc.php	2007-08-16 13:48:44.000000000 -0400
@@ -1331,6 +1331,7 @@
 	$g_mantis_bug_monitor_table				= '%db_table_prefix%_bug_monitor%db_table_suffix%';
 	$g_mantis_bug_relationship_table		= '%db_table_prefix%_bug_relationship%db_table_suffix%';
 	$g_mantis_bug_table						= '%db_table_prefix%_bug%db_table_suffix%';
+	$g_mantis_bug_tag_table					= '%db_table_prefix%_bug_tag%db_table_suffix%';
 	$g_mantis_bug_text_table				= '%db_table_prefix%_bug_text%db_table_suffix%';
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
@@ -1340,6 +1341,7 @@
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
 	$g_mantis_project_user_list_table		= '%db_table_prefix%_project_user_list%db_table_suffix%';
 	$g_mantis_project_version_table			= '%db_table_prefix%_project_version%db_table_suffix%';
+	$g_mantis_tag_table						= '%db_table_prefix%_tag%db_table_suffix%';
 	$g_mantis_user_table					= '%db_table_prefix%_user%db_table_suffix%';
 	$g_mantis_user_profile_table			= '%db_table_prefix%_user_profile%db_table_suffix%';
 	$g_mantis_user_pref_table				= '%db_table_prefix%_user_pref%db_table_suffix%';
@@ -1835,6 +1837,34 @@
 	$g_recently_visited_count = 5;
 
 	#####################
+	# Bug Tagging
+	#####################
+
+	# String that will separate tags as entered for input
+	$g_tag_separator = ',';
+
+	# Access level required to view tags attached to a bug
+	$g_tag_view_threshold = VIEWER;
+
+	# Access level required to attach tags to a bug
+	$g_tag_attach_threshold = REPORTER;
+
+	# Access level required to detach tags from a bug
+	$g_tag_detach_threshold = DEVELOPER;
+
+	# Access level required to detach tags attached by the same user
+	$g_tag_detach_own_threshold = REPORTER;
+
+	# Access level required to create new tags
+	$g_tag_create_threshold = REPORTER;
+
+	# Access level required to edit tag names and descriptions
+	$g_tag_edit_threshold = DEVELOPER;
+
+	# Access level required to edit descriptions by the creating user
+	$g_tag_edit_own_threshold = REPORTER;
+
+	#####################
 	# Time tracking
 	#####################
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/constant_inc.php mantis-tagging/core/constant_inc.php
--- mantis-cvs/core/constant_inc.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/constant_inc.php	2007-08-16 13:48:43.000000000 -0400
@@ -153,6 +153,9 @@
 	define( 'CHECKIN',				22 );
 	define( 'BUG_REPLACE_RELATIONSHIP', 		23 );
 	define( 'BUG_PAID_SPONSORSHIP', 		24 );
+	define( 'TAG_ATTACHED', 				25 );
+	define( 'TAG_DETACHED', 				26 );
+	define( 'TAG_RENAMED', 					27 );
 
 	# bug relationship constants
 	define( 'BUG_DUPLICATE',	0 );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/filter_api.php mantis-tagging/core/filter_api.php
--- mantis-cvs/core/filter_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/filter_api.php	2007-08-17 23:52:18.000000000 -0400
@@ -16,6 +16,7 @@
 	require_once( $t_core_dir . 'bug_api.php' );
 	require_once( $t_core_dir . 'collapse_api.php' );
 	require_once( $t_core_dir . 'relationship_api.php' );
+	require_once( $t_core_dir . 'tag_api.php' );
 
 	###########################################################################
 	# Filter Property Names
@@ -56,6 +57,8 @@
 	define( 'FILTER_PROPERTY_FILTER_BY_DATE', 'do_filter_by_date' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_PROPERTY_TAG_STRING', 'tag_string' );
+	define( 'FILTER_PROPERTY_TAG_SELECT', 'tag_select' );
 
 	###########################################################################
 	# Filter Query Parameter Names
@@ -96,6 +99,8 @@
 	define( 'FILTER_SEARCH_FILTER_BY_DATE', 'filter_by_date' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_SEARCH_TAG_STRING', 'tag_string' );
+	define( 'FILTER_SEARCH_TAG_SELECT', 'tag_select' );
 
 	# Checks the supplied value to see if it is an ANY value.
 	# $p_field_value - The value to check.
@@ -306,6 +311,14 @@
 			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_OS_BUILD, $p_custom_filter[FILTER_PROPERTY_OS_BUILD] );
 		}
 
+		if ( !filter_str_field_is_any( $p_custom_filter[FILTER_PROPERTY_TAG_STRING] ) ) {
+			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_TAG_STRING, $p_custom_filter[FILTER_PROPERTY_TAG_STRING] );
+		}
+
+		if ( !filter_str_field_is_any( $p_custom_filter[FILTER_PROPERTY_TAG_SELECT] ) ) {
+			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_TAG_SELECT, $p_custom_filter[FILTER_PROPERTY_TAG_SELECT] );
+		}
+
 		if ( isset( $p_custom_filter['custom_fields'] ) ) {
 			foreach( $p_custom_filter['custom_fields'] as $t_custom_field_id => $t_custom_field_values ) {
 				if ( !filter_str_field_is_any( $t_custom_field_values ) ) {
@@ -1057,6 +1070,63 @@
 			array_push( $t_where_clauses, '('. implode( ' OR ', $t_clauses ) .')' );
 		}
 
+		# tags
+		$c_tag_string = trim( $t_filter['tag_string'] );
+		if ( !is_blank( $c_tag_string ) ) {
+			require_once( $t_core_path . 'tag_api.php' );
+			$t_tags = tag_parse_filters( $c_tag_string );
+
+			if ( !count( $t_tags ) ) { break; }
+
+			$t_tags_all = array();
+			$t_tags_any = array();
+			$t_tags_none = array();
+
+			foreach( $t_tags as $t_tag_row ) {
+				switch ( $t_tag_row['filter'] ) {
+					case 1:
+						$t_tags_all[] = $t_tag_row;
+						break;
+					case 0:
+						$t_tags_any[] = $t_tag_row;
+						break;
+					case -1:
+						$t_tags_none[] = $t_tag_row;
+						break;
+				}
+			}
+
+			if ( 0 < $t_filter['tag_select'] && tag_exists( $t_filter['tag_select'] ) ) {
+				$t_tags_any[] = tag_get( $t_filter['tag_select'] );
+			}
+
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			if ( count( $t_tags_all ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_all as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE $t_bug_tag_table.tag_id = $t_tag_row[id] )" );
+				}
+				array_push( $t_where_clauses, '('. implode( ' AND ', $t_clauses ) .')' );
+			}
+			
+			if ( count( $t_tags_any ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_any as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+			
+			if ( count( $t_tags_none ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_none as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id NOT IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+		}
+
 		# custom field filters
 		if( ON == config_get( 'filter_by_custom_fields' ) ) {
 			# custom field filtering
@@ -2291,6 +2361,7 @@
 				<a href="<?php PRINT $t_filters_url . 'os_build'; ?>" id="os_build_filter"><?php echo lang_get( 'os_version' ) ?>:</a>
 			</td>
 			<td class="small-caption" valign="top" colspan="5">
+				<a href="<?php PRINT $t_filters_url . 'tag_string'; ?>" id="tag_string_filter"><?php echo lang_get( 'tags' ) ?>:</a>
 			</td>
 			<?php if ( $t_filter_cols > 8 ) {
 				echo '<td class="small-caption" valign="top" colspan="' . ( $t_filter_cols - 8 ) . '">&nbsp;</td>';
@@ -2312,7 +2383,16 @@
 					print_multivalue_field( FILTER_PROPERTY_OS_BUILD, $t_filter[FILTER_PROPERTY_OS_BUILD] );
 				?>
 			</td>
-			<td class="small-caption" colspan="5">
+			<td class="small-caption" valign="top" id="tag_string_filter_target" colspan="5">
+				<?php 
+					$t_tag_string = $t_filter['tag_string'];
+					if ( $t_filter['tag_select'] != 0 ) {
+						$t_tag_string .= ( is_blank( $t_tag_string ) ? '' : config_get( 'tag_separator' ) );
+						$t_tag_string .= tag_get_field( $t_filter['tag_select'], 'name' );
+					}
+					PRINT $t_tag_string 
+				?>
+				<input type="hidden" name="tag_string" value="<?php echo $t_tag_string ?>"/>
 			</td>
 		</tr>
 		<?php
@@ -3025,6 +3105,9 @@
 		if ( !isset( $p_filter_arr['target_version'] ) ) {
 			$p_filter_arr['target_version'] = META_FILTER_ANY;
 		}
+		if ( !isset( $p_filter_arr['tag_string'] ) ) {
+			$p_filter_arr['tag_string'] = gpc_get_string( 'tag_string', '' );
+		}
 
 		$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 		$f_custom_fields_data 	= array();
@@ -3531,6 +3614,22 @@
 
 	}
 
+	function print_filter_tag_string() {
+		global $t_filter;
+		$t_tag_string = $t_filter['tag_string'];
+		if ( $t_filter['tag_select'] != 0 ) {
+			$t_tag_string .= ( is_blank( $t_tag_string ) ? '' : config_get( 'tag_separator' ) );
+			$t_tag_string .= tag_get_field( $t_filter['tag_select'], 'name' );
+		}
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $t_tag_string ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+	}
+
 	function print_filter_custom_field($p_field_id){
 		global $t_filter, $t_accessible_custom_fields_names, $t_accessible_custom_fields_types, $t_accessible_custom_fields_values, $t_accessible_custom_fields_ids, $t_select_modifier;
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/history_api.php mantis-tagging/core/history_api.php
--- mantis-cvs/core/history_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/history_api.php	2007-08-17 15:41:19.000000000 -0400
@@ -165,6 +165,15 @@
 				}
 			}
 
+			// tags
+			if ( $v_type == TAG_ATTACHED ||
+				$v_type == TAG_DETACHED ||
+				$v_type == TAG_RENAMED ) {
+				if ( !access_has_global_level( config_get( 'tag_view_threshold' ) ) ) {
+					continue;
+				}
+			}
+
 			$raw_history[$j]['date']	= db_unixtimestamp( $v_date_modified );
 			$raw_history[$j]['userid']	= $v_user_id;
 
@@ -390,6 +399,16 @@
 				case CHECKIN:
 					$t_note = lang_get( 'checkin' );
 					break;
+				case TAG_ATTACHED:
+					$t_note = lang_get( 'tag_history_attached' ) .': '. $p_old_value;
+					break;
+				case TAG_DETACHED:
+					$t_note = lang_get( 'tag_history_detached' ) .': '. $p_old_value;
+					break;
+				case TAG_RENAMED:
+					$t_note = lang_get( 'tag_history_renamed' );
+					$t_change = $p_old_value . ' => ' . $p_new_value;
+					break;
 			}
 		}
 
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/html_api.php mantis-tagging/core/html_api.php
--- mantis-cvs/core/html_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/html_api.php	2007-08-17 11:56:56.000000000 -0400
@@ -1218,4 +1218,24 @@
 
 		echo '</tr></table>';
 	}
+
+	function html_button_tag_update( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+			|| ( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' )
+				&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) )
+		{
+			html_button( 'tag_update_page.php', lang_get( 'tag_update_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_button_tag_delete( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+			html_button( 'tag_delete.php', lang_get( 'tag_delete_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_buttons_tag_view_page( $p_tag_id ) {
+		html_button_tag_update( $p_tag_id );
+		html_button_tag_delete( $p_tag_id );
+	}
 ?>
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/print_api.php mantis-tagging/core/print_api.php
--- mantis-cvs/core/print_api.php	2007-08-14 10:12:41.000000000 -0400
+++ mantis-tagging/core/print_api.php	2007-08-17 11:02:32.000000000 -0400
@@ -11,6 +11,7 @@
 
 	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
 
+	require_once( $t_core_dir . 'ajax_api.php' );
 	require_once( $t_core_dir . 'current_user_api.php' );
 	require_once( $t_core_dir . 'string_api.php' );
 	require_once( $t_core_dir . 'prepare_api.php' );
@@ -255,6 +256,54 @@
 			PRINT "<option value=\"$t_duplicate_id\">".$t_duplicate_id."</option>";
 		}
 	}
+
+	function print_tag_attach_form( $p_bug_id, $p_string="" ) {
+		?>
+		<small><?php echo sprintf( lang_get( 'tag_separate_by' ), config_get('tag_separator') ) ?></small> 
+		<form method="post" action="tag_attach.php">
+		<input type="hidden" name="bug_id" value="<?php echo $p_bug_id ?>" />
+		<?php
+			print_tag_input( $p_bug_id, $p_string );
+		?>
+		<input type="submit" value="<?php echo lang_get( 'tag_attach' ) ?>" class="button" />
+		</form>
+		<?php
+		return true;
+	}
+
+	function print_tag_input( $p_bug_id = 0, $p_string="" ) {
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $p_string ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+
+		return true;
+	}
+
+	function print_tag_option_list( $p_bug_id = 0 ) {
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id, name FROM $t_tag_table ";
+		if ( 0 != $p_bug_id ) {
+			$c_bug_id = db_prepare_int( $p_bug_id );
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			$query .= "	WHERE id NOT IN ( 
+						SELECT tag_id FROM $t_bug_tag_table WHERE bug_id='$c_bug_id' ) ";
+		}
+
+		$query .= " ORDER BY name ASC ";
+		$result = db_query( $query );
+
+		echo '<option value="0">',lang_get( 'tag_existing' ),'</option>';
+		while ( $row = db_fetch_array( $result ) ) {
+			echo '<option value="',$row['id'],'" onclick="tag_string_append(\'',$row['name'],'\')">',$row['name'],'</option>';
+		}
+	}
+
 	# --------------------
 	# Get current headlines and id  prefix with v_
 	function print_news_item_option_list() {
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/tag_api.php mantis-tagging/core/tag_api.php
--- mantis-cvs/core/tag_api.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/core/tag_api.php	2007-08-20 09:34:48.000000000 -0400
@@ -0,0 +1,474 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: tag_api.php,v 1.3 2007/04/20 08:28:23 vboctor Exp $
+	# --------------------------------------------------------
+
+	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
+
+	require_once( $t_core_dir . 'bug_api.php' );
+	require_once( $t_core_dir . 'history_api.php' );
+	
+	### Tag API ###
+
+	function tag_exists( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table WHERE id='$c_tag_id'";
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) > 0;
+	}
+
+	function tag_ensure_exists( $p_tag_id ) {
+		if ( !tag_exists( $p_tag_id ) ) {
+			error_parameters( $p_tag_id );
+			trigger_error( ERROR_TAG_NOT_FOUND, ERROR );
+		}
+	}
+
+	function tag_is_unique( $p_name ) {
+		$c_name = trim( db_prepare_string( $p_name ) );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id FROM $t_tag_table WHERE name like '$c_name'";
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) == 0;
+	}
+
+	function tag_ensure_unique( $p_name ) {
+		if ( !tag_is_unique( $p_name ) ) {
+			trigger_error( ERROR_TAG_DUPLICATE, ERROR );
+		}
+	}
+
+	# Name must start with letter/number and consist of letters, numbers, hyphen, underscore, period, or spaces
+	function tag_name_is_valid( $p_name, $p_prefix="", &$p_matches=null ) {
+		$t_pattern = "/^$p_prefix([a-zA-Z0-9][a-zA-Z0-9-_. ]*)$/";
+		return preg_match( $t_pattern, $p_name, $p_matches );
+	}
+
+	function tag_ensure_name_is_valid( $p_name ) {
+		if ( !tag_name_is_valid( $p_name ) ) {
+			trigger_error( ERROR_TAG_NAME_INVALID, ERROR );
+		}
+	}
+
+	function tag_cmp_name( $p_tag1, $p_tag2 ) {
+		return strcasecmp( $p_tag1['name'], $p_tag2['name'] );
+	}
+
+	function tag_parse_string( $p_string ) {
+		$t_tags = array();
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( "" == trim( $t_name ) ) { continue; }
+			
+			$t_tag_row = tag_get_by_name( $t_name );
+			if ( $t_tag_row !== false ) {
+				$t_tags[] = $t_tag_row;
+			} else {
+				if ( tag_name_is_valid( $t_name ) ) {
+					$t_id = -1;
+				} else {
+					$t_id = -2;
+				}
+				$t_tags[] = array( 'id' => $t_id, 'name' => $t_name );
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	function tag_parse_filters( $p_string ) {
+		$t_tags = array();
+		$t_prefix = "[+-]{0,1}";
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( "" == trim( $t_name ) || !tag_name_is_valid( $t_name, $t_prefix ) ) { continue; }
+			
+			$t_matches = array();
+			if ( tag_name_is_valid( $t_name, $t_prefix, $t_matches ) ) {
+				$t_tag_row = tag_get_by_name( $t_matches[1] );
+				if ( $t_tag_row !== false ) {
+					$t_filter = substr( $t_name, 0, 1 );
+
+					if ( "+" == $t_filter ) {
+						$t_tag_row['filter'] = 1;
+					} elseif ( "-" == $t_filter ) {
+						$t_tag_row['filter'] = -1;
+					} else {
+						$t_tag_row['filter'] = 0;
+					}
+
+					$t_tags[] = $t_tag_row;
+				}
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	# CRUD
+
+	function tag_get( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+
+		$c_tag_id		= db_prepare_int( $p_tag_id );
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	function tag_get_by_name( $p_name ) {
+		$c_name 		= db_prepare_string( $p_name );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE name LIKE '$c_name'";
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	function tag_get_field( $p_tag_id, $p_field_name ) {
+		$row = tag_get( $p_tag_id );
+
+		if ( isset( $row[$p_field_name] ) ) {
+			return $row[$p_field_name];
+		} else {
+			error_parameters( $p_field_name );
+			trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
+			return '';
+		}
+	}
+
+	function tag_create( $p_name, $p_user_id, $p_description='' ) {
+		
+		tag_ensure_name_is_valid( $p_name );
+		tag_ensure_unique( $p_name );
+
+		$c_name			= trim( db_prepare_string( $p_name ) );
+		$c_description	= db_prepare_string( $p_description );
+		$c_user_id		= db_prepare_int( $p_user_id );
+		$c_date_created	= db_now();
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "INSERT INTO $t_tag_table
+				( user_id, 	
+				  name, 
+				  description, 
+				  date_created, 
+				  date_updated 
+				)
+				VALUES
+				( '$c_user_id', 
+				  '$c_name', 
+				  '$c_description', 
+				  ".$c_date_created.", 
+				  ".$c_date_created."
+				)";
+
+		db_query( $query );
+		return db_insert_id( $t_tag_table );
+	}
+
+	function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description ) {
+		tag_ensure_exists( $p_tag_id );
+		user_ensure_exists( $p_user_id );
+		
+		tag_ensure_name_is_valid( $p_name );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+
+		$t_rename = false;
+		if ( strtolower($p_name) != strtolower($t_tag_name) ) {
+			tag_ensure_unique( $p_name );
+			$t_rename = true;
+		}
+		
+		$c_tag_id		= trim( db_prepare_int( $p_tag_id ) );
+		$c_user_id		= db_prepare_string( $p_user_id );
+		$c_name			= db_prepare_string( $p_name );
+		$c_description	= db_prepare_string( $p_description );
+		$c_date_updated	= db_now();
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "UPDATE $t_tag_table
+					SET user_id='$c_user_id',
+						name='$c_name',
+						description='$c_description',
+						date_updated=".$c_date_updated."
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		if ( $t_rename ) {
+			$t_bugs = tag_get_bugs_attached( $p_tag_id );
+
+			foreach ( $t_bugs as $t_bug_id ) {
+				history_log_event_special( $t_bug_id, TAG_RENAMED, $t_tag_name, $c_name );
+			}
+		}
+
+		return true;
+	}
+
+	function tag_delete( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+		
+		$t_bugs = tag_get_bugs_attached( $p_tag_id );
+		foreach ( $t_bugs as $t_bug_id ) {
+			tag_bug_detach( $p_tag_id, $t_bug_id );
+		}
+		
+		$c_tag_id			= db_prepare_int( $p_tag_id );
+
+		$t_tag_table		= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table	= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		return true;
+	}
+	
+	# Associative
+
+	function tag_bug_is_attached( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+		return ( db_num_rows( $result ) > 0 );
+	}
+
+	function tag_bug_get_row( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		if ( db_num_rows( $result ) == 0 ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+		return db_fetch_array( $result );
+	}
+
+	function tag_bug_get_attached( $p_bug_id ) {
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT t.*, b.user_id as user_attached, b.date_attached
+					FROM $t_tag_table as t
+					LEFT JOIN $t_bug_tag_table as b
+						on t.id=b.tag_id
+					WHERE b.bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		$rows = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$rows[] = $row;
+		}
+		
+		usort( $rows, "tag_cmp_name" );
+		return $rows;
+	}
+
+	function tag_get_bugs_attached( $p_tag_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT bug_id FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		$bugs = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$bugs[] = $row['bug_id'];
+		}
+
+		return $bugs;
+	}
+
+	function tag_bug_attach( $p_tag_id, $p_bug_id, $p_user_id ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+		user_ensure_exists( $p_user_id );
+		
+		if ( tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_ALREADY_ATTACHED, ERROR );
+		}
+
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+		$c_user_id	 	= db_prepare_int( $p_user_id );
+		$c_date_attached= db_now();
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "INSERT INTO $t_bug_tag_table
+					( tag_id,
+					  bug_id,
+					  user_id,
+					  date_attached
+					)
+					VALUES
+					( '$c_tag_id',
+					  '$c_bug_id',
+					  '$c_user_id',
+					  ".$c_date_attached."
+					)";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_ATTACHED, $t_tag_name );
+
+		return true;
+	}
+
+	function tag_bug_detach( $p_tag_id, $p_bug_id ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+
+		if ( !tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_bug_tag_table 
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_DETACHED, $t_tag_name );
+
+		return true;
+	}
+
+	# Display
+
+	function tag_display_link( $p_tag_row, $p_bug_id=0 ) {
+		if ( auth_get_current_user_id() == $p_tag_row[user_attached] ) {
+			$t_detach = config_get( 'tag_detach_own_threshold' );
+		} else {
+			$t_detach = config_get( 'tag_detach_threshold' );
+		}
+		
+		$t_name = string_display_line( $p_tag_row['name'] );
+		$t_description = string_display_line( $p_tag_row['description'] );
+		
+		echo "<a href='tag_view_page.php?tag_id=$p_tag_row[id]' title='$t_description'>$t_name</a>";
+		
+		if ( access_has_global_level($t_detach) ) {
+			$t_tooltip = sprintf( lang_get( 'tag_detach' ), $t_name );
+			echo " [<a href='tag_detach.php?bug_id=$p_bug_id&tag_id=$p_tag_row[id]' title='$t_tooltip'>x</a>]";
+		}
+		
+		return true;
+	}
+
+	function tag_display_attached( $p_bug_id ) {
+		$t_tag_rows = tag_bug_get_attached( $p_bug_id );
+
+		if ( count( $t_tag_rows ) == 0 ) {
+			echo lang_get( 'tag_none_attached' );
+		} else {
+			$i = 0;
+			foreach ( $t_tag_rows as $t_tag ) {
+				echo ( $i > 0 ? config_get('tag_separator')." " : "" );
+				tag_display_link( $t_tag, $p_bug_id );
+				$i++;
+			}
+		}
+
+		return true;
+	}
+
+	# Statistics
+
+	function tag_stats_attached( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT COUNT(*) FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		return db_result( $result );
+	}
+
+	function tag_stats_related( $p_tag_id, $p_limit=5 ) {
+		$t_tag_table = config_get( 'mantis_tag_table' );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+		$c_tag_id = db_prepare_int( $p_tag_id );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id != $c_tag_id
+						AND bug_id IN ( SELECT bug_id FROM $t_bug_tag_table
+									WHERE tag_id=$c_tag_id ) ";
+		$result = db_query( $query );
+
+		$t_tag_counts = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$t_tag_counts[$row['tag_id']]++;
+		}
+
+		arsort( $t_tag_counts );
+
+		$t_tags = array();
+		$i = 1;
+		foreach ( $t_tag_counts as $t_tag_id => $t_count ) {
+			$t_tag_row = tag_get($t_tag_id);
+			$t_tag_row['count'] = $t_count;
+			$t_tags[] = $t_tag_row;
+			$i++;
+			if ( $i > $p_limit ) { break; }
+		}
+
+		return $t_tags;
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/core/xmlhttprequest_api.php mantis-tagging/core/xmlhttprequest_api.php
--- mantis-cvs/core/xmlhttprequest_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/xmlhttprequest_api.php	2007-08-16 13:48:44.000000000 -0400
@@ -31,6 +31,15 @@
 		echo '</select>';
 	}
 
+	function xmlhttprequest_user_combobox() {
+		$f_user_id = gpc_get_int( 'user_id' );
+		$f_user_access = gpc_get_int( 'access_level' );
+		
+		echo '<select name="user_id">';
+		print_user_option_list( $f_user_id, ALL_PROJECTS, $f_user_access );
+		echo '</select>';
+	}
+
 	# ---------------
 	# Echos a serialized list of platforms starting with the prefix specified in the $_POST
 	function xmlhttprequest_platform_get_with_prefix() {
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/javascript/common.js mantis-tagging/javascript/common.js
--- mantis-cvs/javascript/common.js	2007-08-14 10:12:43.000000000 -0400
+++ mantis-tagging/javascript/common.js	2007-08-16 14:57:40.000000000 -0400
@@ -163,3 +163,16 @@
   setDisplay( idTag, (document.getElementById(idTag).style.display == 'none')?1:0 );
 }
 
+/* Tag functionality */
+function tag_string_append( p_string ) {
+	t_tag_separator = document.getElementById('tag_separator').value;
+	t_tag_string = document.getElementById('tag_string');
+	t_tag_select = document.getElementById('tag_select');
+	if ( t_tag_string.value != '' ) {
+		t_tag_string.value = t_tag_string.value + t_tag_separator + p_string;
+	} else {
+		t_tag_string.value = t_tag_string.value + p_string;
+	}
+	t_tag_select.selectedIndex=0;
+}
+
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/lang/strings_english.txt mantis-tagging/lang/strings_english.txt
--- mantis-cvs/lang/strings_english.txt	2007-08-16 13:24:00.000000000 -0400
+++ mantis-tagging/lang/strings_english.txt	2007-08-20 09:28:47.000000000 -0400
@@ -275,6 +275,11 @@
 $MANTIS_ERROR[ERROR_USER_CHANGE_LAST_ADMIN] = 'You cannot change the access level of the only ADMINISTRATOR in the system.';
 $MANTIS_ERROR[ERROR_PAGE_REDIRECTION] = 'Page redirection error, ensure that there are no spaces outside the PHP block (&lt;?php ?&gt;) in config_inc.php or custom_*.php files.';
 $MANTIS_ERROR[ERROR_TWITTER_NO_CURL_EXT] = 'Twitter integration requires PHP CURL extension which is not installed.';
+$MANTIS_ERROR[ERROR_TAG_NOT_FOUND] = 'Could not find a tag with that name.';
+$MANTIS_ERROR[ERROR_TAG_DUPLICATE] = 'A tag already exists with that name.';
+$MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
+$MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
+$MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -1347,6 +1352,38 @@
 # wiki related strings
 $s_wiki = 'Wiki';
 
+# Tagging
+$s_tags = 'Tags';
+$s_tag_details = 'Tag Details: %s';
+$s_tag_id = 'Tag ID';
+$s_tag_name = 'Name';
+$s_tag_creator = 'Creator';
+$s_tag_created = 'Date Created';
+$s_tag_updated = 'Last Updated';
+$s_tag_description = 'Tag Description';
+$s_tag_statistics = 'Usage Statistics';
+$s_tag_update = 'Update Tag: %s';
+$s_tag_update_return = 'Back to Tag';
+$s_tag_update_button = 'Update Tag';
+$s_tag_delete_button = 'Delete Tag';
+$s_tag_delete_message = 'Are you sure you wish to delete this tag?';
+$s_tag_existing = 'Existing tags';
+$s_tag_none_attached = 'No tags attached.';
+$s_tag_attach = 'Attach';
+$s_tag_attach_long = 'Attach Tags';
+$s_tag_attach_failed = 'Tag attachment failed.';
+$s_tag_detach = 'Detach %s';
+$s_tag_separate_by = "(Separate by '%s')";
+$s_tag_invalid_name = 'Invalid tag name.';
+$s_tag_create_denied = 'Create permission denied.';
+$s_tag_filter_default = 'Attached Issues (%s)';
+$s_tag_history_attached = 'Tag Attached';
+$s_tag_history_detached = 'Tag Detached';
+$s_tag_history_renamed = 'Tag Renamed';
+$s_tag_related = 'Related Tags';
+$s_tag_related_issues = 'Shared Issues (%s)';
+$s_tag_stats_attached = 'Issues attached: %s';
+
 # Time Tracking
 $s_time_tracking_billing_link = 'Billing';
 $s_time_tracking = 'Time tracking';
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_attach.php mantis-tagging/tag_attach.php
--- mantis-cvs/tag_attach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_attach.php	2007-08-20 08:43:02.000000000 -0400
@@ -0,0 +1,108 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_bug_id = gpc_get_int( 'bug_id' );
+	$f_tag_select = gpc_get_int( 'tag_select' );
+	$t_user_id = auth_get_current_user_id();
+
+	access_ensure_global_level( config_get( 'tag_attach_threshold' ) );
+
+	$t_tags = tag_parse_string( gpc_get_string( 'tag_string' ) );
+	$t_can_create = access_has_global_level( config_get( 'tag_create_threshold' ) );
+	
+	$t_tags_create = array();
+	$t_tags_attach = array();
+	$t_tags_failed = array();
+
+	foreach ( $t_tags as $t_tag_row ) {
+		if ( -1 == $t_tag_row['id'] ) {
+			if ( $t_can_create ) {
+				$t_tags_create[] = $t_tag_row;
+			} else {
+				$t_tags_failed[] = $t_tag_row;
+			}
+		} elseif ( -2 == $t_tag_row['id'] ) {
+			$t_tags_failed[] = $t_tag_row;
+		} else {
+			$t_tags_attach[] = $t_tag_row;
+		}
+	}
+
+	if ( 0 < $f_tag_select && tag_exists( $f_tag_select ) ) {
+		$t_tags_attach[] = tag_get( $f_tag_select );
+	}
+
+	if ( count( $t_tags_failed ) > 0 ) {
+		html_page_top1( lang_get( 'tag_attach_long' ).' '.bug_format_summary( $f_bug_id, SUMMARY_CAPTION ) );
+		html_page_top2();
+?>
+<br/>
+<table class="width75" align="center">
+	<tr class="row-category">
+	<td colspan="2"><?php echo lang_get( 'tag_attach_failed' ) ?></td>
+	</tr>
+	<tr class="spacer"><td colspan="2"></td></tr>
+<?php		
+		$t_tag_string = "";
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+
+		foreach( $t_tags_failed as $t_tag_row ) {
+			echo '<tr ',helper_alternate_class(),'>';
+			if ( -1 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_invalid_name' ),'</td>';
+			} elseif ( -2 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_create_denied' ),'</td>';
+			}
+			echo '<td>',$t_tag_row['name'],'</td></tr>';
+			
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+?>
+	<tr class="spacer"><td colspan="2"></td></tr>
+	<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td>
+<?php
+		print_tag_input( $f_bug_id, $t_tag_string );
+?>	
+	</td>
+	</tr>
+</table>
+<?php
+		html_page_bottom1(__FILE__);
+	} else {
+		foreach( $t_tags_create as $t_tag_row ) {
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id );
+			$t_tags_attach[] = $t_tag_row;
+		}
+
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( ! tag_bug_is_attached( $t_tag_row['id'], $f_bug_id ) ) {
+				tag_bug_attach( $t_tag_row['id'], $f_bug_id, $t_user_id );
+			}
+		}
+
+		print_successful_redirect_to_bug( $f_bug_id );
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_delete.php mantis-tagging/tag_delete.php
--- mantis-cvs/tag_delete.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_delete.php	2007-08-17 13:34:13.000000000 -0400
@@ -0,0 +1,27 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_edit_threshold' ) );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	helper_ensure_confirmed( lang_get( 'tag_delete_message' ), lang_get( 'tag_delete_button' ) );
+
+	tag_delete( $f_tag_id );
+	
+	print_successful_redirect( config_get( 'default_home_page' ) );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_detach.php mantis-tagging/tag_detach.php
--- mantis-cvs/tag_detach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_detach.php	2007-08-17 11:34:04.000000000 -0400
@@ -0,0 +1,33 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$f_bug_id = gpc_get_int( 'bug_id' );
+
+	$t_tag_row = tag_get( $f_tag_id );
+	$t_tag_bug_row = tag_bug_get_row( $f_tag_id, $f_bug_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_detach_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_bug_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_detach_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	tag_bug_detach( $f_tag_id, $f_bug_id );
+	
+	print_successful_redirect_to_bug( $f_bug_id );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_update_page.php mantis-tagging/tag_update_page.php
--- mantis-cvs/tag_update_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update_page.php	2007-08-17 13:37:45.000000000 -0400
@@ -0,0 +1,105 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'ajax_api.php' );
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+	
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+	
+	html_page_top1( sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<form method="post" action="tag_update.php">
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) ?>
+		<input type="hidden" name="tag_id" value="<?php echo $f_tag_id ?>"/>
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'tag_view_page.php?tag_id='.$f_tag_id, lang_get( 'tag_update_return' ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><input type="text" <?php echo helper_get_tab_index() ?> name="name" value="<?php echo $t_tag_row['name'] ?>"/></td>
+	<td><?php
+			if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+				if ( ON == config_get( 'use_javascript' ) ) {
+					$t_username = prepare_user_name( $t_tag_row['user_id'] );
+					echo ajax_click_to_edit( $t_username, 'user_id', 'entrypoint=user_combobox&user_id=' . $t_tag_row['user_id'] . '&access_level=' . config_get( 'tag_create_threshold' ) );
+				} else {
+					echo '<select ', helper_get_tab_index(), ' name="user_id">';
+					print_user_option_list( $t_tag_row['user_id'], ALL_PROJECTS, config_get( 'tag_create_threshold' ) );
+					echo '</select>';
+				}
+			} else {
+				echo user_get_name($t_tag_row['user_id']);
+			}
+		?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4">
+		<textarea name="description" <?php echo helper_get_tab_index() ?> cols="80" rows="6"><?php echo $t_tag_row['description'] ?></textarea>
+	</td>
+</tr>
+
+<!-- Submit Button -->
+<tr>
+	<td class="center" colspan="6">
+		<input <?php echo helper_get_tab_index() ?> type="submit" class="button" value="<?php echo lang_get( 'tag_update_button' ) ?>" />
+	</td>
+</tr>
+
+</table>
+</form>
+
+<?php
+	html_page_bottom1( __FILE__ );
+?>
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_update.php mantis-tagging/tag_update.php
--- mantis-cvs/tag_update.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update.php	2007-08-16 13:48:44.000000000 -0400
@@ -0,0 +1,55 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2006  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+		$f_new_user_id = gpc_get_int( 'user_id', $t_tag_row['user_id'] );
+	} else {
+		$f_new_user_id = $t_tag_row['user_id'];
+	}
+
+	$f_new_name = gpc_get_string( 'name', $t_tag_row['name'] );
+	$f_new_description = gpc_get_string( 'description', $t_tag_row['description'] );
+
+	$t_update = false;
+
+	if ( $t_tag_row['user_id'] != $f_new_user_id ) {
+		user_ensure_exists( $f_new_user_id );
+		$t_update = true;
+	}
+
+	if ( 	$t_tag_row['name'] != $f_new_name ||
+			$t_tag_row['description'] != $f_new_description ) {
+
+		$t_update = true;
+	}
+
+	tag_update( $f_tag_id, $f_new_name, $f_new_user_id, $f_new_description );
+		
+	$t_url = 'tag_view_page.php?tag_id='.$f_tag_id;
+	print_successful_redirect( $t_url );
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/tag_view_page.php mantis-tagging/tag_view_page.php
--- mantis-cvs/tag_view_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_view_page.php	2007-08-20 09:31:56.000000000 -0400
@@ -0,0 +1,107 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id:  $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+	
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_view_threshold' ) );
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	$t_name = string_display_line( $t_tag_row['name'] );
+	$t_description = string_display( $t_tag_row['description'] );
+
+	html_page_top1( sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) ?>
+
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'search.php?hide_status_id=90&tag_string='.urlencode($t_tag_row['name']), sprintf( lang_get( 'tag_filter_default' ), tag_stats_attached( $f_tag_id ) ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><?php echo $t_name ?></td>
+	<td><?php echo user_get_name($t_tag_row['user_id']) ?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4"><?php echo $t_description ?></td>
+</tr>
+
+<!-- Statistics -->
+<?php
+	$t_tags_related = tag_stats_related( $f_tag_id );
+	if ( count( $t_tags_related ) ) { 
+		echo '<tr ',helper_alternate_class(),'>';
+		echo '<td class="category" rowspan="',count( $t_tags_related ),'">',lang_get( 'tag_related' ),'</td>';
+		
+		$i = 0;
+		foreach( $t_tags_related as $t_tag ) {
+			$t_name = string_display_line( $t_tag['name'] );
+			$t_description = string_display_line( $t_tag['description'] );
+			$t_count = $t_tag['count'];
+
+			echo ( $i > 0 ? '<tr '.helper_alternate_class().'>' : '' );
+			echo "<td><a href='tag_view_page.php?tag_id=$t_tag[id]' title='$t_description'>$t_name</a></td>\n";
+			echo '<td colspan="3">';
+			print_bracket_link( 'search.php?hide_status_id=90&tag_string='.urlencode("+$t_tag_row[name]".config_get('tag_separator')."+$t_name"), sprintf( lang_get( 'tag_related_issues' ), $t_tag['count'] ) );
+			echo '</a></td></tr>';
+			
+			$i++;
+		}
+	}
+?>
+
+<!-- Buttons -->
+<tr>
+	<td colspan="5">
+		<?php html_buttons_tag_view_page( $f_tag_id ); ?>
+	</td>
+</tr>
+
+</table>
+<?php
+	html_page_bottom1( __FILE__ );
+?>
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/view_all_set.php mantis-tagging/view_all_set.php
--- mantis-cvs/view_all_set.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/view_all_set.php	2007-08-17 22:43:15.000000000 -0400
@@ -203,6 +203,9 @@
 	$f_do_filter_by_date	= gpc_get_bool( 'do_filter_by_date' );
 	$f_view_state			= gpc_get_int( 'view_state', META_FILTER_ANY );
 
+	$f_tag_string			= gpc_get_string( 'tag_string', '' );
+	$f_tag_select			= gpc_get_int( 'tag_select', '0' );
+
 	$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 	$f_custom_fields_data 	= array();
 	if ( is_array( $t_custom_fields ) && ( sizeof( $t_custom_fields ) > 0 ) ) {
@@ -414,6 +417,8 @@
 				$t_setting_arr['platform'] = $f_platform;
 				$t_setting_arr['os'] = $f_os;
 				$t_setting_arr['os_build'] = $f_os_build;
+				$t_setting_arr['tag_string'] = $f_tag_string;
+				$t_setting_arr['tag_select'] = $f_tag_select;
 				break;
 		# Set the sort order and direction
 		case '2':
diff -urN --exclude=CVS --exclude=.svn --exclude=.swp --exclude='.git*' --exclude=config_inc.php mantis-cvs/view_filters_page.php mantis-tagging/view_filters_page.php
--- mantis-cvs/view_filters_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/view_filters_page.php	2007-08-17 22:21:01.000000000 -0400
@@ -16,6 +16,7 @@
 	require_once( $t_core_path.'bug_api.php' );
 	require_once( $t_core_path.'string_api.php' );
 	require_once( $t_core_path.'date_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 
 	auth_ensure_user_authenticated();
 
@@ -406,7 +407,8 @@
 </tr>
 <tr class="row-category2">
 <td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"><?php echo lang_get( 'search' ) ?></td>
-<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 1 ) * $t_custom_cols ); ?>"></td>
+<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 2 ) * $t_custom_cols ); ?>"><?php echo lang_get( 'tags' ) ?></td>
+<td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"></td>
 </tr>
 <tr>
 	<!-- Search field -->
@@ -414,13 +416,12 @@
 		<input type="text" size="16" name="search" value="<?php echo string_html_specialchars( $t_filter['search'] ); ?>" />
 	</td>
 
-	<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 3 ) * $t_custom_cols ); ?>"></td>
+	<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 2 ) * $t_custom_cols ); ?>"><?php print_filter_tag_string() ?></td>
 
 	<!-- Submit button -->
 	<td class="right" colspan="<?php echo ( 1 * $t_custom_cols ); ?>">
 		<input type="submit" name="filter" class="button" value="<?php echo lang_get( 'filter_button' ) ?>" />
 	</td>
-	<td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"></td>
 </tr>
 </table>
 </form>
mantis-tagging-2007-08-20.patch (55,957 bytes)   
jreese

jreese

2007-08-20 10:06

reporter   ~0015461

Patch 'mantis-tagging-2007-08-20.patch' was created from CVS head on Monday, Aug 20, 2007.

Major changes from previous patch:

  • Fixed typos for filter URL's
  • Optimized queries per thraxisp
  • Added/Improved functionality when Javascript is disabled:
    -- Editing filters without Javascript is implemented
    -- Tag selected in dropdown box is also included when filtering/attaching
vboctor

vboctor

2007-08-21 02:50

manager   ~0015466

Good job. I've tested the sample website after your second patch and following are my comments (I haven't looked at the code yet):

  1. Use a delete icon rather than [x] - phpMyAdmin has a nice delete icon that you can use.
  2. The bubble for the delete icon should be Detach 'xxx' rather than Detach xxx.
  3. Click "Tags" in the filter box, then select a tag from combobox, then press Apply. This will set the Tags filter to the name of the tag without the + sign, and the filter will do nothing. If a tag name is specified without + or -, then it should default to + behavior. It seems that this works from perma-links, but not from the steps outlined.
  4. Visit an issue, type a new tag, click attach. The tag will be added to the issue but won't appear in the list of "Existing Tags", is this by design? I think there are cases where I see a longer list of tags.
  5. Consider providing a Title and a Description for the tag. The title should appear as the bubble when the mouse hovers over a tag link. Currently you should nothing in this case.
  6. How do you calculate the number of attached issues or shared issues? Do you take the current user access rights into consideration? i.e. Do you use filter API or something similar?
  7. At the moment there is an issue that the user can't see any hints about the tag until it is added to the issue.
  8. There is no way to rename a tag. Once a tag is renamed, it should be reflected on issues that are tagged to it and their history.
  9. When I type a tag like "<b>test</b>", I get some specially formatted error page that indicates that attaching failed, shows the tag, and allows me to edit it. However, there is no point in editing since there is no way to re-submit. I thinking it might be simpler just to have a standard trigger_error and allow the user to go back and fix the error. The error should indicate why a tag was rejected.
  10. If I update the tag with an invalid name (e.g. some javascript), I get "APPLICATION ERROR #ERROR_TAG_NAME_INVALID", shouldn't ERROR_TAG_NAME_INVALID be a number? I am thinking it may not be defined in the constants file.
  11. In the tag view page, I initially didn't see the "Attached Issues" link, I am wondering if it can be more visible. I think at the moment the "Shared Issues" is in a much better position that "Attached Issues", although the latter is more important.
jreese

jreese

2007-08-21 07:48

reporter   ~0015469

1 & 2: Will do.

  1. The filters actually work differently based on whether there is a +, -, or nothing prefixing the tag. I already modified the wiki to match, but:
    "any tag noted with a ‘+’ must be attached to any resulting bug, any tag noted with ‘-’ must not be attached to any resulting bug, and resulting bugs must have any of the un-denoted tags attached, but not necessarily all of them. "

  2. By design, the existing tags dropdown will not show any tags that are already attached, to reduce the size of the list.

  3. Currently, each tag link uses the tag's description for the tooltip. Since probably none of the tags on my public test site have descriptions, you may not have noticed this already, but it's there.

  4. It does not take into account whether the user can view/access every one of those issues, but I feel that for information's sake, it's more important to keep the overall information correct for related tags. Just because you can't see all the issues that have both tags on them does not mean its relation is less connected. And the Shared Issues link directs to the filters page which will take permissions into account, so I don't see this as a problem. Correct me if I am wrong.

  5. Not quite sure what you mean.

  6. When you are viewing a tag's details page, there is an 'Update Tag' button that will allow you to edit the tag's name and description. All attached bugs will have a history entry marking the rename event.

  7. The reason I didn't simply show an error and return is because the user can attach/create multiple tags at one time, and it makes it much more usable to see errors for each individual tag, and to edit the input right then and there. I agree that it seems odd when adding a single tag, but it is very useful otherwise.

  8. Sounds like I missed some constants, yes. I'll get on that.

  9. I simply positioned it similar to the bug view page. If it is agreed that it would be better elsewhere, I will gladly move it.

vboctor

vboctor

2007-08-22 00:54

manager   ~0015474

Replies to your replies:

  1. We should discuss this on the dev mailing list. I would like to get more opinions on this. Please send a message with the current behavior and lets see the response.

  2. How does it work with multi-line and long descriptions?

  3. A lot of people would consider this information disclosure. If an issue is not accessible to a user, then they shouldn't know about it. In this case the importance of knowing about the issue is related to the meaning of the tag.

  4. It is ok to show the details of the tags with errors. However, there was a combo-box that wasn't really used.

vboctor

vboctor

2007-08-22 00:55

manager   ~0015475

I have a quick look at the code, it looks good. Following are my comments:

  1. Documentation (e.g. function headers). Please use phpDocumentor (http://www.phpdoc.org/) format. We have decided to move to this standard. This is a good start point since it is a new API.

  2. "" == trim( $t_name ) -> is_blank( $t_name );

  3. I noticed you are using LIKE instead of = in SQL queries. Is this so that comparisons are case sensitive? If so, this won't work for all DBMSes. Some of them has like / ilike.

  4. tag_bug_attach(), typically user_id is defaulted to null, and if null, it is defaulted to current user.

  5. Access checks for actions like attach/detach should be done in the API.

  6. In bug_delete() we should delete the tag associations. I didn't notice that.

  7. For strings that don't include variables or characters like \n, use '' instead of "".

  8. By default we should restrict $g_tag_create_threshold to DEVELOPER.

  9. We should add an index to mantis_bug_tag_table on the bug_id. This will be used when viewing a bug to get all associated tags. Also in other cases like delete bug.

vboctor

vboctor

2007-08-22 02:09

manager   ~0015482

I've just remembered one more needed feature. We should be able to attach/detach tags as a group action. For example, in the case of the official Mantis Bug Tracker, we will define some tags and then apply them to the applicable issues. Without this feature, it will be very hard to get started with this feature. Group attaching should ignore issues that already have the tag and group detaching should ignore issues that don't have the tag.

I had a look at the wiki requirements now, and found the following:

We should add group actions to add/remove tags. (vboctor)

Did you also implement the following one:

What will happen when a user is deleted? What will happen to the tags assigned by him? I think we should keep them, we should handle this the same way this is handled in other places. I think we keep the user id, and refer to the user as user10 or whatever the id was. (vboctor)

jreese

jreese

2007-08-22 16:33

reporter   ~0015490

Last edited: 2007-08-22 16:38

Multiline descriptions are handled by the string_display_line() function, so that newlines are stripped. However, I did not specifically handle long descriptions, because all browsers show different amounts of tooltip information. If, for example, you want to only show the first 200 characters, that should just a substr() added before string_display_line().

Regarding information, that's a good argument, so I will alter the SQL queries appropriately, and include that in the next patch.

As for the extra combo-box, that's part of the 'standard' tag form I created, and it would allow someone to pick existing tags if, for example, they lack creation privileges and need to see what they can choose from.

1,2,4,5,6,7,9: I will do this tomorrow when I get in to work.

  1. I used LIKE because it is case insenstive in mysql (95% of my experience). Is there a cross-platform method of doing case-insensitive searching that I should use instead?

  2. I have to disagree. One of the benefits to tagging is that it allows everyone to help classify and describe content. If you only allow developers to create new tags, but allow anyone to attach them to reports, it seems awkward and counter-productive in my eyes. Unless others feel the same way, I think it would be best to keep this as public as possible.

I will look into adding tagging to the group actions set. Am I assuming correctly that this is only used from the View Issues page, or are there more locations I would need to modify for this feature?

Finally, I was under the impression that the user_get_name() function would handle this situation, and that is what I used in all places to display the appropriate username. Is there a different approach I should be using for this?

jreese

jreese

2007-08-23 09:41

reporter   ~0015494

  1. The mantis_bug_tag_table already include bug_id in the primary key. Adding bug_id as an index would just be redundant overhead in the database, would it not?

2007-08-24 13:31

 

mantis-tagging-2007-08-24.patch (69,246 bytes)   
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/admin/schema.php mantis-tagging/admin/schema.php
--- mantis-cvs/admin/schema.php	2007-08-07 10:54:56.000000000 -0400
+++ mantis-tagging/admin/schema.php	2007-08-24 13:25:17.000000000 -0400
@@ -330,6 +330,20 @@
 $upgrade[] = Array('CreateIndexSQL',Array('idx_diskfile',config_get('mantis_bug_file_table'),'diskfile'));
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_user_print_pref_table' ), "print_pref C(64) NOTNULL" ) );
 $upgrade[] = Array('AlterColumnSQL', Array( config_get( 'mantis_bug_history_table' ), "field_name C(64) NOTNULL" ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_tag_table' ), "
+	id				I		UNSIGNED NOTNULL PRIMARY AUTOINCREMENT,
+	user_id			I		UNSIGNED NOTNULL DEFAULT '0',
+	name			C(100)	NOTNULL PRIMARY DEFAULT \" '' \",
+	description		XL		NOTNULL,
+	date_created	T		NOTNULL DEFAULT '1970-01-01 00:00:01',
+	date_updated	T		NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
+$upgrade[] = Array('CreateTableSQL', Array( config_get( 'mantis_bug_tag_table' ), "
+	bug_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	tag_id			I	UNSIGNED NOTNULL PRIMARY DEFAULT '0',
+	user_id			I	UNSIGNED NOTNULL DEFAULT '0',
+	date_attached	T	NOTNULL DEFAULT '1970-01-01 00:00:01'
+	", Array( 'mysql' => 'TYPE=MyISAM', 'pgsql' => 'WITHOUT OIDS' ) ) );
 
 # Release marker: 1.1.0a4
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/bug_actiongroup_attach_tags_inc.php mantis-tagging/bug_actiongroup_attach_tags_inc.php
--- mantis-cvs/bug_actiongroup_attach_tags_inc.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/bug_actiongroup_attach_tags_inc.php	2007-08-24 11:00:23.000000000 -0400
@@ -0,0 +1,101 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	$t_core_path = config_get( 'core_path' );
+	require_once( $t_core_path . 'tag_api.php' );
+
+	/**
+	 * Prints the title for the custom action page.	 
+	 */
+	function action_attach_tags_print_title() {
+        echo '<tr class="form-title">';
+        echo '<td colspan="2">';
+        echo lang_get( 'tag_attach_long' );
+        echo '</td></tr>';		
+	}
+
+	/**
+	 * Prints the table and form for the Attach Tags group action page.
+	 */
+	function action_attach_tags_print_fields() {
+		echo '<tr ',helper_alternate_class(),'><td class="category">',lang_get('tag_attach_long'),'</td><td>';
+		print_tag_input();
+		echo '<input type="submit" class="button" value="' . lang_get( 'tag_attach' ) . ' " /></td></tr>';
+	}
+
+	/**
+	 * Validates the Attach Tags group action.
+	 * Gets called for every bug, but performs the real tag validation only
+	 * the first time.  Any invalid tags will be skipped, as there is no simple
+	 * or clean method of presenting these errors to the user.
+	 * @param integer Bug ID
+	 * @return boolean True
+	 */
+	function action_attach_tags_validate( $p_bug_id ) {
+		global $g_action_attach_tags_valid;
+		if ( !isset( $g_action_attach_tags_valid ) ) {
+			$f_tag_string = gpc_get_string( 'tag_string' );
+			$f_tag_select = gpc_get_string( 'tag_select' );
+
+			global $g_action_attach_tags_attach, $g_action_attach_tags_create, $g_action_attach_tags_failed; 
+			$g_action_attach_tags_attach = array();
+			$g_action_attach_tags_create = array();
+			$g_action_attach_tags_failed = array();
+
+			$t_tags = tag_parse_string( $f_tag_string );
+			$t_can_create = access_has_global_level( config_get( 'tag_create_threshold' ) );
+
+			foreach ( $t_tags as $t_tag_row ) {
+				if ( -1 == $t_tag_row['id'] ) {
+					if ( $t_can_create ) {
+						$g_action_attach_tags_create[] = $t_tag_row;
+					} else {
+						$g_action_attach_tags_failed[] = $t_tag_row;
+					}
+				} elseif ( -2 == $t_tag_row['id'] ) {
+					$g_action_attach_tags_failed[] = $t_tag_row;
+				} else {
+					$g_action_attach_tags_attach[] = $t_tag_row;
+				}
+			}
+
+			if ( 0 < $f_tag_select && tag_exists( $f_tag_select ) ) {
+				$g_action_attach_tags_attach[] = tag_get( $f_tag_select );
+			}
+
+		}
+
+		global $g_action_attach_tags_attach, $g_action_attach_tags_create, $g_action_attach_tags_failed; 
+
+		return true;
+	}
+
+	/**
+	 * Attaches all the tags to each bug in the group action.
+	 * @param integer Bug ID
+	 * @return boolean True if all tags attach properly
+	 */
+	function action_attach_tags_process( $p_bug_id ) {
+		global $g_action_attach_tags_attach, $g_action_attach_tags_create; 
+
+		foreach( $g_action_attach_tags_create as $t_tag_row ) {
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'] );
+			$g_action_attach_tags_attach[] = $t_tag_row;
+		}
+
+		foreach( $g_action_attach_tags_attach as $t_tag_row ) {
+			if ( ! tag_bug_is_attached( $t_tag_row['id'], $p_bug_id ) ) {
+				tag_bug_attach( $t_tag_row['id'], $p_bug_id );
+			}
+		}
+
+		return true;
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/bug_view_advanced_page.php mantis-tagging/bug_view_advanced_page.php
--- mantis-cvs/bug_view_advanced_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_advanced_page.php	2007-08-24 11:00:22.000000000 -0400
@@ -20,6 +20,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 
 	$f_bug_id		= gpc_get_int( 'bug_id' );
 	$f_history		= gpc_get_bool( 'history', config_get( 'history_default_visible' ) );
@@ -464,13 +465,35 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
 	<td colspan="6"></td>
 </tr>
 
-
 <!-- Custom Fields -->
 <?php
 	$t_custom_fields_found = false;
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/bug_view_page.php mantis-tagging/bug_view_page.php
--- mantis-cvs/bug_view_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/bug_view_page.php	2007-08-24 11:00:22.000000000 -0400
@@ -22,6 +22,7 @@
 	require_once( $t_core_path.'date_api.php' );
 	require_once( $t_core_path.'relationship_api.php' );
 	require_once( $t_core_path.'last_visited_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 ?>
 <?php
 	$f_bug_id	= gpc_get_int( 'bug_id' );
@@ -341,6 +342,29 @@
 	</td>
 </tr>
 
+<!-- Tagging -->
+<?php if ( access_has_global_level( config_get( 'tag_view_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tags' ) ?></td>
+	<td colspan="5">
+<?php
+	tag_display_attached( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag_view access ?>
+
+<?php if ( access_has_global_level( config_get( 'tag_attach_threshold' ) ) ) { ?>
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td colspan="5">
+<?php
+	print_tag_attach_form( $f_bug_id );
+?>
+	</td>
+</tr>
+<?php } # has tag attach access ?>
+
 
 <!-- spacer -->
 <tr class="spacer">
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/config_defaults_inc.php mantis-tagging/config_defaults_inc.php
--- mantis-cvs/config_defaults_inc.php	2007-08-24 10:54:10.000000000 -0400
+++ mantis-tagging/config_defaults_inc.php	2007-08-24 11:00:21.000000000 -0400
@@ -1334,6 +1334,7 @@
 	$g_mantis_bug_monitor_table				= '%db_table_prefix%_bug_monitor%db_table_suffix%';
 	$g_mantis_bug_relationship_table		= '%db_table_prefix%_bug_relationship%db_table_suffix%';
 	$g_mantis_bug_table						= '%db_table_prefix%_bug%db_table_suffix%';
+	$g_mantis_bug_tag_table					= '%db_table_prefix%_bug_tag%db_table_suffix%';
 	$g_mantis_bug_text_table				= '%db_table_prefix%_bug_text%db_table_suffix%';
 	$g_mantis_bugnote_table					= '%db_table_prefix%_bugnote%db_table_suffix%';
 	$g_mantis_bugnote_text_table			= '%db_table_prefix%_bugnote_text%db_table_suffix%';
@@ -1343,6 +1344,7 @@
 	$g_mantis_project_table					= '%db_table_prefix%_project%db_table_suffix%';
 	$g_mantis_project_user_list_table		= '%db_table_prefix%_project_user_list%db_table_suffix%';
 	$g_mantis_project_version_table			= '%db_table_prefix%_project_version%db_table_suffix%';
+	$g_mantis_tag_table						= '%db_table_prefix%_tag%db_table_suffix%';
 	$g_mantis_user_table					= '%db_table_prefix%_user%db_table_suffix%';
 	$g_mantis_user_profile_table			= '%db_table_prefix%_user_profile%db_table_suffix%';
 	$g_mantis_user_pref_table				= '%db_table_prefix%_user_pref%db_table_suffix%';
@@ -1824,6 +1826,34 @@
 	$g_recently_visited_count = 5;
 
 	#####################
+	# Bug Tagging
+	#####################
+
+	# String that will separate tags as entered for input
+	$g_tag_separator = ',';
+
+	# Access level required to view tags attached to a bug
+	$g_tag_view_threshold = VIEWER;
+
+	# Access level required to attach tags to a bug
+	$g_tag_attach_threshold = REPORTER;
+
+	# Access level required to detach tags from a bug
+	$g_tag_detach_threshold = DEVELOPER;
+
+	# Access level required to detach tags attached by the same user
+	$g_tag_detach_own_threshold = REPORTER;
+
+	# Access level required to create new tags
+	$g_tag_create_threshold = REPORTER;
+
+	# Access level required to edit tag names and descriptions
+	$g_tag_edit_threshold = DEVELOPER;
+
+	# Access level required to edit descriptions by the creating user
+	$g_tag_edit_own_threshold = REPORTER;
+
+	#####################
 	# Time tracking
 	#####################
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/bug_api.php mantis-tagging/core/bug_api.php
--- mantis-cvs/core/bug_api.php	2007-08-24 10:54:11.000000000 -0400
+++ mantis-tagging/core/bug_api.php	2007-08-24 11:00:23.000000000 -0400
@@ -18,6 +18,7 @@
 	require_once( $t_core_dir . 'string_api.php' );
 	require_once( $t_core_dir . 'sponsorship_api.php' );
 	require_once( $t_core_dir . 'twitter_api.php' );
+	require_once( $t_core_dir . 'tag_api.php' );
 
 	# MASC RELATIONSHIP
 	require_once( $t_core_dir.'relationship_api.php' );
@@ -708,6 +709,12 @@
 		# Delete files
 		file_delete_attachments( $p_bug_id );
 
+		# Detach tags
+		$t_tags = tag_bug_get_attached( $p_bug_id );
+		foreach ( $t_tags as $t_tag_row ) {
+			tag_detach( $t_tag_row['id'], $p_bug_id );
+		}
+
 		# Delete the bug history
 		history_delete( $p_bug_id );
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/constant_inc.php mantis-tagging/core/constant_inc.php
--- mantis-cvs/core/constant_inc.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/constant_inc.php	2007-08-24 11:00:23.000000000 -0400
@@ -153,6 +153,9 @@
 	define( 'CHECKIN',				22 );
 	define( 'BUG_REPLACE_RELATIONSHIP', 		23 );
 	define( 'BUG_PAID_SPONSORSHIP', 		24 );
+	define( 'TAG_ATTACHED', 				25 );
+	define( 'TAG_DETACHED', 				26 );
+	define( 'TAG_RENAMED', 					27 );
 
 	# bug relationship constants
 	define( 'BUG_DUPLICATE',	0 );
@@ -295,6 +298,13 @@
 	# ERROR_TWITTER_*
 	define( 'ERROR_TWITTER_NO_CURL_EXT', 2100 );
 
+	# ERROR_TAG_*
+	define( 'ERROR_TAG_NOT_FOUND', 2200 );
+	define( 'ERROR_TAG_DUPLICATE', 2201 );
+	define( 'ERROR_TAG_NAME_INVALID', 2202 );
+	define( 'ERROR_TAG_NOT_ATTACHED', 2203 );
+	define( 'ERROR_TAG_ALREADY_ATTACHED', 2204 );
+
 	# Status Legend Position
 	define( 'STATUS_LEGEND_POSITION_TOP',		1);
 	define( 'STATUS_LEGEND_POSITION_BOTTOM',	2);
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/filter_api.php mantis-tagging/core/filter_api.php
--- mantis-cvs/core/filter_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/filter_api.php	2007-08-24 11:00:22.000000000 -0400
@@ -16,6 +16,7 @@
 	require_once( $t_core_dir . 'bug_api.php' );
 	require_once( $t_core_dir . 'collapse_api.php' );
 	require_once( $t_core_dir . 'relationship_api.php' );
+	require_once( $t_core_dir . 'tag_api.php' );
 
 	###########################################################################
 	# Filter Property Names
@@ -56,6 +57,8 @@
 	define( 'FILTER_PROPERTY_FILTER_BY_DATE', 'do_filter_by_date' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_PROPERTY_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_PROPERTY_TAG_STRING', 'tag_string' );
+	define( 'FILTER_PROPERTY_TAG_SELECT', 'tag_select' );
 
 	###########################################################################
 	# Filter Query Parameter Names
@@ -96,6 +99,8 @@
 	define( 'FILTER_SEARCH_FILTER_BY_DATE', 'filter_by_date' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_TYPE', 'relationship_type' );
 	define( 'FILTER_SEARCH_RELATIONSHIP_BUG', 'relationship_bug' );
+	define( 'FILTER_SEARCH_TAG_STRING', 'tag_string' );
+	define( 'FILTER_SEARCH_TAG_SELECT', 'tag_select' );
 
 	# Checks the supplied value to see if it is an ANY value.
 	# $p_field_value - The value to check.
@@ -306,6 +311,14 @@
 			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_OS_BUILD, $p_custom_filter[FILTER_PROPERTY_OS_BUILD] );
 		}
 
+		if ( !filter_str_field_is_any( $p_custom_filter[FILTER_PROPERTY_TAG_STRING] ) ) {
+			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_TAG_STRING, $p_custom_filter[FILTER_PROPERTY_TAG_STRING] );
+		}
+
+		if ( !filter_str_field_is_any( $p_custom_filter[FILTER_PROPERTY_TAG_SELECT] ) ) {
+			$t_query[] = filter_encode_field_and_value( FILTER_SEARCH_TAG_SELECT, $p_custom_filter[FILTER_PROPERTY_TAG_SELECT] );
+		}
+
 		if ( isset( $p_custom_filter['custom_fields'] ) ) {
 			foreach( $p_custom_filter['custom_fields'] as $t_custom_field_id => $t_custom_field_values ) {
 				if ( !filter_str_field_is_any( $t_custom_field_values ) ) {
@@ -1057,6 +1070,63 @@
 			array_push( $t_where_clauses, '('. implode( ' OR ', $t_clauses ) .')' );
 		}
 
+		# tags
+		$c_tag_string = trim( $t_filter['tag_string'] );
+		if ( !is_blank( $c_tag_string ) ) {
+			require_once( $t_core_path . 'tag_api.php' );
+			$t_tags = tag_parse_filters( $c_tag_string );
+
+			if ( !count( $t_tags ) ) { break; }
+
+			$t_tags_all = array();
+			$t_tags_any = array();
+			$t_tags_none = array();
+
+			foreach( $t_tags as $t_tag_row ) {
+				switch ( $t_tag_row['filter'] ) {
+					case 1:
+						$t_tags_all[] = $t_tag_row;
+						break;
+					case 0:
+						$t_tags_any[] = $t_tag_row;
+						break;
+					case -1:
+						$t_tags_none[] = $t_tag_row;
+						break;
+				}
+			}
+
+			if ( 0 < $t_filter['tag_select'] && tag_exists( $t_filter['tag_select'] ) ) {
+				$t_tags_any[] = tag_get( $t_filter['tag_select'] );
+			}
+
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			if ( count( $t_tags_all ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_all as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE $t_bug_tag_table.tag_id = $t_tag_row[id] )" );
+				}
+				array_push( $t_where_clauses, '('. implode( ' AND ', $t_clauses ) .')' );
+			}
+			
+			if ( count( $t_tags_any ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_any as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+			
+			if ( count( $t_tags_none ) ) {
+				$t_clauses = array();
+				foreach ( $t_tags_none as $t_tag_row ) {
+					array_push( $t_clauses, "$t_bug_tag_table.tag_id = $t_tag_row[id]" );
+				}
+				array_push( $t_where_clauses, "$t_bug_table.id NOT IN ( SELECT bug_id FROM $t_bug_tag_table WHERE ( ". implode( ' OR ', $t_clauses ) .') )' );
+			}
+		}
+
 		# custom field filters
 		if( ON == config_get( 'filter_by_custom_fields' ) ) {
 			# custom field filtering
@@ -2291,6 +2361,7 @@
 				<a href="<?php PRINT $t_filters_url . 'os_build'; ?>" id="os_build_filter"><?php echo lang_get( 'os_version' ) ?>:</a>
 			</td>
 			<td class="small-caption" valign="top" colspan="5">
+				<a href="<?php PRINT $t_filters_url . 'tag_string'; ?>" id="tag_string_filter"><?php echo lang_get( 'tags' ) ?>:</a>
 			</td>
 			<?php if ( $t_filter_cols > 8 ) {
 				echo '<td class="small-caption" valign="top" colspan="' . ( $t_filter_cols - 8 ) . '">&nbsp;</td>';
@@ -2312,7 +2383,16 @@
 					print_multivalue_field( FILTER_PROPERTY_OS_BUILD, $t_filter[FILTER_PROPERTY_OS_BUILD] );
 				?>
 			</td>
-			<td class="small-caption" colspan="5">
+			<td class="small-caption" valign="top" id="tag_string_filter_target" colspan="5">
+				<?php 
+					$t_tag_string = $t_filter['tag_string'];
+					if ( $t_filter['tag_select'] != 0 ) {
+						$t_tag_string .= ( is_blank( $t_tag_string ) ? '' : config_get( 'tag_separator' ) );
+						$t_tag_string .= tag_get_field( $t_filter['tag_select'], 'name' );
+					}
+					PRINT $t_tag_string 
+				?>
+				<input type="hidden" name="tag_string" value="<?php echo $t_tag_string ?>"/>
 			</td>
 		</tr>
 		<?php
@@ -3025,6 +3105,9 @@
 		if ( !isset( $p_filter_arr['target_version'] ) ) {
 			$p_filter_arr['target_version'] = META_FILTER_ANY;
 		}
+		if ( !isset( $p_filter_arr['tag_string'] ) ) {
+			$p_filter_arr['tag_string'] = gpc_get_string( 'tag_string', '' );
+		}
 
 		$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 		$f_custom_fields_data 	= array();
@@ -3531,6 +3614,22 @@
 
 	}
 
+	function print_filter_tag_string() {
+		global $t_filter;
+		$t_tag_string = $t_filter['tag_string'];
+		if ( $t_filter['tag_select'] != 0 ) {
+			$t_tag_string .= ( is_blank( $t_tag_string ) ? '' : config_get( 'tag_separator' ) );
+			$t_tag_string .= tag_get_field( $t_filter['tag_select'], 'name' );
+		}
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $t_tag_string ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+	}
+
 	function print_filter_custom_field($p_field_id){
 		global $t_filter, $t_accessible_custom_fields_names, $t_accessible_custom_fields_types, $t_accessible_custom_fields_values, $t_accessible_custom_fields_ids, $t_select_modifier;
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/history_api.php mantis-tagging/core/history_api.php
--- mantis-cvs/core/history_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/history_api.php	2007-08-24 11:00:22.000000000 -0400
@@ -165,6 +165,15 @@
 				}
 			}
 
+			// tags
+			if ( $v_type == TAG_ATTACHED ||
+				$v_type == TAG_DETACHED ||
+				$v_type == TAG_RENAMED ) {
+				if ( !access_has_global_level( config_get( 'tag_view_threshold' ) ) ) {
+					continue;
+				}
+			}
+
 			$raw_history[$j]['date']	= db_unixtimestamp( $v_date_modified );
 			$raw_history[$j]['userid']	= $v_user_id;
 
@@ -390,6 +399,16 @@
 				case CHECKIN:
 					$t_note = lang_get( 'checkin' );
 					break;
+				case TAG_ATTACHED:
+					$t_note = lang_get( 'tag_history_attached' ) .': '. $p_old_value;
+					break;
+				case TAG_DETACHED:
+					$t_note = lang_get( 'tag_history_detached' ) .': '. $p_old_value;
+					break;
+				case TAG_RENAMED:
+					$t_note = lang_get( 'tag_history_renamed' );
+					$t_change = $p_old_value . ' => ' . $p_new_value;
+					break;
 			}
 		}
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/html_api.php mantis-tagging/core/html_api.php
--- mantis-cvs/core/html_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/html_api.php	2007-08-24 11:00:22.000000000 -0400
@@ -1218,4 +1218,24 @@
 
 		echo '</tr></table>';
 	}
+
+	function html_button_tag_update( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+			|| ( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' )
+				&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) )
+		{
+			html_button( 'tag_update_page.php', lang_get( 'tag_update_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_button_tag_delete( $p_tag_id ) {
+		if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+			html_button( 'tag_delete.php', lang_get( 'tag_delete_button' ), array( 'tag_id' => $p_tag_id ) );
+		}
+	}
+
+	function html_buttons_tag_view_page( $p_tag_id ) {
+		html_button_tag_update( $p_tag_id );
+		html_button_tag_delete( $p_tag_id );
+	}
 ?>
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/print_api.php mantis-tagging/core/print_api.php
--- mantis-cvs/core/print_api.php	2007-08-24 10:54:11.000000000 -0400
+++ mantis-tagging/core/print_api.php	2007-08-24 11:00:23.000000000 -0400
@@ -11,6 +11,7 @@
 
 	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
 
+	require_once( $t_core_dir . 'ajax_api.php' );
 	require_once( $t_core_dir . 'current_user_api.php' );
 	require_once( $t_core_dir . 'string_api.php' );
 	require_once( $t_core_dir . 'prepare_api.php' );
@@ -255,6 +256,54 @@
 			PRINT "<option value=\"$t_duplicate_id\">".$t_duplicate_id."</option>";
 		}
 	}
+
+	function print_tag_attach_form( $p_bug_id, $p_string="" ) {
+		?>
+		<small><?php echo sprintf( lang_get( 'tag_separate_by' ), config_get('tag_separator') ) ?></small> 
+		<form method="post" action="tag_attach.php">
+		<input type="hidden" name="bug_id" value="<?php echo $p_bug_id ?>" />
+		<?php
+			print_tag_input( $p_bug_id, $p_string );
+		?>
+		<input type="submit" value="<?php echo lang_get( 'tag_attach' ) ?>" class="button" />
+		</form>
+		<?php
+		return true;
+	}
+
+	function print_tag_input( $p_bug_id = 0, $p_string="" ) {
+		?>
+		<input type="hidden" id="tag_separator" value="<?php echo config_get( 'tag_separator' ) ?>" />
+		<input type="text" name="tag_string" id="tag_string" size="40" value="<?php echo $p_string ?>" />
+		<select <?php echo helper_get_tab_index() ?> name="tag_select" id="tag_select">
+			<?php print_tag_option_list( $p_bug_id ); ?>
+		</select>
+		<?php
+
+		return true;
+	}
+
+	function print_tag_option_list( $p_bug_id = 0 ) {
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id, name FROM $t_tag_table ";
+		if ( 0 != $p_bug_id ) {
+			$c_bug_id = db_prepare_int( $p_bug_id );
+			$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+			
+			$query .= "	WHERE id NOT IN ( 
+						SELECT tag_id FROM $t_bug_tag_table WHERE bug_id='$c_bug_id' ) ";
+		}
+
+		$query .= " ORDER BY name ASC ";
+		$result = db_query( $query );
+
+		echo '<option value="0">',lang_get( 'tag_existing' ),'</option>';
+		while ( $row = db_fetch_array( $result ) ) {
+			echo '<option value="',$row['id'],'" onclick="tag_string_append(\'',$row['name'],'\')">',$row['name'],'</option>';
+		}
+	}
+
 	# --------------------
 	# Get current headlines and id  prefix with v_
 	function print_news_item_option_list() {
@@ -925,7 +974,9 @@
 							'UP_STATUS' => lang_get('actiongroup_menu_update_status'),
 							'UP_CATEGORY' => lang_get('actiongroup_menu_update_category'),
 							'VIEW_STATUS' => lang_get( 'actiongroup_menu_update_view_status' ),
-							'EXT_ADD_NOTE' => lang_get( 'actiongroup_menu_add_note' ) );
+							'EXT_ADD_NOTE' => lang_get( 'actiongroup_menu_add_note' ),
+							'EXT_ATTACH_TAGS' => lang_get( 'actiongroup_menu_attach_tags' ),
+					);
 
 		$t_project_id = helper_get_current_project();
 
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/tag_api.php mantis-tagging/core/tag_api.php
--- mantis-cvs/core/tag_api.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/core/tag_api.php	2007-08-24 13:30:34.000000000 -0400
@@ -0,0 +1,678 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	/**
+	 * Tag API
+	 *
+	 * @package TagAPI
+	 * @author John Reese
+	 */
+
+	$t_core_dir = dirname( __FILE__ ).DIRECTORY_SEPARATOR;
+
+	require_once( $t_core_dir . 'bug_api.php' );
+	require_once( $t_core_dir . 'history_api.php' );
+	
+	### Tag API ###
+
+	/**
+	 * Determine if a tag exists with the given ID.
+	 * @param integer Tag ID
+	 * @return boolean True if tag exists
+	 */
+	function tag_exists( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table WHERE id='$c_tag_id'";
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) > 0;
+	}
+
+	/**
+	 * Ensure a tag exists with the given ID.
+	 * @param integer Tag ID
+	 */
+	function tag_ensure_exists( $p_tag_id ) {
+		if ( !tag_exists( $p_tag_id ) ) {
+			error_parameters( $p_tag_id );
+			trigger_error( ERROR_TAG_NOT_FOUND, ERROR );
+		}
+	}
+
+	/**
+	 * Determine if a given name is unique (not already used).
+	 * Uses a case-insensitive search of the database for existing tags with the same name.
+	 * @param string Tag name
+	 * @return boolean True if name is unique
+	 */
+	function tag_is_unique( $p_name ) {
+		$c_name = trim( db_prepare_string( $p_name ) );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+
+		$query = "SELECT id FROM $t_tag_table WHERE ".db_helper_like( 'name', $c_name );
+		$result = db_query( $query ) ;
+
+		return db_num_rows( $result ) == 0;
+	}
+
+	/**
+	 * Ensure that a name is unique.
+	 * @param string Tag name
+	 */
+	function tag_ensure_unique( $p_name ) {
+		if ( !tag_is_unique( $p_name ) ) {
+			trigger_error( ERROR_TAG_DUPLICATE, ERROR );
+		}
+	}
+
+	/**
+	 * Determine if a given name is valid.
+	 * Name must start with letter/number and consist of letters, numbers, 
+	 * hyphens, underscores, periods, or spaces.  The prefix parameter is optional,
+	 * but allows you to prefix the regex check, which is useful for filters, etc.
+	 * The matches parameter allows you to also receive an array of regex matches,
+	 * which by default only includes the valid tag name itself.
+	 * @param string Tag name
+	 * @param string Prefix regex pattern
+	 * @param array Array reference for regex matches
+	 * @return boolean True if the name is valid
+	 */
+	function tag_name_is_valid( $p_name, $p_prefix="", &$p_matches=null ) {
+		$t_pattern = "/^$p_prefix([a-zA-Z0-9][a-zA-Z0-9-_. ]*)$/";
+		return preg_match( $t_pattern, $p_name, $p_matches );
+	}
+
+	/**
+	 * Ensure a tag name is valid.
+	 * @param string Tag name
+	 */
+	function tag_ensure_name_is_valid( $p_name ) {
+		if ( !tag_name_is_valid( $p_name ) ) {
+			trigger_error( ERROR_TAG_NAME_INVALID, ERROR );
+		}
+	}
+
+	/**
+	 * Compare two tag rows based on tag name.
+	 * @param array Tag row 1
+	 * @param array Tag row 2
+	 * @return integer -1 when Tag 1 < Tag 2, 1 when Tag 1 > Tag 2, 0 otherwise
+	 */
+	function tag_cmp_name( $p_tag1, $p_tag2 ) {
+		return strcasecmp( $p_tag1['name'], $p_tag2['name'] );
+	}
+
+	/**
+	 * Parse a form input string to extract existing and new tags.
+	 * When given a string, parses for tag names separated by configured separator,
+	 * then returns an array of tag rows for each tag.  Existing tags get the full
+	 * row of information returned.  If the tag does not exist, a row is returned with
+	 * id = -1 and the tag name, and if the name is invalid, a row is returned with
+	 * id = -2 and the tag name.  The resulting array is then sorted by tag name.
+	 * @param string Input string to parse
+	 * @return array Rows of tags parsed from input string
+	 */
+	function tag_parse_string( $p_string ) {
+		$t_tags = array();
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( is_blank( $t_name ) ) { continue; }
+			
+			$t_tag_row = tag_get_by_name( $t_name );
+			if ( $t_tag_row !== false ) {
+				$t_tags[] = $t_tag_row;
+			} else {
+				if ( tag_name_is_valid( $t_name ) ) {
+					$t_id = -1;
+				} else {
+					$t_id = -2;
+				}
+				$t_tags[] = array( 'id' => $t_id, 'name' => $t_name );
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	/**
+	 * Parse a filter string to extract existing and new tags.
+	 * When given a string, parses for tag names separated by configured separator,
+	 * then returns an array of tag rows for each tag.  Existing tags get the full
+	 * row of information returned.  If the tag does not exist, a row is returned with
+	 * id = -1 and the tag name, and if the name is invalid, a row is returned with
+	 * id = -2 and the tag name.  The resulting array is then sorted by tag name.
+	 * @param string Filter string to parse
+	 * @return array Rows of tags parsed from filter string
+	 */
+	function tag_parse_filters( $p_string ) {
+		$t_tags = array();
+		$t_prefix = "[+-]{0,1}";
+
+		$t_strings = explode( config_get( 'tag_separator' ), $p_string );
+		foreach( $t_strings as $t_name ) {
+			$t_name = trim( $t_name );
+			if ( is_blank( $t_name ) || !tag_name_is_valid( $t_name, $t_prefix ) ) { continue; }
+			
+			$t_matches = array();
+			if ( tag_name_is_valid( $t_name, $t_prefix, $t_matches ) ) {
+				$t_tag_row = tag_get_by_name( $t_matches[1] );
+				if ( $t_tag_row !== false ) {
+					$t_filter = substr( $t_name, 0, 1 );
+
+					if ( "+" == $t_filter ) {
+						$t_tag_row['filter'] = 1;
+					} elseif ( "-" == $t_filter ) {
+						$t_tag_row['filter'] = -1;
+					} else {
+						$t_tag_row['filter'] = 0;
+					}
+
+					$t_tags[] = $t_tag_row;
+				}
+			}
+		}
+		usort( $t_tags, "tag_cmp_name" );
+		return $t_tags;
+	}
+
+	# CRUD
+
+	/**
+	 * Return a tag row for the given ID.
+	 * @param integer Tag ID
+	 * @return array Tag row
+	 */
+	function tag_get( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+
+		$c_tag_id		= db_prepare_int( $p_tag_id );
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	/**
+	 * Return a tag row for the given name.
+	 * @param string Tag name
+	 * @return Tag row
+	 */
+	function tag_get_by_name( $p_name ) {
+		$c_name 		= db_prepare_string( $p_name );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "SELECT * FROM $t_tag_table
+					WHERE ".db_helper_like( 'name', $c_name );
+		$result = db_query( $query );
+
+		if ( 0 == db_num_rows( $result ) ) {
+			return false;
+		}
+		$row = db_fetch_array( $result );
+
+		return $row;
+	}
+
+	/**
+	 * Return a single field from a tag row for the given ID.
+	 * @param integer Tag ID
+	 * @param string Field name
+	 * @return mixed Field value
+	 */
+	function tag_get_field( $p_tag_id, $p_field_name ) {
+		$row = tag_get( $p_tag_id );
+
+		if ( isset( $row[$p_field_name] ) ) {
+			return $row[$p_field_name];
+		} else {
+			error_parameters( $p_field_name );
+			trigger_error( ERROR_DB_FIELD_NOT_FOUND, WARNING );
+			return '';
+		}
+	}
+
+	/**
+	 * Create a tag with the given name, creator, and description.
+	 * Defaults to the currently logged in user, and a blank description.
+	 * @param string Tag name
+	 * @param integer User ID
+	 * @param string Description
+	 * @return integer Tag ID
+	 */
+	function tag_create( $p_name, $p_user_id=null, $p_description='' ) {
+		access_ensure_has_global_level( config_get( 'tag_create_threshold' ) );
+
+		tag_ensure_name_is_valid( $p_name );
+		tag_ensure_unique( $p_name );
+
+		if ( null == $p_user_id ) {
+			$p_used_id = auth_get_current_user_id();
+		} else {
+			user_ensure_exists( $p_user_id );
+		}
+
+		$c_name			= trim( db_prepare_string( $p_name ) );
+		$c_description	= db_prepare_string( $p_description );
+		$c_user_id		= db_prepare_int( $p_user_id );
+		$c_date_created	= db_now();
+		
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "INSERT INTO $t_tag_table
+				( user_id, 	
+				  name, 
+				  description, 
+				  date_created, 
+				  date_updated 
+				)
+				VALUES
+				( '$c_user_id', 
+				  '$c_name', 
+				  '$c_description', 
+				  ".$c_date_created.", 
+				  ".$c_date_created."
+				)";
+
+		db_query( $query );
+		return db_insert_id( $t_tag_table );
+	}
+
+	/**
+	 * Update a tag with given name, creator, and description.
+	 * @param integer Tag ID
+	 * @param string Tag name
+	 * @param integer User ID
+	 * @param string Description
+	 */
+	function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description ) {
+		tag_ensure_exists( $p_tag_id );
+		user_ensure_exists( $p_user_id );
+		
+		if ( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' ) ) {
+			$t_update_level = config_get( 'tag_edit_own_threshold' );
+		} else {
+			$t_update_level = config_get( 'tag_edit_threshold' );
+		}
+		access_ensure_has_global_level( $t_update_level );
+		
+		tag_ensure_name_is_valid( $p_name );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+
+		$t_rename = false;
+		if ( strtolower($p_name) != strtolower($t_tag_name) ) {
+			tag_ensure_unique( $p_name );
+			$t_rename = true;
+		}
+		
+		$c_tag_id		= trim( db_prepare_int( $p_tag_id ) );
+		$c_user_id		= db_prepare_string( $p_user_id );
+		$c_name			= db_prepare_string( $p_name );
+		$c_description	= db_prepare_string( $p_description );
+		$c_date_updated	= db_now();
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+
+		$query = "UPDATE $t_tag_table
+					SET user_id='$c_user_id',
+						name='$c_name',
+						description='$c_description',
+						date_updated=".$c_date_updated."
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		if ( $t_rename ) {
+			$t_bugs = tag_get_bugs_attached( $p_tag_id );
+
+			foreach ( $t_bugs as $t_bug_id ) {
+				history_log_event_special( $t_bug_id, TAG_RENAMED, $t_tag_name, $c_name );
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Delete a tag with the given ID.
+	 * @param integer Tag ID
+	 */
+	function tag_delete( $p_tag_id ) {
+		tag_ensure_exists( $p_tag_id );
+		
+		access_ensure_has_global_level( config_get( 'tag_edit_threshold' ) );
+		
+		$t_bugs = tag_get_bugs_attached( $p_tag_id );
+		foreach ( $t_bugs as $t_bug_id ) {
+			tag_bug_detach( $p_tag_id, $t_bug_id );
+		}
+		
+		$c_tag_id			= db_prepare_int( $p_tag_id );
+
+		$t_tag_table		= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table	= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_tag_table
+					WHERE id='$c_tag_id'";
+		db_query( $query );
+
+		return true;
+	}
+	
+	# Associative
+
+	/**
+	 * Determine if a tag is attached to a bug.
+	 * @param integer Tag ID
+	 * @param integer Bug ID
+	 * @return boolean True if the tag is attached
+	 */
+	function tag_bug_is_attached( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+		return ( db_num_rows( $result ) > 0 );
+	}
+
+	/**
+	 * Return the tag attachment row.
+	 * @param integer Tag ID
+	 * @param integer Bug ID
+	 * @return array Tag attachment row
+	 */
+	function tag_bug_get_row( $p_tag_id, $p_bug_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		if ( db_num_rows( $result ) == 0 ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+		return db_fetch_array( $result );
+	}
+
+	/**
+	 * Return an array of tags attached to a given bug sorted by tag name.
+	 * @param Bug ID
+	 * @return array Array of tag rows with attachement information
+	 */
+	function tag_bug_get_attached( $p_bug_id ) {
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_tag_table	= config_get( 'mantis_tag_table' );
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT t.*, b.user_id as user_attached, b.date_attached
+					FROM $t_tag_table as t
+					LEFT JOIN $t_bug_tag_table as b
+						on t.id=b.tag_id
+					WHERE b.bug_id='$c_bug_id'";
+		$result = db_query( $query );
+
+		$rows = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$rows[] = $row;
+		}
+		
+		usort( $rows, "tag_cmp_name" );
+		return $rows;
+	}
+
+	/**
+	 * Return an array of bugs that a tag is attached to.
+	 * @param integer Tag ID
+	 * @return array Array of bug ID's.
+	 */
+	function tag_get_bugs_attached( $p_tag_id ) {
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT bug_id FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		$bugs = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$bugs[] = $row['bug_id'];
+		}
+
+		return $bugs;
+	}
+
+	/**
+	 * Attach a tag to a bug.
+	 * @param integer Tag ID
+	 * @param integer Bug ID
+	 * @param integer User ID
+	 */
+	function tag_bug_attach( $p_tag_id, $p_bug_id, $p_user_id=null ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+		if ( null == $p_user_id ) {
+			$p_user_id = auth_get_current_user_id();
+		} else {
+			user_ensure_exists( $p_user_id );
+		}
+
+		access_ensure_global_level( config_get( 'tag_detach_threshold' ) );
+
+		if ( tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_ALREADY_ATTACHED, ERROR );
+		}
+
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+		$c_user_id	 	= db_prepare_int( $p_user_id );
+		$c_date_attached= db_now();
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "INSERT INTO $t_bug_tag_table
+					( tag_id,
+					  bug_id,
+					  user_id,
+					  date_attached
+					)
+					VALUES
+					( '$c_tag_id',
+					  '$c_bug_id',
+					  '$c_user_id',
+					  ".$c_date_attached."
+					)";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_ATTACHED, $t_tag_name );
+
+		return true;
+	}
+
+	/**
+	 * Detach a tag from a bug.
+	 * @param integer Tag ID
+	 * @param integer Bug ID
+	 */
+	function tag_bug_detach( $p_tag_id, $p_bug_id ) {
+		tag_ensure_exists( $p_tag_id );
+		bug_ensure_exists( $p_bug_id );
+
+		if ( !tag_bug_is_attached( $p_tag_id, $p_bug_id ) ) {
+			trigger_error( TAG_NOT_ATTACHED, ERROR );
+		}
+
+		if ( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' ) ) {
+			$t_detach_level = config_get( 'tag_detach_own_threshold' );
+		} else {
+			$t_detach_level = config_get( 'tag_detach_threshold' );
+		}
+		access_ensure_has_global_level( $t_detach_level );
+		
+		$c_tag_id 		= db_prepare_int( $p_tag_id );
+		$c_bug_id 		= db_prepare_int( $p_bug_id );
+
+		$t_bug_tag_table= config_get( 'mantis_bug_tag_table' );
+
+		$query = "DELETE FROM $t_bug_tag_table 
+					WHERE tag_id='$c_tag_id' AND bug_id='$c_bug_id'";
+		db_query( $query );
+
+		$t_tag_name = tag_get_field( $p_tag_id, 'name' );
+		history_log_event_special( $p_bug_id, TAG_DETACHED, $t_tag_name );
+
+		return true;
+	}
+
+	# Display
+
+	/**
+	 * Display a tag hyperlink.
+	 * If a bug ID is passed, the tag link will include a detach link if the 
+	 * user has appropriate privileges.
+	 * @param array Tag row
+	 * @param integer Bug ID
+	 */
+	function tag_display_link( $p_tag_row, $p_bug_id=0 ) {
+		if ( auth_get_current_user_id() == $p_tag_row[user_attached] ) {
+			$t_detach = config_get( 'tag_detach_own_threshold' );
+		} else {
+			$t_detach = config_get( 'tag_detach_threshold' );
+		}
+		
+		$t_name = string_display_line( $p_tag_row['name'] );
+		$t_description = string_display_line( $p_tag_row['description'] );
+		
+		echo "<a href='tag_view_page.php?tag_id=$p_tag_row[id]' title='$t_description'>$t_name</a>";
+		
+		if ( access_has_global_level($t_detach) ) {
+			$t_tooltip = sprintf( lang_get( 'tag_detach' ), $t_name );
+			echo " <a href='tag_detach.php?bug_id=$p_bug_id&tag_id=$p_tag_row[id]'><img src='images/delete.png' class='delete-icon' title=\"$t_tooltip\"/></a>";
+		}
+		
+		return true;
+	}
+
+	/**
+	 * Display a list of attached tag hyperlinks separated by the configured hyperlinks.
+	 * @param Bug ID
+	 */
+	function tag_display_attached( $p_bug_id ) {
+		$t_tag_rows = tag_bug_get_attached( $p_bug_id );
+
+		if ( count( $t_tag_rows ) == 0 ) {
+			echo lang_get( 'tag_none_attached' );
+		} else {
+			$i = 0;
+			foreach ( $t_tag_rows as $t_tag ) {
+				echo ( $i > 0 ? config_get('tag_separator')." " : "" );
+				tag_display_link( $t_tag, $p_bug_id );
+				$i++;
+			}
+		}
+
+		return true;
+	}
+
+	# Statistics
+
+	/**
+	 * Get the number of bugs a given tag is attached to.
+	 * @param integer Tag ID
+	 * @return integer Number of attached bugs
+	 */
+	function tag_stats_attached( $p_tag_id ) {
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+
+		$query = "SELECT COUNT(*) FROM $t_bug_tag_table
+					WHERE tag_id='$c_tag_id'";
+		$result = db_query( $query );
+
+		return db_result( $result );
+	}
+
+	/**
+	 * Get a list of related tags.
+	 * Returns a list of tags that are the most related to the given tag,
+	 * based on the number of times they have been attached to the same bugs.
+	 * Defaults to a list of five tags.
+	 * @param integer Tag ID
+	 * @param integer List size
+	 * @return array Array of tag rows, with share count added
+	 */
+	function tag_stats_related( $p_tag_id, $p_limit=5 ) {
+		$t_bug_table = config_get( 'mantis_bug_table' );
+		$t_tag_table = config_get( 'mantis_tag_table' );
+		$t_bug_tag_table = config_get( 'mantis_bug_tag_table' );
+		$t_project_user_list_table = config_get( 'mantis_project_user_list_table' );
+		$t_user_table = config_get( 'mantis_user_table' );
+
+		$c_tag_id = db_prepare_int( $p_tag_id );
+		$c_user_id = auth_get_current_user_id();
+
+		$subquery = "SELECT b.id FROM $t_bug_table AS b
+					LEFT JOIN $t_project_user_list_table AS p
+						ON p.project_id=b.project_id AND p.user_id=$c_user_id
+					JOIN $t_user_table AS u
+						ON u.id=$c_user_id
+					JOIN $t_bug_tag_table AS t
+						ON t.bug_id=b.id
+					WHERE ( p.access_level>b.view_state OR u.access_level>b.view_state )
+						AND t.tag_id=$c_tag_id";
+					
+		$query = "SELECT * FROM $t_bug_tag_table
+					WHERE tag_id != $c_tag_id
+						AND bug_id IN ( $subquery ) ";
+
+		$result = db_query( $query );
+
+		$t_tag_counts = array();
+		while ( $row = db_fetch_array( $result ) ) {
+			$t_tag_counts[$row['tag_id']]++;
+		}
+
+		arsort( $t_tag_counts );
+
+		$t_tags = array();
+		$i = 1;
+		foreach ( $t_tag_counts as $t_tag_id => $t_count ) {
+			$t_tag_row = tag_get($t_tag_id);
+			$t_tag_row['count'] = $t_count;
+			$t_tags[] = $t_tag_row;
+			$i++;
+			if ( $i > $p_limit ) { break; }
+		}
+
+		return $t_tags;
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/core/xmlhttprequest_api.php mantis-tagging/core/xmlhttprequest_api.php
--- mantis-cvs/core/xmlhttprequest_api.php	2007-08-07 10:47:13.000000000 -0400
+++ mantis-tagging/core/xmlhttprequest_api.php	2007-08-24 11:00:21.000000000 -0400
@@ -31,6 +31,15 @@
 		echo '</select>';
 	}
 
+	function xmlhttprequest_user_combobox() {
+		$f_user_id = gpc_get_int( 'user_id' );
+		$f_user_access = gpc_get_int( 'access_level' );
+		
+		echo '<select name="user_id">';
+		print_user_option_list( $f_user_id, ALL_PROJECTS, $f_user_access );
+		echo '</select>';
+	}
+
 	# ---------------
 	# Echos a serialized list of platforms starting with the prefix specified in the $_POST
 	function xmlhttprequest_platform_get_with_prefix() {
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/css/default.css mantis-tagging/css/default.css
--- mantis-cvs/css/default.css	2007-08-09 08:19:07.000000000 -0400
+++ mantis-tagging/css/default.css	2007-08-24 11:00:23.000000000 -0400
@@ -100,6 +100,7 @@
 
 img						{}
 img.icon				{ width: 11px; height: 11px; }
+img.delete-icon			{ position: relative; top: 5px; border: 0; }
 
 div						{ padding: 3px; }
 div.menu				{ background-color: #e8e8e8; color: #000000; text-align: center; width: 100%; padding: 1px; }
Binary files mantis-cvs/images/delete.png and mantis-tagging/images/delete.png differ
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/javascript/common.js mantis-tagging/javascript/common.js
--- mantis-cvs/javascript/common.js	2007-08-14 10:12:43.000000000 -0400
+++ mantis-tagging/javascript/common.js	2007-08-24 11:00:22.000000000 -0400
@@ -163,3 +163,16 @@
   setDisplay( idTag, (document.getElementById(idTag).style.display == 'none')?1:0 );
 }
 
+/* Tag functionality */
+function tag_string_append( p_string ) {
+	t_tag_separator = document.getElementById('tag_separator').value;
+	t_tag_string = document.getElementById('tag_string');
+	t_tag_select = document.getElementById('tag_select');
+	if ( t_tag_string.value != '' ) {
+		t_tag_string.value = t_tag_string.value + t_tag_separator + p_string;
+	} else {
+		t_tag_string.value = t_tag_string.value + p_string;
+	}
+	t_tag_select.selectedIndex=0;
+}
+
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/lang/strings_english.txt mantis-tagging/lang/strings_english.txt
--- mantis-cvs/lang/strings_english.txt	2007-08-16 13:24:00.000000000 -0400
+++ mantis-tagging/lang/strings_english.txt	2007-08-24 11:00:23.000000000 -0400
@@ -39,6 +39,7 @@
 $s_actiongroup_menu_update_target_version = 'Update Target Version';
 $s_actiongroup_menu_update_fixed_in_version = 'Update Fixed in Version';
 $s_actiongroup_menu_add_note = 'Add Note';
+$s_actiongroup_menu_attach_tags = 'Attach Tags';
 $s_actiongroup_bugs = 'Selected Issues';
 
 # new strings:
@@ -275,6 +276,11 @@
 $MANTIS_ERROR[ERROR_USER_CHANGE_LAST_ADMIN] = 'You cannot change the access level of the only ADMINISTRATOR in the system.';
 $MANTIS_ERROR[ERROR_PAGE_REDIRECTION] = 'Page redirection error, ensure that there are no spaces outside the PHP block (&lt;?php ?&gt;) in config_inc.php or custom_*.php files.';
 $MANTIS_ERROR[ERROR_TWITTER_NO_CURL_EXT] = 'Twitter integration requires PHP CURL extension which is not installed.';
+$MANTIS_ERROR[ERROR_TAG_NOT_FOUND] = 'Could not find a tag with that name.';
+$MANTIS_ERROR[ERROR_TAG_DUPLICATE] = 'A tag already exists with that name.';
+$MANTIS_ERROR[ERROR_TAG_NAME_INVALID] = 'That tag name is invalid.';
+$MANTIS_ERROR[ERROR_TAG_NOT_ATTACHED] = 'That tag is not attached to that bug.';
+$MANTIS_ERROR[ERROR_TAG_ALREADY_ATTACHED] = 'That tag already attached to that bug.';
 
 $s_login_error = 'Your account may be disabled or blocked or the username/password you entered is incorrect.';
 $s_login_cookies_disabled = 'Your browser either doesn\'t know how to handle cookies, or refuses to handle them.';
@@ -1347,6 +1353,38 @@
 # wiki related strings
 $s_wiki = 'Wiki';
 
+# Tagging
+$s_tags = 'Tags';
+$s_tag_details = 'Tag Details: %s';
+$s_tag_id = 'Tag ID';
+$s_tag_name = 'Name';
+$s_tag_creator = 'Creator';
+$s_tag_created = 'Date Created';
+$s_tag_updated = 'Last Updated';
+$s_tag_description = 'Tag Description';
+$s_tag_statistics = 'Usage Statistics';
+$s_tag_update = 'Update Tag: %s';
+$s_tag_update_return = 'Back to Tag';
+$s_tag_update_button = 'Update Tag';
+$s_tag_delete_button = 'Delete Tag';
+$s_tag_delete_message = 'Are you sure you wish to delete this tag?';
+$s_tag_existing = 'Existing tags';
+$s_tag_none_attached = 'No tags attached.';
+$s_tag_attach = 'Attach';
+$s_tag_attach_long = 'Attach Tags';
+$s_tag_attach_failed = 'Tag attachment failed.';
+$s_tag_detach = 'Detach \'%s\'';
+$s_tag_separate_by = "(Separate by '%s')";
+$s_tag_invalid_name = 'Invalid tag name.';
+$s_tag_create_denied = 'Create permission denied.';
+$s_tag_filter_default = 'Attached Issues (%s)';
+$s_tag_history_attached = 'Tag Attached';
+$s_tag_history_detached = 'Tag Detached';
+$s_tag_history_renamed = 'Tag Renamed';
+$s_tag_related = 'Related Tags';
+$s_tag_related_issues = 'Shared Issues (%s)';
+$s_tag_stats_attached = 'Issues attached: %s';
+
 # Time Tracking
 $s_time_tracking_billing_link = 'Billing';
 $s_time_tracking = 'Time tracking';
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_attach.php mantis-tagging/tag_attach.php
--- mantis-cvs/tag_attach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_attach.php	2007-08-24 13:27:04.000000000 -0400
@@ -0,0 +1,108 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_bug_id = gpc_get_int( 'bug_id' );
+	$f_tag_select = gpc_get_int( 'tag_select' );
+	$t_user_id = auth_get_current_user_id();
+
+	access_ensure_global_level( config_get( 'tag_attach_threshold' ) );
+
+	$t_tags = tag_parse_string( gpc_get_string( 'tag_string' ) );
+	$t_can_create = access_has_global_level( config_get( 'tag_create_threshold' ) );
+	
+	$t_tags_create = array();
+	$t_tags_attach = array();
+	$t_tags_failed = array();
+
+	foreach ( $t_tags as $t_tag_row ) {
+		if ( -1 == $t_tag_row['id'] ) {
+			if ( $t_can_create ) {
+				$t_tags_create[] = $t_tag_row;
+			} else {
+				$t_tags_failed[] = $t_tag_row;
+			}
+		} elseif ( -2 == $t_tag_row['id'] ) {
+			$t_tags_failed[] = $t_tag_row;
+		} else {
+			$t_tags_attach[] = $t_tag_row;
+		}
+	}
+
+	if ( 0 < $f_tag_select && tag_exists( $f_tag_select ) ) {
+		$t_tags_attach[] = tag_get( $f_tag_select );
+	}
+
+	if ( count( $t_tags_failed ) > 0 ) {
+		html_page_top1( lang_get( 'tag_attach_long' ).' '.bug_format_summary( $f_bug_id, SUMMARY_CAPTION ) );
+		html_page_top2();
+?>
+<br/>
+<table class="width75" align="center">
+	<tr class="row-category">
+	<td colspan="2"><?php echo lang_get( 'tag_attach_failed' ) ?></td>
+	</tr>
+	<tr class="spacer"><td colspan="2"></td></tr>
+<?php		
+		$t_tag_string = "";
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+
+		foreach( $t_tags_failed as $t_tag_row ) {
+			echo '<tr ',helper_alternate_class(),'>';
+			if ( -1 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_invalid_name' ),'</td>';
+			} elseif ( -2 == $t_tag_row['id'] ) {
+				echo '<td class="category">',lang_get( 'tag_create_denied' ),'</td>';
+			}
+			echo '<td>',$t_tag_row['name'],'</td></tr>';
+			
+			if ( "" != $t_tag_string ) {
+				$t_tag_string .= config_get( 'tag_separator' );
+			}
+			$t_tag_string .= $t_tag_row['name'];
+		}
+?>
+	<tr class="spacer"><td colspan="2"></td></tr>
+	<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_attach_long' ) ?></td>
+	<td>
+<?php
+		print_tag_input( $f_bug_id, $t_tag_string );
+?>	
+	</td>
+	</tr>
+</table>
+<?php
+		html_page_bottom1(__FILE__);
+	} else {
+		foreach( $t_tags_create as $t_tag_row ) {
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id );
+			$t_tags_attach[] = $t_tag_row;
+		}
+
+		foreach( $t_tags_attach as $t_tag_row ) {
+			if ( ! tag_bug_is_attached( $t_tag_row['id'], $f_bug_id ) ) {
+				tag_bug_attach( $t_tag_row['id'], $f_bug_id, $t_user_id );
+			}
+		}
+
+		print_successful_redirect_to_bug( $f_bug_id );
+	}
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_delete.php mantis-tagging/tag_delete.php
--- mantis-cvs/tag_delete.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_delete.php	2007-08-24 13:27:16.000000000 -0400
@@ -0,0 +1,27 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_edit_threshold' ) );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	helper_ensure_confirmed( lang_get( 'tag_delete_message' ), lang_get( 'tag_delete_button' ) );
+
+	tag_delete( $f_tag_id );
+	
+	print_successful_redirect( config_get( 'default_home_page' ) );
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_detach.php mantis-tagging/tag_detach.php
--- mantis-cvs/tag_detach.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_detach.php	2007-08-24 13:27:26.000000000 -0400
@@ -0,0 +1,33 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$f_bug_id = gpc_get_int( 'bug_id' );
+
+	$t_tag_row = tag_get( $f_tag_id );
+	$t_tag_bug_row = tag_bug_get_row( $f_tag_id, $f_bug_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_detach_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_bug_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_detach_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	tag_bug_detach( $f_tag_id, $f_bug_id );
+	
+	print_successful_redirect_to_bug( $f_bug_id );
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_update_page.php mantis-tagging/tag_update_page.php
--- mantis-cvs/tag_update_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update_page.php	2007-08-24 13:27:45.000000000 -0400
@@ -0,0 +1,105 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'ajax_api.php' );
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+	
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+	
+	html_page_top1( sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<form method="post" action="tag_update.php">
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_update' ), $t_tag_row['name'] ) ?>
+		<input type="hidden" name="tag_id" value="<?php echo $f_tag_id ?>"/>
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'tag_view_page.php?tag_id='.$f_tag_id, lang_get( 'tag_update_return' ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><input type="text" <?php echo helper_get_tab_index() ?> name="name" value="<?php echo $t_tag_row['name'] ?>"/></td>
+	<td><?php
+			if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+				if ( ON == config_get( 'use_javascript' ) ) {
+					$t_username = prepare_user_name( $t_tag_row['user_id'] );
+					echo ajax_click_to_edit( $t_username, 'user_id', 'entrypoint=user_combobox&user_id=' . $t_tag_row['user_id'] . '&access_level=' . config_get( 'tag_create_threshold' ) );
+				} else {
+					echo '<select ', helper_get_tab_index(), ' name="user_id">';
+					print_user_option_list( $t_tag_row['user_id'], ALL_PROJECTS, config_get( 'tag_create_threshold' ) );
+					echo '</select>';
+				}
+			} else {
+				echo user_get_name($t_tag_row['user_id']);
+			}
+		?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4">
+		<textarea name="description" <?php echo helper_get_tab_index() ?> cols="80" rows="6"><?php echo $t_tag_row['description'] ?></textarea>
+	</td>
+</tr>
+
+<!-- Submit Button -->
+<tr>
+	<td class="center" colspan="6">
+		<input <?php echo helper_get_tab_index() ?> type="submit" class="button" value="<?php echo lang_get( 'tag_update_button' ) ?>" />
+	</td>
+</tr>
+
+</table>
+</form>
+
+<?php
+	html_page_bottom1( __FILE__ );
+?>
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_update.php mantis-tagging/tag_update.php
--- mantis-cvs/tag_update.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_update.php	2007-08-24 13:27:35.000000000 -0400
@@ -0,0 +1,55 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id: $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	if ( ! ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) 
+		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
+			&& access_has_global_level( config_get( 'tag_edit_own_threshold' ) ) ) ) 
+	{
+		access_denied();
+	}
+
+	if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
+		$f_new_user_id = gpc_get_int( 'user_id', $t_tag_row['user_id'] );
+	} else {
+		$f_new_user_id = $t_tag_row['user_id'];
+	}
+
+	$f_new_name = gpc_get_string( 'name', $t_tag_row['name'] );
+	$f_new_description = gpc_get_string( 'description', $t_tag_row['description'] );
+
+	$t_update = false;
+
+	if ( $t_tag_row['user_id'] != $f_new_user_id ) {
+		user_ensure_exists( $f_new_user_id );
+		$t_update = true;
+	}
+
+	if ( 	$t_tag_row['name'] != $f_new_name ||
+			$t_tag_row['description'] != $f_new_description ) {
+
+		$t_update = true;
+	}
+
+	tag_update( $f_tag_id, $f_new_name, $f_new_user_id, $f_new_description );
+		
+	$t_url = 'tag_view_page.php?tag_id='.$f_tag_id;
+	print_successful_redirect( $t_url );
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/tag_view_page.php mantis-tagging/tag_view_page.php
--- mantis-cvs/tag_view_page.php	1969-12-31 19:00:00.000000000 -0500
+++ mantis-tagging/tag_view_page.php	2007-08-24 11:00:23.000000000 -0400
@@ -0,0 +1,107 @@
+<?php
+	# Mantis - a php based bugtracking system
+	# Copyright (C) 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
+	# Copyright (C) 2002 - 2007  Mantis Team   - mantisbt-dev@lists.sourceforge.net
+	# This program is distributed under the terms and conditions of the GPL
+	# See the README and LICENSE files for details
+
+	# --------------------------------------------------------
+	# $Id:  $
+	# --------------------------------------------------------
+
+	require_once( 'core.php' );
+	
+	$t_core_path = config_get( 'core_path' );
+
+	require_once( $t_core_path . 'tag_api.php' );
+
+	access_ensure_global_level( config_get( 'tag_view_threshold' ) );
+	compress_enable();
+
+	$f_tag_id = gpc_get_int( 'tag_id' );
+	$t_tag_row = tag_get( $f_tag_id );
+
+	$t_name = string_display_line( $t_tag_row['name'] );
+	$t_description = string_display( $t_tag_row['description'] );
+
+	html_page_top1( sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) );
+	html_page_top2();
+?>
+
+<br/>
+<table class="width100" cellspacing="1">
+
+<!-- Title -->
+<tr>
+	<td class="form-title" colspan="2">
+		<?php echo sprintf( lang_get( 'tag_details' ), $t_tag_row['name'] ) ?>
+
+	</td>
+	<td class="right" colspan="3">
+		<?php print_bracket_link( 'search.php?hide_status_id=90&tag_string='.urlencode($t_tag_row['name']), sprintf( lang_get( 'tag_filter_default' ), tag_stats_attached( $f_tag_id ) ) ); ?>
+	</td>
+</tr>
+
+<!-- Info -->
+<tr class="row-category">
+	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+</tr>
+
+<tr <?php echo helper_alternate_class() ?>>
+	<td><?php echo $t_tag_row['id'] ?></td>
+	<td><?php echo $t_name ?></td>
+	<td><?php echo user_get_name($t_tag_row['user_id']) ?></td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_created'] ) ) ?> </td>
+	<td><?php echo print_date( config_get( 'normal_date_format' ), db_unixtimestamp( $t_tag_row['date_updated'] ) ) ?> </td>
+</tr>
+
+<!-- spacer -->
+<tr class="spacer">
+	<td colspan="5"></td>
+</tr>
+
+<!-- Description -->
+<tr <?php echo helper_alternate_class() ?>>
+	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
+	<td colspan="4"><?php echo $t_description ?></td>
+</tr>
+
+<!-- Statistics -->
+<?php
+	$t_tags_related = tag_stats_related( $f_tag_id );
+	if ( count( $t_tags_related ) ) { 
+		echo '<tr ',helper_alternate_class(),'>';
+		echo '<td class="category" rowspan="',count( $t_tags_related ),'">',lang_get( 'tag_related' ),'</td>';
+		
+		$i = 0;
+		foreach( $t_tags_related as $t_tag ) {
+			$t_name = string_display_line( $t_tag['name'] );
+			$t_description = string_display_line( $t_tag['description'] );
+			$t_count = $t_tag['count'];
+
+			echo ( $i > 0 ? '<tr '.helper_alternate_class().'>' : '' );
+			echo "<td><a href='tag_view_page.php?tag_id=$t_tag[id]' title='$t_description'>$t_name</a></td>\n";
+			echo '<td colspan="3">';
+			print_bracket_link( 'search.php?hide_status_id=90&tag_string='.urlencode("+$t_tag_row[name]".config_get('tag_separator')."+$t_name"), sprintf( lang_get( 'tag_related_issues' ), $t_tag['count'] ) );
+			echo '</a></td></tr>';
+			
+			$i++;
+		}
+	}
+?>
+
+<!-- Buttons -->
+<tr>
+	<td colspan="5">
+		<?php html_buttons_tag_view_page( $f_tag_id ); ?>
+	</td>
+</tr>
+
+</table>
+<?php
+	html_page_bottom1( __FILE__ );
+?>
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/view_all_set.php mantis-tagging/view_all_set.php
--- mantis-cvs/view_all_set.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/view_all_set.php	2007-08-24 11:00:22.000000000 -0400
@@ -203,6 +203,9 @@
 	$f_do_filter_by_date	= gpc_get_bool( 'do_filter_by_date' );
 	$f_view_state			= gpc_get_int( 'view_state', META_FILTER_ANY );
 
+	$f_tag_string			= gpc_get_string( 'tag_string', '' );
+	$f_tag_select			= gpc_get_int( 'tag_select', '0' );
+
 	$t_custom_fields 		= custom_field_get_ids(); # @@@ (thraxisp) This should really be the linked ids, but we don't know the project
 	$f_custom_fields_data 	= array();
 	if ( is_array( $t_custom_fields ) && ( sizeof( $t_custom_fields ) > 0 ) ) {
@@ -414,6 +417,8 @@
 				$t_setting_arr['platform'] = $f_platform;
 				$t_setting_arr['os'] = $f_os;
 				$t_setting_arr['os_build'] = $f_os_build;
+				$t_setting_arr['tag_string'] = $f_tag_string;
+				$t_setting_arr['tag_select'] = $f_tag_select;
 				break;
 		# Set the sort order and direction
 		case '2':
diff -urN --exclude=CVS --exclude=.svn --exclude='.git*' --exclude='*.swp' --exclude=config_inc.php mantis-cvs/view_filters_page.php mantis-tagging/view_filters_page.php
--- mantis-cvs/view_filters_page.php	2007-08-07 10:47:14.000000000 -0400
+++ mantis-tagging/view_filters_page.php	2007-08-24 11:00:22.000000000 -0400
@@ -16,6 +16,7 @@
 	require_once( $t_core_path.'bug_api.php' );
 	require_once( $t_core_path.'string_api.php' );
 	require_once( $t_core_path.'date_api.php' );
+	require_once( $t_core_path.'tag_api.php' );
 
 	auth_ensure_user_authenticated();
 
@@ -406,7 +407,8 @@
 </tr>
 <tr class="row-category2">
 <td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"><?php echo lang_get( 'search' ) ?></td>
-<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 1 ) * $t_custom_cols ); ?>"></td>
+<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 2 ) * $t_custom_cols ); ?>"><?php echo lang_get( 'tags' ) ?></td>
+<td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"></td>
 </tr>
 <tr>
 	<!-- Search field -->
@@ -414,13 +416,12 @@
 		<input type="text" size="16" name="search" value="<?php echo string_html_specialchars( $t_filter['search'] ); ?>" />
 	</td>
 
-	<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 3 ) * $t_custom_cols ); ?>"></td>
+	<td class="small-caption" colspan="<?php echo ( ( $t_filter_cols - 2 ) * $t_custom_cols ); ?>"><?php print_filter_tag_string() ?></td>
 
 	<!-- Submit button -->
 	<td class="right" colspan="<?php echo ( 1 * $t_custom_cols ); ?>">
 		<input type="submit" name="filter" class="button" value="<?php echo lang_get( 'filter_button' ) ?>" />
 	</td>
-	<td class="small-caption" colspan="<?php echo ( 1 * $t_custom_cols ); ?>"></td>
 </tr>
 </table>
 </form>
mantis-tagging-2007-08-24.patch (69,246 bytes)   
jreese

jreese

2007-08-24 13:37

reporter   ~0015523

Patch 'mantis-tagging-2007-08-24.patch' was created from CVS head on Friday, Aug 24, 2007.

This should be a feature complete patch, including group actions, no private information disclosure, fixed/optimized queries, API permissions checks, API documentation, and many other various fixes and modifications.

If nobody finds any major issues with this patch, I am prepared to commit it sometime this afternoon or evening. Otherwise, I will be on the road all day tomorrow traveling to Ohio and will need to wait for Sunday or Monday to be online from my in-laws' house.

vboctor

vboctor

2007-08-24 13:46

manager   ~0015524

Looks good. Some minor comments then go ahead and commit.

  1. New functions in print_api, html_api, and some other file are not documented. I noticed that you did a great job with documenting the new API.

  2. In bug_delete(), it would be more efficient to call a specific API in tag_api which deletes all tags associated with a bug. Such API will be a simple SQL statement and won't do stuff like updating history, etc.

Another question that we need to look at: (Don't hold the commit for that, but it may require some follow up work).

Are nest queries (or sub queries) supported in other DBMSes that we are supporting? If so, then there are other places that we should use. Specifically in areas like filter_api we can get a huge improvement in performance by using them at least for dbs that support them. I was thinking of db_nested_queries_supported() in database_api.php.

jreese

jreese

2007-08-24 13:59

reporter   ~0015525

I was under the impression that MySQL before 4.1 was one of the few DBMS choices that didn't support subqueries. AFAIK, all of the other ADOdb DBMS's support subqueries.

And I will go ahead and comment all the functions, and make your suggested bug_delete() changes, and then I will commit the resulting code after I do the final testing.

jreese

jreese

2007-08-24 15:16

reporter   ~0015528

Testing has completed successfully, and feature has been committed to CVS.

Issue History

Date Modified Username Field Change
2007-08-17 17:57 jreese New Issue
2007-08-17 17:57 jreese File Added: mantis-tagging-2007-08-17.patch
2007-08-17 18:16 jreese Note Added: 0015448
2007-08-17 22:47 thraxisp Note Added: 0015450
2007-08-17 23:47 jreese Note Added: 0015451
2007-08-20 09:57 jreese File Added: mantis-tagging-2007-08-20.patch
2007-08-20 10:06 jreese Note Added: 0015461
2007-08-21 02:50 vboctor Note Added: 0015466
2007-08-21 07:48 jreese Note Added: 0015469
2007-08-22 00:54 vboctor Note Added: 0015474
2007-08-22 00:55 vboctor Note Added: 0015475
2007-08-22 02:09 vboctor Note Added: 0015482
2007-08-22 02:10 vboctor Status new => assigned
2007-08-22 02:10 vboctor Assigned To => jreese
2007-08-22 02:11 vboctor Target Version => 1.1.0rc1
2007-08-22 16:33 jreese Note Added: 0015490
2007-08-22 16:38 jreese Note Edited: 0015490
2007-08-23 09:41 jreese Note Added: 0015494
2007-08-24 13:31 jreese File Added: mantis-tagging-2007-08-24.patch
2007-08-24 13:37 jreese Note Added: 0015523
2007-08-24 13:46 vboctor Note Added: 0015524
2007-08-24 13:59 jreese Note Added: 0015525
2007-08-24 15:16 jreese Status assigned => resolved
2007-08-24 15:16 jreese Fixed in Version => 1.1.0rc1
2007-08-24 15:16 jreese Resolution open => fixed
2007-08-24 15:16 jreese Note Added: 0015528
2007-08-26 05:38 vboctor Category other => tagging
2007-10-04 01:38 vboctor Status resolved => closed
2007-10-28 06:31 giallu Relationship added has duplicate 0006381
2019-09-09 03:53 dregad Relationship added related to 0026119