├── src ├── LS.php ├── class.logsys.php ├── table.sql ├── autoload.php └── Fr │ ├── LS │ └── TwoStepLogin.php │ └── LS.php ├── .gitignore ├── examples ├── basic │ ├── logout.php │ ├── reset.php │ ├── index.php │ ├── status.php │ ├── config.php │ ├── home.php │ ├── change.php │ ├── login.php │ └── register.php ├── material-design │ ├── logout.php │ ├── js │ │ ├── app.js │ │ └── jquery.router.js │ ├── partial │ │ └── header.php │ ├── index.php │ ├── home.php │ ├── manage-devices.php │ ├── profile.php │ ├── config.php │ ├── change.php │ ├── register.php │ ├── reset.php │ └── login.php └── two-step-login │ ├── logout.php │ ├── reset.php │ ├── index.php │ ├── status.php │ ├── home.php │ ├── manage-devices.php │ ├── config.php │ ├── change.php │ ├── register.php │ └── login.php ├── .travis.yml ├── composer.json ├── testing ├── bootstrap.php ├── phpunit.sqlite.xml ├── phpunit.mysql.xml ├── phpunit.postgresql.xml └── tests │ ├── TestDebug.php │ ├── TestUserTwoStepLogin.php │ ├── TestDBSetup.php │ ├── TestUserResetPassword.php │ └── TestUserBasic.php ├── sql ├── sqlite.sql ├── postgresql.sql └── mysql.sql ├── README.md ├── CHANGELOG.md └── LICENSE /src/LS.php: -------------------------------------------------------------------------------- 1 | autoload.php -------------------------------------------------------------------------------- /src/class.logsys.php: -------------------------------------------------------------------------------- 1 | autoload.php -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpintel 2 | composer.lock 3 | vendor/ 4 | -------------------------------------------------------------------------------- /src/table.sql: -------------------------------------------------------------------------------- 1 | /home/simsu/Other/projects/Web/Francium/logSys/sql/mysql.sql -------------------------------------------------------------------------------- /examples/basic/logout.php: -------------------------------------------------------------------------------- 1 | logout(); 4 | -------------------------------------------------------------------------------- /examples/material-design/logout.php: -------------------------------------------------------------------------------- 1 | logout(); 4 | -------------------------------------------------------------------------------- /examples/two-step-login/logout.php: -------------------------------------------------------------------------------- 1 | logout(); 4 | -------------------------------------------------------------------------------- /examples/basic/reset.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | forgotPassword(); 7 | ?> 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/two-step-login/reset.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | forgotPassword(); 7 | ?> 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/basic/index.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |

The Index Page

9 | Login 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/two-step-login/index.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |

The Index Page

9 | Login 10 | Status 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | isLoggedIn()) { 10 | echo "You are logged in. Home"; 11 | } else { 12 | echo "You are not logged in. Log In"; 13 | } 14 | ?> 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/two-step-login/status.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | isLoggedIn()) { 10 | echo "You are logged in. Home"; 11 | } else { 12 | echo "You are not logged in. Log In"; 13 | } 14 | ?> 15 | 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.6' 4 | - '7.0' 5 | - '7.1' 6 | 7 | services: 8 | - mysql 9 | - postgresql 10 | 11 | before_install: 12 | - mysql -e 'CREATE DATABASE IF NOT EXISTS tests;' 13 | - psql -c 'create database tests;' -U postgres 14 | 15 | before_script: 16 | - composer update 17 | 18 | script: 19 | - vendor/bin/phpunit -c testing/phpunit.mysql.xml 20 | - vendor/bin/phpunit -c testing/phpunit.sqlite.xml 21 | - vendor/bin/phpunit -c testing/phpunit.postgresql.xml 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "francium/logsys", 3 | "type": "library", 4 | "description": "PHP Secure, Advanced Login System", 5 | "keywords": ["login","register", "secure"], 6 | "homepage": "http://subinsb.com/php-logsys", 7 | "license": "Apache-2.0", 8 | "authors": [ 9 | { 10 | "name": "Subin Siby", 11 | "homepage": "http://subinsb.com", 12 | "role": "Developer" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Fr\\": "src/Fr" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=5.6", 22 | "duncan3dc/sessions": "1.2.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "5.7.16" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testing/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sql/sqlite.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Table structure for table `users` 3 | -- 4 | 5 | CREATE TABLE IF NOT EXISTS `users` ( 6 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 7 | `username` varchar(10) NOT NULL, 8 | `email` tinytext NOT NULL, 9 | `password` varchar(255) NOT NULL, 10 | `name` varchar(30) NOT NULL, 11 | `created` datetime NOT NULL, 12 | `attempt` varchar(15) NOT NULL DEFAULT '0' 13 | ); 14 | 15 | -- 16 | -- Table structure for table `user_tokens` 17 | -- 18 | 19 | CREATE TABLE IF NOT EXISTS `user_tokens` ( 20 | `token` varchar(40) NOT NULL, 21 | `uid` INTEGER NOT NULL, 22 | `requested` varchar(20) NOT NULL 23 | ); 24 | 25 | -- 26 | -- Table structure for table `user_devices` 27 | -- 28 | 29 | CREATE TABLE IF NOT EXISTS `user_devices` ( 30 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 31 | `uid` INTEGER NOT NULL, 32 | `token` varchar(15) NOT NULL, 33 | `last_access` varchar(20) NOT NULL 34 | ); 35 | -------------------------------------------------------------------------------- /testing/phpunit.mysql.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /testing/phpunit.postgresql.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/basic/config.php: -------------------------------------------------------------------------------- 1 | array( 11 | 'host' => 'localhost', 12 | 'port' => 3306, 13 | 'username' => 'root', 14 | 'password' => '', 15 | 'name' => 'test', 16 | 'table' => 'users', 17 | ), 18 | 'features' => array( 19 | 'auto_init' => true, 20 | ), 21 | 'pages' => array( 22 | 'no_login' => array( 23 | '/', 24 | '/examples/basic/reset.php', 25 | '/examples/basic/register.php', 26 | ), 27 | 'everyone' => array( 28 | '/examples/basic/status.php', 29 | ), 30 | 'login_page' => '/examples/basic/login.php', 31 | 'home_page' => '/examples/basic/home.php', 32 | ), 33 | )); 34 | -------------------------------------------------------------------------------- /examples/material-design/js/app.js: -------------------------------------------------------------------------------- 1 | var base = '/examples/material-design/'; 2 | var pages = [ 3 | 'index.php', 4 | 'login.php', 5 | 'register.php', 6 | 'home.php', 7 | 'manage-devices.php', 8 | 'change.php', 9 | 'reset.php', 10 | 'profile.php' 11 | ]; 12 | 13 | $.each(pages, function(i, page) { 14 | $.router.add(base + page, function() { 15 | $.get(base + page, function(html) { 16 | var htmlObj = $(html); 17 | 18 | $('title').replaceWith(htmlObj.filter('title')); 19 | $('.container').replaceWith(htmlObj.filter('.container')); 20 | 21 | window.history.replaceState({}, '', htmlObj.filter('meta[name=page-path]').attr('content')); 22 | }); 23 | }); 24 | }); 25 | 26 | $(document).ready(function() { 27 | $(document).on('click', 'a[data-ajax]', function(e) { 28 | e.preventDefault(); 29 | $.router.go($(this).attr('href')); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /sql/postgresql.sql: -------------------------------------------------------------------------------- 1 | SET TIME ZONE 'UTC'; 2 | 3 | -- 4 | -- Table structure for table users 5 | -- 6 | 7 | CREATE TABLE IF NOT EXISTS users ( 8 | id SERIAL, 9 | username text NOT NULL, 10 | email text NOT NULL, 11 | password text NOT NULL, 12 | name text NOT NULL, 13 | created text NOT NULL, 14 | attempt text NOT NULL DEFAULT '0', 15 | PRIMARY KEY (id) 16 | ); 17 | 18 | -- 19 | -- Table structure for table user_tokens 20 | -- 21 | 22 | CREATE TABLE IF NOT EXISTS user_tokens ( 23 | token text NOT NULL, -- 'The Unique Token Generated' 24 | uid integer NOT NULL, -- 'The User Id' 25 | requested text NOT NULL -- 'The Date when token was created' 26 | ); 27 | 28 | -- 29 | -- Table structure for table user_devices 30 | -- 31 | 32 | CREATE TABLE IF NOT EXISTS user_devices ( 33 | id SERIAL, 34 | uid int NOT NULL, -- The user's ID 35 | token character(15) NOT NULL, -- A unique token for the user's device 36 | last_access text NOT NULL, 37 | PRIMARY KEY (id) 38 | ); 39 | -------------------------------------------------------------------------------- /examples/material-design/partial/header.php: -------------------------------------------------------------------------------- 1 | 24 | 30 | -------------------------------------------------------------------------------- /examples/material-design/index.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 |
14 |

My Site

15 |

Welcome to my site. You can sign in or create an account.

16 | 17 | isLoggedIn()) { 19 | ?> 20 |

Looks like you are logged in.

21 | Home 22 | Profile 23 | 26 | Sign In 27 | Sign Up 28 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Fr/LS/TwoStepLogin.php: -------------------------------------------------------------------------------- 1 | status = $status; 24 | $this->extraInfo = $extraInfo; 25 | } 26 | 27 | /** 28 | * @return boolean 29 | */ 30 | public function isError() 31 | { 32 | return $this->status !== 'login_success' && $this->status !== 'enter_token_form'; 33 | } 34 | 35 | public function getStatus() 36 | { 37 | return $this->status; 38 | } 39 | 40 | public function getBlockInfo() 41 | { 42 | return $this->extraInfo; 43 | } 44 | 45 | /** 46 | * @param string $option Option name 47 | * @return mixed 48 | */ 49 | public function getOption($option) 50 | { 51 | return isset($this->extraInfo[ $option ]) ? $this->extraInfo[ $option ] : false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/basic/home.php: -------------------------------------------------------------------------------- 1 | updateUser(array( 6 | 'name' => $_POST['newName'], 7 | )); 8 | } 9 | ?> 10 | 11 | 12 | 13 |

Welcome

14 |

You have been successfully logged in.

15 |

16 | Log Out 17 |

18 |

19 | You registered on this website joinedSince(); ?> ago. 20 |

21 |

22 | Here is the full data, the database stores on this user : 23 |

24 | getUser(); 26 | echo '
';
27 |         print_r($details);
28 |         echo '
'; 29 | ?> 30 |

31 | Change the name of your account : 32 |

33 |
34 | 35 | 36 |
37 |

38 | Change Password 39 |

40 | 41 | 42 | -------------------------------------------------------------------------------- /sql/mysql.sql: -------------------------------------------------------------------------------- 1 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 2 | SET time_zone = "+00:00"; 3 | 4 | -- 5 | -- Table structure for table `users` 6 | -- 7 | 8 | CREATE TABLE IF NOT EXISTS `users` ( 9 | `id` int(11) NOT NULL AUTO_INCREMENT, 10 | `username` varchar(10) NOT NULL, 11 | `email` tinytext NOT NULL, 12 | `password` varchar(255) NOT NULL, 13 | `name` varchar(30) NOT NULL, 14 | `created` datetime NOT NULL, 15 | `attempt` varchar(15) NOT NULL DEFAULT '0', 16 | PRIMARY KEY (`id`) 17 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1; 18 | 19 | -- 20 | -- Table structure for table `user_tokens` 21 | -- 22 | 23 | CREATE TABLE IF NOT EXISTS `user_tokens` ( 24 | `token` varchar(40) NOT NULL COMMENT 'The generated unique token', 25 | `uid` int(11) NOT NULL COMMENT 'The User ID', 26 | `requested` varchar(20) NOT NULL COMMENT 'The date when token was created', 27 | PRIMARY KEY (`token`) 28 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 29 | 30 | -- 31 | -- Table structure for table `user_devices` 32 | -- 33 | 34 | CREATE TABLE IF NOT EXISTS `user_devices` ( 35 | `id` int(11) NOT NULL AUTO_INCREMENT, 36 | `uid` int(11) NOT NULL COMMENT 'The user''s ID', 37 | `token` varchar(15) NOT NULL COMMENT 'A unique token for the user''s device', 38 | `last_access` varchar(20) NOT NULL, 39 | PRIMARY KEY (`id`) 40 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; 41 | -------------------------------------------------------------------------------- /examples/two-step-login/home.php: -------------------------------------------------------------------------------- 1 | updateUser(array( 6 | 'name' => $_POST['newName'], 7 | )); 8 | } 9 | ?> 10 | 11 | 12 | 13 |

Welcome

14 |

You have been successfully logged in.

15 |

16 | Log Out 17 |

18 |

19 | You registered on this website joinedSince(); ?> ago. 20 |

21 |

22 | Here is the full data, the database stores on this user : 23 |

24 | getUser(); 26 | echo '
';
27 |         print_r($details);
28 |         echo '
'; 29 | ?> 30 |

31 | Change the name of your account : 32 |

33 |
34 | 35 | 36 |
37 |

38 | Change Password 39 |

40 |

41 | Manage Devices 42 |

43 | 44 | 45 | -------------------------------------------------------------------------------- /testing/tests/TestDebug.php: -------------------------------------------------------------------------------- 1 | true, 11 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 12 | )); 13 | 14 | self::$log_file = __DIR__ . '/logSys.log'; 15 | } 16 | 17 | public function testLogFileExists() 18 | { 19 | $this->assertEquals(file_exists(self::$log_file), false); 20 | 21 | $config = array( 22 | 'db' => array( 23 | 'type' => $GLOBALS['DB_TYPE'], 24 | 'host' => null, 25 | 'port' => 0, 26 | 'sqlite_path' => '/file_do_not_exist', 27 | ), 28 | 'features' => array( 29 | 'auto_init' => false, 30 | 'run_http' => false, 31 | ), 32 | 'debug' => array( 33 | 'enable' => true, 34 | 'log_file' => self::$log_file, 35 | ), 36 | ); 37 | 38 | new \Fr\LS($config); 39 | 40 | $this->assertEquals(true, file_exists(self::$log_file)); 41 | $this->assertContains('Could not connect to database', file_get_contents(self::$log_file)); 42 | } 43 | 44 | public static function tearDownAfterClass() 45 | { 46 | if (file_exists(self::$log_file)) { 47 | unlink(self::$log_file); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/material-design/home.php: -------------------------------------------------------------------------------- 1 | updateUser(array( 6 | 'name' => $_POST['newName'], 7 | )); 8 | } 9 | ?> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 |
20 |

Welcome

21 |

You have been successfully logged in. You registered on this website joinedSince(); ?> ago.

22 |

23 | Here is the full data that the database stores about this user : 24 |

25 |
getUser();
27 |                  print_r($details);
28 |                  ?>
29 |

30 | Change Password 31 | Manage Devices 32 |

33 |

Profile

34 |

35 | Change the name of your account : 36 |

37 |
38 |
39 | 40 | 41 |
42 |
43 |

44 | See Your Profile 45 |

46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/two-step-login/manage-devices.php: -------------------------------------------------------------------------------- 1 | csrf()) { 4 | if ($LS->revokeDevice($_GET['revoke_device'])) { 5 | $revoked = true; 6 | } else { 7 | $revoked = false; 8 | } 9 | } 10 | ?> 11 | 12 | 13 | Log In With Two Step Verification 14 | 15 | 16 |
17 |

Two Step Log In

18 |

The list shows the devices currently authorized to login using your account

19 | Successfully Revoked Device'; 23 | } else { 24 | echo '

Failed to Revoke Device

'; 25 | } 26 | } 27 | $devices = $LS->getDevices(); 28 | if (count($devices) == 0) { 29 | echo '

No devices are authorized to use your account by skipping 2 Step Verification.

'; 30 | } else { 31 | echo " 32 | 33 | 34 | 35 | 36 | 37 | "; 38 | foreach ($devices as $device) { 39 | echo " 40 | 41 | 42 | 43 | "; 44 | } 45 | echo '
Session IDLast Accessed
{$device['token']}{$device['last_access']}csrf('g') . "'>Revoke Access
'; 46 | } 47 | ?> 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/material-design/manage-devices.php: -------------------------------------------------------------------------------- 1 | csrf()) { 4 | if ($LS->revokeDevice($_GET['revoke_device'])) { 5 | $revoked = true; 6 | } else { 7 | $revoked = false; 8 | } 9 | } 10 | ?> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 |
21 |

