Jump to content

Activate user account via email


kyle

Recommended Posts

This tutorial will outline how to create a membership system that requires account activation via email.  If you have a decent understanding of ProcessWire it isn't difficult at all!

Create additional fields

By default ProcessWire has a name, email, and password field for the user template.  To allow for account activation we will have to add a couple more fields. Create the following fields and add them to the systems default user template. You can make ProcessWire show the system templates by going to setup -> templates -> toggle the filter tab -> show system templates -> yes.

  1. user_real_name
  2. user_activation

Okay great, now that you have added the two additional fields to the user template we can start to code the registration form.  I am not going to spend a lot of time on this part, because there is a tutorial that describes creating forms via the API

Create the registration form

Once you have followed the tutorial on creating forms, you will have to add a couple of sections to your new form!

<?php

include("./functions.php");
require("/phpmailer/class.phpmailer.php");

$out = "";
$errors = "";

//create form

//full name field

//email field

//username field

//password field

//submit button

//form submitted
if($input->post->submit) {

    $form->processInput($input->post);
	
    //instantiate variables taking in the form data
    $full_name = $form->get("full-name")->value;
    ....
	
   /*
    * Create the activation code
    * You can add a random string onto the
    * the username variable to keep people
    * from cracking the hash
    * ex $activation = md5($username."processwire");
    */
	
    $activation = md5($username);
    $activation_code = $config->httpHost."/activation/?user=".$username."&hash=".$activation;

    //check for errors
    if($form->getErrors() || username_validation($username) == 0) {
        $out .= $form->render();

        //process errors
		
        /*
	 * this checks to makesure that no one has the username
	 * I have a functions file that I import to the pages I
	 * need it on
	 */
        
	if(strlen($username) != 0){
            if(username_validation($username) == 0) {
                $username->error = " ";
                $errors .= "Sorry that username is already taken!";
            }
        }
    }
   //the registration was successful
    else {
        
	$out = "Thank you for registering!<br><br>";
        $out .= "An email has just been sent to " . $email . " with the url to activate your account";

	/*
	 * In this example I am using phpmailer to send the email
	 * I prefer this, but you can also use the mail() function
	 */
		
        $mail = new PHPMailer();

        $mail->IsHTML(true);

        $mail->From     = "email@domain.com";
        $mail->FromName = "Register @ Your Site";
        $mail->AddAddress($email);
        $mail->AddReplyTo("email@domain.com","Register @ Your Site");

        $mail->Subject  = "Registration";
        $mail->Body     = "
        Hi " . $full_name. ", <br>" .
        "Thanks for registering at Your Site.  To activate your email address click the link below! <br><br>"
        . "Activation Link: <a href='http://".$activation_code."'>".$activation_code."</a>";

        $mail->send();

        //create the new user
        $new_user = new User();
        $new_user->of(false);
        $new_user->name = $username;
        $new_user->email = $email;
        $new_user->pass = $password;
        $new_user->addRole("guest");
        $new_user->user_full_name = $full_name;
        $new_user->user_activation = $activation;
        $new_user->save();
        $new_user->of(true);
    }
}
//form not submitted
else {
    $out .= $form->render();
}
?>

<h2>REGISTER</h2>
<div class="errors"><?php echo $errors; ?></div>
<?php echo $out; ?>

Okay so that outlines the entire form.  Let me get into the important parts.

Checking for a unique username

/*
 * check if username exists
 * return 1 username is valid
 * return 0 username is taken
 */
function username_validation($username) {
    $valid = 1;

    $check = wire("users")->get($username);
    if ($check->id) {
        $valid = 0;
    }

    return $valid;
}

We don't want to try and add a username if the username is already taken, so we need to make sure to validate it.  If this returns 0 you should output that the username is already taken, and the user needs to choose a different one.

Generating an activation code

/*
* Create the activation code
*/
	
    $activation = md5($username);
    $activation_code = $config->httpHost."/activation/?user=".$username."&hash=".$activation;

