# 北科 GDSC Web Backend 講義
2023/11/28
這次的進度:請先把[這個](https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/Ef89RyOczolIo1bpYtypfA8ByBNiDdALvnWqvHhcb_NKrw?e=EsdRlK)載下來然後丟到 htdocs
https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/Ef89RyOczolIo1bpYtypfA8ByBNiDdALvnWqvHhcb_NKrw?e=EsdRlK
:::danger
## 重要
如果你有任何一項沒有做到,都很有可能導致你無法參與課程的實作。在任何環境遇到問題,請馬上舉手問助教。
:::

## 課前要求
### 軟體安裝與檔案放置
- 上一次網頁課交的網頁,不管你怎麼做的,請到[這裡](https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/EaFPHF08_tBNkldHcoMsCwcBT1qvk_2VcEZUYf5gmI5oAA?e=25gebg)下載
- 安裝 phpstorm
- 如果沒有授權,你可以使用30天試用
- 其他選項自行決定,理論上不會影響太多
- xampp 安裝好
- php 版本要是 8.0 以上的
- 不要改預設的安裝目錄(例如:Windows 用戶安裝到安裝在D:\xampp)
- 將上次的網頁檔案放至下方對應作業系統的目錄下(記得解壓縮),整個資料夾丟進去
Windows: `C:\xampp\htdocs\`
macOS: `Application/XAMPP/xamppfiles/htdocs/`
Linux: `/opt/lampp/htdocs/`
### 在課堂開始之前,請嘗試著做這些事
#### 啟動 web server(apache), mysql server

Windows用戶啟動方式
1. 在工作列點一下搜尋按鈕, 輸入 xampp,選擇打開下圖相同的選項(選擇語言你就選英文吧)

2. 點擊紅框中的開始按紐-Apache和MySQL

#### 測試錯誤
1. 把上次的 index.html 檔案的副檔名改成 .php
2. 在 index.php 最上方的地方加上這些代碼
```php=
<?php
echo +++;
?>
```
## 今天要幹啥?

把這個東西做出來~~
還有用 php~
### PHP 簡介
PHP(PHP:Hypertext Preprocessor)是一種廣泛使用的開源伺服器端腳本語言,主要用在網頁開發。PHP 可以內嵌在 HTML 裡。將一個 HTML 檔案(如 `index.html`)的副檔名修改成 `.php` 就可以開始寫 PHP 了!以下是PHP語法的簡介:
1. **基本語法**:PHP 腳本以 `<?php` 開始,以 `?>` 結束。如果一個文件完全是 PHP 代碼,結束標籤可以省略。如果是要在 html 中輸出 php 中的變數,可以用 `<?= ?>` 來代替
3. **變數**:PHP 中的變數以 `$` 符號開始,後跟變數名。例如:`$name`。
4. **數據類型**:PHP 支持多種數據類型,包括整數(int)、浮點數(float)、字符串(string)、布林值(bool)、數組(array)等。
5. **條件語句**:PHP 使用 `if`、`else`、`elseif` 來執行條件判斷。
```php
if ($a > $b) {
echo "a is greater than b";
} elseif ($a == $b) {
echo "a is equal to b";
} else {
echo "a is smaller than b";
}
```
5. **注釋**:單行注釋使用 `//` 或 `#`,多行注釋使用 `/* ... */`。
6. **循環**:常見的循環語句包括 `for`、`foreach`、`while` 和 `do-while`。
```php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
```
7. **函數**:PHP 函數以 `function` 關鍵字定義。
```php
function sayHello() {
echo "Hello, world!";
}
```
## 資料庫概念與建立
CURD
C: Create
U: Update
R: Read
D: Delete
### 哪個部分

### 架構圖

### 實作
#### 建立新的資料庫、資料表並插入資料
- 資料庫 (Database)
- 
- 
- 資料表 (Data table)
- 
- 設定資料結構(Table schema)
- 
- 列 (Columns)
- 插入

