-
Notifications
You must be signed in to change notification settings - Fork 86
/
index.php
418 lines (380 loc) · 16.1 KB
/
index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
<?php
/**
* Class OneFileLoginApplication
*
* An entire php application with user registration, login and logout in one file.
* Uses very modern password hashing via the PHP 5.5 password hashing functions.
* This project includes a compatibility file to make these functions available in PHP 5.3.7+ and PHP 5.4+.
*
* @author Panique
* @link https://github.com/panique/php-login-one-file/
* @license http://opensource.org/licenses/MIT MIT License
*/
class OneFileLoginApplication
{
/**
* @var string Type of used database (currently only SQLite, but feel free to expand this with mysql etc)
*/
private $db_type = "sqlite"; //
/**
* @var string Path of the database file (create this with _install.php)
*/
private $db_sqlite_path = "./users.db";
/**
* @var object Database connection
*/
private $db_connection = null;
/**
* @var bool Login status of user
*/
private $user_is_logged_in = false;
/**
* @var string System messages, likes errors, notices, etc.
*/
public $feedback = "";
/**
* Does necessary checks for PHP version and PHP password compatibility library and runs the application
*/
public function __construct()
{
if ($this->performMinimumRequirementsCheck()) {
$this->runApplication();
}
}
/**
* Performs a check for minimum requirements to run this application.
* Does not run the further application when PHP version is lower than 5.3.7
* Does include the PHP password compatibility library when PHP version lower than 5.5.0
* (this library adds the PHP 5.5 password hashing functions to older versions of PHP)
* @return bool Success status of minimum requirements check, default is false
*/
private function performMinimumRequirementsCheck()
{
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
echo "Sorry, Simple PHP Login does not run on a PHP version older than 5.3.7 !";
} elseif (version_compare(PHP_VERSION, '5.5.0', '<')) {
require_once("libraries/password_compatibility_library.php");
return true;
} elseif (version_compare(PHP_VERSION, '5.5.0', '>=')) {
return true;
}
// default return
return false;
}
/**
* This is basically the controller that handles the entire flow of the application.
*/
public function runApplication()
{
// check is user wants to see register page (etc.)
if (isset($_GET["action"]) && $_GET["action"] == "register") {
$this->doRegistration();
$this->showPageRegistration();
} else {
// start the session, always needed!
$this->doStartSession();
// check for possible user interactions (login with session/post data or logout)
$this->performUserLoginAction();
// show "page", according to user's login status
if ($this->getUserLoginStatus()) {
$this->showPageLoggedIn();
} else {
$this->showPageLoginForm();
}
}
}
/**
* Creates a PDO database connection (in this case to a SQLite flat-file database)
* @return bool Database creation success status, false by default
*/
private function createDatabaseConnection()
{
try {
$this->db_connection = new PDO($this->db_type . ':' . $this->db_sqlite_path);
return true;
} catch (PDOException $e) {
$this->feedback = "PDO database connection problem: " . $e->getMessage();
} catch (Exception $e) {
$this->feedback = "General problem: " . $e->getMessage();
}
return false;
}
/**
* Handles the flow of the login/logout process. According to the circumstances, a logout, a login with session
* data or a login with post data will be performed
*/
private function performUserLoginAction()
{
if (isset($_GET["action"]) && $_GET["action"] == "logout") {
$this->doLogout();
} elseif (!empty($_SESSION['user_name']) && ($_SESSION['user_is_logged_in'])) {
$this->doLoginWithSessionData();
} elseif (isset($_POST["login"])) {
$this->doLoginWithPostData();
}
}
/**
* Simply starts the session.
* It's cleaner to put this into a method than writing it directly into runApplication()
*/
private function doStartSession()
{
if(session_status() == PHP_SESSION_NONE) session_start();
}
/**
* Set a marker (NOTE: is this method necessary ?)
*/
private function doLoginWithSessionData()
{
$this->user_is_logged_in = true; // ?
}
/**
* Process flow of login with POST data
*/
private function doLoginWithPostData()
{
if ($this->checkLoginFormDataNotEmpty()) {
if ($this->createDatabaseConnection()) {
$this->checkPasswordCorrectnessAndLogin();
}
}
}
/**
* Logs the user out
*/
private function doLogout()
{
$_SESSION = array();
session_destroy();
$this->user_is_logged_in = false;
$this->feedback = "You were just logged out.";
}
/**
* The registration flow
* @return bool
*/
private function doRegistration()
{
if ($this->checkRegistrationData()) {
if ($this->createDatabaseConnection()) {
$this->createNewUser();
}
}
// default return
return false;
}
/**
* Validates the login form data, checks if username and password are provided
* @return bool Login form data check success state
*/
private function checkLoginFormDataNotEmpty()
{
if (!empty($_POST['user_name']) && !empty($_POST['user_password'])) {
return true;
} elseif (empty($_POST['user_name'])) {
$this->feedback = "Username field was empty.";
} elseif (empty($_POST['user_password'])) {
$this->feedback = "Password field was empty.";
}
// default return
return false;
}
/**
* Checks if user exits, if so: check if provided password matches the one in the database
* @return bool User login success status
*/
private function checkPasswordCorrectnessAndLogin()
{
// remember: the user can log in with username or email address
$sql = 'SELECT user_name, user_email, user_password_hash
FROM users
WHERE user_name = :user_name OR user_email = :user_name
LIMIT 1';
$query = $this->db_connection->prepare($sql);
$query->bindValue(':user_name', $_POST['user_name']);
$query->execute();
// Btw that's the weird way to get num_rows in PDO with SQLite:
// if (count($query->fetchAll(PDO::FETCH_NUM)) == 1) {
// Holy! But that's how it is. $result->numRows() works with SQLite pure, but not with SQLite PDO.
// This is so crappy, but that's how PDO works.
// As there is no numRows() in SQLite/PDO (!!) we have to do it this way:
// If you meet the inventor of PDO, punch him. Seriously.
$result_row = $query->fetchObject();
if ($result_row) {
// using PHP 5.5's password_verify() function to check password
if (password_verify($_POST['user_password'], $result_row->user_password_hash)) {
// write user data into PHP SESSION [a file on your server]
$_SESSION['user_name'] = $result_row->user_name;
$_SESSION['user_email'] = $result_row->user_email;
$_SESSION['user_is_logged_in'] = true;
$this->user_is_logged_in = true;
return true;
} else {
$this->feedback = "Wrong password.";
}
} else {
$this->feedback = "This user does not exist.";
}
// default return
return false;
}
/**
* Validates the user's registration input
* @return bool Success status of user's registration data validation
*/
private function checkRegistrationData()
{
// if no registration form submitted: exit the method
if (!isset($_POST["register"])) {
return false;
}
// validating the input
if (!empty($_POST['user_name'])
&& strlen($_POST['user_name']) <= 64
&& strlen($_POST['user_name']) >= 2
&& preg_match('/^[a-z\d]{2,64}$/i', $_POST['user_name'])
&& !empty($_POST['user_email'])
&& strlen($_POST['user_email']) <= 64
&& filter_var($_POST['user_email'], FILTER_VALIDATE_EMAIL)
&& !empty($_POST['user_password_new'])
&& strlen($_POST['user_password_new']) >= 6
&& !empty($_POST['user_password_repeat'])
&& ($_POST['user_password_new'] === $_POST['user_password_repeat'])
) {
// only this case return true, only this case is valid
return true;
} elseif (empty($_POST['user_name'])) {
$this->feedback = "Empty Username";
} elseif (empty($_POST['user_password_new']) || empty($_POST['user_password_repeat'])) {
$this->feedback = "Empty Password";
} elseif ($_POST['user_password_new'] !== $_POST['user_password_repeat']) {
$this->feedback = "Password and password repeat are not the same";
} elseif (strlen($_POST['user_password_new']) < 6) {
$this->feedback = "Password has a minimum length of 6 characters";
} elseif (strlen($_POST['user_name']) > 64 || strlen($_POST['user_name']) < 2) {
$this->feedback = "Username cannot be shorter than 2 or longer than 64 characters";
} elseif (!preg_match('/^[a-z\d]{2,64}$/i', $_POST['user_name'])) {
$this->feedback = "Username does not fit the name scheme: only a-Z and numbers are allowed, 2 to 64 characters";
} elseif (empty($_POST['user_email'])) {
$this->feedback = "Email cannot be empty";
} elseif (strlen($_POST['user_email']) > 64) {
$this->feedback = "Email cannot be longer than 64 characters";
} elseif (!filter_var($_POST['user_email'], FILTER_VALIDATE_EMAIL)) {
$this->feedback = "Your email address is not in a valid email format";
} else {
$this->feedback = "An unknown error occurred.";
}
// default return
return false;
}
/**
* Creates a new user.
* @return bool Success status of user registration
*/
private function createNewUser()
{
// remove html code etc. from username and email
$user_name = htmlentities($_POST['user_name'], ENT_QUOTES);
$user_email = htmlentities($_POST['user_email'], ENT_QUOTES);
$user_password = $_POST['user_password_new'];
// crypt the user's password with the PHP 5.5's password_hash() function, results in a 60 char hash string.
// the constant PASSWORD_DEFAULT comes from PHP 5.5 or the password_compatibility_library
$user_password_hash = password_hash($user_password, PASSWORD_DEFAULT);
$sql = 'SELECT * FROM users WHERE user_name = :user_name OR user_email = :user_email';
$query = $this->db_connection->prepare($sql);
$query->bindValue(':user_name', $user_name);
$query->bindValue(':user_email', $user_email);
$query->execute();
// As there is no numRows() in SQLite/PDO (!!) we have to do it this way:
// If you meet the inventor of PDO, punch him. Seriously.
$result_row = $query->fetchObject();
if ($result_row) {
$this->feedback = "Sorry, that username / email is already taken. Please choose another one.";
} else {
$sql = 'INSERT INTO users (user_name, user_password_hash, user_email)
VALUES(:user_name, :user_password_hash, :user_email)';
$query = $this->db_connection->prepare($sql);
$query->bindValue(':user_name', $user_name);
$query->bindValue(':user_password_hash', $user_password_hash);
$query->bindValue(':user_email', $user_email);
// PDO's execute() gives back TRUE when successful, FALSE when not
// @link http://stackoverflow.com/q/1661863/1114320
$registration_success_state = $query->execute();
if ($registration_success_state) {
$this->feedback = "Your account has been created successfully. You can now log in.";
return true;
} else {
$this->feedback = "Sorry, your registration failed. Please go back and try again.";
}
}
// default return
return false;
}
/**
* Simply returns the current status of the user's login
* @return bool User's login status
*/
public function getUserLoginStatus()
{
return $this->user_is_logged_in;
}
/**
* Simple demo-"page" that will be shown when the user is logged in.
* In a real application you would probably include an html-template here, but for this extremely simple
* demo the "echo" statements are totally okay.
*/
private function showPageLoggedIn()
{
if ($this->feedback) {
echo $this->feedback . "<br/><br/>";
}
echo 'Hello ' . $_SESSION['user_name'] . ', you are logged in.<br/><br/>';
echo '<a href="' . $_SERVER['SCRIPT_NAME'] . '?action=logout">Log out</a>';
}
/**
* Simple demo-"page" with the login form.
* In a real application you would probably include an html-template here, but for this extremely simple
* demo the "echo" statements are totally okay.
*/
private function showPageLoginForm()
{
if ($this->feedback) {
echo $this->feedback . "<br/><br/>";
}
echo '<h2>Login</h2>';
echo '<form method="post" action="' . $_SERVER['SCRIPT_NAME'] . '" name="loginform">';
echo '<label for="login_input_username">Username (or email)</label> ';
echo '<input id="login_input_username" type="text" name="user_name" required /> ';
echo '<label for="login_input_password">Password</label> ';
echo '<input id="login_input_password" type="password" name="user_password" required /> ';
echo '<input type="submit" name="login" value="Log in" />';
echo '</form>';
echo '<a href="' . $_SERVER['SCRIPT_NAME'] . '?action=register">Register new account</a>';
}
/**
* Simple demo-"page" with the registration form.
* In a real application you would probably include an html-template here, but for this extremely simple
* demo the "echo" statements are totally okay.
*/
private function showPageRegistration()
{
if ($this->feedback) {
echo $this->feedback . "<br/><br/>";
}
echo '<h2>Registration</h2>';
echo '<form method="post" action="' . $_SERVER['SCRIPT_NAME'] . '?action=register" name="registerform">';
echo '<label for="login_input_username">Username (only letters and numbers, 2 to 64 characters)</label>';
echo '<input id="login_input_username" type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" required />';
echo '<label for="login_input_email">User\'s email</label>';
echo '<input id="login_input_email" type="email" name="user_email" required />';
echo '<label for="login_input_password_new">Password (min. 6 characters)</label>';
echo '<input id="login_input_password_new" class="login_input" type="password" name="user_password_new" pattern=".{6,}" required autocomplete="off" />';
echo '<label for="login_input_password_repeat">Repeat password</label>';
echo '<input id="login_input_password_repeat" class="login_input" type="password" name="user_password_repeat" pattern=".{6,}" required autocomplete="off" />';
echo '<input type="submit" name="register" value="Register" />';
echo '</form>';
echo '<a href="' . $_SERVER['SCRIPT_NAME'] . '">Homepage</a>';
}
}
// run the application
$application = new OneFileLoginApplication();