1. 更新暱稱
先做更新暱稱的介面
再寫 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 -> 編輯留言功能
首頁的介面再利用
先把資料庫的留言撈出來
利用
$id = $_GET['id'];
選到留言在修改留言區,顯示還沒修改的留言
接下來是重點,因為送出表單以後,處理更新留言的 php 檔案,必須要知道要更新哪一個留言,因此要在後面用一個隱藏的 input 把 id 放進去
<input type="hidden" name="id" value="<?php echo $row['id']; ?>">
3. 刪除留言
功能描述:
按下刪除按鈕,可以把留言刪掉
利用 $_GET 的方式,在網址帶參數 id=? 來刪除留言
實作流程:
在 index.php 做刪除介面(其實就是一個按鈕啦)
到 delete_comment.php 檔案裡面,修改 sql
分成 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: 跳過幾筆
- 怎麼計算 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 ?'
);
- 總共有幾筆資料?
在想要顯示的地方,再做一次抓資料
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/3: 現在所在頁碼 / 總共幾頁
$total_page = ceil($count / $limit);
ceil(): 無條件進位
- 做介面
用文字做就好了
首頁 上一頁 下一頁 最後一頁
實作錯誤紀錄:
- sql 語法不熟,忘記要加 order by 還有 limit 5 跟 offset 5 中間沒有逗號
SELECT * FROM comments ORDER BY id DESC LIMIT 5 OFFSET 0
又犯了字串連接之前的空格問題,可以參考上面的「如果程式碼寫成下面的形式,要記得」那一段
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'];
?>
要搞清楚「總共頁數」、「總共留言數」還有怎麼計算它們
首頁無法顯示留言,是因為上一頁,下一頁的 if else 寫錯了,謎底解開!
新增、編輯、刪除後,到首頁時不會顯示留言,原因是上面顯示留言的程式碼寫錯了,應該要寫成下面那樣,可是我在寫
!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
在 users 裡面新增名稱叫 role 的表格,類型選 ENUM,在「編輯 ENUM/SET 值」設定以下三個值:
ADMIN
NORMAL
BANNED預設值選「定義」:NORMAL,然後執行。
如果是 admin 的話,應該要可以看得到「編輯」、「刪除」的按鈕,老師的做法是在 utils.php 裡面,新增一個函式叫做 hasPermission
在 hasPermission 這個函式裡面,老師傳了三個參數 "$user, $action, $comment",因為要達到的功能是「如果是 使用者是 admin 的話,就可以編輯、刪除所有的留言」
把上面那句描述拆開的話,就是 1) 使用者 2) 行為(編輯、刪除)3) 留言
讓 admin 在後端能修改資料
handle_update_comment.php 裡面
先抓到$user = getUserFromUsername($username);
在 utils.php 裡面,寫一個 isAdmin()
function isAdmin($user) {
return $user['role'] === 'ADMIN';
}
- handle_update_comment.php 裡把 admin 權限打開
特別注意這兩行,利用 isAdmin() 這個函式,把權限區分開來
$sql = "UPDATE comments set content=? WHERE id=? AND username=?";
$stmt->bind_param('si', $content, $id);
遇到的困難:
建議用 php 包起來的程式碼,前後都用 <?php ?> 這個形式,比較不會出錯
程式碼都跟老師的一樣,在 admin 底下的身份還是沒有顯示編輯跟刪除這兩個
- 要不函式寫錯,要不 index.php 裡面的那一行程式碼有錯
結果都不是上面說的那種兩種情形,最後解法是把資料的 role 刪掉重做就好了
老師 debug 的方法是,在 utils 的函式裡面,把資料印出來看
print_r($user['role']);
print_r($user['role'] === 'ADMIN');
,但是這個指令無效print_r($user['role'] === 'ADMIN' ? 'true' : 'false');
回傳的是 false,接著我把所有程式碼都對了一遍,確認我真的沒有打錯字,然後我試著把資料庫的 role 重新設一遍,就好了在 index.php 的時候,抓不到留言,只有後面有帶參數的才抓得到 -> 解法:暫時把所有 index.php 都改成 index.php?page=1,但這樣不是治本的辦法
會不會根本的問題就是在於上面的問題?所以我無法抓到東西?
天阿,我又重複犯了昨天的錯,就是 $_GET['page']
沒有寫好,我寫成 $page,寫好以後,我的首頁就回來了
$page = 1;
if (!empty($_GET['page'])) {
$page = intval($_GET['page']);
}
- 在寫 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'];
}
}
- 當我是 admin 身份時,後端不能改資料的原因:
在 handle_update_comment.php 裡面,我必須先拿到 $user 的資料,也就是說必須加這行 $user = getUserFromUsername($username);
下面的函式 isAdmin 才可以執行
- 如果要練習,要特別注意檔案不要開錯。
- 要特別注意 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 的使用者,不能新增留言,只能編輯跟刪除自己的留言
實作:
- 先把 hasPermission() 裡的 banned 功能寫完,邏輯是「如果使用者被停權,那他的權限只有編輯跟刪除留言,不能新增」-> 「如果不是新增,就回傳 true」
if ($user['role'] === 'BANNED') {
return $action !== 'create';
}
- 停權使用者看不到發布留言按鈕
「如果使用者登入且沒有新增權限的話,就出現『你已被停權』訊息」
- 後端權限設定:
- 已經被停權的使用者,比較難測這個功能
在 handle_add_comment.php 裡面,先導入 $user,再用 hasPermission()
if (!hasPermission($user, 'create', NULL)) {
header('Location: index.php');
exit();
}
- 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 身份系統下集(請先寫完作業再看)
實作流程:
- 做後台介面
只有 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 才看得到按鈕
- 後台資料處理:07'45"
2.1 複製檔案 handle_update_comment -> handle_update_role
2.2
遇到的困難
要先登入 admin,不然 實作的 1.2 權限檢查會一直把我導回 index.php
table 的語法不夠熟悉、table 的外、內框線要用 collapse 才會是一條線
border: 1px dashed #ccc;
border-collapse: collapse;
- 在管理權限的選項上
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
實作流程:
- 實作 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');
?>