This generates an activation code.  It does so by encrypting the username variable and then combines the username and activation code into a url for a user to visit.  Now we have to process the activation code.  As the teppo recommended, it is a good idea to add an extra string onto the $username when encrypting it with md5.  If you don't do this, people may crack it and allow for mass signups.  

$activation = md5($username."Cech4tHe");

Activate the user

 

<?php
include("./head.inc");
include("./functions.php");

/*
 * this will pull the username and 
 * activation code from the url
 * it is extremely important to 
 * clean the string
 */
$activate_username =  $sanitizer->text($_GET['user']);
$activate_hash =  $sanitizer->text($_GET['hash']);

if(wire("users")->get($activate_username)->id) {
    if(strcmp(wire("users")->get($activate_username)->user_activation, $activate_hash) == 0 || wire("users")->get($activate_username)->user_activation == 0) {
        echo "Your account has been activated!<br><br>";
        $activate_user = wire("users")->get($activate_username);
        $activate_user->of(false);
        $activate_user->user_activation = "0";
        $activate_user->save();
    }
    else {
        echo "There was an error activating your account!  Please contact us!<br><br>";
    }
}
else {
    echo "Sorry, but that we couldn't find your account in our database!<br><br>";
}

include("./foot.inc");

This pulls the username and activation hash from the url.  It then goes into the database and looks for the user.  Once it finds the user, it get's the user_activation hash and compares it to the one in the URL.  If it matches, it activates the user by setting user_activation to zero. 

Clean the url

You can user the built in $sanitizer->text() method to clean the url.  This will keep people from inserting special characters that can break the page.

Keeping unactivated users out

You aren't done yet!  Now you will have to choose which areas you want to restrict to activated users only.  An activated user's user_activation field will now equal zero.  So if you want to restrict a section check to make sure it is zero......

if($user->user_activation != 0) {
    echo "Sorry, but you need to activate your account!";
}
else {
    // activated users
}
Edited by kyle
  • Like 25
Link to comment
Share on other sites

@kyle: well written and easy to read, thanks for sharing!

Two things I'd like to point out:

  • You might want to add some random element to your activation hashes. MD5 is easy to spot and based on that information anyone mischievous enough could easily do things like automated account creation. Not that it would matter in most cases, but if a site using this was going to be in large-scale use then I'd be a bit worried about that.
  • You're using custom methods to sanitize URLs, usernames, hashes etc. Take a look at $sanitizer -- it provides methods for sanitizing PW usernames, text (this should be a good fit for your hash) and even URLs. No need to reinvent the wheel :)
  • Like 3
Link to comment
Share on other sites

@teppo

Thanks for the feedback.  I would definitely encourage salting the MD5 with something to keep people from cracking the activation code.  I will update that in the tutorial.

I wasn't sure if those sanitizer methods would work for this, so I made my own, but since they will I will take a look at them and then update the tutorial!

  • Like 1
Link to comment
Share on other sites

Thanks for posing this Kyle! A few things to mention: 

For even more security with the activation code, you might want to use something completely random that isn't influenced by the username. We have something built-in that will do the trick:

$p = new Password();
$hash = $p->randomBase64String(100); // 100=length of string 

See here how it works: https://github.com/ryancramerdesign/ProcessWire/blob/master/wire/core/Password.php#L154 (code is originally from Anthony Finta's password_compat library). 

Make sure that you are using $sanitizer->pageName("username"); for your username sanitization. Using $sanitizer->text(); is not adequate for sanitizing usernames or any selector values.  

In your function that checks for duplicate usernames, check for duplicate emails as well.

When you check for users or emails do $users->get("name=$username") rather than $users->get($username); as this will ensure it's checking for only the username, and not potentially confusing a numeric username with a user ID. 

I recommend using $input->get->var rather than $_GET['var'] because the $input->get version makes it so that you never have to worry about PHP's magic_quotes setting. 

