View Issue Details

IDProjectCategoryView StatusLast Update
0011250mantisbtauthenticationpublic2017-05-16 14:21
ReportergthomasAssigned Todregad 
PrioritynormalSeverityfeatureReproducibilityalways
Status closedResolutionwon't fix 
Product Version1.2.0rc2 
Target VersionFixed in Version 
Summary0011250: Allow SHA1 passwords
Description

For migrating from roundup I needed to use '{SHA}hex...' passwords.
So I tweaked authentication_api.php and added SHA1 as a new constant to constants_inc.php.

Additional Information

Not a big thing, but makes password formats more transparent with the {method} prefix.

Tagspatch

Relationships

has duplicate 0007864 closeddregad Native support for SHA1 authentification within Mantis 
related to 0013047 closeddregad PASSLEN constant doesn't match database field size 
related to 0010172 closeddregad Passwords in SHA256 using a static salt 
related to 0022839 assigneddregad Deprecate MD5 login method and replace with BCRYPT hash 

Activities

gthomas

gthomas

2009-12-02 15:53

reporter  

mantis-sha_auth_method.patch (1,691 bytes)
diff -r 7c772059daca core/authentication_api.php
--- a/core/authentication_api.php	Fri Nov 27 13:34:55 2009 +0100
+++ b/core/authentication_api.php	Wed Dec 02 21:47:28 2009 +0100
@@ -344,10 +344,21 @@
 
 	$t_password = user_get_field( $p_user_id, 'password' );
 	$t_login_methods = Array(
+		SHA1,
 		MD5,
 		CRYPT,
 		PLAIN,
 	);
