---
# System prepended metadata

title: 0408 錯誤驗證、原始碼簡化
tags: [Ruby / Rails]

---

# 0408 錯誤驗證、原始碼簡化

###### tags: `Ruby / Rails`

## 課前思考
龍哥的短網址服務系統：[https://ubin.io/](https://ubin.io/) 
（系統還有些問題待修） 
可以輸入網址，做出 6 碼的短網址：
https://ubin.io/zxCSrG -> https://5xruby.tw/
https://ubin.io/u7kmzE -> https://kaochenlong.com/
如果是亂打的網址：
https://ubin.io/abcdefg -> 
因為資料庫裡沒這個資料，會出現「不存在」頁面（404） 
除了短網址外，也有一般的靜態頁面：
https://ubin.io/about
https://ubin.io/contact
https://ubin.io/privacy
如何在 `routes.rb` 裡設計出這樣的功能？

程式在跑路徑時為由上往下比對，比對到了就結束
- 建議將特定的路徑放在上方
- 把須對應用的方法放最後面


`get ':id', to: 'urls#show'`

```ruby=
get '/', to: 'urls#index'
root 'url#show' #同等上方（因為太常用所以 rails 直接內建功能）`
```


環境變數 在這份code看不到的
在建立後台管理系統的時候，把路徑設成一個亂數或非 admin 的網址 => 
(見先修教材 namespace 部分）


---


### admin interface
```ruby=
namespace :admin, path: ENV['admin_path']
  root 'pages#dashboard'
end
```

* 關於api的命名方式建議：
  * /api/books/json
  * /api/v1/books/json
  * /api/v2/books/json
  * 推薦加上版本的寫法，因為可以更新版本但同時保留之前的版本給不願意更新的人繼續使用

**應用程式介面**（英語：**A**pplication **P**rogramming **I**nterface，縮寫為**API**，是一種計算介面，它定義多個軟體中介之間的互動。

API 手冊範例：[藍新金流 API 串接](https://www.newebpay.com/website/Page/download_file?name=%E8%97%8D%E6%96%B0%E9%87%91%E6%B5%81NewebPay_%E5%96%AE%E7%AD%86%E4%BA%A4%E6%98%93%E7%8B%80%E6%85%8B%E6%9F%A5%E8%A9%A2%E4%B8%B2%E6%8E%A5%E6%89%8B%E5%86%8A%20V1.0.5.pdf)

API 串接：
由前端直接處理太危險
會從前端打資料到後端，從網站後端把資料打包外加金鑰打入金流系統的 api，金流系統處理之後在將回應傳會本地系統後端

短網址範例：[Lihi.io AB 分流短網址](https://lihi.io/)

金流系統主流：紅藍綠 三家廠商 [基本常識介紹](https://hsienblog.com/2017/07/17/%E7%B4%85%E9%99%BD%E7%A7%91%E6%8A%80-%E7%B6%A0%E7%95%8C%E7%A7%91%E6%8A%80-%E8%97%8D%E6%96%B0%E7%A7%91%E6%8A%80-%E9%87%91%E6%B5%81%E6%9C%8D%E5%8B%99%E5%95%86%E6%AF%94%E8%BC%83/)
金流二房東：幫助使用者繞過紅藍綠的審查，直接使用金流 API 系統，幫助串流，簡單好用還可以分期，訂單在平台上
同時儲存使用者需要的訂單資料、同時比對金流
金流二房東範例：[貝殼集器](https://backme.tw/)
網址二房東範例：[Heroku](https://www.heroku.com/)

docker：協作平台檔案，輕量化的虛擬器，從作業系統版本、處理器...，讓不同工程師在同一個平台工作，再直接整包連平台推送上架，避免因環境配置不同產生功能壞掉等麻煩。

---

在 form 裡面取得的東西會被整理成 hash

input 裡面的 
name="restaurant[tel]" 轉換成 
restaurant => {
  tel => value
}

可使用 params[:restaurant] 來取用

---
![](https://i.imgur.com/jCdEIcp.png)

  
redirect_to 也是一種 render，如果同時使用會得到 double render 的錯誤訊息
```ruby=
def create
    # 寫入資料庫
    render html: params[:restaurant]

    redirect_to '/restaurants'
  end
  ```
- rails 內建把 password、authenticity_token 隱藏掉（只顯示 filtered）避免敏感資訊被存入 log file 內
  - 會被過濾掉的關鍵字：
```ruby=
2.6.5 :001 > Restaurant.filter_attributes => 
[:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]
```
- [Rails 6 更新功能](https://bigbinary.com/blog/rails-6-adds-activerecord-base-filter_attributes)
new 方法：把 params 存到記憶體
save 方法：把記憶體存起來的東西放到資料庫中
create 方法： new + save 一起做

範例：
```ruby=
@restaurant = Restaurant.new
@restaurant.save
等同
@restaurant = Restaurant.create
```

---

![](https://i.imgur.com/VsAQO1Y.png)

這個錯誤訊息為，想要將一包資料透過model寫進資料庫，但他沒有經過過濾檢查，仍舊可以經過改寫html而產生新的資料送出，所以資料沒有經過清洗就會得到這個錯誤訊息


架站金句：把每個使用者都當成有惡意的笨蛋

架站除錯句
1. 打一個中斷點
2. 在要除錯的地方插入原始碼 `debugger` 或 `byebug` 或`pry` 
3. 會停留在 `debugger` 的下一行程式碼
4. 可以在終端機確認各種變數的值
5. 會造成無窮迴圈，如果要離開就在終端機輸入 continue 或 c

可以使用 strong parameter 方法將只被允許資料送出
```=ruby
private # 讓檔案中所有方法都可以取用但是在外部不能更動
  def restaurant_params
    params.require(:restaurant).permit(:title, :tel, :address, :email, :description)
  end
```

### strong parameter => 面試常考題
- 解釋定義
- 功用
- 什麼時候該用、為什麼
- [Strong Parameters](https://rails.ruby.tw/action_controller_overview.html#strong-parameters)


#### 所有網路上傳輸的東西都是「文字」
## 錯誤驗證
驗證規則通常會直接放在 model 裡面（但通常不會放在同一個地方？？）

使用 validates 方法
```ruby=
class Restaurant < AoolicationRecord
  validates :title, presence: true
  validates :email, presence: true
  validates :title, :email, presence: true, uniqueness: true
  # uniqueness 表示這個項目得到的值在這資料庫只能存在一個
  # 舊版寫法： validate_presence_of :title, :email
end
```
建議驗證的項目各自存在

[「Rails Guide - Validations 各種驗證器 from 官方」](https://guides.rubyonrails.org/active_record_validations.html#uniqueness)


客製化的驗證器寫完之後用 `validate :方法名稱` 來呼叫
普通 rails 內建的驗證器都使用 `validates...` 直接呼叫 (注意單數複數)

提醒使用者該輸入的資料：前端設定 placeholder

### 查看錯誤訊息

- r.errors => 建立 errors 這個 key
- r.errors.any? => 確認 errors 是否有 value
- r.errors.full_messages => 顯示 errors 內所有訊息

實用範例：(印出實際錯誤訊息)
```ruby=
  # 在渲染的時候跑出來
  # 配合上方驗證資料是否存在使用，顯示是否有錯誤訊息
  # 可以使用在 new.html.erb => 在 submit 出去有錯誤訊息產生回到 views/new 的時候顯示
  # 也可以放在 edit.html.erb 裡面
  # 依照頁面需求使用
  
  <% if @restaurant.errors.any? %>
  <ul>
    <% @restaurant.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>
```

排序資料跟選取資料室資料庫的強項，Ruby 的弱項
關於處理資料的功能要用資料庫直接處理

`.all` 方法其實是不知道要用什麼的時候使用，如果知道特定用途的話可以直接指定使用的方法

* 反向排序：desc / descending
* 正向排序：asc  / ascending

```ruby=
@restaurants = Restaurant.all.order(id: :desc)  # 這邊的.all 可省略       
@restaurants = Restaurant.where(title: 'aaaa') # 只拿出 title 為 aaaa 的資料
```

### link_to 方法 
```htmlembedded=
<% @restaurants.each do |restaurant| %>

  <%= link_to restaurant.title, "/restaurants/#{restaurant.id}" %>
# 不建議使用，明顯是硬湊
  <%= link_to restaurant.title, restaurants_path(restaurant.id) %>
# 不建議使用，因為.id可以不用寫
  <%= link_to restaurant.title, restaurants_path(restaurant) %>
# 建議使用
  <%= link_to restaurant.title, restaurant %>
#建議使用，前提理解restaurant在rails的運作模式
```

在 data: {} 裡面的東西會被加上 data-(輸入值) 傳到 html 中建立屬性


因為`method: 'delete'`太常見，所以常會放在外面
好處是會有 `nofollow`（For 爬蟲）
```ruby=
<%= link_to '刪除',
                restaurant_path(restaurant),
                method: 'delete',
                data: { confirm: '確定嗎' } %>
```

也可以寫在裡面，一樣會生成 `data-method / data-confirm`

```ruby=
<%= link_to '刪除',
                restaurant_path(restaurant),
                data: { method: 'delete', confirm: '確定嗎' } %>
```

### 找尋資料方法
`.find` 
- 後面的條件只能帶 id
- 找不到會直接跳錯誤訊息

`.find_by` 
- 後面的條件可以帶條件，但最終只能找一個
- 找不到會回傳 `nil`

範例：
```ruby=
def show
  @restaurant = Restaurant.find(params[:id])
  或
  @restaurant = Restaurant.find_by(id: params[:id])
end


# 讓外部使用者不會直接看到錯誤畫面的方法
def show
  begin
    @restaurant = Restaurant.find(params[:id])
  rescue
  # 把可能會出錯的語法先包起來
  #begin~rescue錯誤的話就執行下方
  redirect_to restaurants_path
  else  # 加 else 代表不管有沒有出錯都會執行 (某些程式使用 finally)
  ......
  end
end

# 範例
def show
  begin  #表示包起來的地方可能會壞，處理例外訊息
    # 開啟檔案
  rescue
    # 跳出錯誤訊息
  else 
    # 關檔
    # 不管有沒有成功開啟檔案，都要關閉檔案
  end
end


# 範例
def show
  begin  #表示包起來的地方可能會壞，處理例外訊息
    @bar = Bar.find(params[:id])
  rescue
    render html: "error"
  else  #某些程式語言叫做final
    ........  #不管有沒有我都要做這件事
  end
#若只有一個方法的話可以省略begin-end
end

-------------------------------------------------------

# 直接在源頭攔截錯誤頁面 (在特定 controller 最上方寫入)
rescue_from ActiveRecord::RecordNotFound, with: :not_found
# 原為 rescue_from(ActiveRecord::RecordNotFound, {with: :not_found})

private
def not_found
  redirect_to restaurants_path # 消極地把頁面轉去首頁
  或
  render file: "#{Rails.root}/public/404.html",  #404絕對路徑 #要記
         status: 404
  # 原為 render({file: "#{Rails.root}/public/404.html", status: 404})
end

# 可直接將層級提高到所有 controller (把方法整個移到 application_controller.rb)
```
- `begin` `rescue` 跟 `end` 要成對出現
- 如果沒有 else 的話，兩個可以一起省略
Exception 例外
Error 錯誤

- 通常在 controller 裡面放 action 時
  - index 跟 show 放一起
  - new 跟 create 放一起
  - edit 跟 update 放一起

- 在 controller 裡的 action 最後如果==沒有==放 render :頁面 的話
  - 框架會跑到 views 裡面去找同名的檔案拿來做渲染

在 rails 裡面用 destroy 方法時會讓 HTTP method 用 DELETE 傳出 request
=> 但是 rails 其實會直接把 request 先轉成用 POST 傳出，然後在資料中塞入 _method: delete 達成一樣效果 （防止瀏覽器支援問題產生）

在 html 標籤中如果加上開頭為 data 的屬性，結果不會被渲染出來
=> Unobtrusive JavaSctipt (UJS) 不打擾屬性的 JS

在 rails 環境底下會有一支 JS 程式在監視：
- 如有 data-method 的話會把 HTTP method 改成指定的方法傳出
- 如有 data-confirm 的話會建立彈出確認視窗
  - 跳出後按"是"傳回true，按"否"傳回 false

## 將重複的原始碼簡化
1. 在後方建立方法，並在需要的地方統一帶入
2. 一樣建立方法後，在開頭使用 before_action 方法直接套用
- before_action :find_restaurant (自訂的方法名稱), only: [:show, :edit, :update, :destroy]
  - 在 controller 內寫入，表示action會先執行這個方法，指定的方法會直接先套入再繼續執行
  (通常會放在最上方，放在哪都好，但不要包到private裡或別的方法裡)
  - 如果同時有兩個 before_action，則會由上而下依序執行
  - 在後面加入 only 跟 except 可以讓該動作用指定的條件套入

---
在 view 裡面再做渲染：partial render 局部渲染


在 views 資料夾中的檔名要加底線，e.g. `_form.html.erb`
在另一個 views 裡呼叫時不加底線，e.g. `<%= render 'form' %>`

不建議在 partial render 的檔案中放入實體變數
通常會：
1. 在檔案中放入區域變數（標準化 partial render 檔案）
2. 在呼叫檔案的時候將區域變數帶入相對應的實體變數

`<%= render 'form', restaurant: @restaurant %>`

---

## 題外話
### SQL 語法
```sql=
SELECT * (列名稱)
FROM restaurant(表格名稱)
WHERE title like '%a%'
```

- 星號 * 是選取所有列
- `'a%' `是指找a開頭的資料
- `'%a'` 是指找a結尾的資料
- `'%a%'`是指找a開頭或a結尾的資料

```ruby=
@bar = Bar.all.where(title like '%a%')
#只要開頭是a的都會找到，包含aa, aaa
```

---
### 資安常見問題
- [SQL injection (SQL 注入)](https://zh.wikipedia.org/wiki/SQL%E6%B3%A8%E5%85%A5)
  - 資安問題，利用資料庫語言特性，讓外部使用者可以直接存取所有資料庫內容
  - 通常框架會設定特殊方法清洗輸入指令，避免 SQL 注入的狀況產生
  
- [CSRF 跨站請求偽造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
  - 資安問題，rails 會透過 CSRF token 防範此種攻擊
  - 一種挾制使用者在當前已登錄的Web應用程式上執行非本意的操作的攻擊方法。

---
### PUT vs. PATCH
- `PUT` ：把資料整包換掉
- `PATCH` ：把資料的一部份抽換（補丁的概念）

---
### 進入 rails 主控台
1. 進入 Terminal
2. 在 rails 專案資料夾內輸入 `$ rails console` ( 也可用 `$ rails c` )
- 沙盒模式
  - 將步驟 2 改成輸入 `$ rails c --sandbox`
  - 進入 rails 主控台但是在離開的時候把所有更動還原到進入前的狀態