Last thing to mention is that it might be good to include what type of fields the user_real_name and user_activation are. I believe these are supposed to be text fields, but wasn't sure (especially for user_activation) until later. 

  • Like 10
Link to comment
Share on other sites

  • 2 months later...

Thanks for posing this Kyle! A few things to mention: 

For even more security with the activation code, you might want to use something completely random that isn't influenced by the username. We have something built-in that will do the trick:

$p = new Password();
$hash = $p->randomBase64String(100); // 100=length of string 

See here how it works: https://github.com/ryancramerdesign/ProcessWire/blob/master/wire/core/Password.php#L154 (code is originally from Anthony Finta's password_compat library). 

Make sure that you are using $sanitizer->pageName("username"); for your username sanitization. Using $sanitizer->text(); is not adequate for sanitizing usernames or any selector values.  

In your function that checks for duplicate usernames, check for duplicate emails as well.

When you check for users or emails do $users->get("name=$username") rather than $users->get($username); as this will ensure it's checking for only the username, and not potentially confusing a numeric username with a user ID. 

I recommend using $input->get->var rather than $_GET['var'] because the $input->get version makes it so that you never have to worry about PHP's magic_quotes setting. 

Last thing to mention is that it might be good to include what type of fields the user_real_name and user_activation are. I believe these are supposed to be text fields, but wasn't sure (especially for user_activation) until later. 

`randomBase64String` contains special chars which makes it impossible to activate via GET.

I prefer something like this:

substr(hash('md5', mt_rand()), 0, 100);
Link to comment
Share on other sites

@Yannick
Are you sure?

$p = new Password();
$hash = $p->randomBase64String(); // 100=length of string? too long for the forum ;-)
echo $hash;
// output example: 3JrVekq0ZRPVJadm8i601Z1

 

What do you mean with 'activate via GET' just transmit or more?
All the best from Paris

  • Like 2
Link to comment
Share on other sites

Unless I'm missing something, randomBase64String should be bound to the same characters that an MD5 hash would be. Base 64 strings are compatible in GET vars or even URL segments. It's just ascii letters and numbers.  

  • Like 2
Link to comment
Share on other sites

Unless I'm missing something, randomBase64String should be bound to the same characters that an MD5 hash would be. Base 64 strings are compatible in GET vars or even URL segments. It's just ascii letters and numbers.

Yeah, damn. You're absolutely right... Was my fault! I had a typo in my routing-hook.

Just forget what I've said :P

  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

Thanks for a great tutorial. I'm having difficulties validating the username field. I've created it as such:

$field = $modules->get("InputfieldText");
$field->label = "Username";
$field->attr('id+name','username');
$field->required = 1;
$form->append($field); 