+	if( substr($t_password, 0, 1) == '{' && strpos($t_password, '}') > 1 ) {
+		$t_method = substr( $t_password, 1, strpos($t_password, '}')-1 );
+		$t_methods_arr = array('SHA'=>SHA1, 'MD5'=>MD5, 'CRYPT'=>CRYPT, 'PLAIN'=>PLAIN);
+		if( in_array($t_method, $t_methods_arr) 
+		&& in_array($t_methods_arr[$t_method], $t_login_methods) ) {
+			$t_login_methods = Array($t_methods_arr[$t_method]);
+			$t_password = substr( $t_password, strlen($t_method)+2 );
+		}
+	}
+
 	foreach( $t_login_methods as $t_login_method ) {
 
 		# pass the stored password in as the salt
@@ -404,6 +415,9 @@
 		case MD5:
 			$t_processed_password = md5( $p_password );
 			break;
+		case SHA1:
+			$t_processed_password = sha1( $p_password );
+			break;
 		case BASIC_AUTH:
 		case PLAIN:
 		default:
diff -r 7c772059daca core/constant_inc.php
--- a/core/constant_inc.php	Fri Nov 27 13:34:55 2009 +0100
+++ b/core/constant_inc.php	Wed Dec 02 21:47:28 2009 +0100
@@ -134,6 +134,7 @@
 define( 'LDAP', 4 );
 define( 'BASIC_AUTH', 5 );
 define( 'HTTP_AUTH', 6 );
+define( 'SHA1', 7 );
 
 # file upload methods
 define( 'DISK', 1 );
@@ -552,6 +553,6 @@
 # Lengths - NOTE: these may represent hard-coded values in db schema and should not be changed.
 define( 'USERLEN', 32);
 define( 'REALLEN', 64);
-define( 'PASSLEN', 32);
+define( 'PASSLEN', 50);
 
 define( 'SECONDS_PER_DAY', 86400 );
vboctor

vboctor

2009-12-04 03:14

manager   ~0023852

Thanks gthomas for your contribution. The request makes sense. Here are my comments:

  1. Aren't you missing a change to increase the width of the password field? My understanding is that it is now 32 characters and we need to increase this to 40 to SHA1. I would recommend changing the password field to 128 characters and be done with it.

  2. You are changing PASSLEN despit of the comment above it that indicates that it shouldn't be changed without changing the schema.

  3. It would be useful to explain your patch. Are you storing the encryption method as part of the password field? I would suggest that we add a separate field that indicates the encryption method that is used for the current password value. On login, we can update this from such method to the current preferred method as per config_inc.php.

  4. Please update comments in config_defaults_inc.php and the docbook documentation as part of the patch so that the documentation matches the code.
dhx

dhx

2009-12-06 07:09

reporter   ~0023862

I'm not so sure about increasing the maximum password length in the database to 128 characters. Which password hashing function produces 1024 bits output?

64 sounds like a better option to me, in case people want to use functions like SHA512 or Whirlpool.

gthomas

gthomas

2009-12-30 07:23

reporter   ~0024013

Hi,

I did increase the password field length, just as PASSLEN - forgot to include it in the patch - now this is supplied, too.
The new patch is against git 58e01600767cf8114a8396595920222d01f424d7.

Some comments on the patch:

  1. this is only a hack yet, I needed the funcionality for a Roundup -> Mantis migration. For a nice feature extension some decisions should be made:
    (i) how to store the password hash method - now it is stored as a '{hasmethod}' prefix in the password field. It could be stored as a separate field, but I think the hash method is so much coupled with the hash that storing it in a separate field would make corruption a lot easier (maybe this is merely a feeling though).
    (ii) how big will the password length? (SHA1 is 256 bits, SHA512 is double).
    (iii) should we use it at all? Or try to find out the best method as before (by trying out every possible login method)?

  2. password and PASSLEN is extended to 64 characters (enough for SHA1 + '{SHA}' prefix).

  3. in the auth_does_password_match function, the main modification is that if such a prefix exists, than is uses that method (filters the $t_login_methods) only - else it goes undisturbedly as before.

  4. it is a one-way only route: the prefix is not inserted every time, so the regular mantis hash method migration path (update the stored hash according to the preferred login method after successful login) will eliminate it by logging in.

GThomas

gthomas

gthomas

2009-12-30 07:23

reporter  

sha_password.patch (3,868 bytes)
diff --git a/admin/schema.php b/admin/schema.php
index c87f0a6..7951c09 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -34,7 +34,7 @@ if ( !function_exists( 'db_null_date' ) ) {
 
 function installer_db_now() {
         global $g_db;
- 
+
        return $g_db->BindTimeStamp( time() );
 }
 
@@ -318,7 +318,7 @@ $upgrade[] = Array('CreateTableSQL',Array(db_get_table('user'),"
   username 		C(32) NOTNULL DEFAULT \" '' \",
   realname 		C(64) NOTNULL DEFAULT \" '' \",
   email 		C(64) NOTNULL DEFAULT \" '' \",
-  password 		C(32) NOTNULL DEFAULT \" '' \",
+  password 		C(64) NOTNULL DEFAULT \" '' \",
   date_created 		T NOTNULL DEFAULT '" . db_null_date() . "',
   last_visit 		T NOTNULL DEFAULT '" . db_null_date() . "',
   enabled		L NOTNULL DEFAULT \" '1' \",
@@ -475,7 +475,7 @@ $upgrade[] = Array( 'RenameColumnSQL', Array( db_get_table( 'bugnote' ), "last_m
 $upgrade[] = Array('CreateIndexSQL',Array('idx_last_mod',db_get_table('bugnote'),'last_modified'));
 $upgrade[] = Array( 'DropColumnSQL', Array( db_get_table( 'bugnote' ), "date_submitted" ) );
 $upgrade[] = Array( 'RenameColumnSQL', Array( db_get_table( 'bugnote' ), "date_submitted_int", "date_submitted", "date_submitted_int		I  UNSIGNED     NOTNULL DEFAULT '1' " ) );
-	
+
 
 $upgrade[] = Array( 'AddColumnSQL', Array( db_get_table( 'bug_file' ), "
 	date_added_int		I  UNSIGNED     NOTNULL DEFAULT '1' " ) );
diff --git a/core/authentication_api.php b/core/authentication_api.php
index a90a720..4737828 100644
--- a/core/authentication_api.php
+++ b/core/authentication_api.php
@@ -345,10 +345,22 @@ function auth_does_password_match( $p_user_id, $p_test_password ) {
 
 	$t_password = user_get_field( $p_user_id, 'password' );
 	$t_login_methods = Array(
+		SHA1,
 		MD5,
 		CRYPT,
 		PLAIN,
 	);
+	if( substr($t_password, 0, 1) == '{' && strpos($t_password, '}') > 1 ) {
+		$t_method = substr( $t_password, 1, strpos($t_password, '}')-1 );
+		$t_methods_arr = Array('SHA'=>SHA1, 'MD5'=>MD5, 'CRYPT'=>CRYPT,
+			'PLAIN'=>PLAIN);
+		if( in_array($t_method, $t_methods_arr)
+			&& in_array($t_methods_arr[$t_method], $t_login_methods) ) {
+			$t_login_methods = Array($t_methods_arr[$t_method]);
+			$t_password = substr( $t_password, strlen($t_method)+2 );
+		}
+	}
+
 	foreach( $t_login_methods as $t_login_method ) {
 
 		# pass the stored password in as the salt
@@ -405,6 +417,9 @@ function auth_does_password_match( $p_user_id, $p_test_password ) {
 		case MD5:
 			$t_processed_password = md5( $p_password );
 			break;
+		case SHA1:
+			$t_processed_password = sha1( $p_password );
+			break;
 		case BASIC_AUTH:
 		case PLAIN:
 		default:
diff --git a/core/constant_inc.php b/core/constant_inc.php
index 7e3e274..3bc1a28 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -134,6 +134,7 @@ define( 'MD5', 3 );
 define( 'LDAP', 4 );
 define( 'BASIC_AUTH', 5 );
 define( 'HTTP_AUTH', 6 );
+define( 'SHA1', 7 );
 
 # file upload methods
 define( 'DISK', 1 );
@@ -552,6 +553,6 @@ define( 'PHPMAILER_METHOD_SMTP',		2 );
 # Lengths - NOTE: these may represent hard-coded values in db schema and should not be changed.
 define( 'USERLEN', 32);
 define( 'REALLEN', 64);
-define( 'PASSLEN', 32);
+define( 'PASSLEN', 64);
 
 define( 'SECONDS_PER_DAY', 86400 );
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index 8d63519..d2313a5 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -2569,8 +2569,9 @@ $g_allow_no_category = OFF;
 
 /**
  * login method
- * CRYPT or PLAIN or MD5 or LDAP or BASIC_AUTH. You can simply change this at
- * will. MantisBT will try to figure out how the passwords were encrypted.
+ * CRYPT or PLAIN or MD5 or SHA1 or LDAP or BASIC_AUTH. 
+ * You can simply change this at will. MantisBT will try to figure out how the 
+ * passwords were encrypted.
  * @global int $g_login_method
  */
 $g_login_method = MD5;

sha_password.patch (3,868 bytes)
vboctor

vboctor

2010-01-02 00:30

manager   ~0024017

Thanks GThomas for the update. Here are some more comments:

  1. The change of the database schema, should be a change in column width at the end of the schema. This will make your patch work for new installation as well as upgrades.

  2. I would prefer having a separate column for the encryption schema. This can be empty for upgraded entries and we can set it as well as the revised hash on successful login.

  3. The reason I was generous with the increase of size for the password, is that I expect the password field to be VARCHAR and hence I don't expect a bigger width will cause an issue. I just would like to avoid revisiting this length in the future. However, if we believe 64 is fine, it is OK by me.

  4. Do we want SHA1 to be the default? I assume that a installation that was using MD5, will automatically upgrade all entries to SHA1 on first login and will create new users as SHA1.

  5. There seems to be a couple of unnecessary spacing changes in the schema file, would be good to get rid of them. I'm assuming these were introduced by the editor removing spaces or something.

  6. Please update the documentation under docbook/adminguide to match the changes you have done to config_defaults_inc.php.
gthomas

gthomas

2010-01-03 15:45

reporter  

sha_password-60f00efe222f05dce178015fc777b03d3dea54ee.patch (47,530 bytes)
diff --git a/.gitignore b/.gitignore
index 8f4e287..0db8385 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ web.config
 #libraries
 library/jpgraph
 library/FirePHPCore
+patches
diff --git a/admin/schema.php b/admin/schema.php
index c87f0a6..b10b315 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -606,3 +606,12 @@ $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_project_hierarchy_parent_id',
 /* 180 */
 $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_tag_name', db_get_table( 'tag' ), 'name' ) );
 $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_bug_tag_tag_id', db_get_table( 'bug_tag' ), 'tag_id' ) );
+
+/* 190 */
+$upgrade[] = Array( 'AddColumnSQL', Array( db_get_table( 'user' ), "
+	password_128 		C(128) NOTNULL DEFAULT \" '' \" " ) );
+$upgrade[] = Array( 'AddColumnSQL', Array( db_get_table( 'user' ), "
+	password_hash 		C(10) NOTNULL DEFAULT \" 'MD5' \" " ) );
+$upgrade[] = Array( 'UpdateFunction', "password_hash_migrate", Array( 'password_128', 'password_hash' ) );
+$upgrade[] = Array( 'DropColumnSQL', Array( db_get_table( 'user' ), "password" ) );
+$upgrade[] = Array( 'RenameColumnSQL', Array( db_get_table( 'user' ), "password_128", "password", "password 		C(128) NOTNULL DEFAULT \" '' \" " ) );
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index 702b5dc..eee3566 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -2571,8 +2571,9 @@ $g_allow_no_category = OFF;
 
 /**
  * login method
- * CRYPT or PLAIN or MD5 or LDAP or BASIC_AUTH. You can simply change this at
- * will. MantisBT will try to figure out how the passwords were encrypted.
+ * CRYPT or PLAIN or MD5 or SHA1 or LDAP or BASIC_AUTH. 
+ * You can simply change this at will. MantisBT will try to figure out how the 
+ * passwords were encrypted.
  * @global int $g_login_method
  */
 $g_login_method = MD5;
diff --git a/core/authentication_api.php b/core/authentication_api.php
index 8e0b422..e7e6afa 100644
--- a/core/authentication_api.php
+++ b/core/authentication_api.php
@@ -345,10 +345,22 @@ function auth_does_password_match( $p_user_id, $p_test_password ) {
 
 	$t_password = user_get_field( $p_user_id, 'password' );
 	$t_login_methods = Array(
+		SHA1,
 		MD5,
 		CRYPT,
 		PLAIN,
 	);
+	if( substr($t_password, 0, 1) == '{' && strpos($t_password, '}') > 1 ) {
+		$t_method = substr( $t_password, 1, strpos($t_password, '}')-1 );
+		$t_methods_arr = Array('SHA'=>SHA1, 'MD5'=>MD5, 'CRYPT'=>CRYPT,
+			'PLAIN'=>PLAIN);
+		if( in_array($t_method, $t_methods_arr)
+			&& in_array($t_methods_arr[$t_method], $t_login_methods) ) {
+			$t_login_methods = Array($t_methods_arr[$t_method]);
+			$t_password = substr( $t_password, strlen($t_method)+2 );
+		}
+	}
+
 	foreach( $t_login_methods as $t_login_method ) {
 
 		# pass the stored password in as the salt
@@ -405,6 +417,9 @@ function auth_does_password_match( $p_user_id, $p_test_password ) {
 		case MD5:
 			$t_processed_password = md5( $p_password );
 			break;
+		case SHA1:
+			$t_processed_password = sha1( $p_password );
+			break;
 		case BASIC_AUTH:
 		case PLAIN:
 		default:
diff --git a/core/bug_api.php b/core/bug_api.php
index fb32839..4326199 100644
--- a/core/bug_api.php
+++ b/core/bug_api.php
@@ -1324,6 +1324,28 @@ function bug_get_newest_bugnote_timestamp( $p_bug_id ) {
 }
 
 /**
+ * return the reporter (user_id) for the most recent time at which a bugnote
+ *  associated with the bug was modified
+ * @param int p_bug_id integer representing bug id
+ * @return bool|int false or user id in integer format representing last bugnote reporter
+ * @access public
+ * @uses database_api.php
+ */
+function bug_get_last_bugnote_reporter( $p_bug_id ) {
+	$c_bug_id = db_prepare_int( $p_bug_id );
+	$t_bugnote_table = db_get_table( 'bugnote' );
+
+	$query = "SELECT MIN(reporter_id)
+				  FROM $t_bugnote_table A
+				  WHERE A.date_submitted = (SELECT MAX(X.date_submitted)
+				                               FROM $t_bugnote_table X
+				                               WHERE X.bug_id = A.bug_id) AND
+				        A.bug_id=" . db_param();
+	$result = db_query_bound( $query, Array( $c_bug_id ), 1 );
+	return db_result( $result );
+}
+
+/**
  * return the timestamp for the most recent time at which a bugnote
  *  associated with the bug was modified and the total bugnote
  *  count in one db query
diff --git a/core/constant_inc.php b/core/constant_inc.php
index 7e3e274..42bde13 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -134,6 +134,7 @@ define( 'MD5', 3 );
 define( 'LDAP', 4 );
 define( 'BASIC_AUTH', 5 );
 define( 'HTTP_AUTH', 6 );
+define( 'SHA1', 7 );
 
 # file upload methods
 define( 'DISK', 1 );
@@ -552,6 +553,6 @@ define( 'PHPMAILER_METHOD_SMTP',		2 );
 # Lengths - NOTE: these may represent hard-coded values in db schema and should not be changed.
 define( 'USERLEN', 32);
 define( 'REALLEN', 64);
-define( 'PASSLEN', 32);
+define( 'PASSLEN', 128);
 
 define( 'SECONDS_PER_DAY', 86400 );
diff --git a/core/filter_api.php b/core/filter_api.php
index 7e61912..01b2292 100644
--- a/core/filter_api.php
+++ b/core/filter_api.php
@@ -1865,19 +1865,19 @@ function filter_get_bug_rows( &$p_page_number, &$p_per_page, &$p_page_count, &$p
 							break;
 						case CUSTOM_FIELD_DATE_NONE:
 							array_push( $t_join_clauses, $t_cf_join_clause );
-							$t_custom_where_clause = '(( ' . $t_table_name . '.bug_id is null) OR ( ' . $t_table_name . '.value = 0)';
+							$t_custom_where_clause = '(( ' . $t_table_name . '.bug_id is null) OR ( CAST(' . $t_table_name . '.value AS FLOAT) = 0)';
 							break;
 						case CUSTOM_FIELD_DATE_BEFORE:
 							array_push( $t_join_clauses, $t_cf_join_clause );
-							$t_custom_where_clause = '(( ' . $t_table_name . '.value != 0 AND (' . $t_table_name . '.value+0) < ' . ( $t_filter['custom_fields'][$t_cfid][2] ) . ')';
+							$t_custom_where_clause = '(( CAST(' . $t_table_name . '.value AS FLOAT) != 0 AND ( CAST(' . $t_table_name . '.value AS FLOAT) + 0) < ' . ( $t_filter['custom_fields'][$t_cfid][2] ) . ')';
 							break;
 						case CUSTOM_FIELD_DATE_AFTER:
 							array_push( $t_join_clauses, $t_cf_join_clause );
-							$t_custom_where_clause = '( (' . $t_table_name . '.value+0) > ' . ( $t_filter['custom_fields'][$t_cfid][1] + 1 );
+							$t_custom_where_clause = '( (CAST(' . $t_table_name . '.value AS FLOAT) + 0) > ' . ( $t_filter['custom_fields'][$t_cfid][1] + 1 );
 							break;
 						default:
 							array_push( $t_join_clauses, $t_cf_join_clause );
-							$t_custom_where_clause = '( (' . $t_table_name . '.value+0) BETWEEN ' . $t_filter['custom_fields'][$t_cfid][1] . ' AND ' . $t_filter['custom_fields'][$t_cfid][2];
+							$t_custom_where_clause = '( ( CAST(' . $t_table_name . '.value AS FLOAT) + 0) BETWEEN ' . $t_filter['custom_fields'][$t_cfid][1] . ' AND ' . $t_filter['custom_fields'][$t_cfid][2];
 							break;
 					}
 				} else {
diff --git a/core/html_api.php b/core/html_api.php
index 4bdf300..1f37ca1 100644
--- a/core/html_api.php
+++ b/core/html_api.php
@@ -1201,11 +1201,19 @@ function html_status_legend() {
 
 	# draw the status bar
 	$width = (int)( 100 / count( $t_status_array ) );
-	foreach( $t_status_array as $t_status => $t_name ) {
-		$t_val = $t_status_names[$t_status];
-		$t_color = get_status_color( $t_status );
+		for($i=0; $i<10; $i++) {
+			if ( $i != 0 ) {
+				echo '<tr>';
+			}
+			foreach( $t_status_array as $t_status => $t_name ) {
+				if($t_status % 10 == $i) {
+					$t_val = $t_status_names[$t_status];
+					$t_color = get_status_color( $t_status );
 
-		echo "<td class=\"small-caption\" width=\"$width%\" bgcolor=\"$t_color\">$t_val</td>";
+					echo "<td class=\"small-caption\" width=\"$width%\" bgcolor=\"$t_color\">$t_val</td>";
+				}
+			}
+		echo '</tr>';
 	}
 
 	echo '</tr>';
diff --git a/core/install_helper_functions_api.php b/core/install_helper_functions_api.php
index f33e4c5..9a36745 100644
--- a/core/install_helper_functions_api.php
+++ b/core/install_helper_functions_api.php
@@ -221,4 +221,63 @@ function install_date_migrate( $p_data) {
 function install_do_nothing() {
 	# return 2 because that's what ADOdb/DataDict does when things happen properly
 	return 2;
-}
\ No newline at end of file
+}
+
+function install_password_hash_migrate( $p_data) {
+	// $p_data = [0] temp_password column, [1] password_hash column
+	global $g_db_log_queries;
+	global $g_login_method;
+
+	if( $g_login_method === LDAP ) {
+		return 2;
+	}
+	
+	$t_login_method = array(PLAIN => 'PLAIN', CRYPT => 'CRYPT',
+		CRYPT_FULL_SALT => 'CRYPT_FULL_SALT', MD5 => 'MD5', SHA1 => 'SHA1'
+		)[$g_login_method];
+
+
+	// disable query logging (even if it's enabled in config for this)
+	if ( $g_db_log_queries !== 0 ) {
+		$t_log_queries = $g_db_log_queries;
+		$g_db_log_queries = 0;
+	} else {
+		$t_log_queries = null;
+	}
+
+	$t_table = db_get_table( 'user' );
+	$t_temp_col = $p_data[0];
+	$t_password_hash_col = $p_data[1];
+
+	// fill password_hash and temp_col
+	$query = "SELECT id, password FROM $t_table";
+	$t_result = db_query_bound( $query );
+
+	while( $row = db_fetch_array( $t_result ) ) {
+		
+		$t_id = (int)$row['id'];
+		$t_hash_mode = $t_login_method;
+		$t_password = $row['password'];
+
+		if( substr($t_password, 0, 1) == '{'
+			&& strpos($t_password, '}') > 1 ) {
+
+			$t_hash_mode = substr($t_password, 1, strpos($t_password, '}') - 1);
+			$t_password = substr($t_password, strlen($t_hash_mode) + 2);
+		}
+
+		$query = "UPDATE $t_table SET $t_temp_col=" . db_param() . ", "
+				    "$t_password_hash_col=" . db_param() . " 
+					WHERE id=" . db_param();
+		db_query_bound( $query, array( $t_password, $t_hash_mode, $t_id ) );
+	}
+
+	// re-enabled query logging if we disabled it
+	if ( $t_log_queries !== null ) {
+		$g_db_log_queries = $t_log_queries;
+	}
+
+	# return 2 because that's what ADOdb/DataDict does when things happen properly
+	return 2;
+
+}
diff --git a/docbook/adminguide/en/authentication.sgml b/docbook/adminguide/en/authentication.sgml
index a960c1e..ffbe0f6 100644
--- a/docbook/adminguide/en/authentication.sgml
+++ b/docbook/adminguide/en/authentication.sgml
@@ -14,6 +14,7 @@
                 <listitem><para>CRYPT_FULL_SALT - deprecated.</para></listitem>
                 <listitem><para>PLAIN - deprecated.</para></listitem>
                 <listitem><para>MD5 - This is default and recommended approach.  See <ulink url="http://en.wikipedia.org/wiki/MD5">MD5 topic on Wikipedia</ulink> for more details.</para></listitem>
+                <listitem><para>SHA1 - Even stronger than MD5.  See <ulink url="http://en.wikipedia.org/wiki/SHA1">SHA1 topic on Wikipedia</ulink> for more details.</para></listitem>
             </itemizedlist>
         </para>
 
diff --git a/docbook/adminguide/en/configuration.sgml b/docbook/adminguide/en/configuration.sgml
index bce4bdd..99df312 100644
--- a/docbook/adminguide/en/configuration.sgml
+++ b/docbook/adminguide/en/configuration.sgml
@@ -1583,6 +1583,9 @@
                 <listitem><para>
                         <itemizedlist>
                             <listitem>
+                                <para>SHA1</para>
+                            </listitem>
+                            <listitem>
                                 <para>MD5</para>
                             </listitem>
                             <listitem>
diff --git a/my_view_inc.php b/my_view_inc.php
index 97c131c..ff2e62d 100644
--- a/my_view_inc.php
+++ b/my_view_inc.php
@@ -434,6 +434,9 @@ echo "($v_start - $v_end / $t_bug_count)";
 	$t_summary = string_display_line_links( $t_bug->summary );
 	$t_last_updated = date( config_get( 'normal_date_format' ), $t_bug->last_updated );
 
+	$t_last_reporter_id = bug_get_last_bugnote_reporter( $t_bug->id );
+	$t_last_reporter = $t_last_reporter_id ? user_get_name( $t_last_reporter_id ) : '';
+
 	# choose color based on status
 	$status_color = get_status_color( $t_bug->status );
 
@@ -498,9 +501,9 @@ echo "($v_start - $v_end / $t_bug_count)";
 	echo string_display_line( category_full_name( $t_bug->category_id, true, $t_bug->project_id ) );
 
 	if( $t_bug->last_updated > strtotime( '-' . $t_filter[FILTER_PROPERTY_HIGHLIGHT_CHANGED] . ' hours' ) ) {
-		echo ' - <b>' . $t_last_updated . '</b>';
+		echo ' - <b>' . $t_last_updated . ' - ' . $t_last_reporter . '</b>';
 	} else {
-		echo ' - ' . $t_last_updated;
+		echo ' - ' . $t_last_updated . ' - ' . $t_last_reporter;
 	}
 	?>
 		</span>
diff --git a/plugins/AutoMonitor/AutoMonitor.API.php b/plugins/AutoMonitor/AutoMonitor.API.php
new file mode 100644
index 0000000..ab86729
--- /dev/null
+++ b/plugins/AutoMonitor/AutoMonitor.API.php
@@ -0,0 +1,89 @@
+<?php
+# TODO : include header
+
+/**
+ * Get the list of users for the specified project (and category)
+ *
+ * This will include the explicit and inherited settings
+ */
+function AutoMonitor_get_users ( $p_project_id, $p_category_id )
+{
+	/* Return values */
+	$t_users     = array();
+
+	/* Tables */
+	$t_user_table        = db_get_table('user');
+	$t_automonitor_table = plugin_table('list');
+
+	/* DB params */
+	$c_project_id  = db_prepare_int($p_project_id);
+	$c_category_id = db_prepare_int($p_category_id);
+
+	/* Add all users with initial settings */
+	foreach ( project_get_all_user_rows($p_project_id) as $val ) {
+		$val['automonitor_state']    = 0;
+		$val['automonitor_explicit'] = false;
+		$t_users[$val['id']] = $val;
+	}
+
+	/* Get list of parent projects */
+	$c_parents = array();
+	$t_parents = project_hierarchy_inheritance($p_project_id);
+	foreach ( $t_parents as $t_parent ) {
+		if ( $t_parent != 0 ) {
+			$c_parents[] = db_prepare_int($t_parent);
+		}
+	}
+
+	/* Query for all data related to relevant projects / categories */
+	$params = array(db_prepare_int(0), $c_category_id);
+	$query  = "SELECT u.id, u.username, u.realname, c.state as automonitor_state,
+							c.project_id, c.category_id
+							FROM $t_user_table u, $t_automonitor_table c
+							WHERE u.id = c.user_id
+							AND c.project_id IN (" . implode(',', $c_parents) . ")
+								AND ( c.category_id = " . db_param() . "
+									OR c.category_id = " . db_param() . ")
+							ORDER BY u.id, c.category_id ASC";
+	$result = db_query_bound($query, $params);
+
+	/* Process into project keyed list
+	 *   Note: since the sub project ids might not be in order we can't
+	 *         use SQL to provide the project ordering
+	 */
+	$t_proj_list = array();
+	$t_proj_list = array();
+	$t_row_count = db_num_rows( $result );
+	for( $i = 0;$i < $t_row_count;$i++ ) {
+		$row = db_fetch_array( $result );
+		$t_proj_list[$row['project_id']][] = $row;
+	}
+
+	/* Process projects in reverse order (oldest ancestor first) */
+	foreach ( array_reverse($t_parents) as $t_parent_id ) {
+
+		/* Ignore the global project - we can't set anything here! */
+		if ( $t_parent_id != 0 ) {
+
+			/* Process each row from project
+			 * Note: categories (max 2) should be in ascending order so the
+			 *       project (cat = 0) will be first and overridden (if
+			 *       applicable) by the explicit category
+			 */
+			if ( array_key_exists($t_parent_id, $t_proj_list) ) {
+				foreach ( $t_proj_list[$t_parent_id] as $row ) {
+					$row['automonitor_explicit'] = ($row['project_id']  == $p_project_id) &&
+					                               ($row['category_id'] == $p_category_id);
+					$t_users[$row['id']] = $row;
+				}
+			}
+		}
+	}
+
+	/* Return result (as simple array) */
+	$t_ret = array();
+	foreach ( $t_users as $key => $val ) {
+		$t_ret[] = $val;
+	}
+	return $t_ret;
+}
diff --git a/plugins/AutoMonitor/AutoMonitor.ViewAPI.php b/plugins/AutoMonitor/AutoMonitor.ViewAPI.php
new file mode 100644
index 0000000..d2fcddf
--- /dev/null
+++ b/plugins/AutoMonitor/AutoMonitor.ViewAPI.php
@@ -0,0 +1,105 @@
+<?php
+# TODO: include header
+
+/**
+ * Display a project configuration table
+ *
+ * Note: this doesn't actually display the entire table, just the main
+ *       contents (3 user selection inputs and control buttons)
+ */
+function AutoMonitor_print_project_config_table ( $p_users )
+{
+?>
+<tr <?php echo helper_alternate_class() ?>>
+		<td style='width:20%'>
+				<select name='inherited[]' multiple='multiple' size='10' style='width:90%'>
+						<?php AutoMonitor_print_user_option_list($p_users, false, 1); ?>
+				</select>
+		</td>
+		<td style='width:13%'>
+				<input style='width:90%' type='submit' name='inherit_inc'
+				       value='<?php echo plugin_lang_get('include'); ?>'/><br/>
+				<input style='width:90%' type='submit' name='inherit_exc'
+				       value='<?php echo plugin_lang_get('exclude'); ?>'/>
+		</td>
+		<td style='width:20%'>
+				<select name='excluded[]' multiple='multiple' size='10' style='width:90%'>
+						<?php AutoMonitor_print_user_option_list($p_users, true, 0); ?>
+				</select>
+		</td>
+		<td style='width:13%'>
+				<input style='width:90%' type='submit' name='exclude_inc'
+				       value='<?php echo plugin_lang_get('include'); ?>'/><br/>
+				<input style='width:90%' type='submit' name='exclude_clr'
+				       value='<?php echo plugin_lang_get('clear'); ?>'/>
+		</td>
+		<td style='width:20%'>
+				<select name='included[]' multiple='multiple' size='10' style='width:90%'>
+						<?php AutoMonitor_print_user_option_list($p_users, true, 1); ?>
+				</select>
+		</td>
+		<td style='width:14%'>
+				<input style='width:90%' type='submit' name='include_exc'
+				       value='<?php echo plugin_lang_get('exclude'); ?>'/><br/>
+				<input style='width:90%' type='submit' name='include_clr'
+				       value='<?php echo plugin_lang_get('clear'); ?>'/>
+		</td>
+</tr>
+<?php
+}
+
+/**
+ * Print a list of users
+ *
+ * @param p_users    The complete list of users
+ * @param p_explicit Print users with matching explicit value
+ * @param p_state    Print users with matching state value (if explicit = true)
+ *                   otherwise if explicit = false this indicates whether to
+ *                   to prefix with Inc/Ext (=1) or not (=0)
+ */
+function AutoMonitor_print_user_option_list ( $p_users, $p_explicit, $p_state )
+{
+		/* Configuration */
+		$t_show_realname     = ( ON == config_get( 'show_realname' ) );
+		$t_sort_by_last_name = ( ON == config_get( 'sort_by_last_name' ) );
+
+		/* Display details */
+		$t_id_a              = array();
+		$t_display_a         = array();
+		$t_sort_a            = array();
+		
+		/* Process each user */
+		foreach ( $p_users as $t_user ) {
+				if ( ($t_user['automonitor_explicit'] == $p_explicit) &&
+				     (!$p_explicit || ($t_user['automonitor_state'] == $p_state)) ) {
+						$t_name = string_attribute($t_user['username']);
+						$t_sort = $t_name;
+						if ( isset($t_user['realname']) && 
+						     !empty($t_user['realname']) &&
+								$t_show_realname ) {
+								$t_name = string_attribute($t_user['realname']);
+								if ( $t_sort_by_last_name ) {
+										$t_bits = explode(' ', utf8_strtolower($t_name));
+										$t_sort = (isset($t_bits[1]) ? $t_bits[1] . ', ' : '') . $t_bits[0];
+								} else {
+										$t_sort = utf8_strtolower($t_name);
+								}
+						}
+						if ( !$p_explicit && ($p_state == 1) ) {
+								$t_name = (($t_user['automonitor_state'] == 1) ? 'Inc: ' : 'Exc: ') . $t_name;
+						}
+						$t_id_a[]      = $t_user['id'];
+						$t_display_a[] = $t_name;
+						$t_sort_a[]    = $t_sort;
+				}
+		}
+
+		/* Sort */
+		array_multisort($t_sort_a, SORT_ASC, SORT_STRING, $t_id_a, $t_display_a);
+
+		/* Output */
+		for ( $i = 0; $i < count($t_sort_a); $i++ ) {
+				echo "<option value='$t_id_a[$i]'>$t_display_a[$i]</option>\n";
+		}
+}
+?>
diff --git a/plugins/AutoMonitor/AutoMonitor.php b/plugins/AutoMonitor/AutoMonitor.php
new file mode 100644
index 0000000..90e8d3b
--- /dev/null
+++ b/plugins/AutoMonitor/AutoMonitor.php
@@ -0,0 +1,146 @@
+<?php
+
+# TODO: create a header (must have one somewhere)
+
+/* Include parent class */
+require_once( config_get( 'class_path' ) . 'MantisPlugin.class.php' );
+
+/*
+ * Auto project/category monitoring
+ */
+class AutoMonitorPlugin
+	extends MantisPlugin
+{
+
+	/*
+	 * Register the module
+	 */
+	function register ()
+	{
+		$this->name        = plugin_lang_get('title');
+		$this->description = plugin_lang_get('description');
+		$this->author      = 'Adam Sutton';
+		$this->contact     = 'adam@adamsutton.co.uk';
+		$this->url         = 'http://www.adamsutton.co.uk';
+		$this->version     = '0.0.0';
+		$this->requires    = array(
+			'MantisCore' => '1.2.0',
+		);
+		$this->page        = 'config';
+	}
+
+	/**
+	 * Initialise
+	 */
+	function init ()
+	{
+		require_once('AutoMonitor.API.php');
+		require_once('AutoMonitor.ViewAPI.php');
+	}
+
+	/**
+	 * Configuration
+	 */
+  function config ()
+	{
+		return array(
+		);
+	}
+
+	/**
+	 * Schema
+	 */
+	function schema ()
+	{
+		return array(
+			array('CreateTableSQL', array(plugin_table('list'), "
+				user_id     I UNSIGNED NOTNULL PRIMARY,
+				project_id  I UNSIGNED NOTNULL PRIMARY,
+				category_id I UNSIGNED NOTNULL PRIMARY,
+				state       L NOTNULL")),
+		);
+	}
+
+	/**
+	 * Event hooks
+	 */
+	function hooks ()
+	{
+		return array(
+			'EVENT_MANAGE_PROJECT_PAGE' => 'manage_project_list',
+			'EVENT_REPORT_BUG' => 'add_monitors',
+		);
+	}
+
+	/* ************************************************************************
+	 * Hook handlers
+	 * ***********************************************************************/
+
+  function manage_project_list ()
+  {
+		/* Check access */
+		$f_project_id = gpc_get_int('project_id');
+		if ( !access_has_project_level(config_get('project_user_threshold'), $f_project_id) ) {
+			return;
+		}
+
+		/* Get user list */
+		$t_users = AutoMonitor_get_users($f_project_id, 0);
+?>
+<!-- AUTOMONITOR LIST -->
+<br/>
+<a name='automonitor'/>
+<div align='center'>
+	<table class='width75' cellspacing='1'>
+
+		<!-- Title -->
+		<tr>
+			<td class='form-title' colspan='6'>
+				<?php echo plugin_lang_get( 'manage_project_title' ); ?>
+			</td>
+		</tr>
+
+		<!-- Titles -->
+		<tr class='row-category'>
+			<td width='33%' colspan='2'><?php echo plugin_lang_get( 'inherit_title' ); ?></td>
+			<td width='33%' colspan='2'><?php echo plugin_lang_get( 'exclude_title' ); ?></td>
+			<td width='34%' colspan='2'><?php echo plugin_lang_get( 'include_title' ); ?></td>
+		</tr>
+
+		<!-- User lists -->
+		<form method='post' action='<?php echo plugin_page('project_update') ?>'>
+			<input type="hidden" name="project_id" value="<?php echo $f_project_id ?>" />
+			<?php echo form_security_field('plugin_AutoMonitor_project_update'); ?>
+			<?php AutoMonitor_print_project_config_table($t_users); ?>
+		</form>
+
+		<!-- Category edit page -->
+		<form method='post' action='<?php echo plugin_page('category_edit_page') ?>'>
+			<input type="hidden" name="project_id" value="<?php echo $f_project_id ?>" />
+			<?php echo form_security_field('plugin_AutoMonitor_project_category_select'); ?>
+			<tr>
+				<td class='left' colspan='6'>
+					<select name='category_id'>
+						<?php print_category_option_list(0, $f_project_id); ?>
+					</select>
+					<input type='submit' name='cat_update' value='<?php echo plugin_lang_get('edit_category'); ?>'/>
+				</td>
+			</tr>
+		</form>
+	</table>
+</div>
+<?php
+	}
+
+	function add_monitors($p_event, $p_bug, $p_bug_id) {
+		if( $p_event == 'EVENT_REPORT_BUG' ) {
+			$t_users = AutoMonitor_get_users($p_bug->project_id, 0);
+			print_r($t_users);
+			foreach($t_users as $t_user) {
+				if($t_user['automonitor_state'] > 0)
+					bug_monitor($p_bug_id, $t_user['id']);
+			}
+		}
+	}
+}
+?>
diff --git a/plugins/AutoMonitor/lang/strings_english.txt b/plugins/AutoMonitor/lang/strings_english.txt
new file mode 100644
index 0000000..7df3ee8
--- /dev/null
+++ b/plugins/AutoMonitor/lang/strings_english.txt
@@ -0,0 +1,17 @@
+<?php
+# TODO: must have a standard header
+
+$s_plugin_AutoMonitor_title = 'AutoMonitor';
+$s_plugin_AutoMonitor_description = 'Allow users to be automatically assigned to new issues in a project/category';
+
+$s_plugin_AutoMonitor_manage_project_title = 'Project Monitors';
+$s_plugin_AutoMonitor_inherit_title = 'Inherited';
+$s_plugin_AutoMonitor_include_title = 'Included';
+$s_plugin_AutoMonitor_exclude_title = 'Excluded';
+$s_plugin_AutoMonitor_exclude = 'Exclude';
+$s_plugin_AutoMonitor_include = 'Include';
+$s_plugin_AutoMonitor_clear   = 'Clear';
+$s_plugin_AutoMonitor_edit_category = 'Edit Category';
+$s_plugin_AutoMonitor_category_edit_title = 'Category Monitors';
+$s_plugin_AutoMonitor_exit_button = 'Exit';
+?>
diff --git a/plugins/AutoMonitor/lang/strings_hungarian.txt b/plugins/AutoMonitor/lang/strings_hungarian.txt
new file mode 100644
index 0000000..3f597b9
--- /dev/null
+++ b/plugins/AutoMonitor/lang/strings_hungarian.txt
@@ -0,0 +1,17 @@
+<?php
+# TODO: must have a standard header
+
+$s_plugin_AutoMonitor_title = 'AutoMonitor';
+$s_plugin_AutoMonitor_description = 'Megadott felhasználók automatikusan ellenőrizzék az új projekteket/ügyeket';
+
+$s_plugin_AutoMonitor_manage_project_title = 'Projekt Ellenőrök';
+$s_plugin_AutoMonitor_inherit_title = 'Örökölt';
+$s_plugin_AutoMonitor_include_title = 'Bennfoglalt';
+$s_plugin_AutoMonitor_exclude_title = 'Kizárt';
+$s_plugin_AutoMonitor_exclude = 'Kizár';
+$s_plugin_AutoMonitor_include = 'Hozzáad';
+$s_plugin_AutoMonitor_clear   = 'Töröl';
+$s_plugin_AutoMonitor_edit_category = 'Kategória Szerkesztése';
+$s_plugin_AutoMonitor_category_edit_title = 'Kategória Ellenőrök';
+$s_plugin_AutoMonitor_exit_button = 'Kilépés';
+?>
diff --git a/plugins/AutoMonitor/pages/category_edit_page.php b/plugins/AutoMonitor/pages/category_edit_page.php
new file mode 100644
index 0000000..c52ffda
--- /dev/null
+++ b/plugins/AutoMonitor/pages/category_edit_page.php
@@ -0,0 +1,72 @@
+<?php
+# TODO: add header
+
+/* Validate authentication */
+auth_reauthenticate();
+
+/* Get form vars */
+$f_project_id  = gpc_get_int('project_id');
+$f_category_id = gpc_get_int('category_id');
+
+/* Validate params */
+project_ensure_exists($f_project_id);
+if ( $f_category_id === 0 ) {
+  print_header_redirect("manage_proj_edit_page.php?project_id=$f_project_id#automonitor");
+  exit;
+}
+category_ensure_exists($f_category_id);
+
+/* Double check permissions */
+access_ensure_project_level(config_get('manage_project_threshold'));
+access_ensure_project_level(config_get('project_user_threshold'));
+
+/* Start page */
+html_page_top(project_get_field($f_project_id, 'name'));
+print_manage_menu();
+
+/* Get user list */
+$t_users = AutoMonitor_get_users($f_project_id, $f_category_id);
+
+/* Display the main table */
+?>
+<div align='center'>
+	<table class='width75' cellspacing='1'>
+
+		<!-- Title -->
+		<tr>
+			<td class='form-title' colspan='6'>
+				<?php echo plugin_lang_get( 'category_edit_title' ); ?>
+			</td>
+		</tr>
+
+		<!-- Titles -->
+		<tr class='row-category'>
+			<td width='33%' colspan='2'><?php echo plugin_lang_get( 'inherit_title' ); ?></td>
+			<td width='33%' colspan='2'><?php echo plugin_lang_get( 'exclude_title' ); ?></td>
+			<td width='34%' colspan='2'><?php echo plugin_lang_get( 'include_title' ); ?></td>
+		</tr>
+
+		<!-- User lists -->
+		<form method='post' action='<?php echo plugin_page('project_update') ?>'>
+			<input type="hidden" name="project_id" value="<?php echo $f_project_id ?>" />
+			<input type='hidden' name='category_id' value='<?php echo $f_category_id ?>'/>
+			<?php echo form_security_field('plugin_AutoMonitor_project_update'); ?>
+			<?php AutoMonitor_print_project_config_table($t_users); ?>
+		</form>
+
+		<!-- Cancel button -->
+		<form method='post' action='manage_proj_edit_page.php'>
+			<input type="hidden" name="project_id" value="<?php echo $f_project_id ?>" />
+			<tr>
+				<td class='left' colspan='6'>
+					<input type='submit' name='cancel' value='<?php echo plugin_lang_get('exit_button') ?>'/>
+				</td>
+			</tr>
+		</form>
+	</table>
+</div>
+<?php
+
+/* End page */
+html_page_bottom();
+?>
diff --git a/plugins/AutoMonitor/pages/project_update.php b/plugins/AutoMonitor/pages/project_update.php
new file mode 100644
index 0000000..afcaca6
--- /dev/null
+++ b/plugins/AutoMonitor/pages/project_update.php
@@ -0,0 +1,78 @@
+<?php
+
+/* Validate authentication */
+form_security_validate('plugin_AutoMonitor_project_update');
+auth_reauthenticate();
+
+/* Get submission values (should all be mutually exclusive) */
+$f_project_id  = gpc_get_int('project_id');
+$f_category_id = gpc_get_int('category_id', 0);
+
+/* Table params */
+$t_automonitor_table = plugin_table('list');
+$c_project_id        = db_prepare_int($f_project_id);
+$c_category_id       = db_prepare_int($f_category_id);
+
+/* Validate params */
+project_ensure_exists($f_project_id);
+if ( $f_category_id !== 0 ) category_ensure_exists($f_category_id);
+
+/* Double check permissions */
+access_ensure_project_level(config_get('manage_project_threshold'));
+access_ensure_project_level(config_get('project_user_threshold'));
+
+/* Get the submit buttons */
+$f_exclude_inc = gpc_get_bool('exclude_inc');
+$f_exclude_clr = gpc_get_bool('exclude_clr');
+$f_include_exc = gpc_get_bool('include_exc');
+$f_include_clr = gpc_get_bool('include_clr');
+$f_inherit_inc = gpc_get_bool('inherit_inc');
+$f_inherit_exc = gpc_get_bool('inherit_exc');
+
+/* Update the inherited users */
+if ( $f_inherit_inc || $f_inherit_exc ) {
+	$f_users = gpc_get_int_array('inherited');
+	$c_state = db_prepare_int($f_inherit_inc ? 1 : 0);
+	$query   = "INSERT INTO $t_automonitor_table 
+		(user_id, project_id, category_id, state)
+		VALUES (" . db_param() . "," . db_param() . "," .  db_param() . "," . db_param() . ")";
+	foreach ( $f_users as $t_user ) {
+		db_query_bound($query, array(db_prepare_int($t_user), $c_project_id, $c_category_id, $c_state));
+	}
+
+/* Clear */
+} else if ( $f_include_clr || $f_exclude_clr ) {
+	$f_users = gpc_get_int_array($f_include_clr ? 'included' : 'excluded');
+	$c_users = array();
+	foreach ( $f_users as $t_user ) {
+		$c_users[] = db_prepare_int($t_user);
+	}
+	$query   = "DELETE FROM $t_automonitor_table 
+		WHERE project_id = " . db_param() . "
+		AND category_id = " . db_param() . "
+		AND user_id IN (" . implode(',', $c_users) . ")";
+	db_query_bound($query, array($c_project_id, $c_category_id));
+
+/* Set */
+} else if ( $f_include_exc || $f_exclude_inc ) {
+	$f_users = gpc_get_int_array($f_include_exc ? 'included' : 'excluded');
+	$c_users = array();
+	foreach ( $f_users as $t_user ) {
+		$c_users[] = db_prepare_int($t_user);
+	}
+	$c_state = db_prepare_int($f_include_exc ? 0 : 1);
+	$query   = "UPDATE $t_automonitor_table 
+		SET state = " . db_param() . "
+		WHERE project_id = " . db_param() . "
+		AND category_id = " . db_param() . "
+		AND user_id IN (" . implode(',', $c_users) . ")";
+	db_query_bound($query, array($c_state, $c_project_id, $c_category_id));
+}
+
+/* Done */
+form_security_purge('plugin_AutoMonitor_project_update');
+if ( $f_category_id == 0 ) {
+	print_header_redirect("manage_proj_edit_page.php?project_id=$f_project_id#automonitor");
+} else {
+	print_header_redirect(plugin_page("category_edit_page&project_id=$f_project_id&category_id=$f_category_id", true));
+}
diff --git a/plugins/FileDistribution/FileDistribution.php b/plugins/FileDistribution/FileDistribution.php
new file mode 100755
index 0000000..db01b70
--- /dev/null
+++ b/plugins/FileDistribution/FileDistribution.php
@@ -0,0 +1,55 @@
+<?php
+
+require_once( config_get( 'class_path' ) . 'MantisPlugin.class.php' );
+
+class FileDistributionPlugin extends MantisPlugin {
+	function register() {
+		$this->name = 'FileDistribution';	# Proper name of plugin
+		$this->description = 'Static file distrubution';	# Short description of the plugin
+		$this->page = '';		   # Default plugin page
+
+		$this->version = '1.0';	 # Plugin version string
+		$this->requires = array(	# Plugin dependencies, array of basename => version pairs
+			'MantisCore' => '1.2.0',  #   Should always depend on an appropriate version of MantisBT
+			);
+
+		$this->author = 'Tamás Gulácsi';		 # Author/team name
+		$this->contact = 'T.Gulacsi@unosoft.hu';		# Author/team e-mail address
+		$this->url = 'http://www.unosoft.hu';			# Support webpage
+	}
+
+	function config() {
+		return array(
+			'url' => NULL,
+			'path' => NULL,
+			'users' => array(),
+			'secret_word' => '1234567890',
+		);
+	}
+
+	function hooks() {
+		return array(
+            'EVENT_MENU_MAIN' => 'menu',
+            'EVENT_MENU_MANAGE' => 'manage',
+        );
+	}
+
+	function menu( ) {
+        //require_once( 'core.php' );
+        require_once( dirname(__FILE__).'/core/filedistrib_api.php' );
+
+        if ( user_allowed() ) {
+            return array( '<a href="' . plugin_page( 'static_files.php' ) . '">' .
+                plugin_lang_get('static_files') . '</a>', );
+        }
+	}
+
+    function manage( ) {
+        require_once( 'core.php' );
+
+        if ( access_get_project_level() >= MANAGER) {
+            return array( '<a href="' . plugin_page( 'config.php' ) . '">'
+                .  plugin_lang_get('config') . '</a>', );
+        }
+    }
+}
diff --git a/plugins/FileDistribution/core/filedistrib_api.php b/plugins/FileDistribution/core/filedistrib_api.php
new file mode 100644
index 0000000..ac978cd
--- /dev/null
+++ b/plugins/FileDistribution/core/filedistrib_api.php
@@ -0,0 +1,49 @@
+<?php
+
+function str2list($p_text) {
+    $t_arr = $p_text != null ? explode(',', $p_text) : array();
+    $t_arr = uids2names(names2uids($t_arr));
+    sort($t_arr);
+    return $t_arr;
+}
+
+function list2str($p_arr) {
+    $p_arr = uids2names(names2uids($p_arr));
+    sort($p_arr);
+    return implode(',', $p_arr);
+}
+
+function names2uids($p_arr) {
+    require_once( 'core.php' );
+    $ret = array();
+    foreach($p_arr as $name) {
+        $t_id = user_get_id_by_name($name);
+        if( $t_id && user_is_enabled($t_id) ) {
+            $ret[] = $t_id;
+        }
+    }
+    return $ret;
+}
+
+function uids2names($p_arr) {
+    require_once( 'core.php' );
+
+    $ret = array();
+    foreach($p_arr as $t_id) {
+        if( user_is_enabled($t_id) ) {
+            $ret[] = user_get_name($t_id);
+        }
+    }
+    return $ret;
+}
+
+function user_allowed() {
+    require_once( 'core.php' );
+
+    $t_act_uid = auth_get_current_user_id();
+
+    $t_users = str2list( plugin_config_get( 'users' ) );
+
+    return in_array( $t_act_uid, names2uids($t_users) );
+}
+?>
diff --git a/plugins/FileDistribution/lang/strings_english.txt b/plugins/FileDistribution/lang/strings_english.txt
new file mode 100755
index 0000000..3fbaf47
--- /dev/null
+++ b/plugins/FileDistribution/lang/strings_english.txt
@@ -0,0 +1,8 @@
+<?php
+$s_plugin_FileDistribution_name = 'FileDistribution';
+$s_plugin_FileDistribution_config = 'FileDistribution configuration';
+$s_plugin_FileDistribution_url = 'URL';
+$s_plugin_FileDistribution_path = 'PATH';
+$s_plugin_FileDistribution_users = 'allowed users';
+$s_plugin_FileDistribution_secret_word = 'shared secret';
+$s_plugin_FileDistribution_static_files = 'Downloadable files';
diff --git a/plugins/FileDistribution/lang/strings_hungarian.txt b/plugins/FileDistribution/lang/strings_hungarian.txt
new file mode 100755
index 0000000..a634ae3
--- /dev/null
+++ b/plugins/FileDistribution/lang/strings_hungarian.txt
@@ -0,0 +1,8 @@
+<?php
+$s_plugin_FileDistribution_name = 'FileDistribution';
+$s_plugin_FileDistribution_config = 'FileDistrib beállítások';
+$s_plugin_FileDistribution_url = 'URL';
+$s_plugin_FileDistribution_path = 'PATH';
+$s_plugin_FileDistribution_users = 'engedélyezett felhasználók';
+$s_plugin_FileDistribution_secret_word = 'megosztott titok';
+$s_plugin_FileDistribution_static_files = 'Letölthető fájlok';
diff --git a/plugins/FileDistribution/pages/config.php b/plugins/FileDistribution/pages/config.php
new file mode 100644
index 0000000..5ee1244
--- /dev/null
+++ b/plugins/FileDistribution/pages/config.php
@@ -0,0 +1,91 @@
+<?php
+# MantisBT - a php based bugtracking system
+# Copyright (C) 2002 - 2009  MantisBT Team - mantisbt-dev@lists.sourceforge.net
+# MantisBT is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# MantisBT is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.
+
+auth_reauthenticate( );
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+html_page_top( plugin_lang_get( 'name' ) );
+
+print_manage_menu( );
+
+?>
+
+<br/>
+<form action="<?php echo plugin_page( 'config_edit' )?>" method="post">
+<?php echo form_security_field( 'plugin_filedistrib_config_edit' ) ?>
+<table align="center" class="width50" cellspacing="1">
+
+<tr>
+	<td class="form-title" colspan="3">
+		<?php echo plugin_lang_get( 'config' )?>
+	</td>
+</tr>
+
+<tr <?php echo helper_alternate_class( )?>>
+	<td class="category" width="60%">
+		<?php echo plugin_lang_get( 'url' )?>
+	</td>
+	<td class="center" width="20%">
+		<label><?php echo plugin_lang_get( 'url' )?></label>
+		<input type="text" name="url" value="<?php echo plugin_config_get( 'url', NULL ); ?>" />
+	</td>
+</tr>
+<tr <?php echo helper_alternate_class( )?>>
+	<td class="category" width="60%">
+		<?php echo plugin_lang_get( 'path' )?>
+	</td>
+	<td class="center" width="20%">
+		<label><?php echo plugin_lang_get( 'path' )?></label>
+		<input type="text" name="path" value="<?php echo plugin_config_get( 'path', NULL ); ?>" />
+	</td>
+</tr>
+<tr <?php echo helper_alternate_class( )?>>
+	<td class="category" width="60%">
+		<?php echo plugin_lang_get( 'secret_word' )?>
+	</td>
+	<td class="center" width="20%">
+		<label><?php echo plugin_lang_get( 'secret_word' )?></label>
+		<input type="text" name="secret_word" value="<?php echo plugin_config_get( 'secret_word', NULL ); ?>" />
+	</td>
+</tr>
+<tr <?php echo helper_alternate_class( )?>>
+	<td class="category" width="60%">
+		<?php echo plugin_lang_get( 'users' )?>
+	</td>
+<?php
+
+	require_once( dirname(__FILE__).'/../core/filedistrib_api.php' );
+
+	$t_users_s = list2str(str2list(plugin_config_get( 'users', '' )));
+?>
+	<td class="center" width="20%">
+		<label><?php echo plugin_lang_get( 'users' )?></label>
+		<input type="text" name="users" value="<?php echo $t_users_s; ?>" />
+	</td>
+</tr>
+
+<tr>
+	<td class="center" colspan="3">
+		<input type="submit" class="button" value="<?php echo lang_get( 'change_configuration' )?>" />
+	</td>
+</tr>
+
+</table>
+</form>
+
+<?php
+html_page_bottom();
+?>
diff --git a/plugins/FileDistribution/pages/config_edit.php b/plugins/FileDistribution/pages/config_edit.php
new file mode 100644
index 0000000..2fc7aee
--- /dev/null
+++ b/plugins/FileDistribution/pages/config_edit.php
@@ -0,0 +1,51 @@
+<?php
+# MantisBT - a php based bugtracking system
+# Copyright (C) 2002 - 2009  MantisBT Team - mantisbt-dev@lists.sourceforge.net
+# MantisBT is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# MantisBT is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.
+
+form_security_validate( 'plugin_filedistrib_config_edit' );
+
+auth_reauthenticate( );
+access_ensure_global_level( config_get( 'manage_plugin_threshold' ) );
+
+$f_url = gpc_get_string( 'url', NULL );
+$f_path = gpc_get_string( 'path', NULL );
+$f_secret_word = gpc_get_string( 'secret_word', NULL );
+/*
+echo '<pre>old_url='.plugin_config_get( 'url' ).', new_url='.$f_url.'</pre>';
+*/
+
+if( plugin_config_get( 'url' ) != $f_url ) {
+	plugin_config_set( 'url', $f_url );
+}
+if( plugin_config_get( 'path' ) != $f_path && is_dir($f_path) && file_exists($f_path) ) {
+	plugin_config_set( 'path', $f_path );
+}
+if( plugin_config_get( 'secret_word' ) != $f_secret_word && $f_secret_word != NULL) {
+	plugin_config_set( 'secret_word', $f_secret_word );
+}
+
+//require_once( 'core.php' );
+require_once( dirname(__FILE__).'/../core/filedistrib_api.php' );
+
+$t_users_old = list2str(str2list(plugin_config_get( 'users', '' )));
+$f_users = list2str(str2list(gpc_get_string( 'users', '' )));
+if( $t_users_old != $f_users ) {
+	plugin_config_set( 'users', list2str(str2list($f_users)) );
+}
+
+form_security_purge( 'plugin_filedistrib_config_edit' );
+
+print_successful_redirect( plugin_page( 'config', true ) );
+?>
diff --git a/plugins/FileDistribution/pages/send_file.php b/plugins/FileDistribution/pages/send_file.php
new file mode 100755
index 0000000..cde6946
--- /dev/null
+++ b/plugins/FileDistribution/pages/send_file.php
@@ -0,0 +1,84 @@
+<?php
+require_once( dirname(__FILE__).'/../../../core.php' );
+
+/*
+form_security_validate( 'plugin_filedistrib_send' );
+form_security_purge( 'plugin_filedistrib_send' );
+*/
+
+function phpMinV($v) {
+    $phpV = PHP_VERSION;
+
+    if ($phpV[0] >= $v[0]) {
+        if (empty($v[2]) || $v[2] == '*') {
+            return true;
+        } elseif ($phpV[2] >= $v[2]) {
+            if (empty($v[4]) || $v[4] == '*' || $phpV[4] >= $v[4]) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+//header('HTTP/1.1 200 OK');
+//header('Content-Type: text/plain');
+
+$f_url = $_REQUEST['url'];
+if($f_url == false) {
+    $p = strpos($_SERVER['QUERY_STRING'], '%3Furl%3D');
+    if($p >= 0) {
+        //echo '!' . substr($_SERVER['QUERY_STRING'], $p+9) . "!\n";
+        $f_url = urldecode(substr($_SERVER['QUERY_STRING'], $p+9));
+    } else {
+        //echo '!QS='.$_SERVER['QUERY_STRING']."! p=$p\n";
+    }
+}
+//if($f_url == false) phpinfo();
+//echo "URL=$f_url\n";
+
+$arr = explode('/', $f_url);
+//print_r($arr);
+$f_time = array_pop(&$arr);
+$t_time = hexdec($f_time);
+$t_now = time();
+if ($t_now >= $t_time) {
+    header('HTTP/1.1 403 Timeout');
+    header('Content-Type: text/plain');
+    echo "Your link has expired (actual=$t_now, link=$t_time).\n";
+    exit();
+}
+$f_hash = array_pop(&$arr);
+$f_fn = implode('/', $arr);
+//echo "fn=$f_fn, hash=$f_hash, time=$f_time";
+
+$t_secret = plugin_config_get('secret_word');
+$t_hash = md5($f_fn . '/' . $t_secret . '/' . $f_time);
+if($t_hash != $f_hash) {
+    header('HTTP/1.1 403 Bad Request');
+    header('Content-Type: text/plain');
+    echo "Your link has bad hash (actual=$t_hash, link=$f_hash).\n";
+    exit();
+}
+
+$t_path_base = plugin_config_get('path');
+$t_url_base = plugin_config_get('url');
+
+$f_path = $t_path_base . '/' . $f_fn;
+if ($f_fn != false && file_exists($f_path)) {
+    $mime = mime_content_type($f_fn);
+    header('Content-Description: File Transfer');
+    header('Content-Type: ' . $mime);
+    header('Content-Disposition: attachment; filename=' . basename($f_fn));
+    header('Content-Transfer-Encoding: binary');
+    header('Expires: 0');
+    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+    header('Pragma: public');
+    header('Content-Length: ' . filesize($f_path));
+
+    header('X-Accel-Redirect: /protected' . $t_url_base . '/' . $f_fn);
+} else {
+    header('Content-Type: text/plain');
+    echo "NOT EXISTS (fn=$f_fn, path=$f_path, url_base=$t_url_base, path_base=$t_path_base)";
+}
diff --git a/plugins/FileDistribution/pages/static_files.php b/plugins/FileDistribution/pages/static_files.php
new file mode 100755
index 0000000..288a107
--- /dev/null
+++ b/plugins/FileDistribution/pages/static_files.php
@@ -0,0 +1,115 @@
+<?php
+
+// auth_reauthenticate( );
+
+html_page_top( plugin_lang_get( 'name' ) );
+
+$t_this_page = plugin_page('static_files'); //FIXME with plugins this does not work...
+/*
+print_manage_menu( $t_this_page );
+*/
+require_once( 'core.php' );
+require_once( dirname(__FILE__).'/../core/filedistrib_api.php' );
+
+if(!user_allowed()) {
+    exit;
+}
+
+$t_url = plugin_config_get('url');
+$t_path = plugin_config_get('path');
+//print 'URL='.$t_url.' PATH='.$t_path;
+
+$t_secu = form_security_field( 'plugin_filedistrib_send' );
+
+/*
+echo "Handle: " . $d->handle . "\n";
+echo "Path: " . $d->path . "\n";
+*/
+$t_post_url = plugin_page( 'send_file.php' );
+/*
+$t_post_url = str_replace('FileDistribution/', 'FileDistribution/pages/',
+    str_replace('plugin.php?page=', 'plugins/', $t_post_url ));
+*/
+/*
+$d = scandir($t_url);
+echo '<ul>';
+foreach($d as $entry) {
+    if(preg_match('/\.(tar\.|t)(gz|bz2)$/', $entry)) {
+    ?>
+    <li>
+        <form action="<?php echo $t_post_url;?>" method="post">
+            <?php echo $t_secu; ?>
+            <input type="hidden" name="fn"
+                value="<?php echo $t_url.'/'.$entry;?>" />
+            <input type="submit" class="button"
+                value="<?php echo $entry;?>" />
+        </form>
+    </li>
+<?php
+    }
+}
+echo '</ul>';
+*/
+function get_dirs($path) {
+    $arr = array();
+    foreach(scandir($path, TRUE) as $elt) {
+        $p = $path . '/' . $elt;
+        if( is_dir($p) && $elt != '.' && $elt != '..' ) {
+            $arr[] = $p;
+        }
+    }
+    return $arr;
+}
+
+function get_files($path, $regex) {
+    $arr = array();
+    foreach(scandir($path, FALSE) as $elt) {
+        $p = $path . '/' . $elt;
+        if( is_file($p) && is_readable($p) && preg_match($regex, $elt) ) {
+            $arr[] = $p;
+        }
+    }
+    return $arr;
+}
+
+function print_struct($path, $regex, $level=0) {
+    if($level > 10) return;
+    $t_path = plugin_config_get('path');
+    $t_secret = plugin_config_get('secret_word');
+    $t_timeout = plugin_config_get('timeout', 300);
+
+    $t_plugin_page = plugin_page('send_file.php');
+    $t_url = $t_plugin_page;
+    $t_url = substr($t_url, 0, strpos($t_url, '/plugin.php'))
+        . plugin_config_get('url');
+    print '<p>';
+    print '<h'.($level+1).'>'.basename($path).'</h'.($level+1).'>';
+    $i = 0;
+    foreach(get_dirs($path) as $d) {
+        # DFS
+        //print 'd: '.$d.' lev='.$level;
+        print_struct($d, $regex, $level+1);
+        if($i++ > 100) break;
+    }
+    print '<ul>';
+    $i = 0;
+    foreach(get_files($path, $regex) as $f) {
+        # md5 (reference, secret_word);
+        $t_resource = substr($f, strlen($t_path)+1);
+        $t_pre = $t_resource; #$t_url . '/' . $t_resource;
+        $t_time = sprintf("%X", time() + $t_timeout);
+        $t_hash = md5($t_pre . '/' . $t_secret . '/' . $t_time);
+        $t_link = $t_pre . '/' . $t_hash . '/' . $t_time;
+        //print "<pre>pre=$t_pre, time=$t_time, hash=$t_hash, secr=$t_secret, $t_pre/$t_secret/$t_time</pre>";
+        //print '<li><a href="' . $t_link . '">' . basename($f).'</a></li>';
+        print '<li><a href="' . $t_plugin_page . urlencode('?url=' . $t_link) . '">'.basename($f).'</a></li>';
+        if($i++ > 100) break;
+    }
+    print '</ul>';
+    print '</p>';
+} #mantIs3456
+
+print_struct($t_path, '/.*/');
+
+
+html_page_bottom();
gthomas

gthomas

2010-01-03 15:49

reporter   ~0024026

Hi,

Added a new patch targeting 1, 2, 3, 5, 6 (MD5 is still the default).
I don't want to switch the default behaviour till it really works.

OOPS!
I forgot to modify authentication_api to use the new password_hash field - I'll fix it this week (and will do some tests, I hope).

grangeway

grangeway

2010-01-03 17:07

reporter   ~0024027

gthomas,

The authentication changes is something we'll probably commit to core as part of authentication plugins.

If we added this now, it would just make it more difficult to implement plugins.

Paul

gthomas

gthomas

2011-06-26 15:52

reporter   ~0029060

What about using some semi-standard library?

For example, phpass (http://www.openwall.com/phpass/) is in the public domain, salts password hashes and uses the strongest available crypto method.

Of course, password field length should be beefed up a little bit...

GThomas

dregad

dregad

2011-07-26 04:39

developer   ~0029292

Removed trojan from attachments.

dregad

dregad

2011-07-26 04:47

developer   ~0029293

There is also a patch attached in 0007864

dregad

dregad

2017-05-06 17:31

developer   ~0056783

It does not make much sense to implement SHA1 / SHA256 nowadays. BCRYPT is a much better option for hashing passwords, and is now offered by PHP as default method via password_hash() function.

I am therefore resolving this issue as "won't fix"; please follow-up in 0022839 for implementation of BCRYPT as default login method in future releases of MantisBT.

Issue History

Date Modified Username Field Change
2009-12-02 15:53 gthomas New Issue
2009-12-02 15:53 gthomas File Added: mantis-sha_auth_method.patch
2009-12-04 03:07 vboctor Tag Attached: patch
2009-12-04 03:14 vboctor Note Added: 0023852
2009-12-04 03:14 vboctor Status new => acknowledged
2009-12-06 07:09 dhx Note Added: 0023862
2009-12-30 07:23 gthomas Note Added: 0024013
2009-12-30 07:23 gthomas File Added: sha_password.patch
2010-01-02 00:30 vboctor Note Added: 0024017
2010-01-03 15:45 gthomas File Added: sha_password-60f00efe222f05dce178015fc777b03d3dea54ee.patch
2010-01-03 15:49 gthomas Note Added: 0024026
2010-01-03 17:07 grangeway Note Added: 0024027
2011-06-04 02:41 atrol Relationship added related to 0013047
2011-06-26 15:52 gthomas Note Added: 0029060
2011-06-27 09:04 442832953 File Added: 自己的PHP马.php
2011-06-27 09:04 442832953 File Added: 3.asp;.jpg
2011-07-26 04:38 dregad File Deleted: 自己的PHP马.php
2011-07-26 04:38 dregad File Deleted: 3.asp;.jpg
2011-07-26 04:39 dregad Note Added: 0029292
2011-07-26 04:41 dregad Relationship added has duplicate 0007864
2011-07-26 04:43 dregad Relationship added related to 0010172
2011-07-26 04:47 dregad Note Added: 0029293
2017-05-06 17:26 dregad Relationship added related to 0022839
2017-05-06 17:31 dregad Note Added: 0056783
2017-05-06 17:31 dregad Status acknowledged => resolved
2017-05-06 17:31 dregad Resolution open => won't fix
2017-05-06 17:31 dregad Assigned To => dregad
2017-05-16 14:21 atrol Status resolved => closed