W11_作業一實作_完成留言板


Posted by Christy on 2021-08-30

1. 更新暱稱

  1. 先做更新暱稱的介面

  2. 再寫 update_user.php 功能

sql 語法:

$sql = "UPDATE users set nickname=? WHERE username=?";

使用 left join 遇到的問題

如果用原本的 sql 語法,會有選出來的欄位被覆蓋的問題,所以可以給一個別名

$stmt = $conn->prepare(
  'SELECT C.id as id, C.content as content, ' . 
  'C.created_at as created_at, U.nickname as nickname ' . 
  'FROM comments as C ' .
  'left join users as U on C.username = U.username ' . 
  'ORDER BY C.id DESC'
);

! 絕對不要把 comments 裡面的 username 設成唯一

Visual JOIN

說明

sql: SELECT * FROM comments left join users on comments.username = users.username order by comments.id desc;

要顯示評論表裡面所有的資料,因此使用 left join 語法,與使用者表做連結,以 username 為關聯的橋樑。


2. 編輯留言實作

BE101 -> 編輯留言功能

  1. 首頁的介面再利用

  2. 先把資料庫的留言撈出來

  3. 利用 $id = $_GET['id']; 選到留言

  4. 在修改留言區,顯示還沒修改的留言

  5. 接下來是重點,因為送出表單以後,處理更新留言的 php 檔案,必須要知道要更新哪一個留言,因此要在後面用一個隱藏的 input 把 id 放進去

<input type="hidden" name="id" value="<?php echo $row['id']; ?>">


3. 刪除留言

功能描述:

按下刪除按鈕,可以把留言刪掉

利用 $_GET 的方式,在網址帶參數 id=? 來刪除留言

實作流程:

  1. 在 index.php 做刪除介面(其實就是一個按鈕啦)

  2. 到 delete_comment.php 檔案裡面,修改 sql

  3. 分成 hard delete, soft  delete

    3.1 在資料庫 comments 新增一個欄位叫 is_deleted,型態設成 TINYINT,預設值是 NULL

    3.2 把 sql 改成 update comments set is_deleted=1 where id=?

    3.3 在 index.php 前台頁面,把 sql 加上「顯示未刪除的留言」
    where C.is_deleted is NULL

    • 要加在 left join 後面

code_3. 刪除留言

<?php
  session_start(); 
  require_once('conn.php');
  require_once('utils.php');

  if (empty($_GET['id'])) {
    header('Location: index.php?errCode=1');
    die();
  }

  $username = $_SESSION['username'];
  $user = getUserFromUsername($username);
  $id = $_GET['id'];

  $sql = "UPDATE comments set is_deleted=1 WHERE id=? AND username=?";
  if (isAdmin($user)) {
    $sql = "UPDATE comments set is_deleted=1 WHERE id=?";
  }

  $stmt = $conn->prepare($sql);

  if (isAdmin($user)) {
    $stmt->bind_param('i', $id);
  } else {
    $stmt->bind_param('is', $id, $username);
  }

  $result = $stmt->execute();

  if (!$result) {
    die($conn->error);
  }

  header('Location: index.php');
?>

4. 分頁功能

最後成品

在留言板的最後加上

總共有 15 筆留言,頁數:2/3

首頁  上一頁 下一頁  最後一頁

功能描述:

一頁有五筆資料,下面有分頁介面可以換頁

sql 語法:offset, limit

SELECT * FROM comments ORDER BY id DESC LIMIT 5 OFFSET 0

limit: 顯示幾筆

offset: 跳過幾筆

  1. 怎麼計算 page, offset 與 limit 之間的關係?

第一頁:1 - 5
第二頁:6 - 10

page = 1

limit = 5

offset = (page - 1) * limit

遇到的問題:

如果程式碼寫成下面的形式,要記得字串連接之前的空格問題

$stmt = $conn->prepare(
  'SELECT C.id as id, C.content as content, ' . 
  'C.created_at as created_at, U.nickname as nickname, U.username as username ' . 
  'FROM comments as C ' .
  'left join users as U on C.username = U.username ' . 
  'WHERE C.is_deleted IS NULL ' .
  'ORDER BY C.id DESC ' . 
  'limit ? offset ?'
);
  1. 總共有幾筆資料?