This works fine (although I'm wondering whether it should be "InputfieldPageNameURL"). To make sure that users are not allowed to enter spaces or other illegal characters not allowed in the username field, I've pulled the JS code from "InpufieldPageName.js", and this also works fine. However, how would I validate it server-side? 

I guess I'm trying to do exactly what you the "/processwire/access/users/add/" page does when adding new users on the PW backend. How would I do achieve this with the API?

Thanks.

Link to comment
Share on other sites

jacmaes,

Regarding adding users, is this what you are looking for:

http://processwire.com/api/variables/user/

Look at the very last item on the page entitled: "Adding a new user with the guest role"

If you are entering text and you don't want to allow spaces etc, look at sanitizing the input with one of the "name" options mentioned here:

http://processwire.com/api/variables/sanitizer/

Link to comment
Share on other sites

Thanks, adrian. You pointed me to the right direction. This is what I ended up using, in case it can help out someone else:

1. Create username field from API:


$field = $modules->get("InputfieldName");
$field->label = "Username";
$field->attr('id+name','username');
$field->required = 1;
$form->append($field);

2. Sanitize "username" field:

$username = $sanitizer->pageName($input->post->username);

3. And pull "InputfieldPageName.js" for JS validation on frontend. 

  • Like 1
Link to comment
Share on other sites

  • 8 months later...

/*

* this will pull the username and

* activation code from the url

* it is extremely important to

* clean the string

*/

$activate_username = $sanitizer->text($_GET['user']);

$activate_hash = $sanitizer->text($_GET['hash']);

I have created a new page called "activation.php" where I have all the code for the activation if an user click the link in the email

I dont know why but in my case the two lines of code are doing nothing. If I click the activation link, I come to the page but it always shows the error that there is no user in the database with this values.

I have not included the functions.php as in your code because I dont know what it should contain.

Best regards Jürgen

PROBLEM SOLVED

I had a mistake by configuring the activation link in the email. I use a multilingual site and therefore the activation link has to include the multilingual page url.

In my case:

Wrong example: www.my-page.at/activation/?user=testuser&hash=31901670cf14cf4baa816f3fc69e8c7a

Correct example: www.my-page.at/de/activation/?user=testuser&hash=31901670cf14cf4baa816f3fc69e8c7a

So the language part in the url was missing.

Now it works :)

Link to comment
Share on other sites

  • 8 months later...

I've been trying for a couple of hours to set this up. But for the last hour it has given me an "Internal Server Error".

The error log says "Error: Call to undefined function username_validation()".

I have no idea what could be wrong. I have not included any functions.php file, as I do not have one or do not have any idea what code that should contain.

I could paste more code, or tell more about my setup, but I'll start with the error.

Link to comment
Share on other sites

Thanks, cstevensjr.

Here's my code from my template "registration-page":

Code has been removed by me to clean up this forum post a little. All that was missing was a section of code inlcuded in the first forum post (that the original poster had moved to functions.php). So it was not really a problem :)

I have removed the include functions (don't know what it is for) and the require phpmailer, and have setup WireMailSwiftMailer instead. Therefore I have also changed some lines from $mail = new wireMail();

I have added new fields to the system template user, which is why I have first name and last name in here.

I am running my test site on a MAMP installation.

The error that show in errors.txt is (lines should match up with the pasted code):

2015-04-18 21:44:50 guest http://localhost/registration/ Error: Call to undefined function username_validation() (line 102 of /Applications/MAMP/htdocs/sitename/site/templates/registration-page.php)

I think that's most of it.

Link to comment
Share on other sites

Yes there is no function username_validation($username). You call it in Line 102 and in 114 (?).

I think it is in a dependent file that you have excluded. :) Can you have a look where it is and post the code of this function too please?

Link to comment
Share on other sites

I try to build a basic login, logout and register module which could be extended by plugins (PW hooks), but have to be optimized and maybe merged into one module before it will be released...

Basic register module (check if username / email already exists)

https://bitbucket.org/pwFoo/frontenduser/src/ac39eb355e49016efd2e5f8595bfc1917ebea80f?at=master

Plugin (PW hooks) examples like email pre-validation, add default registration role without hack the module itself.

https://bitbucket.org/pwFoo/frontenduser/wiki/FrontendUserRegister

It's work in progress. So some things may change (thinking about to merge login and register module...). Form handling is done by FormHelper (simplify value sanitizing, take care about form creation, submit button, processing, field errors and prepare file uploads).

You can take a look at username / email validation (checks if already exists). It's done as a Inputfield hook.

  • Like 1
Link to comment
Share on other sites

I have read and re-read the first post so many times I was getting blind. I never saw the code under Checking for a unique username.

With that included, I got it working. I should really read better on my own in the future!

Thanks, cstevensjr, horst and pwFoo!

  • Like 1
Link to comment
Share on other sites

  • 3 months later...

How to you apply styling to form fields created by the API?

For example, when using either a bootstrap css framework, or when using a custom css file.

I apologize if I have just overlooked the correct search results. If someone could point me in the right direction, I would appreciate it.

