iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 22
2
Modern Web

寫給朋友的 PHP 從 0 到 100 實戰教程系列 第 22

Day 22. PHP教學: 使用 monolog 來追蹤問題

廢話不多說,直接來看範例,先來優化忘記密碼送出 email 後的接收頁

controller/do_forget.php

<?php
//if form has been submitted process it
if(isset($_POST['submit']) AND isset($_POST['email'])) 
{
  $email = $_POST['email'];
  $postVeridator = new PostVeridator();
  $userVeridator = new UserVeridator();
  $userAction = new UserAction();
  $log = new Log();
  if($postVeridator->isValidEmail($email)) { // 信箱是否合法
    if($userVeridator->isEmailDuplicate($email)) { // 信箱是否存在
      try {
        $resetToken = $userAction->getResetToken(); // 創建 Token 並存到資料庫
        $userAction->sendResetEmail($resetToken); // 用 Token 組出重置信件並寄出
        $userAction->redir2login(); // 重導向登入頁並顯示成功
      } catch(PDOException $e) {
        $error[] = $e->getMessage();
        $log->error(__FILE__, json_encode($error));
      }
    }else{ // 不存在就假裝成功, 避免被試出會員信箱
      $log->warning(__FILE__, 'WRONG EMAIL: ' .$email);
      sleep(rand(1,2));
      $userAction->redir2login(); // 重導向登入頁並顯示成功
      exit;
    }
  } else { // 不合法就顯示踢回上一頁顯示錯誤
    header('Location: ' . $_SERVER['HTTP_REFERER']);
    exit;
  }
}else{ // 非正常進入就踢回首頁
  header('Location: ' . Config::BASE_URL);
  exit;
}

所需要新材料有:

veridators/PostVeridator.php

<?php 
class PostVeridator {

    public function isValidEmail( $email )
    {
        $data_array['email'] = $email;
        $gump = new GUMP();
        $data_array = $gump->sanitize($data_array); 
        $validation_rules_array = array(
            'email'    => 'required|valid_email'
        );
        $gump->validation_rules($validation_rules_array);
        $filter_rules_array = array(
            'email' => 'trim|sanitize_email'
        );
        $gump->filter_rules($filter_rules_array);
        $validated_data = $gump->run($data_array);
        if($validated_data === false) {
            $error = $gump->get_readable_errors(false);
            $msg = new \Plasticbrain\FlashMessages\FlashMessages();
            foreach( $error as $e) {
                $msg->error($e);
            }
            return false;
        } else {
            return true;
        }
    }
}

veridators/UserVeridator.php

<?php
/**
 * 耦合使用 Database 物件進行資料庫驗證 username 與 email 是否已存在於資料庫
 */
class UserVeridator {

    private $error;

    /**
     * 驗證是否已登入
     */
    public static function isLogin($username){
        if($username != ''){
            return true; 
        } else{
            return false; 
        }
    }

    /**
     * 可取出錯誤訊息字串陣列
     */
    public function getErrorArray(){
        return $this->error;
    }

    /**
     * 驗證二次密碼輸入是否相符
     */
    public function isPasswordMatch($password, $passwrodConfirm){
		if ($password != $passwrodConfirm){
            $this->error[] = 'Passwords do not match.';
            return false;
        }
		return true;
    }

    /**
     * 驗證帳號密碼是否正確可登入
     */
    public function loginVerification($username, $password){
        $result = Database::get()->execute('SELECT * FROM members WHERE active = "Yes" AND username = :username', array(':username' => $username));
        if(isset($result[0]['memberID']) and !empty($result[0]['memberID'])) {
            $passwordObject = new Password();
            if($passwordObject->password_verify($password,$result[0]['password'])){
                return true;
            }
        }
        $this->error[] = 'Wrong username or password or your account has not been activated.';
        return false;
    }

    /**
     * 驗證帳號是否已存在於資料庫中
     */
    public function isUsernameDuplicate($username){
        $result = Database::get()->execute('SELECT username FROM members WHERE username = :username', array(':username' => $username));
        if(isset($result[0]['username']) and !empty($result[0]['username'])){
          $this->error[] = 'Username provided is already in use.';
          return false;
        }
		return true;
    }

    /**
     * 驗證此帳號 ID 跟 開通碼 hash 是否已存在於資料庫中
     */
    public function isReady2Active($id, $active){
        $result = Database::get()->execute('SELECT username FROM members WHERE memberID = :memberID AND active = :active', array(':memberID' => $id, ':active' => $active));
        if(isset($result[0]['username']) and !empty($result[0]['username'])){
          return true;
        }else{
          $this->error[] = 'Username provided is already in use.';
          return false;
        }
    }

    /**
     * 驗證信箱是否已存在於資料庫中
     */
    public function isEmailDuplicate($email){
        $result = Database::get()->execute('SELECT email FROM members WHERE email = :email', array(':email' => $email));
        if(isset($result[0]['email']) AND !empty($result[0]['email'])){
            $this->error[] = 'Email provided is already in use.';
            return true;
        }
		return false;
    }
}

