PHP For Beginnersチュートリアル　その15　会員登録フォームにパスワードリセットを実装する

##このシリーズの目的

体系的なwebコーディングの訓練ができるようになるためにPHPの初学のきっかけかつ、PHPでログインフォームやフォームを実装することができるようになるために

[PHP For Beginners](https://www.youtube.com/playlist?list=PLgt-G5tEOlKX4LKrBCYfjcyrp7wmnRdAL)

上記のチュートリアルを進めているのでその備忘録。

[前回](https://qiita.com/shitikakei/items/8604cf063639fbd8ee94)

##内容

**今回のチュートリアル**

[How To Create Forgot Password System In PHP & MySQLi [2018]](https://www.youtube.com/watch?v=JQkhoBxyOjM&list=PLgt-G5tEOlKX4LKrBCYfjcyrp7wmnRdAL&index=17)

**このチュートリアルでやること**

・ログインフォームにパスワードリセットを実装する
(その12で作成した成果物に追加する）

**成果物**

```php:forgotPassword.php

<?php 

	$msg = "";
	use PHPMailer\PHPMailer\PHPMailer;
	use PHPMailer\PHPMailer\Exception;
	use PHPMailer\PHPMailer\SMPT;

	require_once 'function.php';


	if (isset($_POST['email'])) {
		$conn = new mysqli('localhost','root','','register');

		$email = $conn->real_escape_string($_POST['email']);

		$sql = $conn->query("SELECT id FROM users WHERE email='$email' ");
		if ($sql->num_rows > 0) {

			$token = generateNewString();
			
			$conn->query("UPDATE users SET token='$token',
							tokenExpire = DATE_ADD(NOW(), INTERVAL 5 MINUTE)
							WHERE email = '$email'
				");

			mb_language("japanese");
			mb_internal_encoding("UTF-8");
			require 'vendor/autoload.php';
			require 'Mailtrap-config.php';


			    $mail = new PHPMailer();

			    // Server

			    $mail->SMTPDebug = 0; //本番では0とかにする。
			    $mail->isSMTP();
			    $mail->SMTPAuth = true;
			    $mail->Host = MAIL_HOST;
			    $mail->Username = MAIL_USERNAME;
			    $mail->Password = MAIL_PASSWORD;
			    $mail->SMTPSecure = MAIL_ENCRPT;
			    $mail->Port = SMTP_PORT;

			    // Recipients
			    $mail->setFrom(FROM_MAIL);
				// $toname = mb_encode_mimeheader("$name", 'ISO-2022-JP', 'B', "\n");
			    $mail->addAddress($email);
			    // $mail->addAttachment($attachment);

			    // Content
			    $mail->Subject = mb_encode_mimeheader("Reset Your Password", "ISO-2022-JP", "UTF-8");
			    $mail->Body = mb_convert_encoding("
					パスワードリセットのリクエストがありましたので以下のリンクをクリックしてパスワードのリセットを行ってください。:<br><br>
					<a href='http://localhost/Laravel/PHPMailer/Training/phptutorial17/resetPassword.php?email=$email&token=$token'>パスワードリセットはこちらから</a><br>

					七花 京
				","JIS","UTF-8");
			    $mail->CharSet = 'ISO-2022-JP';
			    $mail->Encoding = "7bit";

			    // Select HTML or NOT

			    $mail->isHTML(true);

			   if ($mail->send())
					exit(json_encode(array("status" => 1, "msg" => 'Please Check Your Email Inbox!')));
			    
			     else 
					exit(json_encode(array("status" => 0, "msg" => 'Something Wrong Just Happened')));

				} else
				 	exit(json_encode(array("status" => 0, "msg" => 'Please Check Your Input!')));
	}

 ?>



 <!DOCTYPE html>
 <html>
 <head>
 	<title>Forgot Password?</title>
 	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
 	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js">
    </script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" type="text/javascript">
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js">
    </script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js">
    </script>
    <link rel="stylesheet" type="text/css" href="css/forgotPassword.css">
 </head>

 <body>
 	<div class="container">
 		<div class="row justify-content-center">
 			<div class="form col-md-6">
 				<form action="forgotPassword.php" method="post" enctype="multipart/form-data">
					<input  class="form-control" id="email" placeholder="Your Email Address"><br><br>
					<input type="button" class="btn btn-primary" value="Reset Your Password"><br><br>

					<p id="response"></p>
 				</form>
 			</div>
 		</div>
 	</div>

 	<script src="https://code.jquery.com/jquery-3.4.1.min.js"integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="crossorigin="anonymous"></script>

 	<script>
 		let email = $('#email');

 			$(document).ready(function () {
 				$(".btn-primary").on('click',function () {
 				if(email.val() !="") {

 					email.css("border","1px solid green");

 					$.ajax({
 						url:'forgotPassword.php',
 						method:'POST',
 						dataType:'json',
 						data:{
 							email:email.val()
 						}, success:function(response) {
 							if (!response.success) 
 								$("#response").html(response.msg).css('color','red');

 							else

 							$("#response").html(response.msg).css('color','green');
 						}
 					});

 				} else
 					email.css("border","1px solid red");
 				
 			});
 		});
 	</script>
 </body>
 </html>
```

```php:function.php

<?php 
	function generateNewString($len = 10) {
		$token = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789!$/()*';
		$token = str_shuffle($token);
		$token = substr($token, 0, $len);

		return $token;

	}

	function redirectToLoginPage() {
	header('location:login.php');
 	exit();
}

 ?>

```

```php:resetPassword.php


<?php
	require_once "function.php";

	if (isset($_GET['email']) && isset($_GET['token'])) {
		$conn = new mysqli('localhost','root','','register');

		$email = $conn->real_escape_string($_GET['email']);
		$token = $conn->real_escape_string($_GET['token']);

		$sql = $conn->query("SELECT id FROM users WHERE
			email='$email' AND token='$token' AND token<>'' AND tokenExpire > NOW()
		");

		if ($sql->num_rows > 0) {
			$newPassword = generateNewString();
			$newPasswordEncrypted = password_hash($newPassword, PASSWORD_BCRYPT);
			$conn->query("UPDATE users SET token='', password = '$newPasswordEncrypted'
				WHERE email='$email'
			");

			echo "Your New Password Is $newPassword<br><a href='login.php'>Click Here To Log In</a>";
		} else
			redirectToLoginPage();
	} else {
		redirectToLoginPage();
	}
?>

```


## 動作

https://youtu.be/oHF7gYZt0RA

## 手順

・大枠のアルゴリズムを考える

1.まず、パスワードリセット申請フォーム(以下forgotPassword.php)に入力されたEmailアドレスが、登録情報が登録されているテーブルに存在するか検証


2.検証の結果、存在するのであれば任意の時間制限付きのトークンを発行し、再登録フォームへのリンク誘導を添えたEmailを送信する。
（その12でやったEmail認証リンクの仕組みに時間制限トークンを導入する）

3.リンクをクリックした際にトークン及びEmail認証が正常に通った場合、ランダム文字列でパスワードを作りそれを新しいパスワードとして、成功メッセージとともに画面上に表示し、同時にそれをハッシュ化してデータベースに新しいパスワードとして登録し直す。

なお、通常の実装の場合はこれの後にユーザーがログインして任意のパスワードに自分で登録し直すかまたは、3の段階でユーザーに新しいパスワードを登録させることが考えられる。


・各段階でのアルゴリズムを考える

＊データベースの接続などは割愛。

1.forgotPassword.php
→
・フォームに入力された文字列をエスケープする
・データベースにアクセスする
・エスケープした文字列を変数に代入する
・メールアドレスを検索フォームにし、入力されたメールアドレスがテーブルに存在する時はidを返す。存在しない時はエラーメッセージを表示する

・idが返ってきたらトークンを生成し、検索フォームをメールアドレスにし、データベースに再度アクセスして該当するメールアドレスが存在するデータに時間制限付きのトークンを発行する

・認証リンク付きのメールアドレスを送信し、送信完了の旨のメッセージを表示する。メールが送信できない場合はエラーメッセージを表示する

・PHPMaillerなどを用いて、テーブルに登録されたユーザー宛に認証リンクが記載されたメールを送信する。

2.function.php
→
・ランダム文字列を生成する処理とリダイレクト処理を書いておいて、requireなどで呼び出せるようにしていおく。
文字列生成とリダイレクトはこれまでのチュートリアルでやったことなのでここでは割愛。

3.resetPassword.php
→
・[その12](https://qiita.com/shitikakei/items/4f20491751107bd99e2f) で作ったものと同じ処理をさせる。

・トークンとメールアドレスを取得する(認証リンクが踏まれているか確認する）。取得できたらデータベースに接続し、取得したメールアドレス及びトークンをエスケープして変数に代入する

・データベースに接続し、メールアドレス・トークン(空ではないという条件を付随する)・トークンに設定された有効期限が現在の時刻より先であるという条件で検索しする

・該当するデータが有ればデータベースに接続し、新しいパスワードを生成する。それをハッシュ化し、データのトークンを削除し、新しく生成したパスワードを登録する。該当するデータがない場合はログインフォームへリダイレクトする

・生成したパスワードとログインフォームへのリンクを表示する



## 今回のコードの注釈

```php:tokenExpire周りについて

>query("UPDATE users SET token='$token',
		tokenExpire = DATE_ADD(NOW(), INTERVAL 5 MINUTE)
						WHERE email = '$email'
				");

```
今回新しく出てきたのはDATE_ADD(NOW(), INTERVAL)の形。
DATE_ADDは日付と時刻を取得する関数、第一引数に取得する日付及び時刻を設定する。
今回のNOW()は現在の日付と時刻を取得する、つまり処理が行われた時点での日付と時刻を取得することになる。
INTERVALには任意の時間を設定することで第一引数に指定した時刻から設定した分先の時間を取得する。
つまり、今回はINTERVALに5分と設定したので処理をが行われた時点の日付と時刻から5分先の時間を取得し、tokenExpaireに代入するということになる。

```php:<>演算子

$sql = $conn->query("SELECT id FROM users WHERE
			email='$email' AND token='$token' AND token <> '' AND tokenExpire > NOW()
		");


```
<>は左辺と右辺が等しくないということを表す比較演算子及び配列演算子。
今回は万が一他者にメールアドレスと認証リンク及び空のトークン利用されてパスワードをリセットされないようにするために用いている。