Thanks!

Link to comment
Share on other sites

  • 7 months later...

Hello @ all,

I have used for a long time the code from the first entry: https://processwire.com/talk/topic/4066-activate-user-account-via-email/?p=39846

I have the latest PW devs installed an today I have discovered that this code doesnt work anymore.

The registration works well and the user get the email to verify his account by clicking the activation link.

The link looks like this: : www........../registrierung-abgeschlossen?user=tester1&hash=47eeaa6d0363b323d48730fce39039bf

The strange behavior starts when the user click this link in the email. If the links was clicked the landing page is not the same url as above. It is www........../registrierung-abgeschlossen/  ->without the GET parameter.

So it seems that there will be a redirect. If I want to echo the values of the GET parameter all will be empty.

Here is the code of the activation page:

<?php
/*************************/
/** Activation template **/
/*************************/
include("./inc/head.inc");
echo '<div class="maincontent registration-complete row">';
echo '<main class="col-md-12">';
$activate_username = $_GET['user'];
$activate_hash     = $_GET['hash'];
echo wire("users")->get($activate_username)->id;//this only for testing and shows always 41 for the admin
$mister = _t('Mister', 'Generic');
$miss   = _t('Miss', 'Generic');
if (wire("users")->get($activate_username)->id) {
    if (strcmp(wire("users")->get($activate_username)->user_activation, $activate_hash) == 0 || wire("users")->get($activate_username)->user_activation == 0) {
        $mainheadline   = $page->get("headline|title");
        $useractivehead = _t('Your registration was successfull', 'Generic');
        $useractivetext = _t('Your account has just been successfully activated. You can register now with your access data at our website.', 'Generic');
        $useractive     = '<div class="alert alert-success" role="alert"><h4><i class="fa fa-info-circle"></i> ' . $useractivehead . '</h4><p>' . $useractivetext . '</p></div>';
        echo '<div class="page-header"><h1>' . $mainheadline . '</h1></div>';        
        $activate_user = wire("users")->get($activate_username);
        $activate_user->of(false);
        $activate_user->addRole("guest");
        $activate_user->user_activation = "0";
        $activate_user->save();
        //$activate_user->of(true);
        echo $useractive; //show success message
        $gender = $activate_user->usergender;
        if ($gender == "1") {
            $gendertext = $mister;
        } else {
            $gendertext = $miss;
        }
        $first_name          = $activate_user->user_first_name;
        $last_name           = $activate_user->user_real_name;
        $useremail           = $activate_user->email;
        $currentuserlanguage = $activate_user->language;
    } else {
        $mainheadline  = _t('Errors occur during the activation', 'Generic');
        $usererrorhead = _t('An error occured', 'Generic');
        $usererrortext = _t('We are sorry but an error occured during the activation. Please contact the owner of the website.', 'Generic');
        $usererror     = '<div class="alert alert-danger" role="alert"><h4><i class="fa fa-warning"></i> ' . $usererrorhead . '</h4><p>' . $usererrortext . '</p></div>';
        echo '<h1 itemprop="name">' . $mainheadline . '</h1>';
        echo $usererror; //show error message
    }
} else {
    $mainheadline     = _t('Errors occur during the activation', 'Generic');
    $usernotfoundhead = _t('Account not found.', 'Generic');
    $usernotfoundtext = _t('Sorry, but your account was not found in our database.', 'Generic');
    $usernotfound     = '<div class="alert alert-danger" role="alert"><h4><i class="fa fa-warning"></i> ' . $usernotfoundhead . '</h4><p>' . $usernotfoundtext . '</p></div>';
    echo '<h1 itemprop="name">' . $mainheadline . '</h1>';
    echo $usernotfound; //show error message
}
echo '</main>';
echo '</div>';
include("./inc/foot.inc");
?>

Has anyone an idea, why the GET parameter arent there?

Best regards
 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...