在想要顯示的地方,再做一次抓資料

sql 語法:

select count(id) as count from comments where is_deleted is NULL

犯的錯:sql 語法 沒有寫到 from comments

<div class="board__hr"></div>
<?php
  $stmt = $conn->prepare(
    'select count(id) as count from comments where is_deleted is NULL'
  );

  $result = $stmt->execute();
  $result = $stmt->get_result();
  $row = $result->fetch_assoc();
  $count = $row['count'];
?>

<div>
  <div>總共有 <?php echo $count ?> 筆資料,</div>
</div>
  1. 1/3: 現在所在頁碼 / 總共幾頁

$total_page = ceil($count / $limit);

ceil(): 無條件進位

  1. 做介面

用文字做就好了

首頁 上一頁 下一頁 最後一頁

實作錯誤紀錄:

  1. sql 語法不熟,忘記要加 order by 還有 limit 5 跟 offset 5 中間沒有逗號

SELECT * FROM comments ORDER BY id DESC LIMIT 5 OFFSET 0

  1. 又犯了字串連接之前的空格問題,可以參考上面的「如果程式碼寫成下面的形式,要記得」那一段

  2. sql 拿資料語法不熟:忘了要 $row

<?php 
  $stmt = $conn->prepare('SELECT count(id) as count FROM comments)');

  $result = $stmt->execute();
  $result = $stmt->get_result();
  $row = $result->fetch_assoc();
  $count = $row['count'];
