Skip to content

Commit 1fd5363

Browse files
committed
Implement user registration feature
1 parent 212e89e commit 1fd5363

File tree

8 files changed

+282
-12
lines changed

8 files changed

+282
-12
lines changed

config.sample.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
"BNETDocs has to take a brief moment to do some system maintenance. We'll be back shortly."
66
],
77
"user_login_disabled": false,
8+
"user_password_pepper": "bnetdocs-INSERTRANDOMVALUEHERE",
89
"user_register_disabled": false,
910
"user_register_requirements": {
1011
"email_duplicate_allowed": false,
11-
"email_validate_regex": true,
12+
"email_validate_quick": true,
1213
"password_allow_email": false,
1314
"password_allow_username": false,
1415
"password_length_max": null,
@@ -39,5 +40,10 @@
3940
],
4041
"timeout": 3,
4142
"username": "bnetdocs"
43+
},
44+
"recaptcha": {
45+
"secret": "google-provided-value",
46+
"sitekey": "google-provided-value",
47+
"url": "https://www.google.com/recaptcha/api/siteverify"
4248
}
4349
}

controllers/User/ChangePassword.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ protected function tryChangePassword(
8989
$model->user_session->user_id,
9090
getenv("REMOTE_ADDR"),
9191
json_encode([
92-
"error" => $model->error,
92+
"error" => $model->error,
9393
"old_password_hash" => $old_password_hash,
9494
"old_password_salt" => $old_password_salt,
9595
"new_password_hash" => $new_password_hash,

controllers/User/Register.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
namespace BNETDocs\Controllers\User;
44

5+
use \BNETDocs\Libraries\CSRF;
56
use \BNETDocs\Libraries\Common;
67
use \BNETDocs\Libraries\Controller;
8+
use \BNETDocs\Libraries\Exceptions\RecaptchaException;
79
use \BNETDocs\Libraries\Exceptions\UnspecifiedViewException;
10+
use \BNETDocs\Libraries\Exceptions\UserNotFoundException;
11+
use \BNETDocs\Libraries\Exceptions\QueryException;
12+
use \BNETDocs\Libraries\Logger;
813
use \BNETDocs\Libraries\Router;
14+
use \BNETDocs\Libraries\User;
915
use \BNETDocs\Libraries\UserSession;
1016
use \BNETDocs\Models\User\Register as UserRegisterModel;
1117
use \BNETDocs\Views\User\RegisterHtml as UserRegisterHtmlView;
@@ -21,7 +27,14 @@ public function run(Router &$router) {
2127
throw new UnspecifiedViewException();
2228
}
2329
$model = new UserRegisterModel();
30+
$model->csrf_id = mt_rand();
31+
$model->csrf_token = CSRF::generate($model->csrf_id);
32+
$model->captcha_key = Common::$config->recaptcha->sitekey;
33+
$model->error = null;
2434
$model->user_session = UserSession::load($router);
35+
if ($router->getRequestMethod() == "POST") {
36+
$this->tryRegister($router, $model);
37+
}
2538
ob_start();
2639
$view->render($model);
2740
$router->setResponseCode(200);
@@ -31,4 +44,134 @@ public function run(Router &$router) {
3144
ob_end_clean();
3245
}
3346

47+
protected function tryRegister(Router &$router, UserRegisterModel &$model) {
48+
$data = $router->getRequestBodyArray();
49+
$model->email = (isset($data["email" ]) ? $data["email" ] : null);
50+
$model->username = (isset($data["username"]) ? $data["username"] : null);
51+
if (isset($model->user_session)) {
52+
$model->error = "ALREADY_LOGGED_IN";
53+
return;
54+
}
55+
$csrf_id = (isset($data["csrf_id" ]) ? $data["csrf_id" ] : null);
56+
$csrf_token = (isset($data["csrf_token"]) ? $data["csrf_token"] : null);
57+
$csrf_valid = CSRF::validate($csrf_id, $csrf_token);
58+
if (!$csrf_valid) {
59+
$model->error = "INVALID_CSRF";
60+
return;
61+
}
62+
CSRF::invalidate($csrf_id);
63+
$email = $model->email;
64+
$username = $model->username;
65+
$pw1 = (isset($data["pw1"]) ? $data["pw1"] : null);
66+
$pw2 = (isset($data["pw2"]) ? $data["pw2"] : null);
67+
$captcha = (
68+
isset($data["g-recaptcha-response"]) ?
69+
$data["g-recaptcha-response"] :
70+
null
71+
);
72+
if (!self::verifyCaptcha($captcha)) {
73+
$model->error = "INVALID_CAPTCHA";
74+
return;
75+
}
76+
if ($pw1 !== $pw2) {
77+
$model->error = "NONMATCHING_PASSWORD";
78+
return;
79+
}
80+
$pwlen = strlen($pw1);
81+
$usernamelen = strlen($username);
82+
$req = Common::$config->bnetdocs->user_register_requirements;
83+
if ($req->email_validate_quick
84+
&& !filter_var($email, FILTER_VALIDATE_EMAIL)) {
85+
$model->error = "INVALID_EMAIL";
86+
return;
87+
}
88+
if (!$req->password_allow_email && stripos($pw1, $email)) {
89+
$model->error = "PASSWORD_CONTAINS_EMAIL";
90+
return;
91+
}
92+
if (!$req->password_allow_username && stripos($pw1, $username)) {
93+
$model->error = "PASSWORD_CONTAINS_USERNAME";
94+
return;
95+
}
96+
if (is_numeric($req->username_length_max)
97+
&& $usernamelen > $req->username_length_max) {
98+
$model->error = "USERNAME_TOO_LONG";
99+
return;
100+
}
101+
if (is_numeric($req->username_length_min)
102+
&& $usernamelen < $req->username_length_min) {
103+
$model->error = "USERNAME_TOO_SHORT";
104+
return;
105+
}
106+
if (is_numeric($req->password_length_max)
107+
&& $pwlen > $req->password_length_max) {
108+
$model->error = "PASSWORD_TOO_LONG";
109+
return;
110+
}
111+
if (is_numeric($req->password_length_min)
112+
&& $pwlen < $req->password_length_min) {
113+
$model->error = "PASSWORD_TOO_SHORT";
114+
return;
115+
}
116+
if (Common::$config->bnetdocs->user_register_disabled) {
117+
$model->error = "REGISTER_DISABLED";
118+
return;
119+
}
120+
try {
121+
if (!$req->email_duplicate_allowed && User::findIdByEmail($email)) {
122+
$model->error = "EMAIL_ALREADY_USED";
123+
return;
124+
}
125+
} catch (UserNotFoundException $e) {}
126+
try {
127+
$success = User::create($email, $username, null, $pw1, 0);
128+
} catch (QueryException $e) {
129+
// SQL error occurred. We can show a friendly message to the user while
130+
// also notifying this problem to staff.
131+
Logger::logException($e);
132+
}
133+
if (!$success) {
134+
$model->error = "INTERNAL_ERROR";
135+
} else {
136+
$model->error = false;
137+
}
138+
Logger::logEvent(
139+
"user_created",
140+
null,
141+
getenv("REMOTE_ADDR"),
142+
json_encode([
143+
"error" => $model->error,
144+
"requirements" => $req,
145+
"email" => $email,
146+
"username" => $username,
147+
"display_name" => null,
148+
"options_bitmask" => 0,
149+
])
150+
);
151+
}
152+
153+
protected static function verifyCaptcha($g_captcha_response) {
154+
$data = [
155+
"secret" => Common::$config->recaptcha->secret,
156+
"response" => $g_captcha_response,
157+
"remoteip" => getenv("REMOTE_ADDR"),
158+
];
159+
$r = Common::curlRequest(Common::$config->recaptcha->url, $data);
160+
Logger::logMetric("response_code", $r->code);
161+
Logger::logMetric("response_type", $r->type);
162+
Logger::logMetric("response_data", $r->data);
163+
if ($r->code != 200)
164+
throw new RecaptchaException("Received bad HTTP status");
165+
if (stripos($r->type, "json") === false)
166+
throw new RecaptchaException("Received unknown content type");
167+
if (empty($data))
168+
throw new RecaptchaException("Received empty response");
169+
$j = json_decode($r->data);
170+
$e = json_last_error();
171+
Logger::logMetric("json_last_error", $e);
172+
if (!$j || $e !== JSON_ERROR_NONE || !property_exists($j, "success"))
173+
throw new RecaptchaException("Received invalid response");
174+
return ($j->success);
175+
}
176+
34177
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace BNETDocs\Libraries\Exceptions;
4+
5+
use \BNETDocs\Libraries\Exceptions\BNETDocsException;
6+
use \Exception;
7+
8+
class RecaptchaException extends BNETDocsException {
9+
10+
public function __construct($message, Exception &$prev_ex = null) {
11+
parent::__construct($message, 15, $prev_ex);
12+
}
13+
14+
}

libraries/Exceptions/Reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ All of the following errors are subclassed from the `BNETDocsException` class.
1919
| 12 | `NewsCategoryNotFoundException` | News category not found |
2020
| 13 | `PacketNotFoundException` | Packet not found |
2121
| 14 | `DocumentNotFoundException` | Document not found |
22+
| 15 | `RecaptchaException` | `$message` |

templates/User/Login.phtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ require("./header.inc.phtml");
4242
?>
4343
<article>
4444
<?php if ($this->getContext()->error !== false) { ?>
45-
<header>Log In</header>
45+
<header>Account Login</header>
4646
<?php if (!empty($message)) { ?>
4747
<section class="red">
4848
<p><?php echo $message; ?></p>

templates/User/Logout.phtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require("./header.inc.phtml");
2525
?>
2626
<article>
2727
<?php if (is_null($this->getContext()->error)) { ?>
28-
<header>Logout</header>
28+
<header>Account Logout</header>
2929
<form method="POST" action="?">
3030
<input type="hidden" name="csrf_id" value="<?php echo $this->getContext()->csrf_id; ?>"/>
3131
<input type="hidden" name="csrf_token" value="<?php echo $this->getContext()->csrf_token; ?>"/>

templates/User/Register.phtml

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,129 @@ use \BNETDocs\Libraries\Common;
55
use \BNETDocs\Libraries\Pair;
66

77
$title = "Create Account";
8-
$description = "This form allows an individual to create an account on BNETDocs";
8+
$description = "This form allows an individual to create an account on BNETDocs.";
99
$this->opengraph->attach(new Pair("url", "/user/register"));
1010

11+
switch ($this->getContext()->error) {
12+
case null:
13+
$af = "email";
14+
$message = null;
15+
break;
16+
case "ALREADY_LOGGED_IN":
17+
$af = null;
18+
$message = "You are already logged in, you must log out first.";
19+
break;
20+
case "INVALID_CSRF":
21+
$af = null;
22+
$message = "The Cross-Site Request Forgery token was invalid. "
23+
. "Either the account creation form expired, or "
24+
. "this may have been a malicious attempt to create an account.";
25+
break;
26+
case "INVALID_CAPTCHA":
27+
$af = "";
28+
$message = "The captcha code did not successfully verify, try again.";
29+
break;
30+
case "NONMATCHING_PASSWORD":
31+
$af = "pw1";
32+
$message = "The password does not match its confirmation.";
33+
break;
34+
case "INVALID_EMAIL":
35+
$af = "email";
36+
$message = "The email address is invalid.";
37+
break;
38+
case "PASSWORD_CONTAINS_EMAIL":
39+
$af = "pw1";
40+
$message = "The password contains the email address, "
41+
. "use a better password.";
42+
break;
43+
case "PASSWORD_CONTAINS_USERNAME":
44+
$af = "pw1";
45+
$message = "The password contains the username, use a better password.";
46+
break;
47+
case "USERNAME_TOO_LONG":
48+
$af = "username";
49+
$message = "The username is too long, choose a different username.";
50+
break;
51+
case "USERNAME_TOO_SHORT":
52+
$af = "username";
53+
$message = "The username is too short, choose a different username.";
54+
break;
55+
case "PASSWORD_TOO_LONG":
56+
$af = "pw1";
57+
$message = "The password is too long, shorten it.";
58+
break;
59+
case "PASSWORD_TOO_SHORT":
60+
$af = "pw1";
61+
$message = "The password is too short, use a better password.";
62+
break;
63+
case "REGISTER_DISABLED":
64+
$af = null;
65+
$message = "Creating accounts has been administratively disabled "
66+
. "indefinitely.";
67+
break;
68+
case "EMAIL_ALREADY_USED":
69+
$af = "email";
70+
$message = "The email address is already in use, use another.";
71+
break;
72+
case "INTERNAL_ERROR":
73+
$af = null;
74+
$message = "An internal error occurred while processing your request. "
75+
. "Our staff has been notified of the issue. Try again later.";
76+
break;
77+
default:
78+
$af = null;
79+
$message = $this->getContext()->error;
80+
}
81+
82+
$safe_email = htmlentities($this->getContext()->email , ENT_HTML5, "UTF-8");
83+
$safe_username = htmlentities($this->getContext()->username, ENT_HTML5, "UTF-8");
84+
1185
$this->additional_css[] = "/a/forms.css";
1286
require("./header.inc.phtml");
1387
?>
1488
<article>
15-
<?php if (!isset($this->getContext()->user_session)) { ?>
89+
<?php if ($this->getContext()->error !== false) { ?>
90+
<script async defer="defer" src="https://www.google.com/recaptcha/api.js"><![CDATA[]]></script>
1691
<header>Create Account</header>
17-
<section>
18-
<?php require("./NYI.inc.phtml"); ?>
92+
<?php if (!empty($message)) { ?>
93+
<section class="red">
94+
<p><?php echo $message; ?></p>
1995
</section>
96+
<?php } ?>
97+
<form method="POST" action="?">
98+
<input type="hidden" name="csrf_id" value="<?php echo $this->getContext()->csrf_id; ?>"/>
99+
<input type="hidden" name="csrf_token" value="<?php echo $this->getContext()->csrf_token; ?>"/>
100+
<section>
101+
<table><tbody><tr>
102+
<td>
103+
<label for="email">Email address:</label><br/>
104+
<input type="email" name="email" id="email" tabindex="1"
105+
value="<?php echo $safe_email; ?>" required<?php if ($af == "email") { ?> autofocus="autofocus"<?php } ?>/>
106+
</td><td>
107+
<label for="username">Username:</label><br/>
108+
<input type="text" name="username" id="username" tabindex="2"
109+
value="<?php echo $safe_username; ?>" required<?php if ($af == "username") { ?> autofocus="autofocus"<?php } ?>/>
110+
</td></tr><tr><td>
111+
<label for="pw1">Password:</label><br/>
112+
<input type="password" name="pw1" id="pw1" tabindex="3"
113+
value="" required<?php if ($af == "pw1") { ?> autofocus="autofocus"<?php } ?>/>
114+
</td><td>
115+
<label for="pw2">Confirm password:</label><br/>
116+
<input type="password" name="pw2" id="pw2" tabindex="4"
117+
value="" required<?php if ($af == "pw2") { ?> autofocus="autofocus"<?php } ?>/>
118+
</td></tr><tr><td colspan="2">
119+
<div class="g-recaptcha" data-theme="dark" data-sitekey="<?php echo $this->getContext()->captcha_key; ?>"></div>
120+
</td></tr><tr><td colspan="2" style="padding-top:16px;">
121+
<input type="submit" value="Register" tabindex="5"/>
122+
</td>
123+
</tr></tbody></table>
124+
</section>
125+
</form>
20126
<?php } else { ?>
21-
<header class="red">Create Account</header>
22-
<section class="red">
23-
<p>You are currently logged in to an account. You must first logout of your account before using this form.</p>
24-
<p><a class="button" href="<?php echo Common::relativeUrlToAbsolute("/user/logout"); ?>">Logout</a></p>
127+
<header class="green">Account Created</header>
128+
<section class="green">
129+
<p>You have successfully created an account!</p>
130+
<p>Use the navigation to the left to move to another page.</p>
25131
</section>
26132
<?php } ?>
27133
</article>

0 commit comments

Comments
 (0)