- account: admin
password: 1234
permission: 1
- [關於 `utf8mb4` 和編碼的選擇](https://i30101000023b.nc.com.tw/modules/news/article.php?storyid=12)
- `_ci` 的意思:不區分大小寫的意思 (e.g. admin == Admin)
所以如果帳戶裡面有這兩個使用者的話,查 admin, Admin 也有可能在搜尋結果中。
#### 連接資料庫
在專案資料夾中新建一個檔案

叫 `sql.php`,我打算在這邊寫資料庫連線的資訊


```php=
<?php
// sql.php
$config = [
"host" => "127.0.0.1",
"dbname" => "ntut-gdsc-20231128-pre", // database name
"charset" => "utf8",
"account" => "root",
"password" => ""
];
```
這是一個 [PHP Array](https://www.php.net/manual/zh/language.types.array.php)。
複製、貼上,改一改。
```php!
<?php
// sql.php
$db = new PDO("mysql:host={$config['host']};dbname={$config['dbname']}", $config['account'], $config['password']);
```
#### 嘗試執行一個 command
[說明書的範例](https://www.php.net/manual/zh/pdo.prepare.php)

---
回到 PHPMyAdmin,點一下 `users`

會看到所有資料,上面那一條也會告訴你看到所有資料的query

開寫!
```php!
<?php
// sql.php
$query = "SELECT * FROM `users`";
$thread = $db->prepare($query);
$thread->execute($param);
var_dump($thread->fetchAll());
```
包起來
注意一下 php function 的語法。
```php!
<?php
// sql.php
// ...
/**
* @param string $query
* @param array $param
* @param bool $get_res
* @return array|bool|PDOStatement
*/
function runCommand(string $query,array $param = [], bool $get_res = false): array|bool|PDOStatement
{
global $db;
$thread = $db->prepare($query);
$thread->execute($param);
if ($get_res) {
return $thread->fetchAll();
}
return $thread;
}
```
再用看看
```php!
var_dump(runCommand("SELECT * FROM `users`", get_res: true));
```
:::success
#### 搞定

:::
---
## 登入區塊

### 拿資料
表單的資料送去哪?(index.php)

前端表單的輸入匡,哪一個是哪一個呢?(index.php)

後端怎麼拿?
`$_GET['field-name']`
拿之前要確認有沒有存在
`isset(你要確認的東西)`

login.php
```php!
<?php
// isset -> 檢查要存取的東西是否存在
if (!isset($_GET['account']) || !isset($_GET['password'])) {
die("no param"); // 直接強迫終止,後面的code都不會執行,括號內放輸出的字串。
}
```
ok,參數都有了,接下來是去資料庫查資料
使用資料庫要用到 `sql.php` 中的內容
所以: `require_once "sql.php";`
再加上前面寫的 runCommand
```php!
<?php
// login.php
// ...
require_once "sql.php";
$res = runCommand("SELECT * FROM `users` WHERE `account` = :account", [
"account" => $_GET['account']
], true);
```
查到的資料會裝在 res 裡面
但你總得先 var_dump 看一下吧

正確的長怎麼樣?
錯誤的長怎麼樣?
ok
知道怎麼拿資料,也知道拿到的資料長啥樣了
### 判斷密碼是不是正確的
```php=
if ($this_user["password"] == $_GET["password"]) {
```
>Passwords should be verified using the password_verify function, which uses constant time and is timing attack safe.
ok, so...


### 登入成功後,轉跳至對應的畫面

利用 `header("Location: ...")`
```php!
<?php
// login.php
// ...
switch ($this_user["permission"]) {
case 1: // admin
header("Location: admin.php");
break;
case 2: // teacher
header("Location: teacher.php");
break;
case 3:
header("Location: student.php");
break;
}
```
:::danger
**(我們在這)**
:::
登入失敗勒?
先放一個 flag (旗標)
```php!
<?php
// ...
// login.php
$login_success = false; // < this line
if (count($res) > 0) { // user exists
if (password_verify($_GET['password'], $res_user["password"])) {
$login_success = true; // < this line
// ...
}
}
```
轉跳到對應畫面: header("Location: ...")
```php!
<?php
// login.php
// ...
if (! $login_success) { // 前面加驚嘆號是顛倒是非的意思
header("Location: login_error.html");
}
```
:::success
login.php 目前進度
```php=
<?php
// isset -> 檢查要存取的東西是否存在
if (!isset($_GET['account']) || !isset($_GET['password'])) {
die("no param"); // 直接強迫終止,後面的code都不會執行,括號內放輸出的字串。
}
require_once "sql.php";
$res = runCommand("SELECT * FROM `users` WHERE `account` = :account", [
"account" => $_GET['account']
], true);
var_dump($res);
$login_success = false; // < this line
if (count($res) > 0) {
$this_user = $res[0];
if ($this_user["password"] == $_GET["password"]) {
$login_success = true;
switch ($this_user["permission"]) {
case 1: // admin
header("Location: admin.php");
break;
case 2: // teacher
header("Location: teacher.php");
break;
case 3:
header("Location: student.php");
break;
}
}
}
if (! $login_success) { // 前面加驚嘆號是顛倒是非的意思
header("Location: login_error.html");
}
```
:::
### 防止猴子

#### set 一個 cookie 吧
login.php, 在登入成功的時候
```php!
<?php
// ...
setcookie("user_id", $res_user["id"]);
```
怎麼看我設定的cookie?

怎麼刪掉?
```php!
<?php
// ...
setcookie("user_id", null);
```
#### 做一個海關
:::spoiler 如果你不知道那個 runCommand 裡面的指令是什麼鬼


:::
```php!
<?php
// sql.php
// ...
function requireLogin(): void {
if (!isset($_COOKIE["user_id"])) {
die("no login!");
}
}
function requirePermission(int $level): void {
requireLogin();
$user_id = $_COOKIE['user_id'];
$this_user = runCommand("SELECT * FROM `users` WHERE `id` = :id", [
"id" => $user_id
], get_res: true);
// check exists
if (count($this_user) == 0) {
die("not exists");
}
// check right level
if ($this_user[0]["permission"] != $level) {
die("permission deny");
}
}
```
怎麼用?在你需要海關的地方(ex: admin.php) 加上這些
```php!
<?php
require_once "sql.php";
requirePermission(1);
?>
```
## 新增使用者
邏輯:
1. 接收($_GET)帳號、密碼、權限
2. 在資料庫查看(SELECT * FROM `users` WHERE...)有沒有重複的帳戶名稱
3. 寫進資料庫 (INSERT INTO)
4. 返回(header("Locatoin: ...")) admin.php
```htmlembedded
<form action="user_add.php" method="post">
<h1>Add User</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi deleniti eaque explicabo fugiat harum ipsa itaque iusto, laboriosam laudantium libero necessitatibus odit, optio porro quaerat quam repellat, vero voluptatum. Nostrum.</p>
<label>
帳號
<input type="text" name="user">
</label>
<label>
密碼
<input type="password" name="passwd">
</label>
<label>
權限
<!-- 選單 -->
<select name="permission" id="">
<option value="1">1 -> admin</option>
<option value="2">2 -> teacher</option>
<option value="3" selected>3 -> student</option> <!-- selected: 預選取的選項 -->
</select>
</label>
<br><!-- 換行 -->
<a href="admin.php">back</a>
<br><!-- 換行 -->
<div class="submit-container">
<input type="submit">
</div>
</form>
```
```php=
<?php
require "sql.php";
$sqlstr="Insert INTO data (account, passwd, per) VALUES ('".$_POST["user"]."','".$_POST["passwd"]."','".$_POST["permission"]."');";
$res = runCommand($sqlstr,[], true);
header("Location: admin.php");
```
## 更改
1. (update_user.php)接收待更改的ID
2. (update_user.php)把原先使用者的資料放到畫面上
3. (update_user.php)接收($_GET)帳號、密碼、權限、用戶ID
4. (update_user_write.php)在資料庫查看(SELECT * FROM `users` WHERE...)有沒有重複的帳戶名稱
5. (update_user_write.php)寫進資料庫 (INSERT INTO)
6. 返回(header("Locatoin: ...")) admin.php
## 刪除
1. (del_user.php)接收($_GET)待更改的ID
2. 送刪除指令到SQL
3. 返回(header("Locatoin: ...")) admin.php