Manage Devices

22 |

The list shows the devices currently authorized to login using your account

23 | Successfully Revoked Device'; 27 | } else { 28 | echo '

Failed to Revoke Device

'; 29 | } 30 | } 31 | $devices = $LS->getDevices(); 32 | if (count($devices) == 0) { 33 | echo '

No devices are authorized to use your account by skipping 2 Step Verification.

'; 34 | } else { 35 | echo << 37 | 38 | Device ID 39 | Last Accessed 40 | 41 | 42 | 43 | HTML; 44 | foreach ($devices as $device) { 45 | $lastAccess = date('Y-m-d H:i:s e', $device['last_access']); 46 | 47 | echo << 49 | {$device['token']} 50 | $lastAccess 51 | Revoke Access 52 | 53 | HTML; 54 | } 55 | echo << 57 | 58 | HTML; 59 | } 60 | ?> 61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/two-step-login/config.php: -------------------------------------------------------------------------------- 1 | array( 11 | 'company' => 'My Site', 12 | 'email' => 'emails@mysite.com', 13 | ), 14 | 'db' => array( 15 | 'host' => 'localhost', 16 | 'port' => 3306, 17 | 'username' => 'root', 18 | 'password' => '', 19 | 'name' => 'test', 20 | 'table' => 'users', 21 | ), 22 | 'features' => array( 23 | 'auto_init' => true, 24 | 'two_step_login' => true, 25 | ), 26 | /** 27 | * These are my localhost paths, change it to yours 28 | */ 29 | 'pages' => array( 30 | 'no_login' => array( 31 | '/examples/two-step-login/', 32 | '/examples/two-step-login/reset.php', 33 | '/examples/two-step-login/register.php', 34 | ), 35 | 'everyone' => array( 36 | '/examples/two-step-login/status.php', 37 | ), 38 | 'login_page' => '/examples/two-step-login/login.php', 39 | 'home_page' => '/examples/two-step-login/home.php', 40 | ), 41 | 'two_step_login' => array( 42 | 'instruction' => 'A token was sent to your E-Mail Address. Please see the mail in your inbox and paste the token found in the textbox below :', 43 | 'send_callback' => function (&$LS, $userID, $token) { 44 | $email = $LS->getUser('email', $userID); 45 | $LS->sendMail($email, "Verify Yourself", "Someone tried to login to your account. If it was you, then use the following token to complete logging in :
". $token ."
If it was not you, then ignore this email and please consider to change your account's password."); 46 | }, 47 | ), 48 | ); 49 | 50 | $LS = new \Fr\LS($config); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logSys 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/subins2000/logSys.svg?branch=master)](https://travis-ci.org/subins2000/logSys) 5 | 6 | PHP Advanced Login System as part of the [Francium Project](http://subinsb.com/the-francium-project) 7 | 8 | See this [Blog Post](http://subinsb.com/php-logsys) for complete documentation. 9 | 10 | [Features](http://subinsb.com/php-logsys#Features) 11 | 12 | Installation 13 | ============ 14 | 15 | Use [Composer](http://getcomposer.org) : 16 | 17 | ```bash 18 | composer require francium/logsys 19 | ``` 20 | 21 | Instructions 22 | ============ 23 | 24 | The **[Blog Post](http://subinsb.com/php-logsys)** contains the entire information on how to install and use logSys. 25 | 26 | The following folders contain examples of usage 27 | * example-basic 28 | * example-two-step-login 29 | 30 | PHP's mail() function is used to send emails. Most likely, emails sent through it will reach the SPAM folder. To avoid this, add an email function in `config` -> `basic` -> `email_callback`. 31 | 32 | I recommend to use [PHPMailer](https://github.com/PHPMailer/PHPMailer/) (SMTP) or [Mailgun API](https://mailgun.com) to send emails. 33 | 34 | Versions & Upgrading 35 | ==================== 36 | 37 | See [CHANGELOG](https://github.com/subins2000/logSys/blob/master/CHANGELOG.md) 38 | 39 | Contributing 40 | ============ 41 | 42 | * Follow [PSR standards](http://www.php-fig.org/psr) 43 | * Write or modify unit tests for changes you make (if applicable) 44 | * Run unit tests before pull request. 45 | 46 | ## Security Bugs 47 | 48 | Please report security bugs directly to me via [email](https://subinsb.com/contact). 49 | 50 | ## Testing 51 | 52 | First of all do a `composer update` in the main folder. This will install phpunit. 53 | 54 | Edit the database configuration in the XML files located in `testing` folder and run : 55 | 56 | ``` 57 | vendor/bin/phpunit -c testing/phpunit.mysql.xml && vendor/bin/phpunit -c testing/phpunit.postgresql.xml && vendor/bin/phpunit -c testing/phpunit.sqlite.xml 58 | ``` 59 | -------------------------------------------------------------------------------- /examples/material-design/profile.php: -------------------------------------------------------------------------------- 1 | userIDExists($_GET['user'])) { 5 | $uid = $_GET['user']; 6 | } else if ($LS->isLoggedIn()) { 7 | $uid = $LS->getUser('id'); 8 | } else { 9 | $LS->redirect('index.php'); 10 | } 11 | 12 | $userInfo = $LS->getUser('*', $uid); 13 | ?> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 |
24 | ' . $firstName . ''; 29 | 30 | if ($LS->isLoggedIn() && $uid === $LS->getUser('id')) { 31 | echo '

This is you.

'; 32 | } 33 | ?> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Username@
Full Name
Email Domain
Member For joinedSince($uid); ?>.
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/basic/change.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | Change Password 8 | 9 | 10 |

Passwords Doesn't match

The passwords you entered didn't match. Try again.

"; 19 | } elseif ($LS->login($LS->getUser('username'), $curpass, false, false) == false) { 20 | echo '

Current Password Wrong!

The password you entered for your account is wrong.

'; 21 | } else { 22 | $change_password = $LS->changePassword($new_password); 23 | if ($change_password === true) { 24 | echo '

Password Changed Successfully

'; 25 | } 26 | } 27 | } else { 28 | echo '

Password Fields was blank

Form fields were left blank

'; 29 | } 30 | } 31 | ?> 32 |
33 | 37 | 41 | 45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/two-step-login/change.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | Change Password 8 | 9 | 10 |

Passwords Doesn't match

The passwords you entered didn't match. Try again.

"; 19 | } elseif ($LS->login($LS->getUser('username'), $curpass, false, false) == false) { 20 | echo '

Current Password Wrong!

The password you entered for your account is wrong.

'; 21 | } else { 22 | $change_password = $LS->changePassword($new_password); 23 | if ($change_password === true) { 24 | echo '

Password Changed Successfully

'; 25 | } 26 | } 27 | } else { 28 | echo '

Password Fields was blank

Form fields were left blank

'; 29 | } 30 | } 31 | ?> 32 |
33 | 37 | 41 | 45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/basic/login.php: -------------------------------------------------------------------------------- 1 | login($identification, $password, isset($_POST['remember_me'])); 10 | if ($login === false) { 11 | $msg = array( 'Error', 'Username / Password Wrong !' ); 12 | } elseif (is_array($login) && $login['status'] == 'blocked') { 13 | $msg = array( 'Error', 'Too many login attempts. You can attempt login after ' . $login['minutes'] . ' minutes (' . $login['seconds'] . ' seconds)' ); 14 | } 15 | } 16 | } 17 | ?> 18 | 19 | 20 | Log In 21 | 22 | 23 |
24 |

Log In

25 | {$msg[0]}

{$msg[1]}

"; 28 | } 29 | ?> 30 |
31 |
35 |
39 | 44 |
45 | 46 |
47 | 52 |

53 |

Don't have an account ?

54 | Register 55 |

56 |

57 |

Forgot Your Password ?

58 | Reset Password 59 |