?>
  1. 要搞清楚「總共頁數」、「總共留言數」還有怎麼計算它們

  2. 首頁無法顯示留言,是因為上一頁,下一頁的 if else 寫錯了,謎底解開!

  3. 新增、編輯、刪除後,到首頁時不會顯示留言,原因是上面顯示留言的程式碼寫錯了,應該要寫成下面那樣,可是我在寫 !empty($_GET['page'] 的時候,寫成 !empty($page] 了。

$page = 1;
if (!empty($_GET['page'])) {
  $page = intval($_GET['page']);
}

code_4. 如何做分頁功能

<?php
  session_start();
  require_once('conn.php');
  require_once('utils.php');

  $username = NULL;
  $user = NULL;

  if (!empty($_SESSION['username'])) {
    $username = $_SESSION['username'];
    $user = getUserFromUsername($username);
  }

  $page = 1;
  if (!empty($page)) {
    $page = intval($_GET['page']);
  }
  $limit = 5;
  $offset = ($page - 1) * $limit;

  $stmt = $conn->prepare(
    'SELECT C.id as id, C.content as content, ' . 
    'C.created_at as created_at, U.nickname as nickname, U.username as username ' . 
    'FROM comments as C ' .
    'left join users as U on C.username = U.username ' . 
    'WHERE C.is_deleted IS NULL ' .
    'ORDER BY C.id DESC ' .
    'limit ? offset ?'
  );

  $stmt->bind_param('ii', $limit, $offset);
  $result = $stmt->execute();
  if (!$result) {
    die('error:' . $conn->error);
  }
  $result = $stmt->get_result();
?>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>demo</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <header class="warning">
    This is a practice for back end, please DO NOT register!
  </header>
  <main class="board">
    <?php if (!$username) { ?>
      <div>
        <a class="board__btn" href="register.php">註冊</a>
        <a class="board__btn" href="login.php">登入</a>
      </div>
    <?php } else { ?>
      <a class="board__btn" href="logout.php">登出</a>
      <span class="board__btn update-nickname">編輯暱稱</span>

      <form class="board__nickname-form board__new-comment-form hide" method="POST" action="update_user.php">
        <div class="board__nickname">
          <span>新的暱稱:</span>
          <input type="text" name="nickname">
        </div>
        <input class="board__submit-btn" type="submit" />
      </form>
      <h3>你好!<?php echo $user['nickname']; ?></h3>
    <?php } ?>
    <h1 class="board__title">
      Comments
    </h1>
    <?php
      if (!empty($_GET['errCode']))  {
        $code = $_GET['errCode'];
        $msg =  'Error';
        if ($code === '1') {
          $msg = '資料不齊全';
        }
        echo '<h2 class="error">' . $msg . '</h2>';
      }
    ?>
      <form class="board__new-comment-form" method="POST" action="handle_add_comment.php">
        <textarea name="content" id="" cols="30" rows="10"></textarea>
        <?php if ($username) { ?>
          <input class="board__submit-btn" type="submit" />
        <?php } else { ?>
          <h3>請登入發布留言</h3>
        <?php } ?>
      </form>
      <div class="board__hr"></div>
    <section>
      <?php  while ($row = $result->fetch_assoc()) { ?>
        <div class="card">
        <div class="card__avatar">
        </div>
        <div class="card__body">
          <div class="card__info">
            <span class="card__author">
              <?php echo escape($row['nickname']); ?>
              (@<?php echo escape($row['username']); ?>)
            </span>
            <span class="card__time">
              <?php echo escape($row['created_at']); ?>
            </span>
            <?php if ($row['username'] === $username) { ?>
              <a href="update_comment.php?id=<?php echo $row['id']; ?>">編輯</a>
              <a href="delete_comment.php?id=<?php echo $row['id']; ?>">刪除</a>
            <?php } ?>  
          </div>
          <p class="card__content"><?php echo escape($row['content']); ?></p>
        </div>
        </div>
      <?php } ?>
    </section>

    <div class="board__hr"></div>

    <?php
        $stmt = $conn->prepare(
          'SELECT count(id) as count FROM comments WHERE is_deleted is NULL'
        );
        $result = $stmt->execute();
        $result = $stmt->get_result();
        $row = $result->fetch_assoc();
        $count = $row['count'];
        $total_page = ceil($count / $limit);
    ?>
    <div class="page-info">
      <span>總共有 <?php echo $count ?> 筆留言,頁數:</span>
      <span><?php echo $page ?> / <?php echo $total_page ?></span>
    </div>
    <div class="paginator">

      <?php if ($page != 1) { ?>
        <a href="index.php?page=1">首頁</a>
        <a href="index.php?page=<?php echo $page - 1 ?>">上一頁</a>
      <?php } ?>

      <?php if ($page != $total_page) { ?>
        <a href="index.php?page=<?php echo $page + 1 ?>">下一頁</a>
        <a href="index.php?page=<?php echo $total_page ?>">最後一頁</a>
      <?php } ?>
    </div>
  </main>
  <script>
    var btn = document.querySelector('.update-nickname')
    btn.addEventListener('click', function() {
      var form = document.querySelector('.board__nickname-form')
      form.classList.toggle('hide')
    })
  </script>
</body>
</html>

5. 權限管理功能

  • 在修改留言跟刪除留言的時候,加上權限檢查。

因為是用網址帶參數的關係,就算是我們把刪除、編輯的按鈕藏起來了,知道參數還是可以改到留言。

解決辦法:在寫 sql 的時候,除了驗證原本有的 id 以外,再多驗證 username

原本的 sql 長這樣

$sql = "UPDATE comments set content=? WHERE id=?";

多寫一個驗證 username 的語法

$sql = "UPDATE comments set content=? WHERE id=? AND username=?";


6. 身份系統_admin編輯留言_資料庫新增 role、前端介面、後端權限

MTR05 -> 參考範例:hw1 身份系統上集(請先寫完作業再看)2':46?"

做身份系統,有三種身份,分別有不同權限

實作流程:

先決定身份系統要存在哪,通常是存在 users

  1. 在 users 裡面新增名稱叫 role 的表格,類型選 ENUM,在「編輯 ENUM/SET 值」設定以下三個值:
    ADMIN
    NORMAL
    BANNED

  2. 預設值選「定義」:NORMAL,然後執行。

  3. 如果是 admin 的話,應該要可以看得到「編輯」、「刪除」的按鈕,老師的做法是在 utils.php 裡面,新增一個函式叫做 hasPermission

  4. 在 hasPermission 這個函式裡面,老師傳了三個參數 "$user, $action, $comment",因為要達到的功能是「如果是 使用者是 admin 的話,就可以編輯、刪除所有的留言」

把上面那句描述拆開的話,就是 1) 使用者 2) 行為(編輯、刪除)3) 留言