libraries/Ip.php 專門取得網站用戶的 IP 已便存到 LOG

<?php
class Ip {
    static function get(){
        if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
          $ip = $_SERVER["HTTP_CLIENT_IP"];
        } elseif (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) {
          $ip = $_SERVER["HTTP_X_FORWARDED_FOR"];
        } else {
           $ip = $_SERVER["REMOTE_ADDR"];
        }
        return $ip;
    }
}

action/User.php 幫助我們對使用者操作封裝

<?php
class UserAction {
    function getResetToken(){
        $data_array['resetComplete'] = 'No';
        $data_array['resetToken'] = md5(rand().time());
        Database::get()->update('members', $data_array, "memberID", $memberID);
        $resetToken = $data_array['resetToken'];
    }

    function sendResetEmail($resetToken){
        $body = "<p>Someone requested that the password be reset.</p>
        <p>If this was a mistake, just ignore this email and nothing will happen.</p>
        <p>To reset your password, visit the following address: <a href='".Config::BASE_URL."reset/$resetToken'>".Config::BASE_URL."reset/$resetToken</a></p>";
        $mail = new Mail(Config::MAIL_USER_NAME, Config::MAIL_USER_PASSWROD);
        $mail->setFrom(Config::MAIL_FROM, Config::MAIL_FROM_NAME);
        $mail->addAddress($email);
        $mail->subject("Password Reset");
        $mail->body($body);
        $mail->send();
    }

    function redir2login(){
        $msg = new \Plasticbrain\FlashMessages\FlashMessages();
        $msg->success("Please check your inbox for a reset link.");
        header('Location: '.Config::BASE_URL.'login');
        exit;
    }
}

libraries/Log.php

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;

class Log {
    function error($page, $msg){        
        $logger = new Logger($page);
        $logger->pushHandler(new StreamHandler('log/error.log', Logger::DEBUG));
        $logger->pushHandler(new FirePHPHandler());
        $logger->error(Ip::get().': '.$msg);
    }

    function info($page, $msg){ 
        $logger = new Logger($page);
        $logger->pushHandler(new StreamHandler('log/info.log', Logger::DEBUG));
        $logger->pushHandler(new FirePHPHandler());
        $logger->info(Ip::get().': '.$msg);
    }

    function warning($page, $msg){
        $logger = new Logger($page);
        $logger->pushHandler(new StreamHandler('log/warning.log', Logger::DEBUG));
        $logger->pushHandler(new FirePHPHandler());
        $logger->warning(Ip::get().': '.$msg);
    }
}

我們創了一個更容易使用 monolog 的 Log class 物件
在使用上只需要

$log = new Log();
$log->error(__FILE__, 'error msg put here');

另外也創了一個資料夾專門放 log 裡面有三個檔案

log/error.log
log/warning.log
log/info.log

記得要對 log 資料夾與其底下所有檔案變更權限成可讀可寫,指令是:

chmod -R 777 log

這樣一來,我們就可以在發生問題時快速追蹤 log 找到錯誤,或是抓出想亂嘗試 email 的 IP 位置,防患未然,以免發生狀況時完全沒有 log 紀錄那真的蠻悲劇的...


上一篇
Day 21. PHP教程: 優化架構與實作 flash session 附原始碼
下一篇
Day 23. PHP教學: 串接簡訊王 API
系列文
寫給朋友的 PHP 從 0 到 100 實戰教程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
e3561025
iT邦新手 5 級 ‧ 2021-11-08 23:45:17

仔細確認後才發現大大的 userVeridator 的方法中,幾個確認資料庫數值重複的部分的回傳值跟舊版本的完全相反了,所以userVeridator如果不跟著修改,forget是不會寄信出去,因為判定部分剛好相反,所以打了正確的mail會被當作不存在而收不到信件

0
e3561025
iT邦新手 5 級 ‧ 2021-11-09 13:58:23

class UserAction的方法有少寫一些東西,會導致無法寄出信件
function getResetToken($email){
$result = Database::get()->execute('SELECT memberID FROM members WHERE email = :email', array(':email' => $email));
$memberID = $result[0]['memberID'];
$data_array['resetComplete'] = 'No';
$data_array['resetToken'] = md5(rand().time());
Database::get()->update('members', $data_array, "memberID", $memberID);
$resetToken = $data_array['resetToken'];
return $resetToken;
}
這樣可以取得memberID,並且回傳resetToken回去
而 function sendResetEmail($resetToken,$email){...}
兩個方法都須加上$email才能使用,不然會因為沒有$email而不知傳送到哪(在do_forget中因為下一行是直接跳轉到login網頁,因此報錯訊息沒看到就直接轉頁面了)

在 do_forget.php
因應上面方法的修改,要跟著改成這樣,把參數加上去
$resetToken = $userAction->getResetToken($email); // 創建 Token 並存到資料庫
$userAction->sendResetEmail($resetToken,$email); // 用 Token 組出重置信件並寄出

我要留言

立即登入留言