diff --git a/config.inc.php b/config.inc.php index 491297e1..7c4c04e9 100644 --- a/config.inc.php +++ b/config.inc.php @@ -129,10 +129,6 @@ $CONF['admin_email'] = ''; // This will be used as signature in notification messages $CONF['admin_name'] = 'Postmaster'; -// Site admin phone number -// This will be used if a user cannot access his/her email and needs support -$CONF['admin_phone'] = ''; - // Mail Server // Hostname (FQDN) of your mail server. // This is used to send email to Postfix in order to create mailboxes. @@ -588,16 +584,34 @@ $CONF['create_mailbox_subdirs_hostoptions'] = array(); // Optional: // Allows a user to reset his forgotten password with a code sent by email/SMS -$CONF['forgotten_user_password_reset'] = false; # INSECURE, DO NOT ENABLE! See https://github.com/postfixadmin/postfixadmin/pull/18 for details +$CONF['forgotten_user_password_reset'] = true; // Allows an admin to reset his forgotten password with a code sent by email/SMS -$CONF['forgotten_admin_password_reset'] = false; # INSECURE, DO NOT ENABLE! see https://github.com/postfixadmin/postfixadmin/pull/18 for details - -// Clickatell gateway to send SMS code for password reset -// API type: HTTP -$CONF['clickatell_api_id'] = ''; -$CONF['clickatell_user'] = ''; -$CONF['clickatell_password'] = ''; -$CONF['clickatell_sender'] = ''; +$CONF['forgotten_admin_password_reset'] = false; + +// Name of the function to send a SMS +// Please use a name that begins with "x_" to prevent collisions +// This function must accept 2 parameters: phone number and message, +// and return true on success or false on failure +$CONF['sms_send_function'] = ''; + +/* +// Example of send SMS function using Clickatell HTTP API +function x_send_sms_clickatell($to, $message) { + + $clickatell_api_id = 'CHANGEME'; + $clickatell_user = 'CHANGEME'; + $clickatell_password = 'CHANGEME'; + $clickatell_sender = 'CHANGEME'; + + $url = 'https://api.clickatell.com/http/sendmsg?api_id=%s&user=%s&password=%s&to=%s&from=%s&text=%s'; + + $url = sprintf($url, $clickatell_api_id, $clickatell_user, $clickatell_password, $to, $clickatell_sender, urlencode($message)); + + $result = file_get_contents($url); + + return $result !== false; +} +*/ // Theme Config // Specify your own logo and CSS file diff --git a/functions.inc.php b/functions.inc.php index 47fc63ef..3eac8b0f 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -16,7 +16,7 @@ */ $version = '3.1'; -$min_db_version = 1836; # update (at least) before a release with the latest function numbrer in upgrade.php +$min_db_version = 1837; # update (at least) before a release with the latest function numbrer in upgrade.php /** * check_session @@ -89,6 +89,23 @@ function authentication_require_role($role) { exit(0); } +/** + * Initialize a user or admin session + * + * @param String $username the user or admin name + * @param boolean $is_admin true if the user is an admin, false otherwise + * @return boolean true on success + */ +function init_session($username, $is_admin = false) { + $status = session_regenerate_id(true); + $_SESSION['sessid'] = array(); + $_SESSION['sessid']['roles'] = array(); + $_SESSION['sessid']['roles'][] = $is_admin ? 'admin' : 'user'; + $_SESSION['sessid']['username'] = $username; + $_SESSION['PFA_token'] = md5(uniqid(rand(), true)); + + return $status; +} /** * Add an error message for display on the next page that is rendered. @@ -806,7 +823,7 @@ function encode_header ($string, $default_charset = "utf-8") { // function generate_password () { // length of the generated password - $length = 8; + $length = 12; // define possible characters $possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l @@ -1942,21 +1959,4 @@ function getRemoteAddr() { return $REMOTE_ADDR; } - -/** - * Returns a hash for a username valid for one day - * - * @param String $username user name - * @return String password recovery code - */ -function getPasswordRecoveryCode($username) -{ - $username = trim(strtolower($username)); - $date = date('Y-m-d'); - - $code = substr(strtoupper(md5('SECRET SALTING PHRASE' . $username . $date)), 0, 6); - - return $code; -} - /* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ diff --git a/languages/en.lang b/languages/en.lang index 4b0c65b2..f68a2542 100644 --- a/languages/en.lang +++ b/languages/en.lang @@ -154,7 +154,7 @@ $PALANG['pCreate_mailbox_mail'] = 'Send Welcome mail'; $PALANG['pCreate_mailbox_result_error'] = 'Creating the mailbox %s failed!'; $PALANG['pCreate_mailbox_result_success'] = 'The mailbox %s has been added to the mailbox table.'; $PALANG['pCreate_mailbox_result_succes_nosubfolders'] = 'The mailbox %s has been added to the mailbox table, but none (or only some) of the predefined sub-folders could be created.'; -$PALANG['mailbox_updated'] = "The mailbox %s has been updated."; +$PALANG['mailbox_updated'] = "The mailbox %s has been updated."; $PALANG['mailbox_update_failed'] = "Updating the mailbox %s failed!"; $PALANG['pEdit_mailbox_welcome'] = 'Edit a mailbox for your domain.'; @@ -180,11 +180,9 @@ $PALANG['pPassword_result_success'] = 'The password for %s has been changed.'; $PALANG['pPassword_recovery_title'] = 'Follow the instructions to reset your password.'; $PALANG['pPassword_recovery_button'] = 'Send me the code'; -$PALANG['pPassword_recovery_email_body'] = "Hello,\n\nUse the following link to change your email password:\n%s\n\nRegards,\n\n" . $CONF['admin_name']; -$PALANG['pPassword_recovery_email_sent'] = 'An email was sent to:'; +$PALANG['pPassword_recovery_email_body'] = "Hello,\n\nUse the following link to change your email password :\n%s\n\nRegards,\n\n" . $CONF['admin_name']; $PALANG['pPassword_recovery_sms_body'] = "Hello,\nThe code to change your password is: %s\n" . $CONF['admin_name']; -$PALANG['pPassword_recovery_sms_sent'] = 'An SMS was sent to:'; -$PALANG['pPassword_recovery_no_alternative'] = 'No alternative contact info were found. Please contact the support at ' . $CONF['admin_email'] . 'or by phone to ' . $CONF['admin_phone']; +$PALANG['pPassword_recovery_processed'] = "We processed your request. If you entered a valid username, you'll receive an email/SMS with a password code."; $PALANG['pPassword_password_code'] = 'Code sent by email/SMS'; $PALANG['pPassword_code_text_error'] = 'Invalid code'; diff --git a/languages/fr.lang b/languages/fr.lang index 78701df4..f31fb6b7 100644 --- a/languages/fr.lang +++ b/languages/fr.lang @@ -179,10 +179,8 @@ $PALANG['pPassword_result_success'] = 'Le mot de passe de %s a été changé !'; $PALANG['pPassword_recovery_title'] = 'Suivez les instructions pour réinitialiser votre mot de passe.'; $PALANG['pPassword_recovery_button'] = 'Envoyez-moi le code'; $PALANG['pPassword_recovery_email_body'] = "Bonjour,\n\nUtilisez le lien suivant pour modifier votre mot de passe :\n%s\n\nSalutations,\n\n" . $CONF['admin_name']; -$PALANG['pPassword_recovery_email_sent'] = 'Un code a été envoyé à :'; $PALANG['pPassword_recovery_sms_body'] = "Bonjour,\nLe code pour modifier votre mot de passe: %s\n" . $CONF['admin_name']; -$PALANG['pPassword_recovery_sms_sent'] = 'Un code a été envoyé par SMS à :'; -$PALANG['pPassword_recovery_no_alternative'] = "Aucun moyen de contact alternatif n'a été trouvé. Contactez le support à " . $CONF['admin_email'] . ' ou par téléphone ' . $CONF['admin_phone']; +$PALANG['pPassword_recovery_processed'] = "Nous avons traité votre demande. Si le nom d'utilisateur que vous avez saisi est valide, vous recevrez par e-mail/SMS un code de réinitialisation du mot de passe."; $PALANG['pPassword_password_code'] = 'Code reçu par email/SMS'; $PALANG['pPassword_code_text_error'] = 'Code invalide'; @@ -339,7 +337,7 @@ $PALANG['pBroadcast_title'] = 'Envoyer un message général'; $PALANG['pBroadcast_name'] = 'Votre nom'; $PALANG['pBroadcast_success'] = 'Votre message général a été envoyé.'; $PALANG['pAdminMenu_broadcast_message'] = 'message général'; -$PALANG['pBroadcast_error_empty'] = 'Les champs "Nom", "Sujet" et "Message" ne peuvent pas être vides!'; +$PALANG['pBroadcast_error_empty'] = 'Les champs "Nom", "Sujet" et "Message" ne peuvent pas être vides !'; $PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes'; # XXX $PALANG['broadcast_to_domains'] = 'Send to domains:'; # XXX $PALANG['pStatus_undeliverable'] = 'Non délivrable '; diff --git a/login.php b/login.php index f91ca378..c24f3066 100644 --- a/login.php +++ b/login.php @@ -52,13 +52,8 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") $h = new AdminHandler; if ( $h->login($fUsername, $fPassword) ) { - session_regenerate_id(true); - $_SESSION['sessid'] = array(); - $_SESSION['sessid']['roles'] = array(); - $_SESSION['sessid']['roles'][] = 'admin'; - $_SESSION['sessid']['username'] = $fUsername; - $_SESSION['PFA_token'] = md5(uniqid(rand(), true)); + init_session($fUsername, true); # they've logged in, so see if they are a domain admin, as well. diff --git a/model/AdminHandler.php b/model/AdminHandler.php index e33a6232..bce422fa 100644 --- a/model/AdminHandler.php +++ b/model/AdminHandler.php @@ -35,6 +35,8 @@ class AdminHandler extends PFAHandler { $domains_grouped = 'group_concat(domain)'; } + $passwordReset = Config::read('forgotten_admin_password_reset'); + $this->struct=array( # field name allow display in... type $PALANG label $PALANG description default / options / ... # editing? form list @@ -47,10 +49,6 @@ class AdminHandler extends PFAHandler { /*select*/ 'password as password2' ), - 'phone' => pacol( 1, 1, 0, 'text', 'pCreate_mailbox_phone', 'pCreate_mailbox_phone_desc', ''), - - 'email_other' => pacol( 1, 1, 0, 'mail', 'pCreate_mailbox_email', 'pCreate_mailbox_email_desc', ''), - 'superadmin' => pacol( 1, 1, 0, 'bool', 'super_admin' , 'super_admin_desc' , 0 # TODO: (finally) replace the ALL domain with a column in the admin table # TODO: current status: 'superadmin' column exists and is written when storing an admin with AdminHandler, @@ -78,6 +76,10 @@ class AdminHandler extends PFAHandler { ' ) AS __domain on username = __domain_username'), 'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ), + 'phone' => pacol( 1, $passwordReset, 0, 'text', 'pCreate_mailbox_phone', 'pCreate_mailbox_phone_desc', ''), + 'email_other' => pacol( 1, $passwordReset, 0, 'mail', 'pCreate_mailbox_email', 'pCreate_mailbox_email_desc', ''), + 'token' => pacol( 1, 0, 0, 'text', '' , '' ), + 'token_validity' => pacol( 1, 0, 0, 'ts', '' , '' ), 'created' => pacol( 0, 0, 0, 'ts', 'created' , '' ), 'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ), ); diff --git a/model/MailboxHandler.php b/model/MailboxHandler.php index 4efe6f90..0e0fbccb 100644 --- a/model/MailboxHandler.php +++ b/model/MailboxHandler.php @@ -13,6 +13,7 @@ class MailboxHandler extends PFAHandler { # init $this->struct, $this->db_table and $this->id_field protected function initStruct() { + $passwordReset = Config::read('forgotten_user_password_reset'); $this->struct=array( # field name allow display in... type $PALANG label $PALANG description default / options / ... # editing? form list @@ -35,11 +36,13 @@ class MailboxHandler extends PFAHandler { # read_from_db_postprocess() also sets 'quotabytes' for use in init() # TODO: read used quota from quota/quota2 table 'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ), - 'phone' => pacol( 1, 1, 0, 'text', 'pCreate_mailbox_phone' , 'pCreate_mailbox_phone_desc' , ''), - 'email_other' => pacol( 1, 1, 0, 'mail', 'pCreate_mailbox_email' , 'pCreate_mailbox_email_desc' , ''), 'welcome_mail' => pacol( $this->new, $this->new, 0, 'bool', 'pCreate_mailbox_mail' , '' , 1, /*options*/ '', /*not_in_db*/ 1 ), + 'phone' => pacol( 1, $passwordReset, 0, 'text', 'pCreate_mailbox_phone' , 'pCreate_mailbox_phone_desc' , ''), + 'email_other' => pacol( 1, $passwordReset, 0, 'mail', 'pCreate_mailbox_email' , 'pCreate_mailbox_email_desc' , ''), + 'token' => pacol( 1, 0, 0, 'text', '' , '' ), + 'token_validity'=> pacol( 1, 0, 0, 'ts', '' , '' ), 'created' => pacol( 0, 0, 1, 'ts', 'created' , '' ), 'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ), # TODO: add virtual 'notified' column and allow to display who received a vacation response? diff --git a/model/PFAHandler.php b/model/PFAHandler.php index 8e1c5a46..66754639 100644 --- a/model/PFAHandler.php +++ b/model/PFAHandler.php @@ -780,6 +780,55 @@ abstract class PFAHandler { return false; } + /** + * Generate and store a unique password reset token valid for one hour + * @param string $username + * @return false|string + */ + function getPasswordRecoveryCode($username) { + if ($this->init($username)) { + $token = generate_password(); + $table = table_by_key($this->db_table); + $updatedRows = db_update($table, $this->id_field, $username, array( + 'token' => pacrypt($token), + 'token_validity' => date("Y-m-d H:i:s", strtotime('+ 1 hour')), + )); + + if ($updatedRows == 1) { + return $token; + } + } + return false; + } + + /** + * Verify user's one time password reset token + * @param string $username + * @param string $token + * @return boolean true on success (i.e. code matches etc) + */ + public function checkPasswordRecoveryCode($username, $token) { + $username = escape_string($username); + + $table = table_by_key($this->db_table); + $active = db_get_boolean(True); + $query = "SELECT token FROM $table WHERE " . $this->id_field . "='$username' AND token <> '' AND active='$active' AND NOW() < token_validity"; + + $result = db_query ($query); + if ($result['rows'] == 1) { + $row = db_array($result['result']); + $crypt_token = pacrypt($token, $row['token']); + + if($row['token'] == $crypt_token) { + db_update($table, $this->id_field, $username, array( + 'token' => '', + 'token_validity' => '2000-01-01 00:00:00', + )); + return true; + } + } + return false; + } /************************************************************************** * functions to read protected variables diff --git a/templates/login.tpl b/templates/login.tpl index 2dabec64..01bddfd0 100644 --- a/templates/login.tpl +++ b/templates/login.tpl @@ -15,8 +15,11 @@