讓 admin 在後端能修改資料

  1. handle_update_comment.php 裡面
    先抓到 $user = getUserFromUsername($username);

  2. 在 utils.php 裡面,寫一個 isAdmin()

function isAdmin($user) {
  return $user['role'] === 'ADMIN';
}
  1. handle_update_comment.php 裡把 admin 權限打開

特別注意這兩行,利用 isAdmin() 這個函式,把權限區分開來

$sql = "UPDATE comments set content=? WHERE id=? AND username=?";

$stmt->bind_param('si', $content, $id);


遇到的困難:

  1. 建議用 php 包起來的程式碼,前後都用 <?php ?> 這個形式,比較不會出錯

  2. 程式碼都跟老師的一樣,在 admin 底下的身份還是沒有顯示編輯跟刪除這兩個

    • 要不函式寫錯,要不 index.php 裡面的那一行程式碼有錯

結果都不是上面說的那種兩種情形,最後解法是把資料的 role 刪掉重做就好了

老師 debug 的方法是,在 utils 的函式裡面,把資料印出來看

  1. print_r($user['role']);

  2. print_r($user['role'] === 'ADMIN');,但是這個指令無效

  3. print_r($user['role'] === 'ADMIN' ? 'true' : 'false');
    回傳的是 false,接著我把所有程式碼都對了一遍,確認我真的沒有打錯字,然後我試著把資料庫的 role 重新設一遍,就好了

  4. 在 index.php 的時候,抓不到留言,只有後面有帶參數的才抓得到 -> 解法:暫時把所有 index.php 都改成 index.php?page=1,但這樣不是治本的辦法

會不會根本的問題就是在於上面的問題?所以我無法抓到東西?

天阿,我又重複犯了昨天的錯,就是 $_GET['page'] 沒有寫好,我寫成 $page,寫好以後,我的首頁就回來了

$page = 1;
if (!empty($_GET['page'])) {
$page = intval($_GET['page']);
}
  1. 在寫 hasPermission 函式時,我把 $comment['username'] 寫成 $username

我的邏輯是使用者登入後,跟資料庫比對,如果是同一個人,那就可以改他自己的留言。

但是這個邏輯是錯的,因為這就跟一般的登入一樣啊,那我要怎麼驗證身份為 NORMAL?

老師這裡的邏輯是,如果是一般使用者,文章的 username 跟資料庫裡面的 username 一樣的話,那就可以改自己的留言

這個文章是我在 hasPermission 函式設的變數,實際上我在 index.php 用的時候,輸入的是 $row,也就是所有留言。

if ($user['role'] === 'NORMAL') {
  return $comment['username'] === $user['username'];
}
// 在 index.php 裡面執行這個函式,老師把 $action 代換成 'modify',但我發現打 $action 還是可以跑,雖然不懂為什麼

function hasPermission ($user, $action, $comment) {
  if ($user['role'] === 'ADMIN') {
   return true;
  }

  if ($user['role'] === 'NORMAL') {
    return $comment['username'] === $user['username'];
  }
}
  1. 當我是 admin 身份時,後端不能改資料的原因:

在 handle_update_comment.php 裡面,我必須先拿到 $user 的資料,也就是說必須加這行 $user = getUserFromUsername($username);
下面的函式 isAdmin 才可以執行

  • 如果要練習,要特別注意檔案不要開錯。
  1. 要特別注意 handle_update_comment.php 程式碼執行的順序,尤其是在修改下面這兩行的時候,注意邏輯及順序

$sql = "UPDATE comments set content=? WHERE id=? AND username=?";

$stmt->bind_param('sis', $content, $id, $username);


code_6. 身份系統_admin編輯留言_資料庫新增 role、前端介面、後端更改