60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /testing/tests/TestUserTwoStepLogin.php: -------------------------------------------------------------------------------- 1 | true, 19 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 20 | )); 21 | 22 | $config = array( 23 | 'db' => array( 24 | 'type' => $GLOBALS['DB_TYPE'], 25 | 'host' => isset($GLOBALS['DB_HOST']) ? $GLOBALS['DB_HOST'] : null, 26 | 'port' => isset($GLOBALS['DB_PORT']) ? $GLOBALS['DB_PORT'] : null, 27 | 'username' => $GLOBALS['DB_USERNAME'], 28 | 'password' => $GLOBALS['DB_PASSWORD'], 29 | 'name' => $GLOBALS['DB_NAME'], 30 | ), 31 | 'features' => array( 32 | 'auto_init' => false, 33 | 'run_http' => false, 34 | ), 35 | 36 | 'two_step_login' => array( 37 | 'send_callback' => function () { 38 | }, 39 | ), 40 | ); 41 | 42 | if ($GLOBALS['DB_TYPE'] === 'sqlite') { 43 | $config['db']['sqlite_path'] = $GLOBALS['DB_SQLITE_PATH']; 44 | } 45 | 46 | $this->LS = new LS($config); 47 | } 48 | 49 | public function testUserLogin() 50 | { 51 | /** 52 | * Login with incorrect password 53 | */ 54 | try { 55 | $this->LS->twoStepLogin('test', 'abc', true, false); 56 | } catch (TwoStepLogin $TSL) { 57 | $this->assertEquals('login_fail', $TSL->getStatus()); 58 | } 59 | 60 | /** 61 | * Login with correct password 62 | */ 63 | try { 64 | $this->LS->twoStepLogin('test', 'xyz', true, false); 65 | } catch (TwoStepLogin $TSL) { 66 | $this->assertEquals('enter_token_form', $TSL->getStatus()); 67 | $this->assertEquals(true, $TSL->getOption('remember_me')); 68 | } 69 | } 70 | 71 | public static function tearDownAfterClass() 72 | { 73 | self::$pdo->exec('DROP TABLE users;DROP TABLE user_devices;DROP TABLE user_tokens;'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /testing/tests/TestDBSetup.php: -------------------------------------------------------------------------------- 1 | pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USERNAME'], $GLOBALS['DB_PASSWORD'], array( 10 | \PDO::ATTR_PERSISTENT => true, 11 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 12 | )); 13 | } catch (\Exception $e) { 14 | $this->fail($e->getMessage()); 15 | } 16 | } 17 | 18 | public function testCreateTables() 19 | { 20 | if ($GLOBALS['DB_TYPE'] === 'sqlite') { 21 | $sql = file_get_contents(__DIR__ . '/../../sql/sqlite.sql'); 22 | $this->pdo->exec($sql); 23 | 24 | $sth = $this->pdo->query("SELECT COUNT(1) FROM `sqlite_master` WHERE type='table' AND `name` LIKE 'users'"); 25 | $this->assertEquals(1, $sth->fetchColumn()); 26 | 27 | $sth = $this->pdo->query("SELECT COUNT(1) FROM `sqlite_master` WHERE type='table' AND `name` LIKE 'user_devices'"); 28 | $this->assertEquals(1, $sth->fetchColumn()); 29 | 30 | $sth = $this->pdo->query("SELECT COUNT(1) FROM `sqlite_master` WHERE type='table' AND `name` LIKE 'user_tokens'"); 31 | $this->assertEquals(1, $sth->fetchColumn()); 32 | } elseif ($GLOBALS['DB_TYPE'] === 'postgresql') { 33 | $sql = file_get_contents(__DIR__ . '/../../sql/postgresql.sql'); 34 | $this->pdo->exec($sql); 35 | 36 | $sth = $this->pdo->query("SELECT * FROM pg_catalog.pg_tables WHERE tablename = 'users'"); 37 | $this->assertEquals(1, $sth->rowCount()); 38 | 39 | $sth = $this->pdo->query("SELECT * FROM pg_catalog.pg_tables WHERE tablename = 'user_devices'"); 40 | $this->assertEquals(1, $sth->rowCount()); 41 | 42 | $sth = $this->pdo->query("SELECT * FROM pg_catalog.pg_tables WHERE tablename = 'user_tokens'"); 43 | $this->assertEquals(1, $sth->rowCount()); 44 | } else { 45 | $sql = file_get_contents(__DIR__ . '/../../sql/mysql.sql'); 46 | $this->pdo->exec($sql); 47 | 48 | $sth = $this->pdo->query("SHOW TABLES LIKE 'users'"); 49 | $this->assertEquals(1, $sth->rowCount()); 50 | 51 | $sth = $this->pdo->query("SHOW TABLES LIKE 'user_devices'"); 52 | $this->assertEquals(1, $sth->rowCount()); 53 | 54 | $sth = $this->pdo->query("SHOW TABLES LIKE 'user_tokens'"); 55 | $this->assertEquals(1, $sth->rowCount()); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/basic/register.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |
9 |

Register

10 |
11 | 14 | 17 | 20 | 23 | 26 | 29 |
30 | Fields Left Blank', '

Some Fields were left blank. Please fill up all fields.

'; 39 | } elseif (! $LS->validEmail($email)) { 40 | echo '

E-Mail Is Not Valid

', '

The E-Mail you gave is not valid

'; 41 | } elseif (! ctype_alnum($username)) { 42 | echo '

Invalid Username

', "

The Username is not valid. Only ALPHANUMERIC characters are allowed and shouldn't exceed 10 characters.

"; 43 | } elseif ($password != $retyped_password) { 44 | echo "

Passwords Don't Match

", "

The Passwords you entered didn't match

"; 45 | } else { 46 | $createAccount = $LS->register($username, $password, 47 | array( 48 | 'email' => $email, 49 | 'name' => $name, 50 | 'created' => date('Y-m-d H:i:s'), // Just for testing 51 | ) 52 | ); 53 | if ($createAccount === 'exists') { 54 | echo ''; 55 | } elseif ($createAccount === true) { 56 | echo ""; 57 | } 58 | } 59 | } 60 | ?> 61 | 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/two-step-login/register.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |
9 |

Register

10 |
11 | 14 | 17 | 20 | 23 | 26 | 29 |
30 | Fields Left Blank', '

Some Fields were left blank. Please fill up all fields.

'; 39 | } elseif (! $LS->validEmail($email)) { 40 | echo '

E-Mail Is Not Valid

', '

The E-Mail you gave is not valid

'; 41 | } elseif (! ctype_alnum($username)) { 42 | echo '

Invalid Username

', "

The Username is not valid. Only ALPHANUMERIC characters are allowed and shouldn't exceed 10 characters.

"; 43 | } elseif ($password != $retyped_password) { 44 | echo "

Passwords Don't Match

", "

The Passwords you entered didn't match

"; 45 | } else { 46 | $createAccount = $LS->register($username, $password, 47 | array( 48 | 'email' => $email, 49 | 'name' => $name, 50 | 'created' => date('Y-m-d H:i:s'), // Just for testing 51 | ) 52 | ); 53 | if ($createAccount === 'exists') { 54 | echo ''; 55 | } elseif ($createAccount === true) { 56 | echo ""; 57 | } 58 | } 59 | } 60 | ?> 61 | 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/material-design/config.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'company' => 'My Site', 14 | 'email' => 'emails@mysite.com', 15 | ), 16 | 17 | 'db' => array( 18 | 'host' => 'localhost', 19 | 'port' => 3306, 20 | 'username' => 'root', 21 | 'password' => '', 22 | 'name' => 'test', 23 | 'table' => 'users', 24 | ), 25 | 26 | 'features' => array( 27 | 'auto_init' => true, 28 | 'two_step_login' => true, 29 | ), 30 | 31 | /** 32 | * These are my localhost paths, change it to yours 33 | */ 34 | 'pages' => array( 35 | 'no_login' => array( 36 | '/examples/material-design/', 37 | '/examples/material-design/reset.php', 38 | '/examples/material-design/register.php', 39 | '/examples/material-design/profile.php', 40 | ), 41 | 'everyone' => array( 42 | '/examples/material-design/', 43 | '/examples/material-design/index.php', 44 | '/examples/material-design/profile.php', 45 | ), 46 | 'login_page' => '/examples/material-design/login.php', 47 | 'home_page' => '/examples/material-design/home.php', 48 | ), 49 | 50 | 'two_step_login' => array( 51 | 'instruction' => 'A token was sent to your E-Mail Address. Please see the mail in your inbox and paste the token found in the textbox below :', 52 | 'send_callback' => function (&$LS, $userID, $token) { 53 | $email = $LS->getUser('email', $userID); 54 | $LS->sendMail($email, 'Verify Yourself', 'Someone tried to login to your account. If it was you, then use the following token to complete logging in :
' . $token . "
If it was not you, then ignore this email and please consider to change your account's password."); 55 | }, 56 | ), 57 | ); 58 | 59 | $LS = new \Fr\LS($config); 60 | 61 | function printHead($title = 'My Site') 62 | { 63 | ?> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | <?php echo $title; ?> 79 | 80 | 81 | 82 | 87 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 |
14 |

Change Password

15 | 'red', 27 | 'text' => 'The passwords you entered didn\'t match.', 28 | ); 29 | } else if ($LS->login($LS->getUser('username'), $curpass, false, false) == false) { 30 | $msg = array( 31 | 'color' => 'red', 32 | 'text' => 'Current password you entered is wrong !', 33 | ); 34 | } else { 35 | $change_password = $LS->changePassword($new_password); 36 | if ($change_password === true) { 37 | $msg = array( 38 | 'color' => 'green', 39 | 'text' => 'Your password was changed successfully.', 40 | ); 41 | } 42 | } 43 | } else { 44 | $msg = array( 45 | 'color' => 'teal', 46 | 'text' => 'Please enter all fields.', 47 | ); 48 | } 49 | } 50 | 51 | if (isset($msg)) { 52 | echo << 54 | {$msg['text']} 55 |
56 | HTML; 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 | -------------------------------------------------------------------------------- /testing/tests/TestUserResetPassword.php: -------------------------------------------------------------------------------- 1 | true, 23 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 24 | )); 25 | 26 | $that = $this; 27 | 28 | $config = array( 29 | 'db' => array( 30 | 'type' => $GLOBALS['DB_TYPE'], 31 | 'host' => isset($GLOBALS['DB_HOST']) ? $GLOBALS['DB_HOST'] : null, 32 | 'port' => isset($GLOBALS['DB_PORT']) ? $GLOBALS['DB_PORT'] : null, 33 | 'username' => $GLOBALS['DB_USERNAME'], 34 | 'password' => $GLOBALS['DB_PASSWORD'], 35 | 'name' => $GLOBALS['DB_NAME'], 36 | ), 37 | 38 | 'features' => array( 39 | 'auto_init' => false, 40 | 'run_http' => false, 41 | ), 42 | 43 | 'basic' => array( 44 | 'email_callback' => function ($LS, $email, $subject, $body) use (&$that) { 45 | $that->emails[] = array( 46 | 'to' => $email, 47 | 'subject' => $subject, 48 | 'body' => $body, 49 | ); 50 | }, 51 | ), 52 | ); 53 | 54 | if ($GLOBALS['DB_TYPE'] === 'sqlite') { 55 | $config['db']['sqlite_path'] = $GLOBALS['DB_SQLITE_PATH']; 56 | } 57 | 58 | $this->LS = new LS($config); 59 | } 60 | 61 | public function sendToken() 62 | { 63 | $that = $this; 64 | $pdo = self::$pdo; 65 | $token_in_db = null; 66 | 67 | setServerArray(); 68 | 69 | $this->LS->sendResetPasswordToken(1, function ($token) use (&$that, &$pdo, &$token_in_db) { 70 | $sth = $pdo->prepare('SELECT token FROM user_tokens WHERE uid = ?'); 71 | $sth->execute(array(1)); 72 | 73 | while ($r = $sth->fetch()) { 74 | if ($r['token'] === $token) { 75 | $foundToken = true; 76 | } 77 | } 78 | 79 | $that->assertEquals(true, isset($foundToken)); 80 | 81 | $token_in_db = $token; 82 | 83 | // email body should only be the token 84 | return $token; 85 | }); 86 | 87 | return $token_in_db; 88 | } 89 | 90 | public function testSendToken() 91 | { 92 | $token = $this->sendToken(); 93 | 94 | $this->assertEquals('test1@test.com', $this->emails[0]['to']); 95 | $this->assertEquals($token, $this->emails[0]['body']); 96 | 97 | $this->assertEquals(true, $this->LS->verifyResetPasswordToken($token)); 98 | } 99 | 100 | public function testRemoveToken() 101 | { 102 | $token = $this->sendToken(); 103 | 104 | $this->assertEquals(true, $this->LS->removeToken($this->emails[0]['body'])); 105 | 106 | $sth = self::$pdo->query('SELECT COUNT(1) FROM user_tokens'); 107 | 108 | $this->assertEquals(1, $sth->fetchColumn()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/material-design/register.php: -------------------------------------------------------------------------------- 1 | 'teal', 14 | 'text' => 'Some fields were left blank. Please fill up all fields.', 15 | ); 16 | } else if (!$LS->validEmail($email)) { 17 | $msg = array( 18 | 'color' => 'red', 19 | 'The Email is not valid', 20 | ); 21 | } else if (!ctype_alnum($username)) { 22 | $msg = array( 23 | 'color' => 'red', 24 | 'text' => "The Username is not valid. Only ALPHANUMERIC characters are allowed and shouldn't exceed 10 characters.", 25 | ); 26 | } else if ($password != $retyped_password) { 27 | $msg = array( 28 | 'color' => 'red', 29 | 'text' => "The Passwords you entered didn't match", 30 | ); 31 | } else { 32 | $createAccount = $LS->register($username, $password, 33 | array( 34 | 'email' => $email, 35 | 'name' => $name, 36 | 'created' => date('Y-m-d H:i:s'), // Just for testing 37 | ) 38 | ); 39 | if ($createAccount === 'exists') { 40 | $msg = array( 41 | 'color' => 'red', 42 | 'text' => 'User Exists', 43 | ); 44 | } else if ($createAccount === true) { 45 | $msg = array( 46 | 'color' => 'green', 47 | 'text' => "Successfully created account. Log In", 48 | ); 49 | } 50 | } 51 | } 52 | ?> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 62 |
63 |

Sign Up

64 | 68 | {$msg['text']} 69 |
70 | HTML; 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 | -------------------------------------------------------------------------------- /examples/material-design/reset.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 12 |
13 |

Reset Password

14 | verifyResetPasswordToken($_GET['resetPassToken'])) { 21 | $msg = array( 22 | 'color' => 'green', 23 | 'text' => << 'red', 30 | 'text' => 'Invalid reset token', 31 | ); 32 | } 33 | } else { 34 | 35 | if (isset($_POST['identification'])) { 36 | 37 | if (!$LS->userExists($_POST['identification'])) { 38 | $msg = array( 39 | 'color' => 'red', 40 | 'text' => 'User does not exist', 41 | ); 42 | } else { 43 | $hide_reset_pass_form = true; 44 | $uid = $LS->login($_POST['identification'], false, false, false); 45 | 46 | $LS->sendResetPasswordToken( 47 | $uid, 48 | function ($encodedToken, $url) { 49 | return << 52 | Reset Password 53 | 54 | Or you may enter this token in the page : 55 |
56 | {urldecode($encodedToken)} 57 |
58 | HTML; 59 | } 60 | ); 61 | 62 | $url = Fr\LS::curPageURL(); 63 | $msg = array( 64 | 'color' => 'black', 65 | 'text' => << 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 | HTML 81 | ); 82 | } 83 | } 84 | 85 | if (isset($msg)) { 86 | echo << 88 | {$msg['text']} 89 |
90 | HTML; 91 | } 92 | 93 | if (!isset($hide_reset_pass_form)) { 94 | ?> 95 |
96 |
97 |
98 | 99 | 100 |
101 |
102 |
103 | 104 |
105 |
106 |
107 |
108 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /testing/tests/TestUserBasic.php: -------------------------------------------------------------------------------- 1 | true, 18 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 19 | )); 20 | 21 | $config = array( 22 | 'db' => array( 23 | 'type' => $GLOBALS['DB_TYPE'], 24 | 'host' => isset($GLOBALS['DB_HOST']) ? $GLOBALS['DB_HOST'] : null, 25 | 'port' => isset($GLOBALS['DB_PORT']) ? $GLOBALS['DB_PORT'] : null, 26 | 'username' => $GLOBALS['DB_USERNAME'], 27 | 'password' => $GLOBALS['DB_PASSWORD'], 28 | 'name' => $GLOBALS['DB_NAME'], 29 | ), 30 | 'features' => array( 31 | 'auto_init' => false, 32 | 'run_http' => false, 33 | ), 34 | ); 35 | 36 | if ($GLOBALS['DB_TYPE'] === 'sqlite') { 37 | $config['db']['sqlite_path'] = $GLOBALS['DB_SQLITE_PATH']; 38 | } 39 | 40 | $this->LS = new LS($config); 41 | } 42 | 43 | public function testUserRegister() 44 | { 45 | $info = array( 46 | 'email' => 'test@test.com', 47 | 'name' => 'ABC', 48 | 'created' => date('Y-m-d H:i:s'), 49 | ); 50 | $this->LS->register('test', 'abc', $info); 51 | 52 | $sth = self::$pdo->query("SELECT * FROM users WHERE id = '1'"); 53 | $r = $sth->fetch(\PDO::FETCH_ASSOC); 54 | 55 | $this->assertEquals('test', $r['username']); 56 | $this->assertEquals($info['email'], $r['email']); 57 | $this->assertEquals($info['name'], $r['name']); 58 | $this->assertEquals($info['created'], $r['created']); 59 | } 60 | 61 | public function testUserExists() 62 | { 63 | $this->assertEquals(true, $this->LS->userExists('test')); 64 | $this->assertEquals(true, $this->LS->userExists('test@test.com')); 65 | 66 | $this->assertEquals(true, $this->LS->userIDExists('1')); 67 | $this->assertEquals(true, $this->LS->userIDExists(1)); 68 | $this->assertEquals(false, $this->LS->userIDExists('0')); 69 | } 70 | 71 | public function testUserLogin() 72 | { 73 | // Login with password 74 | $this->assertNotEquals(false, $this->LS->login('test', 'abc', false, false)); 75 | 76 | // Login without password and get user ID 77 | $this->assertEquals('1', $this->LS->login('test', false, false, false)); 78 | } 79 | 80 | public function testUserInfo() 81 | { 82 | $user = $this->LS->getUser('*', 1); 83 | 84 | $sth = self::$pdo->query("SELECT * FROM users WHERE id = '1'"); 85 | $r = $sth->fetch(\PDO::FETCH_ASSOC); 86 | 87 | $this->assertEquals($r['username'], $user['username']); 88 | $this->assertEquals($r['email'], $user['email']); 89 | $this->assertEquals($r['name'], $user['name']); 90 | $this->assertEquals($r['created'], $user['created']); 91 | } 92 | 93 | public function testUpdateUserInfo() 94 | { 95 | $email = $this->LS->getUser('email', 1); 96 | $this->assertEquals('test@test.com', $email); 97 | 98 | $this->LS->updateUser(array( 99 | 'email' => 'test1@test.com', 100 | ), 1); 101 | 102 | $email = $this->LS->getUser('email', 1); 103 | $this->assertEquals('test1@test.com', $email); 104 | } 105 | 106 | public function testChangePassword() 107 | { 108 | $this->assertNotEquals(false, $this->LS->login('test', 'abc', false, false)); 109 | 110 | $this->LS->changePassword('xyz', 1); 111 | 112 | $this->assertNotEquals(false, $this->LS->login('test', 'xyz', false, false)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [Shamil Kashmeri](https://plus.google.com/u/0/105291845791114608759) asked whether he should upgrade from 0.1 to 0.3 and I thought it would be a good idea for making a changelog instead of explaining the new features everytime someone asks. 4 | 5 | Each version section also has instructions on how to modify your existing logSys calls to work on the new version. 6 | 7 | ## 1.0 Series 8 | 9 | ### 1.0.1 10 | 11 | * Fix #36 12 | * Fix composer.json 13 | * Fix LS.php version code 14 | 15 | ### 1.0 16 | 17 | * Use non blocking sessions 18 | * Fix bugs 19 | 20 | ## 0.9 21 | 22 | * 2 Step Login 23 | * 2 Step Login process is done now with exceptions 24 | * Added `Fr\LS\TwoStepLogin` exception class 25 | * Added try limit for entering token. For this `config` -> `two_step_login` -> `token_tries` has been added. Default : 3 26 | * Revoking the device which is used by user will force a logout 27 | * Requesting tokens more than **5** times will cause account to be blocked 28 | This can be changed with `config` -> `brute_force` -> `max_tokens` 29 | * All result from database is regarded [as strings or null](https://phpdelusions.net/pdo#returntypes) 30 | * Fixed `Fr\LS::joinedSince()` bug 31 | * Added `Fr\LS::getDeviceID()` 32 | * Added `Fr\LS::userIDExists()` 33 | * Added `Fr\LS::removeToken()` 34 | * Follow PSR standards 35 | * Changed `Fr\LS::rand_string()` to `Fr\LS::randStr()` 36 | 37 | ## 0.8 38 | 39 | * logSys is now an object class. Static methods has been replaced 40 | * logSys object is passed by reference as first parameter to all callbacks 41 | 42 | ### Object 43 | 44 | Instead of this : 45 | 46 | ``` 47 | \Fr\LS::config($config); 48 | ``` 49 | 50 | This is the new way : 51 | 52 | ``` 53 | $LS = new \Fr\LS($config); 54 | ``` 55 | 56 | All the functions can be called just like before but on `$LS` object : 57 | 58 | ``` 59 | $LS->login(); 60 | $LS->register(); 61 | $LS->forgotPassword(); 62 | ``` 63 | 64 | ### Callbacks 65 | 66 | The first parameter of all callbacks will be the logSys object passed by reference. So, if you had a callback like this before : 67 | 68 | ``` 69 | "send_callback" => function($userID, $token){ 70 | ... 71 | } 72 | ``` 73 | 74 | You should replace that with this : 75 | 76 | ``` 77 | "send_callback" => function(&$LS, $userID, $token){ 78 | ... 79 | } 80 | ``` 81 | 82 | ## 0.7 83 | 84 | * Added SQLite support 85 | * Fixed bugs 86 | * Added custom message callback 87 | 88 | ## 0.6.2 89 | 90 | * [New! #16](https://github.com/subins2000/logSys/issues/16) 91 | 92 | ## 0.6.1 93 | 94 | * [Fixed bug #15](https://github.com/subins2000/logSys/issues/16) 95 | 96 | ## 0.6 97 | 98 | - Fixed bugs 99 | - Removed SHA256 and instead use Bcrypt 100 | 101 | ## 0.5 102 | 103 | - Two Step Login 104 | - Fixed Bugs 105 | - Manage Devices 106 | - More Examples 107 | - Improved Examples 108 | - `config` -> `info` is now `config` -> `basic` 109 | - Added Email Callback so that developer can change the mechanism of sending email 110 | Previously, developer had to change the contents of \Fr\LS::sendMail() function 111 | Callback can be added in `config` -> `basic` -> `email_callback` 112 | 113 | ## 0.4 114 | 115 | - Updates to existing features 116 | - logSys is now part of the Francium Project 117 | - logSys is a static class and not an object class 118 | - "class.loginsys.php" is now "class.logsys.php" 119 | - \Fr\LS::changePassword() is merely a function and does not anymore prints the form for changing the password 120 | - Configuration is done by \Fr\LS::$config and Default Config in \Fr\LS::$default_config 121 | - \Fr\LS::timeSinceJoin() is now \Fr\LS::joinedSince() 122 | - More detailed comments 123 | - Tidied up parts of the code 124 | - Updates to the examples 125 | - Bugs reported has been fixed 126 | 127 | ## 0.3 128 | 129 | - Updates to existing features 130 | - Added feature for blocking brute force attacks with settings variables 131 | 132 | ## 0.2 133 | 134 | - Updates to existing features 135 | - Remember Me option on login 136 | - Improved $LS->init() 137 | - Added $LS->getUser() for easy retrieval of user information 138 | - Added $LS->updateUser() to easily update user information 139 | - Added functionality for getting time since the user joined 140 | - Added $LS->sendMail() for easily updating the preferred way of sending mails 141 | 142 | ## 0.1 143 | 144 | - Login, Register and basic functions of a login system 145 | - Forgot Password feature 146 | - $LS->init() 147 | -------------------------------------------------------------------------------- /examples/two-step-login/login.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Log In With Two Step Verification 7 | 8 | 9 |
10 |

Two Step Log In

11 |

This demo shows how logSys can be used to implement two step verification.

12 | twoStepLogin($_POST['login'], $_POST['password'], isset($_POST['remember_me'])); 20 | } else { 21 | /** 22 | * Handle 2 Step Login 23 | */ 24 | $LS->twoStepLogin(); 25 | } 26 | } catch (Fr\LS\TwoStepLogin $TSL) { 27 | if ($TSL->getStatus() === 'login_fail') { 28 | echo '

Error

Username / Password Wrong !

'; 29 | } elseif ($TSL->getStatus() === 'blocked') { 30 | $blockInfo = $TSL->getBlockInfo(); 31 | echo '

Error

Too many login attempts. You can attempt login after ' . $blockInfo['minutes'] . ' minutes (' . $blockInfo['seconds'] . ' seconds)

'; 32 | } elseif ($TSL->getStatus() === 'enter_token_form' || $TSL->getStatus() === 'invalid_token') { 33 | $two_step_login_form_display = true; 34 | 35 | if ($TSL->getStatus() === 'invalid_token') { 36 | echo '

Wrong token. You have ' . $TSL->getOption('tries_left') . ' tries left

'; 37 | } ?> 38 |
39 |

A token was sent to your E-Mail address. Paste the token in the box below :

40 |

43 |

47 | ' /> 48 | getOption('remember_me')) { 50 | ?> 51 | 52 | csrf('i'); ?> 56 | 60 |
61 | getStatus() === 'login_success') { 64 | // Nothing to do. Auto Init will do the redirect if it's enabled 65 | } elseif ($TSL->isError()) { 66 | echo '

Error

' . $TSL->getStatus() . '

'; 67 | } 68 | } 69 | if (! $two_step_login_form_display) { 70 | ?> 71 |
72 |
76 |
80 | 85 |
86 | 87 |
88 | 92 | 97 |

98 |

Don't have an account ?

99 | Register 100 |

101 |

102 |

Forgot Your Password ?

103 | Reset Password 104 |

105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /examples/material-design/login.php: -------------------------------------------------------------------------------- 1 | 'teal', 14 | 'text' => 'Please enter username and password', 15 | ); 16 | } else { 17 | $login = $LS->twoStepLogin($identification, $password, isset($_POST['remember_me'])); 18 | } 19 | } else { 20 | $LS->twoStepLogin(); 21 | } 22 | } catch (Fr\LS\TwoStepLogin $TSL) { 23 | if ($TSL->getStatus() === 'login_fail') { 24 | $msg = array( 25 | 'color' => 'red', 26 | 'text' => 'Username / Password Wrong !', 27 | ); 28 | } else if ($TSL->getStatus() === 'blocked') { 29 | 30 | $blockInfo = $TSL->getBlockInfo(); 31 | 32 | $msg = array( 33 | 'color' => 'red', 34 | 'text' => 'Too many login/token attempts. You can attempt login after ' . $blockInfo['minutes'] . ' minutes (' . $blockInfo['seconds'] . ' seconds)', 35 | ); 36 | 37 | } else if ($TSL->getStatus() === 'enter_token_form' || $TSL->getStatus() === 'invalid_token') { 38 | $two_step_login_enter_token_form = true; 39 | $remember_me = $TSL->getOption('remember_me'); 40 | 41 | if ($TSL->getStatus() === 'invalid_token') { 42 | $msg = array( 43 | 'color' => 'red', 44 | 'Wrong token. You have ' . $TSL->getOption('tries_left') . ' tries left', 45 | ); 46 | } 47 | 48 | } else if ($TSL->getStatus() === 'login_success') { 49 | // Nothing to do. Auto Init will do the redirect if it's enabled 50 | } else if ($TSL->isError()) { 51 | echo '

Error

' . $TSL->getStatus() . '

'; 52 | } 53 | } 54 | 55 | if (isset($_POST['ajax'])) { 56 | 57 | } 58 | ?> 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 |
69 |

Sign In

70 | 74 | {$msg['text']} 75 |
76 | HTML; 77 | } 78 | if (isset($two_step_login_enter_token_form)) { 79 | ?> 80 |
81 |

A token was sent to your E-Mail address. Paste the token in the box below :

82 | 85 |
86 |
87 | 88 | 89 |
90 |
91 | ' /> 92 | 95 | 96 | csrf('i'); 99 | ?> 100 |
101 |
102 | 103 | Resend Token 104 |
105 |
106 |
107 | 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 | Don't have an account ? Register 135 |

136 |

137 | Forgot Your Password ? Reset Password 138 |

139 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /examples/material-design/js/jquery.router.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | plugin name: router 4 | jquery plugin to handle routes with both hash and push state 5 | why? why another routing plugin? because i couldnt find one that handles both hash and pushstate 6 | created by 24hr // camilo.tapia 7 | author twitter: camilo.tapia 8 | 9 | Copyright 2011 camilo tapia // 24hr (email : camilo.tapia@gmail.com) 10 | 11 | This program is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License, version 2, as 13 | published by the Free Software Foundation. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | 24 | */ 25 | 26 | 27 | (function($) { 28 | 29 | var hasPushState = (history && history.pushState); 30 | var hasHashState = !hasPushState && ("onhashchange" in window) && false; 31 | var router = {}; 32 | var routeList = []; 33 | var eventAdded = false; 34 | var currentUsedUrl = location.href; //used for ie to hold the current url 35 | var firstRoute = true; 36 | var errorCallback = function() {}; 37 | 38 | // hold the latest route that was activated 39 | router.currentId = ""; 40 | router.currentParameters = {}; 41 | 42 | // Create a default error handler 43 | router.errorCallback = errorCallback; 44 | 45 | router.capabilities = { 46 | hash: hasHashState, 47 | pushState: hasPushState, 48 | timer: !hasHashState && !hasPushState 49 | }; 50 | 51 | // reset all routes 52 | router.reset = function() { 53 | var router = {}; 54 | var routeList = []; 55 | router.currentId = ""; 56 | router.currentParameters = {}; 57 | } 58 | 59 | router.add = function(route, id, callback) { 60 | // if we only get a route and a callback, we switch the arguments 61 | if (typeof id == "function") { 62 | callback = id; 63 | delete id; 64 | } 65 | 66 | var isRegExp = typeof route == "object"; 67 | 68 | if (!isRegExp) { 69 | 70 | // remove the last slash to unifiy all routes 71 | if (route.lastIndexOf("/") == route.length - 1) { 72 | route = route.substring(0, route.length - 1); 73 | } 74 | 75 | // if the routes where created with an absolute url ,we have to remove the absolut part anyway, since we cant change that much 76 | route = route.replace(location.protocol + "//", "").replace(location.hostname, ""); 77 | } 78 | 79 | var routeItem = { 80 | route: route, 81 | callback: callback, 82 | type: isRegExp ? "regexp" : "string", 83 | id: id 84 | } 85 | 86 | routeList.push(routeItem); 87 | 88 | // we add the event listener after the first route is added so that we dont need to listen to events in vain 89 | if (!eventAdded) { 90 | bindStateEvents(); 91 | } 92 | }; 93 | 94 | router.addErrorHandler = function(callback) { 95 | this.errorCallback = callback; 96 | }; 97 | 98 | function bindStateEvents() { 99 | eventAdded = true; 100 | 101 | // default value telling router that we havent replaced the url from a hash. yet. 102 | router.fromHash = false; 103 | 104 | 105 | if (hasPushState) { 106 | // if we get a request with a qualified hash (ie it begins with #!) 107 | if (location.hash.indexOf("#!/") === 0) { 108 | // replace the state 109 | var url = location.pathname + location.hash.replace(/^#!\//gi, ""); 110 | history.replaceState({}, "", url); 111 | 112 | // this flag tells router that the url was converted from hash to popstate 113 | router.fromHash = true; 114 | } 115 | 116 | $(window).bind("popstate", handleRoutes); 117 | } else if (hasHashState) { 118 | $(window).bind("hashchange.router", handleRoutes); 119 | } else { 120 | // if no events are available we use a timer to check periodically for changes in the url 121 | setInterval( 122 | function() { 123 | if (location.href != currentUsedUrl) { 124 | handleRoutes(); 125 | currentUsedUrl = location.href; 126 | } 127 | }, 500 128 | ); 129 | } 130 | 131 | } 132 | 133 | bindStateEvents(); 134 | 135 | router.go = function(url, title) { 136 | if (hasPushState) { 137 | history.pushState({}, title, url); 138 | checkRoutes(); 139 | } else { 140 | // remove part of url that we dont use 141 | url = url.replace(location.protocol + "//", "").replace(location.hostname, ""); 142 | var hash = url.replace(location.pathname, ""); 143 | 144 | if (hash.indexOf("!") < 0) { 145 | hash = "!/" + hash; 146 | } 147 | location.hash = hash; 148 | } 149 | }; 150 | 151 | // do a check without affecting the history 152 | router.check = router.redo = function() { 153 | checkRoutes(true); 154 | }; 155 | 156 | // parse and wash the url to process 157 | function parseUrl(url) { 158 | var currentUrl = url ? url : location.pathname; 159 | 160 | currentUrl = decodeURI(currentUrl); 161 | 162 | // if no pushstate is availabe we have to use the hash 163 | if (!hasPushState) { 164 | if (location.hash.indexOf("#!/") === 0) { 165 | currentUrl += location.hash.substring(3); 166 | } else { 167 | return ''; 168 | } 169 | } 170 | 171 | // and if the last character is a slash, we just remove it 172 | currentUrl = currentUrl.replace(/\/$/, ""); 173 | 174 | return currentUrl; 175 | } 176 | 177 | // get the current parameters for either a specified url or the current one if parameters is ommited 178 | router.parameters = function(url) { 179 | // parse the url so that we handle a unified url 180 | var currentUrl = parseUrl(url); 181 | 182 | // get the list of actions for the current url 183 | var list = getParameters(currentUrl); 184 | 185 | // if the list is empty, return an empty object 186 | if (list.length == 0) { 187 | router.currentParameters = {}; 188 | } 189 | 190 | // if we got results, return the first one. at least for now 191 | else { 192 | router.currentParameters = list[0].data; 193 | } 194 | 195 | return router.currentParameters; 196 | } 197 | 198 | function getParameters(url) { 199 | 200 | var dataList = []; 201 | 202 | // console.log("ROUTES:"); 203 | 204 | for (var i = 0, ii = routeList.length; i < ii; i++) { 205 | var route = routeList[i]; 206 | 207 | // check for mathing reg exp 208 | if (route.type == "regexp") { 209 | var result = url.match(route.route); 210 | if (result) { 211 | var data = {}; 212 | data.matches = result; 213 | 214 | dataList.push({ 215 | route: route, 216 | data: data 217 | }); 218 | 219 | // saves the current route id 220 | router.currentId = route.id; 221 | 222 | // break after first hit 223 | break; 224 | } 225 | } 226 | 227 | // check for mathing string routes 228 | else { 229 | var currentUrlParts = url.split("/"); 230 | var routeParts = route.route.split("/"); 231 | 232 | //console.log("matchCounter ", matchCounter, url, route.route) 233 | 234 | // first check so that they have the same amount of elements at least 235 | if (routeParts.length == currentUrlParts.length) { 236 | var data = {}; 237 | var matched = true; 238 | var matchCounter = 0; 239 | 240 | for (var j = 0, jj = routeParts.length; j < jj; j++) { 241 | var isParam = routeParts[j].indexOf(":") === 0; 242 | if (isParam) { 243 | data[routeParts[j].substring(1)] = decodeURI(currentUrlParts[j]); 244 | matchCounter++; 245 | } else { 246 | if (routeParts[j] == currentUrlParts[j]) { 247 | matchCounter++; 248 | } 249 | } 250 | } 251 | 252 | // break after first hit 253 | if (routeParts.length == matchCounter) { 254 | dataList.push({ 255 | route: route, 256 | data: data 257 | }); 258 | 259 | // saved the current route id 260 | router.currentId = route.id; 261 | router.currentParameters = data; 262 | 263 | break; 264 | } 265 | 266 | } 267 | } 268 | 269 | } 270 | 271 | return dataList; 272 | } 273 | 274 | function checkRoutes() { 275 | var currentUrl = parseUrl(location.pathname); 276 | 277 | // check if something is catched 278 | var actionList = getParameters(currentUrl); 279 | 280 | // If no routes have been matched 281 | if (actionList.length == 0) { 282 | // Invoke error handler 283 | return router.errorCallback(currentUrl); 284 | } 285 | 286 | // ietrate trough result (but it will only kick in one) 287 | for (var i = 0, ii = actionList.length; i < ii; i++) { 288 | actionList[i].route.callback(actionList[i].data); 289 | } 290 | } 291 | 292 | 293 | function handleRoutes(e) { 294 | if (e != null && e.originalEvent && e.originalEvent.state !== undefined) { 295 | checkRoutes(); 296 | } else if (hasHashState) { 297 | checkRoutes(); 298 | } else if (!hasHashState && !hasPushState) { 299 | checkRoutes(); 300 | } 301 | } 302 | 303 | 304 | 305 | if (!$.router) { 306 | $.router = router; 307 | } else { 308 | if (window.console && window.console.warn) { 309 | console.warn("jQuery.status already defined. Something is using the same name."); 310 | } 311 | } 312 | 313 | })(jQuery); 314 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | .---------------------------------------------------------------------------. 2 | | The Francium Project | 3 | | http://subinsb.com/the-francium-project | 4 | | ------------------------------------------------------------------------- | 5 | | This software logSys uses the Apache License | 6 | '---------------------------------------------------------------------------' 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | License Copy (In case of no internet) 21 | ------------------------------------- 22 | 23 | Apache License 24 | Version 2.0, January 2004 25 | http://www.apache.org/licenses/ 26 | 27 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 28 | 29 | 1. Definitions. 30 | 31 | "License" shall mean the terms and conditions for use, reproduction, 32 | and distribution as defined by Sections 1 through 9 of this document. 33 | 34 | "Licensor" shall mean the copyright owner or entity authorized by 35 | the copyright owner that is granting the License. 36 | 37 | "Legal Entity" shall mean the union of the acting entity and all 38 | other entities that control, are controlled by, or are under common 39 | control with that entity. For the purposes of this definition, 40 | "control" means (i) the power, direct or indirect, to cause the 41 | direction or management of such entity, whether by contract or 42 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 43 | outstanding shares, or (iii) beneficial ownership of such entity. 44 | 45 | "You" (or "Your") shall mean an individual or Legal Entity 46 | exercising permissions granted by this License. 47 | 48 | "Source" form shall mean the preferred form for making modifications, 49 | including but not limited to software source code, documentation 50 | source, and configuration files. 51 | 52 | "Object" form shall mean any form resulting from mechanical 53 | transformation or translation of a Source form, including but 54 | not limited to compiled object code, generated documentation, 55 | and conversions to other media types. 56 | 57 | "Work" shall mean the work of authorship, whether in Source or 58 | Object form, made available under the License, as indicated by a 59 | copyright notice that is included in or attached to the work 60 | (an example is provided in the Appendix below). 61 | 62 | "Derivative Works" shall mean any work, whether in Source or Object 63 | form, that is based on (or derived from) the Work and for which the 64 | editorial revisions, annotations, elaborations, or other modifications 65 | represent, as a whole, an original work of authorship. For the purposes 66 | of this License, Derivative Works shall not include works that remain 67 | separable from, or merely link (or bind by name) to the interfaces of, 68 | the Work and Derivative Works thereof. 69 | 70 | "Contribution" shall mean any work of authorship, including 71 | the original version of the Work and any modifications or additions 72 | to that Work or Derivative Works thereof, that is intentionally 73 | submitted to Licensor for inclusion in the Work by the copyright owner 74 | or by an individual or Legal Entity authorized to submit on behalf of 75 | the copyright owner. For the purposes of this definition, "submitted" 76 | means any form of electronic, verbal, or written communication sent 77 | to the Licensor or its representatives, including but not limited to 78 | communication on electronic mailing lists, source code control systems, 79 | and issue tracking systems that are managed by, or on behalf of, the 80 | Licensor for the purpose of discussing and improving the Work, but 81 | excluding communication that is conspicuously marked or otherwise 82 | designated in writing by the copyright owner as "Not a Contribution." 83 | 84 | "Contributor" shall mean Licensor and any individual or Legal Entity 85 | on behalf of whom a Contribution has been received by Licensor and 86 | subsequently incorporated within the Work. 87 | 88 | 2. Grant of Copyright License. Subject to the terms and conditions of 89 | this License, each Contributor hereby grants to You a perpetual, 90 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 91 | copyright license to reproduce, prepare Derivative Works of, 92 | publicly display, publicly perform, sublicense, and distribute the 93 | Work and such Derivative Works in Source or Object form. 94 | 95 | 3. Grant of Patent License. Subject to the terms and conditions of 96 | this License, each Contributor hereby grants to You a perpetual, 97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 98 | (except as stated in this section) patent license to make, have made, 99 | use, offer to sell, sell, import, and otherwise transfer the Work, 100 | where such license applies only to those patent claims licensable 101 | by such Contributor that are necessarily infringed by their 102 | Contribution(s) alone or by combination of their Contribution(s) 103 | with the Work to which such Contribution(s) was submitted. If You 104 | institute patent litigation against any entity (including a 105 | cross-claim or counterclaim in a lawsuit) alleging that the Work 106 | or a Contribution incorporated within the Work constitutes direct 107 | or contributory patent infringement, then any patent licenses 108 | granted to You under this License for that Work shall terminate 109 | as of the date such litigation is filed. 110 | 111 | 4. Redistribution. You may reproduce and distribute copies of the 112 | Work or Derivative Works thereof in any medium, with or without 113 | modifications, and in Source or Object form, provided that You 114 | meet the following conditions: 115 | 116 | (a) You must give any other recipients of the Work or 117 | Derivative Works a copy of this License; and 118 | 119 | (b) You must cause any modified files to carry prominent notices 120 | stating that You changed the files; and 121 | 122 | (c) You must retain, in the Source form of any Derivative Works 123 | that You distribute, all copyright, patent, trademark, and 124 | attribution notices from the Source form of the Work, 125 | excluding those notices that do not pertain to any part of 126 | the Derivative Works; and 127 | 128 | (d) If the Work includes a "NOTICE" text file as part of its 129 | distribution, then any Derivative Works that You distribute must 130 | include a readable copy of the attribution notices contained 131 | within such NOTICE file, excluding those notices that do not 132 | pertain to any part of the Derivative Works, in at least one 133 | of the following places: within a NOTICE text file distributed 134 | as part of the Derivative Works; within the Source form or 135 | documentation, if provided along with the Derivative Works; or, 136 | within a display generated by the Derivative Works, if and 137 | wherever such third-party notices normally appear. The contents 138 | of the NOTICE file are for informational purposes only and 139 | do not modify the License. You may add Your own attribution 140 | notices within Derivative Works that You distribute, alongside 141 | or as an addendum to the NOTICE text from the Work, provided 142 | that such additional attribution notices cannot be construed 143 | as modifying the License. 144 | 145 | You may add Your own copyright statement to Your modifications and 146 | may provide additional or different license terms and conditions 147 | for use, reproduction, or distribution of Your modifications, or 148 | for any such Derivative Works as a whole, provided Your use, 149 | reproduction, and distribution of the Work otherwise complies with 150 | the conditions stated in this License. 151 | 152 | 5. Submission of Contributions. Unless You explicitly state otherwise, 153 | any Contribution intentionally submitted for inclusion in the Work 154 | by You to the Licensor shall be under the terms and conditions of 155 | this License, without any additional terms or conditions. 156 | Notwithstanding the above, nothing herein shall supersede or modify 157 | the terms of any separate license agreement you may have executed 158 | with Licensor regarding such Contributions. 159 | 160 | 6. Trademarks. This License does not grant permission to use the trade 161 | names, trademarks, service marks, or product names of the Licensor, 162 | except as required for reasonable and customary use in describing the 163 | origin of the Work and reproducing the content of the NOTICE file. 164 | 165 | 7. Disclaimer of Warranty. Unless required by applicable law or 166 | agreed to in writing, Licensor provides the Work (and each 167 | Contributor provides its Contributions) on an "AS IS" BASIS, 168 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 169 | implied, including, without limitation, any warranties or conditions 170 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 171 | PARTICULAR PURPOSE. You are solely responsible for determining the 172 | appropriateness of using or redistributing the Work and assume any 173 | risks associated with Your exercise of permissions under this License. 174 | 175 | 8. Limitation of Liability. In no event and under no legal theory, 176 | whether in tort (including negligence), contract, or otherwise, 177 | unless required by applicable law (such as deliberate and grossly 178 | negligent acts) or agreed to in writing, shall any Contributor be 179 | liable to You for damages, including any direct, indirect, special, 180 | incidental, or consequential damages of any character arising as a 181 | result of this License or out of the use or inability to use the 182 | Work (including but not limited to damages for loss of goodwill, 183 | work stoppage, computer failure or malfunction, or any and all 184 | other commercial damages or losses), even if such Contributor 185 | has been advised of the possibility of such damages. 186 | 187 | 9. Accepting Warranty or Additional Liability. While redistributing 188 | the Work or Derivative Works thereof, You may choose to offer, 189 | and charge a fee for, acceptance of support, warranty, indemnity, 190 | or other liability obligations and/or rights consistent with this 191 | License. However, in accepting such obligations, You may act only 192 | on Your own behalf and on Your sole responsibility, not on behalf 193 | of any other Contributor, and only if You agree to indemnify, 194 | defend, and hold each Contributor harmless for any liability 195 | incurred by, or claims asserted against, such Contributor by reason 196 | of your accepting any such warranty or additional liability. 197 | 198 | END OF TERMS AND CONDITIONS 199 | 200 | APPENDIX: How to apply the Apache License to your work. 201 | 202 | To apply the Apache License to your work, attach the following 203 | boilerplate notice, with the fields enclosed by brackets "[]" 204 | replaced with your own identifying information. (Don't include 205 | the brackets!) The text should be enclosed in the appropriate 206 | comment syntax for the file format. We also recommend that a 207 | file or class name and description of purpose be included on the 208 | same "printed page" as the copyright notice for easier 209 | identification within third-party archives. 210 | 211 | Copyright [yyyy] [name of copyright owner] 212 | 213 | Licensed under the Apache License, Version 2.0 (the "License"); 214 | you may not use this file except in compliance with the License. 215 | You may obtain a copy of the License at 216 | 217 | http://www.apache.org/licenses/LICENSE-2.0 218 | 219 | Unless required by applicable law or agreed to in writing, software 220 | distributed under the License is distributed on an "AS IS" BASIS, 221 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 222 | See the License for the specific language governing permissions and 223 | limitations under the License. 224 | -------------------------------------------------------------------------------- /src/Fr/LS.php: -------------------------------------------------------------------------------- 1 | array( 46 | 'company' => 'My Site', 47 | 'email' => 'email@mysite.com', 48 | 'email_callback' => false, 49 | 50 | /** 51 | * Callback to override output content 52 | */ 53 | 'output_callback' => false, 54 | ), 55 | 56 | /** 57 | * Database Configuration 58 | */ 59 | 'db' => array( 60 | /** 61 | * @var string 'mysql' or 'postgresql' or 'sqlite' 62 | */ 63 | 'type' => 'mysql', 64 | 65 | /** 66 | * MySQL/PostgreSQL options 67 | */ 68 | 'host' => '', 69 | 'port' => '3306', 70 | 'username' => '', 71 | 'password' => '', 72 | 73 | /** 74 | * SQLite options 75 | */ 76 | 'sqlite_path' => '', 77 | 78 | 'name' => '', 79 | 'table' => 'users', 80 | 'token_table' => 'user_tokens', 81 | 82 | 'columns' => array( 83 | 'id' => 'id', 84 | 'username' => 'username', 85 | 'password' => 'password', 86 | 'email' => 'email', 87 | 'attempt' => 'attempt', 88 | 'created' => 'created', 89 | ), 90 | ), 91 | 92 | /** 93 | * Keys used for encryption 94 | * DONT MAKE THIS PUBLIC 95 | */ 96 | 'keys' => array( 97 | /** 98 | * Changing cookie key will expire all current active login sessions 99 | */ 100 | 'cookie' => 'ckxc436jd*^30f840v*9!@#$', 101 | /** 102 | * `salt` should not be changed after users are created 103 | */ 104 | 'salt' => '^#$4%9f+1^p9)M@4M)V$', 105 | ), 106 | 107 | /** 108 | * Enable/Disable certain features 109 | */ 110 | 'features' => array( 111 | 112 | /** 113 | * Enable/Disable Login using Username & E-Mail 114 | */ 115 | 'email_login' => true, 116 | 117 | /** 118 | * Enable/Disable `Remember Me` feature 119 | */ 120 | 'remember_me' => true, 121 | 122 | /** 123 | * Should HTTP related functions should be ran. 124 | * This includes cookie, session, URL handling. 125 | * Useful when logSys is not used in a web app 126 | */ 127 | 'run_http' => true, 128 | 129 | /** 130 | * Should \Fr\LS::init() be called automatically 131 | */ 132 | 'auto_init' => false, 133 | 134 | /** 135 | * Prevent Brute Forcing 136 | * --------------------- 137 | * By enabling this, logSys will deny login for the time mentioned 138 | * in the 'brute_force'->'time_limit' seconds after 'brute_force'->'tries' 139 | * number of incorrect login tries. 140 | */ 141 | 'block_brute_force' => true, 142 | 143 | /** 144 | * Two Step Login 145 | * -------------- 146 | * By enabling this, a checking is done when user visits 147 | * whether the device he/she uses is approved by the user. 148 | * Allows the original user to revoke logins in other devices/places 149 | * Useful if the user forgot to logout in some place. 150 | */ 151 | 'two_step_login' => false, 152 | 153 | ), 154 | 155 | /** 156 | * `Blocking Brute Force Attacks` options 157 | */ 158 | 'brute_force' => array( 159 | /** 160 | * No of tries alloted to each user 161 | */ 162 | 'tries' => 5, 163 | /** 164 | * The time IN SECONDS for which block from login action should be done after 165 | * incorrect login attempts. Use http://www.easysurf.cc/utime.htm#m60s 166 | * for converting minutes to seconds. Default : 5 minutes 167 | */ 168 | 'time_limit' => 300, 169 | 170 | /** 171 | * Maximum bumber of tokens that can be generated 172 | */ 173 | 'max_tokens' => 5, 174 | ), 175 | 176 | /** 177 | * Information about pages 178 | */ 179 | 'pages' => array( 180 | /** 181 | * Pages that doesn't require logging in. 182 | * Exclude login page, but include REGISTER page. 183 | * Use RELATIVE links. To find the relative link of 184 | * a page, do var_dump( Fr\LS::curPage() ); 185 | */ 186 | 'no_login' => array(), 187 | 188 | /** 189 | * Pages that both logged in and not logged in users can access 190 | */ 191 | 'everyone' => array(), 192 | 193 | /** 194 | * The login page. ex : /login.php or /accounts/login.php 195 | */ 196 | 'login_page' => '', 197 | 198 | /** 199 | * The home page. The main page for logged in users. 200 | * logSys redirects to here after user logs in 201 | */ 202 | 'home_page' => '', 203 | ), 204 | 205 | /** 206 | * Settings about cookie creation 207 | */ 208 | 'cookies' => array( 209 | /** 210 | * Default : cookies expire in 30 days. The value is 211 | * for setting in strtotime() function 212 | * http://php.net/manual/en/function.strtotime.php 213 | */ 214 | 'expire' => '+30 days', 215 | 'path' => '/', 216 | 'domain' => '', 217 | 218 | /** 219 | * Names of cookies created 220 | */ 221 | 'names' => array( 222 | 'login_token' => 'lg', 223 | 'remember_me' => 'rm', 224 | 'device' => 'dv', 225 | 'session' => 'a', 226 | ), 227 | ), 228 | 229 | /** 230 | * 2 Step Login 231 | */ 232 | 'two_step_login' => array( 233 | /** 234 | * Message to show before displaying 'Enter Token' form. 235 | */ 236 | 'instruction' => '', 237 | 238 | /** 239 | * Callback when token is generated. 240 | * Used to send message to user ( Phone/E-Mail ) 241 | */ 242 | 'send_callback' => '', 243 | 244 | /** 245 | * The table to stoe user's sessions 246 | */ 247 | 'devices_table' => 'user_devices', 248 | 249 | /** 250 | * The length of token generated. 251 | * A low value is better for tokens sent via Mobile SMS 252 | */ 253 | 'token_length' => 4, 254 | 255 | /** 256 | * Maximum number of tries the user can make for entering token 257 | */ 258 | 'token_tries' => 3, 259 | 260 | /** 261 | * Whether the token should be numeric only ? 262 | * Default Token : Alphabetic + Numeric mixed strings 263 | */ 264 | 'numeric' => false, 265 | 266 | /** 267 | * The expire time of cookie that authorizes the device 268 | * to login using the user's account with 2 Step Verification 269 | * The value is for setting in strtotime() function 270 | * http://php.net/manual/en/function.strtotime.php 271 | */ 272 | 'expire' => '+45 days', 273 | 274 | /** 275 | * Should logSys checks if device is valid, everytime 276 | * logSys is initiated ie everytime a page loads 277 | * If you want to check only the first time a user loads 278 | * a page, then set the value to TRUE, else FALSE 279 | */ 280 | 'first_check_only' => true, 281 | ), 282 | 283 | /** 284 | * Debug info 285 | */ 286 | 'debug' => array( 287 | /** 288 | * Enable debugging 289 | */ 290 | 'enable' => false, 291 | 292 | /** 293 | * Absolute path 294 | */ 295 | 'log_file' => '', 296 | ), 297 | ); 298 | 299 | /** 300 | * @var array 301 | */ 302 | protected $config = array(); 303 | 304 | /** 305 | * Config 306 | * @param array $config [description] 307 | * @return [type] [description] 308 | */ 309 | public function config($config = array()) 310 | { 311 | /** 312 | * Callback to display messages for different states 313 | * @var array 314 | */ 315 | self::$default_config['basic']['output_callback'] = function (&$LS, $state, $extraInfo = array()) { 316 | if ($state === 'invalidToken') { 317 | return '

Error : Wrong/Invalid Token

'; 318 | } elseif ($state === 'fieldsLeftBlank') { 319 | return '

Error : Fields Left Blank

'; 320 | } elseif ($state === 'passwordDontMatch') { 321 | return '

Error : Passwords Don\'t Match

'; 322 | } elseif ($state === 'passwordChanged') { 323 | return '

Success : Password Reset Successful

You may now login with your new password.

'; 324 | } elseif ($state === 'identityNotProvided') { 325 | return '

Error : ' . $extraInfo['identity_type'] . ' not provided

'; 326 | } elseif ($state === 'userNotFound') { 327 | return '

Error : User Not Found

'; 328 | } elseif ($state === 'notLoggedIn') { 329 | return '

Error : Not Logged In

'; 330 | } elseif ($state === 'resetPasswordRequestForm') { 331 | $curPageURL = self::curPageURL(); 332 | return << 334 | 338 |

339 | 340 | HTML; 341 | } elseif ($state === 'resetPasswordForm') { 342 | $curPageURL = self::curPageURL(); 343 | return <<The Token key was Authorized. Now, you can change the password

345 |
346 | 347 |
351 |
355 |

356 |
357 | HTML; 358 | } 359 | }; 360 | 361 | $this->config = array_replace_recursive(self::$default_config, $config); 362 | 363 | /** 364 | * Add the login page to the array of pages that doesn't need logging in 365 | */ 366 | array_push($this->config['pages']['no_login'], $this->config['pages']['login_page']); 367 | 368 | if ($this->config['debug']['enable']) { 369 | ini_set('display_errors', 'on'); 370 | } 371 | } 372 | 373 | /** 374 | * Add messages to log file 375 | * 376 | * @param string $msg Message 377 | * @return boolean Whether message was written 378 | */ 379 | public function log($msg = '') 380 | { 381 | if ($this->config['debug']['enable']) { 382 | $log_file = $this->config['debug']['log_file']; 383 | 384 | if ($log_file === '') { 385 | $log_file = __DIR__ . '/Francium.log'; 386 | } 387 | 388 | if ($msg !== '') { 389 | $message = '[' . date('Y-m-d H:i:s') . '] ' . $msg; 390 | $fh = fopen($log_file, 'a'); 391 | fwrite($fh, $message . '\n'); 392 | fclose($fh); 393 | 394 | return true; 395 | } 396 | } 397 | 398 | return false; 399 | } 400 | 401 | /** 402 | * @var boolean Is user logged in 403 | */ 404 | public $loggedIn = false; 405 | 406 | /** 407 | * @var int|boolean User ID 408 | */ 409 | public $userID = false; 410 | 411 | /** 412 | * @var PDO Database handler 413 | */ 414 | protected $dbh; 415 | 416 | /** 417 | * @var Session Session Manager 418 | */ 419 | protected $session = false; 420 | 421 | /** 422 | * @var boolean Whether Fr\LS::init() was called 423 | */ 424 | protected $initCalled = false; 425 | 426 | /** 427 | * Intialize 428 | * @param array $config Configuration 429 | * @param boolean|SessionInstance $session Use a session 430 | */ 431 | public function __construct($config = array(), $session = false) 432 | { 433 | $this->config($config); 434 | 435 | if ($session) { 436 | $this->session = $session; 437 | } else if ($this->config['features']['run_http']) { 438 | $this->session = new SessionInstance( 439 | $this->config['cookies']['names']['session'] 440 | ); 441 | } 442 | 443 | /** 444 | * Try connecting to Database Server 445 | */ 446 | try { 447 | if ($this->config['db']['type'] === 'sqlite') { 448 | $this->dbh = new PDO( 449 | 'sqlite:' . $this->config['db']['sqlite_path'], 450 | $this->config['db']['username'], 451 | $this->config['db']['password'], 452 | array( 453 | PDO::ATTR_PERSISTENT => true, 454 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 455 | PDO::ATTR_STRINGIFY_FETCHES => true, 456 | ) 457 | ); 458 | 459 | /** 460 | * Enable Multithreading Read/Write 461 | */ 462 | $this->dbh->exec('PRAGMA journal_mode=WAL;'); 463 | } elseif ($this->config['db']['type'] === 'postgresql') { 464 | $this->dbh = new PDO( 465 | 'pgsql:dbname=' . $this->config['db']['name'] 466 | . ';host=' . $this->config['db']['host'] 467 | . ';port=' . $this->config['db']['port'] . ';', 468 | $this->config['db']['username'], 469 | $this->config['db']['password'], 470 | array( 471 | PDO::ATTR_PERSISTENT => true, 472 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 473 | PDO::ATTR_STRINGIFY_FETCHES => true, 474 | ) 475 | ); 476 | } else { 477 | $this->dbh = new PDO( 478 | 'mysql:dbname=' . $this->config['db']['name'] 479 | . ';host=' . $this->config['db']['host'] 480 | . ';port=' . $this->config['db']['port'] 481 | . ';charset=utf8', 482 | $this->config['db']['username'], 483 | $this->config['db']['password'], 484 | array( 485 | PDO::ATTR_PERSISTENT => true, 486 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 487 | PDO::ATTR_STRINGIFY_FETCHES => true, 488 | )); 489 | } 490 | 491 | $cookieToken = isset($_COOKIE[$this->config['cookies']['names']['login_token']]) ? $_COOKIE[$this->config['cookies']['names']['login_token']] : false; 492 | 493 | if ($this->session) { 494 | /** 495 | * @var int User ID is stored in session 496 | */ 497 | $sessionUID = $this->session->get('user_id'); 498 | } else { 499 | $sessionUID = false; 500 | } 501 | 502 | $rememberMe = isset($_COOKIE[$this->config['cookies']['names']['remember_me']]) ? $_COOKIE[$this->config['cookies']['names']['remember_me']] : false; 503 | 504 | if ($rememberMe) { 505 | /** 506 | * Remember Me cookie is present. Decrypt its value 507 | * to get the user ID who needs to be remembered 508 | */ 509 | $rememberMeParts = explode(':|:', urldecode($rememberMe)); 510 | 511 | if (count($rememberMeParts) !== 2) { 512 | $this->logout(); 513 | return false; 514 | } 515 | 516 | list($rememberMeUser, $iv) = $rememberMeParts; 517 | $rememberMeUser = base64_decode($rememberMeUser, true); 518 | $iv = base64_decode($iv, true); 519 | 520 | $rememberMe = openssl_decrypt($rememberMeUser, 'AES-128-CBC', $this->config['keys']['cookie'], 0, $iv); 521 | } 522 | 523 | if ($cookieToken) { 524 | $loginToken = hash('sha256', $this->config['keys']['cookie'] . $sessionUID . $this->config['keys']['cookie']); 525 | 526 | if ($cookieToken === $loginToken) { 527 | $this->loggedIn = true; 528 | } else if ($rememberMe && $this->config['features']['remember_me'] === true) { 529 | /** 530 | * If there is a Remember Me Cookie and the user is not logged in, 531 | * then log in the user with the ID in the remember cookie if it 532 | * matches with the decrypted value in `logSyslogin` cookie 533 | */ 534 | $loginTokenFromRememberMeCookie = hash('sha256', $this->config['keys']['cookie'] . $rememberMe . $this->config['keys']['cookie']); 535 | 536 | if ($cookieToken === $loginTokenFromRememberMeCookie) { 537 | $this->loggedIn = true; 538 | 539 | $this->session->set('user_id', $rememberMe); 540 | $sessionUID = $rememberMe; 541 | } 542 | } 543 | } 544 | 545 | $this->userID = $sessionUID; 546 | 547 | /** 548 | * Check if devices is authorized to use the account 549 | */ 550 | 551 | if ($this->session && $this->config['features']['two_step_login'] === true && $this->loggedIn) { 552 | $login_page = self::curPage() === $this->config['pages']['login_page']; 553 | 554 | $deviceVerified = $this->session->get('device_verified', false); 555 | 556 | if 557 | ( 558 | !$deviceVerified && 559 | !isset($_COOKIE[$this->config['cookies']['names']['device']]) && 560 | $login_page === false 561 | ) { 562 | /** 563 | * The device is not verfied for the session 564 | */ 565 | $this->logout(); 566 | return false; 567 | } elseif 568 | ( 569 | $this->config['two_step_login']['first_check_only'] === false || 570 | ( 571 | $this->config['two_step_login']['first_check_only'] === true && 572 | !$deviceVerified 573 | ) 574 | ) { 575 | $sql = $this->dbh->prepare('SELECT "1" FROM ' . $this->config['two_step_login']['devices_table'] . ' WHERE uid = ? AND token = ?'); 576 | $sql->execute(array($this->userID, $_COOKIE[$this->config['cookies']['names']['device']])); 577 | 578 | /** 579 | * Device not authorized, so remove device cookie & logout 580 | */ 581 | 582 | if ($sql->fetchColumn() !== '1' && $login_page === false) { 583 | $this->logout(true); 584 | return false; 585 | } else { 586 | /** 587 | * This session has been checked and verified 588 | */ 589 | $this->session->set('device_verified', true); 590 | } 591 | } 592 | } 593 | 594 | if ($this->config['features']['auto_init'] === true) { 595 | $this->init(); 596 | } 597 | 598 | return true; 599 | } catch (PDOException $e) { 600 | /** 601 | * Couldn't connect to Database 602 | */ 603 | self::log('Could not connect to database. Check config->db credentials. PDO Output: ' . $e->getMessage()); 604 | 605 | return false; 606 | } 607 | } 608 | 609 | /** 610 | * A function that will automatically redirect user according to his/her login status 611 | * @return void 612 | */ 613 | public function init() 614 | { 615 | if (in_array(self::curPage(), $this->config['pages']['everyone'])) { 616 | /** 617 | * No redirects as this page can be accessed 618 | * by anyone whether he/she is logged in or not 619 | */ 620 | } elseif ($this->loggedIn === true && in_array(self::curPage(), $this->config['pages']['no_login'])) { 621 | self::redirect($this->config['pages']['home_page']); 622 | } elseif ($this->loggedIn === false && array_search(self::curPage(), $this->config['pages']['no_login']) === false) { 623 | self::redirect($this->config['pages']['login_page']); 624 | } 625 | 626 | $this->initCalled = true; 627 | } 628 | 629 | /** 630 | * Login the user 631 | * @param string $username Username or email 632 | * @param boolean|string $password Password as string or 633 | * FALSE for skipping password check 634 | * @param boolean $remember_me Remember me 635 | * @param boolean $cookies Should cookies be created and redirected 636 | * @return array|boolean|string Array if user is blocked 637 | * TRUE if login was success 638 | * User ID if login was success and $cookies FALSE 639 | */ 640 | public function login($username, $password, $remember_me = false, $cookies = true) 641 | { 642 | /** 643 | * We Add LIMIT to 1 in SQL query because to 644 | * get an array with key as the column name. 645 | */ 646 | 647 | if ($this->config['features']['email_login'] === true) { 648 | $query = 'SELECT ' . $this->config['db']['columns']['id'] . ', ' . $this->config['db']['columns']['password'] . ', ' . $this->config['db']['columns']['attempt'] . ' FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['username'] . '=:login OR ' . $this->config['db']['columns']['email'] . '=:login ORDER BY ' . $this->config['db']['columns']['id'] . ' LIMIT 1'; 649 | } else { 650 | $query = 'SELECT ' . $this->config['db']['columns']['id'] . ', ' . $this->config['db']['columns']['password'] . ', ' . $this->config['db']['columns']['attempt'] . ' FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['username'] . '=:login ORDER BY ' . $this->config['db']['columns']['id'] . ' LIMIT 1'; 651 | } 652 | 653 | $sql = $this->dbh->prepare($query); 654 | $sql->bindValue(':login', $username); 655 | 656 | $sql->execute(); 657 | $cols = $sql->fetch(PDO::FETCH_ASSOC); 658 | 659 | if (empty($cols)) { 660 | // No such user like that 661 | return false; 662 | } else { 663 | /** 664 | * Get the user details 665 | */ 666 | $userID = $cols[$this->config['db']['columns']['id']]; 667 | $user_password = $cols[$this->config['db']['columns']['password']]; 668 | $status = $cols[$this->config['db']['columns']['attempt']]; 669 | 670 | if (substr($status, 0, 2) === 'b-') { 671 | $blockedTime = substr($status, 2); 672 | 673 | if (time() < $blockedTime) { 674 | $blocked = true; 675 | 676 | return array( 677 | 'status' => 'blocked', 678 | 'minutes' => round(abs($blockedTime - time()) / 60, 0), 679 | 'seconds' => round(abs($blockedTime - time()) / 60 * 60, 2), 680 | ); 681 | } else { 682 | // remove the block, because the time limit is over 683 | $this->updateUser(array( 684 | $this->config['db']['columns']['attempt'] => '', // No tries at all 685 | ), $userID); 686 | } 687 | } 688 | 689 | if (!isset($blocked) && ($password === false || password_verify($password . $this->config['keys']['salt'], $user_password))) { 690 | if ($cookies === true) { 691 | $this->session->set('user_id', $userID); 692 | 693 | setcookie( 694 | $this->config['cookies']['names']['login_token'], 695 | hash('sha256', $this->config['keys']['cookie'] . $userID . $this->config['keys']['cookie']), 696 | strtotime($this->config['cookies']['expire']), 697 | $this->config['cookies']['path'], $this->config['cookies']['domain'] 698 | ); 699 | 700 | if ($remember_me === true && $this->config['features']['remember_me'] === true) { 701 | $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('AES-128-CBC')); 702 | $rememberMeCookie = base64_encode(openssl_encrypt($userID, 'AES-128-CBC', $this->config['keys']['cookie'], 0, $iv)) . ':|:' . base64_encode($iv); 703 | 704 | setcookie( 705 | $this->config['cookies']['names']['remember_me'], 706 | $rememberMeCookie, 707 | strtotime($this->config['cookies']['expire']), 708 | $this->config['cookies']['path'], 709 | $this->config['cookies']['domain'] 710 | ); 711 | } 712 | 713 | $this->loggedIn = true; 714 | 715 | if ($this->config['features']['block_brute_force'] === true) { 716 | /** 717 | * If Brute Force Protection is Enabled, 718 | * Reset the attempt status 719 | */ 720 | $this->updateUser(array( 721 | $this->config['db']['columns']['attempt'] => '0', 722 | ), $userID); 723 | } 724 | 725 | if ($this->initCalled) { 726 | self::redirect($this->config['pages']['home_page']); 727 | } 728 | 729 | return true; 730 | } else { 731 | /** 732 | * If cookies shouldn't be set, 733 | * it means login() was called 734 | * to get the user's ID. So, return it 735 | */ 736 | return $userID; 737 | } 738 | } else { 739 | /** 740 | * Incorrect password 741 | * ------------------ 742 | * Check if brute force protection is enabled 743 | */ 744 | if ($this->config['features']['block_brute_force'] === true) { 745 | $max_tries = $this->config['brute_force']['tries']; 746 | 747 | if ($status === '') { 748 | // User was not logged in before 749 | $this->updateUser(array( 750 | $this->config['db']['columns']['attempt'] => '1', // Tried 1 time 751 | ), $userID); 752 | } elseif ((int) $status === $max_tries) { 753 | /** 754 | * Account Blocked. User will be only able to 755 | * re-login at the time in UNIX timestamp 756 | */ 757 | $eligible_for_next_login_time = strtotime('+' . $this->config['brute_force']['time_limit'] . ' seconds', time()); 758 | $this->updateUser(array( 759 | $this->config['db']['columns']['attempt'] => 'b-' . $eligible_for_next_login_time, 760 | ), $userID); 761 | 762 | return array( 763 | 'status' => 'blocked', 764 | 'minutes' => round(abs($eligible_for_next_login_time - time()) / 60, 0), 765 | 'seconds' => round(abs($eligible_for_next_login_time - time()) / 60 * 60, 2), 766 | ); 767 | } elseif ($status < $max_tries) { 768 | // If the attempts are less than Max and not Max 769 | $this->updateUser(array( 770 | $this->config['db']['columns']['attempt'] => $status + 1, // Increase the no of tries by +1. 771 | ), $userID); 772 | } 773 | } 774 | 775 | return false; 776 | } 777 | } 778 | } 779 | 780 | /** 781 | * Register a user 782 | * @param string $identification Username or email 783 | * @param string $password Password 784 | * @param array $other Values for other columns 785 | * @return boolean|string Whether user exists ( exists ) or account was created 786 | */ 787 | public function register($identification, $password, $other = array()) 788 | { 789 | if ($this->userExists($identification) || (isset($other['email']) && $this->userExists($other['email']))) { 790 | return 'exists'; 791 | } else { 792 | $hashedPass = password_hash($password . $this->config['keys']['salt'], PASSWORD_DEFAULT); 793 | 794 | if (count($other) === 0) { 795 | /* If there is no other fields mentioned, make the default query */ 796 | $sql = $this->dbh->prepare('INSERT INTO ' . $this->config['db']['table'] . ' ( ' . $this->config['db']['columns']['username'] . ', ' . $this->config['db']['columns']['password'] . ' ) VALUES(:username, :password )'); 797 | } else { 798 | /* if there are other fields to add value to, make the query and bind values according to it */ 799 | $keys = array_keys($other); 800 | $columns = implode(',', $keys); 801 | $colVals = implode(',:', $keys); 802 | $sql = $this->dbh->prepare( 803 | 'INSERT INTO ' . $this->config['db']['table'] . ' ( ' . 804 | $this->config['db']['columns']['username'] . ', ' . $this->config['db']['columns']['password'] . ", $columns " . 805 | ') VALUES( ' . 806 | ":username, :password, :$colVals" . 807 | ')' 808 | ); 809 | 810 | foreach ($other as $key => $value) { 811 | $value = $value; 812 | $sql->bindValue(":$key", $value); 813 | } 814 | } 815 | 816 | /* Bind the default values */ 817 | $sql->bindValue(':username', $identification); 818 | $sql->bindValue(':password', $hashedPass); 819 | $sql->execute(); 820 | 821 | return true; 822 | } 823 | } 824 | 825 | /** 826 | * Logout the user by destroying the cookies and session 827 | * @param boolean $removeDeviceCookie Should device cookie be removed 828 | * @return boolean 829 | */ 830 | public function logout($removeDeviceCookie = false) 831 | { 832 | $this->session->destroy(); 833 | 834 | if ($this->config['features']['run_http']) { 835 | setcookie($this->config['cookies']['names']['login_token'], '', time() - 10, $this->config['cookies']['path'], $this->config['cookies']['domain']); 836 | setcookie($this->config['cookies']['names']['remember_me'], '', time() - 10, $this->config['cookies']['path'], $this->config['cookies']['domain']); 837 | 838 | if ($removeDeviceCookie) { 839 | setcookie( 840 | $this->config['cookies']['names']['device'], 841 | '', 842 | time() - 10, 843 | $this->config['cookies']['path'], 844 | $this->config['cookies']['domain'] 845 | ); 846 | } 847 | 848 | self::redirect($this->config['pages']['login_page']); 849 | } 850 | 851 | return true; 852 | } 853 | 854 | /** 855 | * A function to handle the Forgot Password process 856 | * @return string The current state of the process 857 | */ 858 | public function forgotPassword() 859 | { 860 | $curStatus = 'initial'; // The Current Status of Forgot Password process 861 | $identName = $this->config['features']['email_login'] === false ? 'Username' : 'Username / E-Mail'; 862 | 863 | if (!isset($_POST['logSysForgotPass']) && !isset($_GET['resetPassToken']) && !isset($_POST['logSysForgotPassChange'])) { 864 | echo $this->getOutput( 865 | 'resetPasswordRequestForm', 866 | array( 867 | 'identity_type' => $identName, 868 | ) 869 | ); 870 | 871 | /** 872 | * The user had moved to the reset password form ie she/he is currently seeing the forgot password form 873 | */ 874 | $curStatus = 'resetPasswordRequestForm'; 875 | } elseif (isset($_GET['resetPassToken']) && !isset($_POST['logSysForgotPassChange'])) { 876 | /** 877 | * The user gave the password reset token. Check if the token is valid. 878 | */ 879 | $reset_pass_token = urldecode($_GET['resetPassToken']); 880 | 881 | if (!$this->verifyResetPasswordToken($reset_pass_token)) { 882 | $curStatus = 'invalidToken'; // The token user gave was not valid 883 | echo $this->getOutput($curStatus); 884 | } else { 885 | /** 886 | * The token is valid, display the new password form 887 | */ 888 | echo $this->getOutput('resetPasswordForm', array( 889 | 'resetPassToken' => $reset_pass_token, 890 | )); 891 | 892 | /** 893 | * The token was correct, displayed the change/new password form 894 | */ 895 | $curStatus = 'resetPasswordForm'; 896 | } 897 | } elseif (isset($_POST['logSysForgotPassChange']) && isset($_POST['logSysForgotPassNewPassword']) && isset($_POST['logSysForgotPassRetypedPassword'])) { 898 | $reset_pass_token = urldecode($_POST['token']); 899 | 900 | $sql = $this->dbh->prepare('SELECT uid FROM ' . $this->config['db']['token_table'] . ' WHERE token = ?'); 901 | $sql->execute(array($reset_pass_token)); 902 | 903 | $userID = $sql->fetchColumn(); 904 | 905 | if ($userID === false) { 906 | $curStatus = 'invalidToken'; // The token user gave was not valid 907 | echo $this->getOutput($curStatus); 908 | } else { 909 | if (empty($_POST['logSysForgotPassNewPassword']) || empty($_POST['logSysForgotPassRetypedPassword'])) { 910 | $curStatus = 'fieldsLeftBlank'; 911 | echo $this->getOutput($curStatus); 912 | } elseif ($_POST['logSysForgotPassNewPassword'] !== $_POST['logSysForgotPassRetypedPassword']) { 913 | $curStatus = 'passwordDontMatch'; // The new password and retype password submitted didn't match 914 | echo $this->getOutput($curStatus); 915 | } else { 916 | $this->changePassword($_POST['logSysForgotPassNewPassword'], $userID); 917 | 918 | /** 919 | * The token shall not be used again, so remove it. 920 | */ 921 | $this->removeToken($reset_pass_token); 922 | 923 | $curStatus = 'passwordChanged'; // The password was successfully changed 924 | echo $this->getOutput($curStatus); 925 | } 926 | } 927 | } elseif (isset($_POST['identification'])) { 928 | /** 929 | * Check if username/email is provided and if it's valid and exists 930 | */ 931 | $identification = $_POST['identification']; 932 | 933 | if (empty($identification)) { 934 | $curStatus = 'identityNotProvided'; // The identity was not given 935 | echo $this->getOutput($curStatus, array( 936 | 'identity_type' => $identName, 937 | )); 938 | } else { 939 | $sql = $this->dbh->prepare('SELECT ' . $this->config['db']['columns']['id'] . ' FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['username'] . '=:login OR ' . $this->config['db']['columns']['email'] . '=:login'); 940 | $sql->bindValue(':login', $identification); 941 | 942 | $sql->execute(); 943 | $cols = $sql->fetch(PDO::FETCH_ASSOC); 944 | 945 | if (empty($cols)) { 946 | $curStatus = 'userNotFound'; // The user with the identity given was not found in the users database 947 | echo $this->getOutput($curStatus); 948 | } else { 949 | $this->sendResetPasswordToken($cols[$this->config['db']['columns']['id']]); 950 | 951 | echo '

An email has been sent to your email inbox with instructions. Check Your Mail Inbox and SPAM Folders.

You can close this window.

'; 952 | 953 | $curStatus = 'emailSent'; // E-Mail has been sent 954 | } 955 | } 956 | } 957 | 958 | return $curStatus; 959 | } 960 | 961 | /** 962 | * Send token to reset password 963 | * @param int $uid User ID 964 | * @param boolean|function $messageCallback Callback to make the body of email 965 | * @return boolean 966 | */ 967 | public function sendResetPasswordToken($uid, $messageCallback = false) 968 | { 969 | $token = self::randStr(40); 970 | $email = $this->getUser($this->config['db']['columns']['email'], $uid); 971 | 972 | if (!$email) { 973 | return false; 974 | } 975 | 976 | $sth = $this->dbh->prepare('INSERT INTO ' . $this->config['db']['token_table'] . ' ( token, uid, requested ) VALUES (?, ?, ?)'); 977 | $sth->execute( 978 | array( 979 | $token, 980 | $uid, 981 | time(), 982 | ) 983 | ); 984 | 985 | /** 986 | * Prepare the email to be sent 987 | */ 988 | $subject = 'Reset Password'; 989 | 990 | if (is_callable($messageCallback)) { 991 | $body = call_user_func_array( 992 | $messageCallback, 993 | array( 994 | $token, 995 | self::curPageURL(), 996 | ) 997 | ); 998 | } else { 999 | $body = 'You requested for resetting your password on ' . $this->config['basic']['company'] . '. For this, please click the following link : 1000 |
1001 | Reset Password : ' . $token . ' 1002 |
'; 1003 | } 1004 | 1005 | $this->sendMail($email, $subject, $body); 1006 | 1007 | return true; 1008 | } 1009 | 1010 | /** 1011 | * Check if token for resetting password is valid 1012 | * @return boolean 1013 | */ 1014 | public function verifyResetPasswordToken($reset_pass_token) 1015 | { 1016 | $sql = $this->dbh->prepare('SELECT COUNT(1) FROM ' . $this->config['db']['token_table'] . ' WHERE token = ?'); 1017 | $sql->execute(array($reset_pass_token)); 1018 | 1019 | return $sql->fetchColumn() !== '0'; 1020 | } 1021 | 1022 | /** 1023 | * Remove a token 1024 | * @param string $token Token 1025 | * @return boolean 1026 | */ 1027 | public function removeToken($token) 1028 | { 1029 | $sql = $this->dbh->prepare('DELETE FROM ' . $this->config['db']['token_table'] . ' WHERE token = ?'); 1030 | $sql->execute(array($token)); 1031 | 1032 | return $sql->rowCount() != 0; 1033 | } 1034 | 1035 | /** 1036 | * A function that handles the logged in user to change her/his password 1037 | * 1038 | * @param string $newPassword The new password 1039 | * @return boolean Whether operation was succesful 1040 | */ 1041 | public function changePassword($newPassword, $userID = null) 1042 | { 1043 | if ($userID === null) { 1044 | $userID = $this->userID; 1045 | } 1046 | 1047 | $hashedPassword = password_hash($newPassword . $this->config['keys']['salt'], PASSWORD_DEFAULT); 1048 | $this->updateUser(array( 1049 | $this->config['db']['columns']['password'] => $hashedPassword, 1050 | ), $userID); 1051 | 1052 | return true; 1053 | } 1054 | 1055 | /** 1056 | * Check if user exists 1057 | * @param string $identification Username or email 1058 | * @return boolean Whether user exist 1059 | */ 1060 | public function userExists($identification) 1061 | { 1062 | if ($this->config['features']['email_login'] === true) { 1063 | $query = 'SELECT COUNT(1) FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['username'] . '=:login OR ' . $this->config['db']['columns']['email'] . '=:login'; 1064 | } else { 1065 | $query = 'SELECT COUNT(1) FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['username'] . '=:login'; 1066 | } 1067 | 1068 | $sql = $this->dbh->prepare($query); 1069 | $sql->execute(array( 1070 | ':login' => $identification, 1071 | )); 1072 | 1073 | return $sql->fetchColumn() === '0' ? false : true; 1074 | } 1075 | 1076 | /** 1077 | * Check if a user ID exists 1078 | * @param string $userID User ID 1079 | * @return boolean 1080 | */ 1081 | public function userIDExists($userID) 1082 | { 1083 | $sql = $this->dbh->prepare('SELECT COUNT(1) FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['id'] . ' = :login'); 1084 | $sql->execute(array( 1085 | ':login' => $userID, 1086 | )); 1087 | 1088 | return $sql->fetchColumn() !== '0'; 1089 | } 1090 | 1091 | /** 1092 | * Get user's info 1093 | * @param string $what Column name 1094 | * @param string $user User ID 1095 | * @return array|string Value 1096 | */ 1097 | public function getUser($what = '*', $user = null) 1098 | { 1099 | if ($user === null) { 1100 | $user = $this->userID; 1101 | } 1102 | 1103 | if (is_array($what)) { 1104 | $columns = implode(',', $what); 1105 | } else { 1106 | $columns = $what !== '*' ? $what : '*'; 1107 | } 1108 | 1109 | $sql = $this->dbh->prepare('SELECT ' . $columns . ' FROM ' . $this->config['db']['table'] . ' WHERE ' . $this->config['db']['columns']['id'] . ' = ? ORDER BY ' . $this->config['db']['columns']['id'] . ' LIMIT 1'); 1110 | $sql->execute(array($user)); 1111 | 1112 | $data = $sql->fetch(PDO::FETCH_ASSOC); 1113 | 1114 | if (!is_array($what)) { 1115 | $data = $what === '*' ? $data : $data[$what]; 1116 | } 1117 | 1118 | return $data; 1119 | } 1120 | 1121 | /** 1122 | * Updates the info of user 1123 | * @param array $toUpdate Fields to update 1124 | * @param [type] $user User ID 1125 | * @return boolean Whether it was a success 1126 | */ 1127 | public function updateUser($toUpdate = array(), $user = null) 1128 | { 1129 | if (is_array($toUpdate) && !isset($toUpdate[$this->config['db']['columns']['id']])) { 1130 | if ($user === null) { 1131 | $user = $this->userID; 1132 | } 1133 | 1134 | $columns = ''; 1135 | 1136 | foreach ($toUpdate as $k => $v) { 1137 | $columns .= "$k = :$k, "; 1138 | } 1139 | 1140 | $columns = substr($columns, 0, -2); // Remove last ',' 1141 | 1142 | $sql = $this->dbh->prepare('UPDATE ' . $this->config['db']['table'] . ' SET ' . $columns . ' WHERE ' . $this->config['db']['columns']['id'] . '=:id'); 1143 | $sql->bindValue(':id', $user); 1144 | 1145 | foreach ($toUpdate as $key => $value) { 1146 | $sql->bindValue(":$key", $value); 1147 | } 1148 | 1149 | $sql->execute(); 1150 | 1151 | return true; 1152 | } else { 1153 | return false; 1154 | } 1155 | } 1156 | 1157 | /** 1158 | * Get the time since user joined 1159 | * @param string $user User ID 1160 | * @return string Time since 1161 | */ 1162 | public function joinedSince($userID = null) 1163 | { 1164 | if ($userID === null) { 1165 | $userID = $this->userID; 1166 | } 1167 | 1168 | $created = $this->getUser($this->config['db']['columns']['created'], $userID); 1169 | $timeFirst = strtotime($created); 1170 | $timeSecond = strtotime('now'); 1171 | $memsince = $timeSecond - strtotime($created); 1172 | $regged = date('n/j/Y', strtotime($created)); 1173 | 1174 | if ($memsince < 60) { 1175 | $memfor = $memsince . ' Seconds'; 1176 | } elseif ($memsince < 120) { 1177 | $memfor = floor($memsince / 60) . ' Minute'; 1178 | } elseif ($memsince < 3600 && $memsince > 120) { 1179 | $memfor = floor($memsince / 60) . ' Minutes'; 1180 | } elseif ($memsince < 7200 && $memsince > 3600) { 1181 | $memfor = floor($memsince / 3600) . ' Hour'; 1182 | } elseif ($memsince < 86400 && $memsince > 3600) { 1183 | $memfor = floor($memsince / 3600) . ' Hours'; 1184 | } elseif ($memsince < 172800) { 1185 | $memfor = floor($memsince / 86400) . ' Day'; 1186 | } elseif ($memsince < 604800 && $memsince > 172800) { 1187 | $memfor = floor($memsince / 86400) . ' Days'; 1188 | } elseif ($memsince < 1209600 && $memsince > 604800) { 1189 | $memfor = floor($memsince / 604800) . ' Week'; 1190 | } elseif ($memsince < 2419200 && $memsince > 1209600) { 1191 | $memfor = floor($memsince / 604800) . ' Weeks'; 1192 | } elseif ($memsince < 4838400) { 1193 | $memfor = floor($memsince / 2419200) . ' Month'; 1194 | } elseif ($memsince < 31536000 && $memsince > 4838400) { 1195 | $memfor = floor($memsince / 2419200) . ' Months'; 1196 | } elseif ($memsince < 63072000) { 1197 | $memfor = floor($memsince / 31536000) . ' Year'; 1198 | } elseif ($memsince > 63072000) { 1199 | $memfor = floor($memsince / 31536000) . ' Years'; 1200 | } 1201 | 1202 | return (string) $memfor; 1203 | } 1204 | 1205 | /** 1206 | * 2 Step Verification Login Process 1207 | * --------------------------------- 1208 | * When user logs in, it checks whether there is a device cookie. 1209 | * If there is : 1210 | * * Checks whether device is registered 1211 | * * If registered and username & password is correct, user is logged in 1212 | * If there is not : 1213 | * * Token is sent 1214 | * * The 'Enter Received token' form is shown 1215 | * * If the token entered is correct, then a device ID is inserted to table 1216 | * * The cookie with device ID is created 1217 | * @see LS::login() Parameters are similar 1218 | * 1219 | * @param string $identification Similar to LS::login() 1220 | * @param string $password Similar to LS::login() 1221 | * @param boolean $remember_me Similar to LS::login() 1222 | * @return boolean|string Whether login was successful 1223 | * Current state of 2 Step Login 1224 | */ 1225 | public function twoStepLogin($identification = '', $password = '', $remember_me = false, $cookies = true) 1226 | { 1227 | if ($cookies) { 1228 | $twoStepLoginSession = $this->session->createNamespace('two_step_login'); 1229 | $twoStepLoginStep = $twoStepLoginSession->get('step'); 1230 | $twoStepLoginUserID = $twoStepLoginSession->get('user_id'); 1231 | } else { 1232 | $twoStepLoginSession = 1233 | $twoStepLoginStep = 1234 | $twoStepLoginUserID = false; 1235 | } 1236 | 1237 | if (isset($_POST['two_step_login_token']) && $twoStepLoginStep === '1' && $twoStepLoginUserID) { 1238 | if (!$this->csrf()) { 1239 | throw new TwoStepLogin('invalid_csrf_token'); 1240 | 1241 | return; 1242 | } 1243 | 1244 | $twoStepLoginTries = $twoStepLoginSession->get('tries') + 1; 1245 | $token = $_POST['two_step_login_token']; 1246 | 1247 | $sql = $this->dbh->prepare('SELECT COUNT(1) FROM ' . $this->config['db']['token_table'] . ' WHERE token = ? AND uid = ?'); 1248 | $sql->execute(array($token, $twoStepLoginUserID)); 1249 | 1250 | if ($sql->fetchColumn() === '0') { 1251 | if ($twoStepLoginTries >= $this->config['two_step_login']['token_tries']) { 1252 | /** 1253 | * Prevent user from brute forcing the token 1254 | */ 1255 | $this->logout(); 1256 | } else { 1257 | $twoStepLoginSession->set('tries', $twoStepLoginTries); 1258 | } 1259 | 1260 | throw new TwoStepLogin( 1261 | 'invalid_token', 1262 | array( 1263 | 'tries_left' => $this->config['two_step_login']['token_tries'] - $twoStepLoginTries, 1264 | ) 1265 | ); 1266 | } else { 1267 | /** 1268 | * Register User's new device if and only if 1269 | * the user wants to remember the device from 1270 | * which the user is logging in 1271 | */ 1272 | 1273 | if (isset($_POST['two_step_login_remember_device'])) { 1274 | $device_token = self::randStr(10); 1275 | 1276 | $sth = $this->dbh->prepare('INSERT INTO ' . $this->config['two_step_login']['devices_table'] . ' ( uid, token, last_access ) VALUES ( ?, ?, ? )'); 1277 | $sth->execute(array($twoStepLoginUserID, $device_token, time())); 1278 | 1279 | setcookie( 1280 | $this->config['cookies']['names']['device'], 1281 | $device_token, 1282 | strtotime($this->config['two_step_login']['expire']), 1283 | $this->config['cookies']['path'], 1284 | $this->config['cookies']['domain'] 1285 | ); 1286 | } 1287 | 1288 | /** 1289 | * Revoke all tokens of user 1290 | */ 1291 | $sql = $this->dbh->prepare('DELETE FROM ' . $this->config['db']['token_table'] . ' WHERE uid = ?'); 1292 | $sql->execute(array($twoStepLoginUserID)); 1293 | 1294 | if ($cookies) { 1295 | $this->session->set('device_verified', true); 1296 | 1297 | $this->login($this->getUser($this->config['db']['columns']['username'], $twoStepLoginUserID), false, isset($_POST['two_step_login_remember_me'])); 1298 | } 1299 | 1300 | throw new TwoStepLogin( 1301 | 'login_success', 1302 | array( 1303 | 'uid' => $twoStepLoginUserID, 1304 | ) 1305 | ); 1306 | } 1307 | } elseif ($identification !== '' && $password !== '') { 1308 | $login = $this->login($identification, $password, $remember_me, false); 1309 | 1310 | if ($login === false) { 1311 | /** 1312 | * Username/Password wrong 1313 | */ 1314 | throw new TwoStepLogin('login_fail'); 1315 | } elseif (is_array($login) && $login['status'] === 'blocked') { 1316 | throw new TwoStepLogin('blocked', $login); 1317 | 1318 | return $login; 1319 | } else { 1320 | /** 1321 | * Get the user ID from \Fr\LS::login() 1322 | */ 1323 | $twoStepLoginUserID = $login; 1324 | 1325 | /** 1326 | * Check if device is verfied so that 2 Step Verification can be skipped 1327 | */ 1328 | 1329 | if (isset($_COOKIE[$this->config['cookies']['names']['device']])) { 1330 | $sql = $this->dbh->prepare('SELECT 1 FROM ' . $this->config['two_step_login']['devices_table'] . ' WHERE uid = ? AND token = ?'); 1331 | $sql->execute(array($twoStepLoginUserID, $_COOKIE[$this->config['cookies']['names']['device']])); 1332 | 1333 | if ($sql->fetchColumn() === '1') { 1334 | $verfied = true; 1335 | /** 1336 | * Update last accessed time 1337 | */ 1338 | $sth = $this->dbh->prepare('UPDATE ' . $this->config['two_step_login']['devices_table'] . ' SET last_access = ? WHERE uid = ? AND token = ?'); 1339 | $sth->execute( 1340 | array( 1341 | time(), 1342 | $twoStepLoginUserID, 1343 | $_COOKIE[$this->config['cookies']['names']['device']], 1344 | ) 1345 | ); 1346 | 1347 | /** 1348 | * Delete all session vars used for 2 Step Login 1349 | */ 1350 | $twoStepLoginSession->delete('step', 'user_id', 'tries'); 1351 | 1352 | if ($cookies) { 1353 | $this->login($this->getUser($this->config['db']['columns']['username'], $twoStepLoginUserID), false, $remember_me); 1354 | } 1355 | 1356 | throw new TwoStepLogin( 1357 | 'login_success', 1358 | array( 1359 | 'uid' => $twoStepLoginUserID, 1360 | ) 1361 | ); 1362 | } 1363 | } 1364 | 1365 | /** 1366 | * Start the 2 Step Verification Process 1367 | * Do only if callback is present and if 1368 | * the device is not verified 1369 | */ 1370 | 1371 | if (is_callable($this->config['two_step_login']['send_callback']) && !isset($verified)) { 1372 | /** 1373 | * The first part of 2 Step Login is completed 1374 | */ 1375 | if ($twoStepLoginSession) { 1376 | $twoStepLoginSession->set([ 1377 | 'step' => '1', 1378 | 'tries' => 0, 1379 | 'user_id' => $twoStepLoginUserID, 1380 | ]); 1381 | } 1382 | 1383 | if ($this->config['features']['block_brute_force']) { 1384 | $sth = $this->dbh->prepare('SELECT COUNT(1) FROM ' . $this->config['db']['token_table'] . ' WHERE uid = ? '); 1385 | $sth->execute(array($twoStepLoginUserID)); 1386 | 1387 | if ($sth->fetchColumn() >= $this->config['brute_force']['max_tokens']) { 1388 | /** 1389 | * Account Blocked. User will be only able to 1390 | * re-login at the time in UNIX timestamp 1391 | */ 1392 | $eligible_for_next_login_time = strtotime('+' . $this->config['brute_force']['time_limit'] . ' seconds', time()); 1393 | 1394 | $this->updateUser(array( 1395 | $this->config['db']['columns']['attempt'] => 'b-' . $eligible_for_next_login_time, 1396 | ), $twoStepLoginUserID); 1397 | 1398 | /** 1399 | * Delete all 5 tokens 1400 | */ 1401 | $sth = $this->dbh->prepare('DELETE FROM ' . $this->config['db']['token_table'] . ' WHERE uid = :uid LIMIT :max_tokens'); 1402 | $sth->bindValue(':uid', $twoStepLoginUserID); 1403 | $sth->bindValue(':max_tokens', $this->config['brute_force']['max_tokens'], PDO::PARAM_INT); 1404 | $sth->execute(); 1405 | 1406 | $twoStepLoginSession->set('step', '0'); 1407 | 1408 | $this->logout(); 1409 | } 1410 | } 1411 | 1412 | /** 1413 | * The 2nd parameter depends on `config` -> `two_step_login` -> `numeric` 1414 | */ 1415 | $token = self::randStr($this->config['two_step_login']['token_length'], $this->config['two_step_login']['numeric']); 1416 | 1417 | /** 1418 | * Save the token in DB 1419 | */ 1420 | $sth = $this->dbh->prepare('INSERT INTO ' . $this->config['db']['token_table'] . ' ( token, uid, requested ) VALUES (?, ?, ?)'); 1421 | $sth->execute( 1422 | array( 1423 | $token, 1424 | $twoStepLoginUserID, 1425 | time(), 1426 | ) 1427 | ); 1428 | 1429 | $that = $this; 1430 | call_user_func_array($this->config['two_step_login']['send_callback'], array(&$that, $twoStepLoginUserID, $token)); 1431 | 1432 | throw new TwoStepLogin('enter_token_form', array( 1433 | 'remember_me' => $remember_me, 1434 | 'uid' => $twoStepLoginUserID, 1435 | )); 1436 | } 1437 | } 1438 | } 1439 | 1440 | /** 1441 | * 2 Step Login is not doing any actions or 1442 | * hasn't returned anything before. If so, 1443 | * then return false to indicate that the 1444 | * function is not doing anything 1445 | */ 1446 | 1447 | return false; 1448 | } 1449 | 1450 | /** 1451 | * Get authorized devices 1452 | * @return array Authorized devices 1453 | */ 1454 | public function getDevices() 1455 | { 1456 | if ($this->loggedIn) { 1457 | $sql = $this->dbh->prepare('SELECT * FROM ' . $this->config['two_step_login']['devices_table'] . ' WHERE uid = ?'); 1458 | $sql->execute(array($this->userID)); 1459 | 1460 | return $sql->fetchAll(PDO::FETCH_ASSOC); 1461 | } else { 1462 | return false; 1463 | } 1464 | } 1465 | 1466 | /** 1467 | * Revoke a device 1468 | * @param string $device_id Device ID 1469 | * @return boolean Whether it was revoked 1470 | */ 1471 | public function revokeDevice($device_id) 1472 | { 1473 | if ($this->loggedIn) { 1474 | $sql = $this->dbh->prepare('DELETE FROM ' . $this->config['two_step_login']['devices_table'] . ' WHERE uid = ? AND token = ?'); 1475 | $sql->execute(array($this->userID, $device_id)); 1476 | 1477 | if ($this->session && $this->getDeviceID() === $device_id) { 1478 | $this->session->delete('device_verified'); 1479 | $this->logout(true); 1480 | } 1481 | 1482 | return $sql->rowCount() == 1; 1483 | } 1484 | } 1485 | 1486 | /** 1487 | * Get output for each states 1488 | * @param string $state The output of the state 1489 | * @param array $extraInfo Extra parameters about the state 1490 | * @return string HTML output 1491 | */ 1492 | public function getOutput($state, $extraInfo = array()) 1493 | { 1494 | if (is_callable($this->config['basic']['output_callback'])) { 1495 | $that = $this; 1496 | 1497 | return call_user_func_array($this->config['basic']['output_callback'], array( 1498 | &$that, 1499 | $state, 1500 | $extraInfo, 1501 | )); 1502 | } else { 1503 | return null; 1504 | } 1505 | } 1506 | 1507 | /** 1508 | * Whether a user is logged in 1509 | * @return boolean 1510 | */ 1511 | public function isLoggedIn() 1512 | { 1513 | return $this->loggedIn; 1514 | } 1515 | 1516 | /** 1517 | * Get device ID 1518 | * @return boolean|string FALSE if device cookie does not exist 1519 | */ 1520 | public function getDeviceID() 1521 | { 1522 | if (isset($_COOKIE[$this->config['cookies']['names']['device']])) { 1523 | return $_COOKIE[$this->config['cookies']['names']['device']]; 1524 | } else { 1525 | return false; 1526 | } 1527 | } 1528 | 1529 | /** 1530 | * --------------------- 1531 | * Extra Tools/Functions 1532 | * --------------------- 1533 | */ 1534 | 1535 | /** 1536 | * Check if E-Mail is valid 1537 | * @param string $email Email 1538 | * @return boolean 1539 | */ 1540 | public static function validEmail($email = '') 1541 | { 1542 | return filter_var($email, FILTER_VALIDATE_EMAIL); 1543 | } 1544 | 1545 | /** 1546 | * Get the current page URL 1547 | * @return string URL 1548 | */ 1549 | public static function curPageURL() 1550 | { 1551 | $pageURL = 'http'; 1552 | 1553 | if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { 1554 | $pageURL .= 's'; 1555 | } 1556 | 1557 | if ($_SERVER['SERVER_NAME'] == null) { 1558 | $serverName = $_SERVER['SERVER_ADDR']; 1559 | } else { 1560 | $serverName = $_SERVER['SERVER_NAME']; 1561 | } 1562 | 1563 | $pageURL .= '://'; 1564 | 1565 | if ($_SERVER['SERVER_PORT'] !== '80') { 1566 | $pageURL .= $serverName . ':' . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI']; 1567 | } else { 1568 | $pageURL .= $serverName . $_SERVER['REQUEST_URI']; 1569 | } 1570 | 1571 | return $pageURL; 1572 | } 1573 | 1574 | /** 1575 | * Generate a Random String 1576 | * @param int $length Length of string 1577 | * @param boolean $int Should string be integer 1578 | * @return string Random string 1579 | */ 1580 | public static function randStr($length, $int = false) 1581 | { 1582 | $random_str = ''; 1583 | $chars = $int ? '0516243741506927589' : 'subinsblogabcdefghijklmanopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 1584 | $size = strlen($chars) - 1; 1585 | 1586 | for ($i = 0; $i < $length; $i++) { 1587 | $random_str .= $chars[rand(0, $size)]; 1588 | } 1589 | 1590 | return $random_str; 1591 | } 1592 | 1593 | /** 1594 | * Get the current page path. 1595 | * @return string Example: /mypage, /folder/mypage.php 1596 | */ 1597 | public static function curPage() 1598 | { 1599 | $parts = parse_url(self::curPageURL()); 1600 | 1601 | return $parts['path']; 1602 | } 1603 | 1604 | /** 1605 | * Do a redirect 1606 | * @param [type] $url Where to redirect to 1607 | * @param integer $status Redirect status code 1608 | * @return void 1609 | */ 1610 | public static function redirect($url, $status = 302) 1611 | { 1612 | header('Location: ' . $url, true, $status); 1613 | exit; 1614 | } 1615 | 1616 | /** 1617 | * Send an email 1618 | * @param string $email Recipient's email 1619 | * @param string $subject Subject 1620 | * @param string $body Message 1621 | * @return void 1622 | */ 1623 | public function sendMail($email, $subject, $body) 1624 | { 1625 | /** 1626 | * If there is a callback for email sending, use it else PHP's mail() 1627 | */ 1628 | 1629 | if (is_callable($this->config['basic']['email_callback'])) { 1630 | $that = $this; 1631 | call_user_func_array($this->config['basic']['email_callback'], array(&$that, $email, $subject, $body)); 1632 | } else { 1633 | $headers = array(); 1634 | $headers[] = 'MIME-Version: 1.0'; 1635 | $headers[] = 'Content-type: text/html; charset=iso-8859-1'; 1636 | $headers[] = 'From: ' . $this->config['basic']['email']; 1637 | $headers[] = 'Reply-To: ' . $this->config['basic']['email']; 1638 | mail($email, $subject, $body, implode('\r\n', $headers)); 1639 | } 1640 | } 1641 | 1642 | /** 1643 | * CSRF Protection 1644 | */ 1645 | public function csrf($type = '') 1646 | { 1647 | if (!isset($_COOKIE['csrf_token'])) { 1648 | $csrf_token = self::randStr(5); 1649 | setcookie('csrf_token', $csrf_token, 0, $this->config['cookies']['path'], $this->config['cookies']['domain']); 1650 | } else { 1651 | $csrf_token = $_COOKIE['csrf_token']; 1652 | } 1653 | 1654 | if ($type === 's') { 1655 | /** 1656 | * Output as string 1657 | */ 1658 | return urlencode($csrf_token); 1659 | } elseif ($type === 'g') { 1660 | /** 1661 | * Output as a GET parameter 1662 | */ 1663 | return '&csrf_token=' . urlencode($csrf_token); 1664 | } elseif ($type === 'i') { 1665 | /** 1666 | * Output as an input field 1667 | */ 1668 | return ''; 1669 | } else { 1670 | /** 1671 | * Check CSRF validity 1672 | */ 1673 | 1674 | if ((isset($_POST['csrf_token']) && $_COOKIE['csrf_token'] === $_POST['csrf_token']) || (isset($_GET['csrf_token']) && $_COOKIE['csrf_token'] === $_GET['csrf_token'])) { 1675 | return true; 1676 | } else { 1677 | /** 1678 | * CSRF Token doesn't match. 1679 | */ 1680 | return false; 1681 | } 1682 | } 1683 | } 1684 | 1685 | /** 1686 | * ------------------------- 1687 | * End Extra Tools/Functions 1688 | * ------------------------- 1689 | */ 1690 | } 1691 | --------------------------------------------------------------------------------