<?php
  session_start(); 
  require_once('conn.php');
  require_once('utils.php');

  if (empty($_POST['content'])) {
    header('Location: update_comment.php?errCode=1&id=' . $_POST['id']);
    die();
  }

  $username = $_SESSION['username'];
  $user = getUserFromUsername($username);
  $id = $_POST['id'];
  $content = $_POST['content'];

  $sql = "UPDATE comments set content=? WHERE id=? AND username=?";
  if (isAdmin($user)) {
    $sql = "UPDATE comments set content=? WHERE id=?";
  }

  $stmt = $conn->prepare($sql);

  if (isAdmin($user)) {
    $stmt->bind_param('si', $content, $id);
  } else {
    $stmt->bind_param('sis', $content, $id, $username);
  }

  $result = $stmt->execute();

  if (!$result) {
    die($conn->error);
  }

  header('Location: index.php');
?>

6. 身份系統_admin 刪除留言

MTR05 -> 參考範例:hw1 身份系統上集(請先寫完作業再看)14':52"

使用跟編輯一樣的邏輯來做刪除功能

Normal 不用做,因為就跟原本的功能一樣


7. 身份系統_banned 功能

MTR05 -> 參考範例:hw1 身份系統上集(請先寫完作業再看)16':48"

功能描述:身爲 banned 的使用者,不能新增留言,只能編輯跟刪除自己的留言

實作:

  1. 先把 hasPermission() 裡的 banned 功能寫完,邏輯是「如果使用者被停權,那他的權限只有編輯跟刪除留言,不能新增」-> 「如果不是新增,就回傳 true」
if ($user['role'] === 'BANNED') {
  return $action !== 'create';
}
  1. 停權使用者看不到發布留言按鈕

「如果使用者登入且沒有新增權限的話,就出現『你已被停權』訊息」

  1. 後端權限設定:
  • 已經被停權的使用者,比較難測這個功能

在 handle_add_comment.php 裡面,先導入 $user,再用 hasPermission()

if (!hasPermission($user, 'create', NULL)) {
  header('Location: index.php');
  exit();
}
  1. banned 的使用者,可以編輯、刪除自己的留言

把 hasPermission banned 的部分改成下面

if ($user['role'] === 'BANNED') {
  return $action !== 'create' && $comment['username'] === $user['username'];
}
  • 能夠自己解決問題真的太爽了!

code_7. banned 編輯、刪除權限

utils.php

<?php
  require_once('conn.php');

  function generateToken() {
    $s = '';
    for ($i=1; $i<=16; $i++) {
      $s .= chr(rand(65, 90));
    }
    return $s;
   }

  function getUserFromUsername($username) {
    global $conn;
    $sql = sprintf(
      "SELECT * from users WHERE username = '%s'", $username
    );
    $result = $conn->query($sql);
    $row = $result->fetch_assoc();
      return $row;
   }

   function escape ($str) {
     return htmlspecialchars($str, ENT_QUOTES);
   }

   function hasPermission ($user, $action, $comment) {
     if ($user['role'] === 'ADMIN') {
       return true;
     }

     if ($user['role'] === 'NORMAL') {
       if ($action === 'create') return true;
       return $comment['username'] === $user['username'];
     }

     if ($user['role'] === 'BANNED') {
       return $action !== 'create' && $comment['username'] === $user['username'];
     }
  }

    function isAdmin($user) {
      return $user['role'] === 'ADMIN';
    }
?>

handle_add_comment.php

<?php
  session_start(); 
  require_once('conn.php');
  require_once('utils.php');

  if (empty($_POST['content'])) {
    header('Location: index.php?errCode=1');
    die('資料不齊全');
  }

  $username = $_SESSION['username'];
  $user = getUserFromUsername($username);

  if (!hasPermission($user, 'create', NULL)) {
    header('Location: index.php');
    exit;
  }

  $content = $_POST['content'];
  $sql = "INSERT INTO comments(username, content) VALUES(?, ?)";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('ss', $username, $content);
  $result = $stmt->execute();

  if (!$result) {
    die($conn->error);
  }

  header('Location: index.php');
?>

delete_comment.php

<?php
  session_start(); 
  require_once('conn.php');
  require_once('utils.php');

  if (empty($_GET['id'])) {
    header('Location: index.php?errCode=1');
    die();
  }

  $username = $_SESSION['username'];
  $user = getUserFromUsername($username);
  $id = $_GET['id'];

  $sql = "UPDATE comments set is_deleted=1 WHERE id=? AND username=?";

  if (isAdmin($user)) {
    $sql = "UPDATE comments set is_deleted=1 WHERE id=?";
  }

  $stmt = $conn->prepare($sql);

  if (isAdmin($user)) {
    $stmt->bind_param('i', $id);
  } else {
    $stmt->bind_param('is', $id, $username);
  }
  $result = $stmt->execute();

  if (!$result) {
    die($conn->error);
  }

  header('Location: index.php');
?>

8. 身份系統_後台介面

MTR05 -> 參考範例:hw1 身份系統下集(請先寫完作業再看)

實作流程:

  1. 做後台介面
    只有 admin 可以進去的一個介面,裡面會顯示 users 的 id, role, nickname, username

1.1 新增一個檔案及做權限檢查

新增一個檔案 admin.php(複製 index.php)
如果沒有登入或登入身分不是 admin 就導回首頁

1.2 更改 sql 指令

1.3 做表格

| th  | th  | th  | th  | -> tr
|- - -|- - -|- - -|- - -|
| td  | td  | td  | td  | -> tr
|- - -|- - -|- - -|- - -|
| td  | td  | td  | td  | -> tr

<table>
   <tr>
     <th>id</th>
     <th>role</th>
     <th>nickname</th>
     <th>username</th>
   </tr>
  <?php  while ($row = $result->fetch_assoc()) { ?>
    <tr>
      <td><?php echo escape($row['id']); ?></td>
      <td><?php echo escape($row['role']); ?></td>
      <td><?php echo escape($row['nickname']); ?></td>
      <td><?php echo escape($row['username']); ?></td>
    </tr>
  <?php } ?>
</table>

1.4 在首頁新增管理後台的按鈕:
如果登入時的使用者是 admin 才看得到按鈕

  1. 後台資料處理:07'45"

2.1 複製檔案 handle_update_comment -> handle_update_role

2.2

遇到的困難

  1. 要先登入 admin,不然 實作的 1.2 權限檢查會一直把我導回 index.php

  2. table 的語法不夠熟悉、table 的外、內框線要用 collapse 才會是一條線

border: 1px dashed #ccc;
border-collapse: collapse;
  1. 在管理權限的選項上
    3.1 網址裡面 ADMIN 不能用引號包起來
    3.2 後面應該要印出 $row['id]
<a href="handle_update_role.php?role=ADMIN&id=<?php echo escape($row['id']) ?>">管理員</a>

9. 身份系統_後台後端

MTR05 -> 參考範例:hw1 身份系統下集(請先寫完作業再看)08'44"

功能描述:

接續上一個實作的想法,我們已經在 admin.php 裡面利用網址帶資訊的方式,把 id, role 用 get 傳進去了,接下來,我們要實作 handle_update_role 的功能面

在後台點擊三種不同身份選項,可以更改使用者的 role

實作流程:

  1. 實作 handle_update_role

1.1 先處理伺服器錯誤訊息,如果沒有帶到資訊,就 die()

if (
    empty($_GET['id']) ||
    empty($_GET['role'])
  ) {
    die('資料不齊全');
  }

code_9. 身份系統_後台後端

<?php
  session_start(); 
  require_once('conn.php');
  require_once('utils.php');

  if (
    empty($_GET['id']) ||
    empty($_GET['role'])
  ) {
    die('資料不齊全');
  }

  $username = $_SESSION['username'];
  $user = getUserFromUsername($username);
  $id = $_GET['id'];
  $role = $_GET['role'];

  if (!$user || $user['role'] !== 'ADMIN') {
    header('Location: admin.php');
    exit;
  }

  $sql = "UPDATE users set role=? WHERE id=?";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('si', $role, $id);

  $result = $stmt->execute();

  if (!$result) {
    die($conn->error);
  }

  header('Location: admin.php');
?>









Related Posts

Airflow 動手玩:(六)Airflow with Kuberentes Executor

Airflow 動手玩:(六)Airflow with Kuberentes Executor

[Day03]: 容器與映像技術原理

[Day03]: 容器與映像技術原理

閉包

閉包


Comments