20200516のRailsに関する記事は23件です。

dockerで作ったRailsプロジェクトをロリポップ!マネージドクラウドにあげるのに手こずった話

前置き

dockerを使ってRailsの開発を趣味でやっていた。
ひと段落ついたので、サーバーに公開しようと思ったがエラーが出てなかなかうまくいかなかった話です。

エラーの内容

console
ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `rails credentials:edit`

解決法

console
EDITOR=vim rails credentials:edit --environment production

dockerに入って上のコマンドを実行していたが、Vimが入っていなかったので、一見できてるようでできていなかった。
編集画面が出てこないのでおかしいとは思っていたが、ファイルはしっかり生成されているし大丈夫だと思っていたのが罠だった。

あとはマネクラのプロジェクト管理画面で環境変数の設定をしておく。
スクリーンショット 2020-05-16 23.24.27.png
ここに入れる値の内容は、/config/credentials/production.keyの内容をコピーして貼り付ける。

その後以下のファイルを作った。

/config/deploy.rb
append :linked_files, 'config/credentials/production.key'

これは

Railsがproduction.keyを参照するためのシンボリックリンクを貼る記述を追加する。

という感じらしい。

最後にSSHでマネクラにアクセスして以下のファイルを作っておいてからPushするとOK

console
vim user_command.sh
user_command.sh
test -f ~/shared/.env && ln -s ~/shared/.env ./.env || true
export RAILS_ENV=production

bundle install --deployment --without development,test --path vendor/bundle
bin/rails db:create
bin/rails db:migrate

最後に

なかなか解決せずに6時間くらい悩みました。
30分休憩してから再度取り掛かったらすぐに解決しました。
煮詰まったら一回休むってほんと大事

お世話になった記事

Rails 6でCapistranoでデプロイする際のCredentials関連エラーに対処する
[Ruby on Rails]ロリポップ!マネージドクラウドでRailsアプリを稼働させるまで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 1対多 保存の方法 紛らわしいやつまとめ(メモ)

1対多のリレーションでも保存の方法の違いが紛らしいな

1番基本

  • 1つ1つリレーションを作るパターン
## リレーション

User 1 - * Post

## user.rb
has_many :posts

## post.rb
belongs_to :user

## controller

def new
  @post = Post.new

def create
  @post = Post.new(post_params)

private

def post_params

  params.require(:post).permit(:name, :body).merge(user_id: current_user.id)

## view
= form_for @post do |form|

  = form.label :title
  = form.text_field :title

  = form.label :body
  = form.text_field :body
  = form.submit '送信'

## 要点

11 userpostの繋がりを作っていくパターン

複数一気に作るパターン

パターン1(子要素のデータがある場合)

  • xx_idsを使う
## リレーション
User 1 - * Skill

## user.rb
has_many :skills

## skill.rb
belongs_to :user


## 子要素のデータ存在する

Skill
  id1 name: "筋トレ"
  id2 name: "イケメン"
  id3 name: "話術"

## controller

def new
  @user = User.new

private

def post_params

  params.require(:post).permit(:name, skill_ids: [])

## view

= form_for @user do |f|
  = f.label :name
  = f.text_field :name  
  = f.fields_for :skill_ids do |skill|
    = skill.collection_select :name, Skill.all, :id, :name  
  = f.submit "送信"

パターン2(子要素のデータがない場合)

  • accepts_nested_attributes_for使う
## リレーション
User 1 - * Address

## user.rb
  has_many :addresses
  accepts_nested_attributes_for :addresses,allow_destroy: true

## address.rb
 - state
 - city
 belongs_to :user

## controller

  def new 
    @user = User.new
    @user.addresses.build

  private
  def user_params
      params.require(:user).permit(:name, addresses_attributes: [:id, :state, :city])
    end

## view
= form_for @user do |form|
  .field
    = form.label :name
    = form.text_field :name
  .fidle
    = form.fields_for :addresses do |address|
      = address.label :state
      = address.text_field :state
      = address.label :city
      = address.text_field :city
  .fidle
    = form.fields_for :addresses do |address|
      = address.label :state
      = address.text_field :state
      = address.label :city
      = address.text_field :city

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails リレーション色々

  • 1対多

ツイート機能

class User
  has_many :twees
end

class Twitter
  belongs_to user
end

## schema
users
 name:string

tweets
 body:string
 user:refrences
  • 自己参照型

like機能

class User < ApplicationRecord
  has_many :likers,foreign_key: "liker_id",class_name: "Like"
  has_many :likeds,foreign_key: "liked_id",class_name: "Like"
end

class Like < ApplicationRecord
  belongs_to :user, foreign_key: "liker_id", class_name: "User"
  belongs_to :user, foreign_key: "liked_id" , class_name: "User"
end

## schema
users
 name:string

likes
  liker_id:integer
  liked_id:integer
  • 多対多 && accepted_nested_for

投稿記事とカテゴリー

class Post
  has_many :post_categoris
  has_many :categories, through: :post_categoris
  accepts_nested_attributes_for :post_categoris, allow_destroy: true, reject_if: :all_blank
end

class Category
  has_many :post_categoris
  has_many :posts, through: :post_categoris
end

class PostCategory
  belongs_to :post 
  belongs_to :category
end

## schema
posts
- body
categoris
- name

post_categories
- post_id
- category_id

## 実装イメージ
どちらからも画面を作るときに、多対多になるのかも
=> カテゴリー主体の機能がなければ 1 - *でいいのではないか

投稿一覧機能

投稿1 カテゴリー1 カテゴリー2
投稿2 カテゴリー3 カテゴリー4

カテゴリー一覧機能

カテゴリー1 投稿数
カテゴリー2 投稿数
  • ポリモーフィック(中々体に染み込まない)

Userが複数のタグをもつ
Postが複数のタグを持つ

## 図
User 1 - * Taguser.tags user.tags.first.tagable == user
Post 1 - * Tagpost.tags post.tags.first.tagable == post


class User
  has_many :tags ,as: :tagable
end

class Post
  has_many :tags ,as: :tagable
end


class Tag
  belongs_to :tagable, polymorphic: true
end


## schema

  "posts"
    t.string "name"
    t.string "desc"

  "tags"
    t.string "name"
    t.string "tagable_type"
    t.integer "tagable_id"

  "users"
    t.string "name"
    t.integer "age"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの基礎知識の備忘録

個人的に忘れやすいことをメモ的な意味で書きます。

ActiveRecordクラス

テーブルから情報を取得するため位必要なメソッドを兼ね備えたクラス。
メソッド一例

メソッド 用途

●全てのデータを取得する
使用例)postテーブルに保存されているデータの全てを取得して@postsにインスタンス変数し、ビューファイルに使用できるようになる。
@post = Posts.all
↓複数のデータを表示しようとするとエラーが出る。
<h1>トップページ</h1>
<%= @posts.content %>
<%= @posts.created_at %>

↓eachメソッドを使って順番に処理させることで解決。
<h1>トップページ</h1>
<% @posts.each do |post| %>
<%= post.content %>
<%= post.created_at %>
<% end %>

●テーブルのレコードの内、ある一つのデータを取得する
使用例)1番目のレコードのみ取得
@post = Post.find(1)

●クラスのインスタンス(レコード)を生成する
post.new

●クラスのインスタンス(レコード)を保存する
post.save

ヘルパーメソッド

ViewでHTMLタグを出現させたりテキストを加工するために予めソッドが用意されている。

●投稿ページなどにおけるフォームの実装
<%= form_tag('/posts', method: :post) do %>
<input type="text" name="content">
<input type="submit" value="投稿する">
<% end %>

●その他のパーツ
type="text" 1行テキストボックス
type="password" パスワード入力ボックス
type="checkbox" 複数選択可
type="radio" ラジオボタン(一つだけ選択)
type="submit" 送信ボタン

●リンクの実装
aタグの代わりに使用できる、リンクを仕込むためのヘルパーメソッド
<%= link_to '新規投稿', '/posts/new' %>

ストロングパラメーターとプライベートメソッド

Railsではセキュリティ上の概念などから、ストロングパラメーターという技術を用いてデータを保存する。

●ストロングパラメーターとは
指定したキーを持つパラメーターのみを受け取るようにするもの。以下に記述することで特定のキーしか受け取れないようにする仕組みを構築する。
def post_params
params.permit(:キー名, :キー名) # 受け取りたいキーを指定する
end

●プライベートメソッド
クラス外から呼び出すことのできないメソッドで、ストロングパラメーターを記述したメソッドはprivate以下に記載して、プライベートメソッドとして扱う。
private
def post_params
params.permit(:content)
end

★プライベートメソッドのメリット

1.classの外部から呼ばれたら困るメソッドの隔離

メソッドの中には、classの外部から呼び出されてしまうとエラーを起こすメソッドも存在します。そのような事態を事前に防ぐ事ができる。

2.可読性

classの外部から呼び出されるメソッドを探すときに、private以下の部分は目を通さなくて良くなります。また、繰り返し使用するメソッドもprivate以下に集約する事で、コードをシンプルにできる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails セキュリティー

前提

本日学んだセキュリティーについて書いていきます。

本題

リダイレクトとファイル

セキュリティ上の脆弱性として検討したいのは、Webアプリケーションにおける「リダイレクトとファイル」。

リダイレクト

Webアプリケーションにおけるリダイレクトは、過小評価されがちなクラッキングツール。
攻撃者はこれを使ってユーザーを危険なWebサイトに送り込んだり、Webサイト自体に罠を仕掛けたりすることもできる。

リダイレクト用のURL (の一部) を渡すことをユーザーに許すと、潜在的な脆弱性となる。
最もあからさまな攻撃方法としては、ユーザーを本物そっくりの偽Webサイトにリダイレクトすることが考えられる。
これは俗に「フィッシング(phishing)」や「釣り」などと呼ばれる攻撃手法。
具体的には、無害を装ったリンクを含むメールをユーザーに送りつけ、XSSを使ってそのリンクをWebアプリケーションに注入するか、リンクを外部サイトに配置する。
このリンクの冒頭部分はそのWebアプリケーションのURLなので、一見無害に見える。

ファイルアップロード

ファイルがアップロードされたときに重要なファイルが上書きされることのないようにする。
また、メディアファイルの処理は非同期で行なう。

多くのWebアプリケーションでは、ユーザーがファイルをアップロードできるようになっている。
ユーザーが選択/入力できるファイル名 (またはその一部) は必ずフィルタする。
攻撃者が危険なファイル名をわざと使ってサーバーのファイルを上書きしようとする可能性があるため。
ファイルが /var/www/uploads ディレクトリにアップロードされ、そのときにファイル名が「../../../etc/passwd」と入力されていると、重要なファイルが上書きされてしまう可能性がある。
言うまでもなく、Rubyインタプリタにそれだけの実行権限が与えられていなければ、そのような上書きは実行できない。
Webサーバー、データベースサーバーなどのプログラムは、比較的低い権限を持つUnixユーザーとして実行されているのが普通。

さらにもう一つ注意。
ユーザーが入力したファイル名をフィルタするときに、ファイル名から危険な部分を取り除くアプローチを使わないこと。
Webアプリケーションがファイル名から「../」という文字を取り除くことができるとしても、今度は攻撃者が「....//」のようなその裏をかくパターンを使えば、やはり「../」という相対パスが通ってしまい、きりがない。
最も良いのは「ホワイトリスト」によるアプローチ。
これはファイル名が有効であるかどうか (指定された文字だけが使われているかどうか) をチェックするもの。
これは「ブラックリスト」アプローチと逆の手法であり、利用が許されてない文字を除去する。
ファイル名が無効の場合は、拒否するか、無効な文字を置き換えますが、取り除くわけではない。

ファイルアップロードで実行可能なコードを送り込む

アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれると、ソースコードが実行可能になってしまう可能性がある。
Railsの/publicディレクトリがApacheのホームディレクトリになっている場合は、ここにアップロードファイルを置いてはいけない。

広く使われているApache WebサーバーにはDocumentRootというオプションがある。
これはWebサイトのホームディレクトリであり、このディレクトリツリーに置かれているものはすべてWebサーバーによって取り扱われる。
そこに置かれているファイルの名前に特定の拡張子が与えられていると、それに対してリクエストが送信された時に実行されてしまうことがある。
実行される可能性のある拡張子は、たとえばPHPやCGIなど。
攻撃者が「file.cgi」というファイルをアップロードし、その中に危険なコードが仕込まれているとする。
このファイルを誰かがダウンロードすると、このコードが実行される。

ApacheのDocumentRootがRailsの/publicディレクトリを指している場合、アップロードファイルをここに置かない。
少なくとも1階層上に保存する必要がある。

ファイルのダウンロード

ユーザーが任意のファイルをダウンロードできる状態を作らないこと。

ファイルアップロード時にファイル名のフィルタが必要になるのと同様、ファイルのダウンロード時にもファイル名をフィルタする必要がある。
以下のsend_file()メソッドは、サーバーからクライアントにファイルを送信します。フィルタ処理されていないファイル名を使うと、ユーザーが任意のファイルをダウンロードできるようになってしまう。

send_file('/var/www/uploads/' + params[:filename])

「../../../etc/passwd」のようなファイル名を渡せば、サーバーのログイン情報をダウンロードできてしまう。
これに対するシンプルな対応策は、リクエストされたファイル名が、想定されているディレクトリの下にあるかどうかをチェックすること。

その他に、ファイル名をデータベースに保存しておき、データベースのidをサーバーのディスク上に置く実際のファイル名の代りに使う方法も併用できる。
この方法も、アップロードファイルが実行される可能性を回避する方法として優れている。
attachment_fuプラグインでも同様の手法が採用されている。

イントラネットAdminのセキュリティ

イントラネットおよび管理画面インターフェイスは、強い権限が許されているため、何かと攻撃の目標にされがち。
イントラネットおよび管理画面インターフェイスには、他よりも手厚いセキュリティ対策が必要ですが、現実には逆にむしろこれらの方がセキュリティ対策が薄いということがしばしばある。

イントラネットや管理アプリケーションにとって最も脅威なのはXSSとCSRF。

XSS: 悪意のあるユーザーがイントラネットの外から入力したデータがWebアプリケーションで再表示されると、WebアプリケーションがXSS攻撃に対して脆弱になる。
ユーザー名、コメント、スパムレポート、注文フォームの住所のような情報すらXSS攻撃に使われることがある。

管理画面やイントラネットで1箇所でもサニタイズ漏れがあれば、アプリケーション全体が脆弱になる。
想定される攻撃としては、管理者のcookieの盗み出し、管理者パスワードを盗み出すためのiframe注入、管理者権限奪取のためにブラウザのセキュリティホールを経由して邪悪なソフトウェアをインストールする、などが考えられる。

CSRF: クロスサイトリクエストフォージェリ (Cross-Site Request Forgery) はクロスサイトリファレンスフォージェリ (XSRF: Cross-Site Reference Forgery) とも呼ばれ、非常に強力な攻撃手法。
この攻撃を受けると、管理者やイントラネットユーザーができることをすべて行えるようになってしまう。

RailsのURLはかなり構造が素直であるため、オープンソースの管理画面を使っていると構造を容易に推測できてしまう。
攻撃者は、ありそうなIDとパスワードの組み合わせを総当りで試す危険なImageタグを送り込むだけで、数千件ものまぐれ当たりを獲得することもある。

その他予防策

管理画面は、多くの場合次のような作りになっている。www.example.com/admin のようなURLに置かれ、Userモデルのadminフラグがセットされている場合に限り、ここにアクセスできる。
ユーザー入力が管理画面で再表示されると、管理者の権限でどんなデータでも削除/追加/編集できてしまう。

常に最悪の事態を想定することは極めて重要。
「誰かが自分のcookieやユーザー情報を盗み出すことができたらどうなるか」。
管理画面にロール (role)を導入することで、攻撃者が行える操作の範囲を狭めることができる。
1人の管理者に全権を与えるのではなく、権限を複数管理者で分散する方法や、管理画面用に特別なログイン情報を別途設置するという方法もある。
一般ユーザーが登録されているUserモデルに管理者も登録し、管理者フラグで分類していると攻撃されやすいことから、これを避けるため。
極めて重要な操作では別途特殊なパスワードを要求する方法もある。

管理者は、必ずしも世界中どこからでもそのWebアプリケーションにアクセスできる必要性はないはず。
送信元IPアドレスを一定の範囲に制限するという方法。request.remote_ipメソッドを使えばユーザーのIPアドレスをチェックできる。
この方法は攻撃に対する直接の防御にはならないが、検問としては非常に有効。
ただし、プロキシを用いて送信元IPアドレスを偽る方法がある。

管理画面を特別なサブドメインに置き ( admin.application.com など)、さらに管理アプリケーションを独立させてユーザー管理を独自に行えるようにする。
このような構成にすることで、通常の www.application.com ドメインからの管理者cookieを盗み出すことは不可能。
ブラウザには同一生成元ポリシーがあるので www.application.com に注入されたXSSスクリプトからはadmin.application.comのcookieは読み出せず、逆についても同様に読み出し不可となる。

ユーザー管理

認証 (authentication) と認可 (authorization) はほぼすべてのWebアプリケーションにおいて不可欠。
認証システムは自前で作るよりも、広く使われているプラグイン (訳注: 現在ならgem) を使うべき。
ただし、常に最新の状態にアップデートするようにする。

Railsでは多数の認証用プラグインを利用できる。
人気の高いdeviseやauthlogicなどの優れたプラグインは、パスワードを平文ではなく常に暗号化した状態で保存する。
Rails 3.1では、同様の機能を持つビルトインのhas_secure_passwordメソッドを使える。

新規ユーザーは必ずメール経由でアクティベーションコードを受け取り、メール内のリンク先でアカウントを有効にするようになっている。
アカウントが有効になると、データベース上のアクティベーションコードのカラムはNULLに設定される。
以下のようなURLをリクエストするユーザーは、データベースで見つかる最初に有効になったユーザーとしてWebサイトにログインできてしまう可能性がある。そしてそれがたまたま管理者である可能性もありえる。

http://localhost:3006/user/activate
http://localhost:3006/user/activate?id=

一部のサーバーでは、params[:id]で参照されるパラメータidがnilになってしまっていることがあるため、上のURLが通用してしまう可能性がある。アクティベーション操作中にこのことが敵に突き止められるまでの流れは以下のとおり。

User.find_by_activation_code(params[:id])

パラメータがnilの場合、以下のSQLが生成される。

SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1

この結果、データベースに実在する最初のユーザーが検索で見つかり、結果が返されてログインされてしまう。

アカウントに対する総当たり攻撃

アカウントに対する総当たり攻撃 (Brute-force attack) とは、ログイン情報に対して試行錯誤を繰り返す攻撃。
エラーメッセージを具体的でない、より一般的なものにすることで回避可能 だが、CAPTCHA (相手がコンピュータでないことを確認するためのテスト) への情報入力の義務付けも必要。

Webアプリケーション用のユーザー名リスト (名簿) は、パスワードへの総当たり攻撃に悪用される可能性がある。
パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすい。
辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくある。
従って、名簿と辞書を使って総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られている。

このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションではわざと具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしている。
ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせる。
「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを表示したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成する。

しかし、Webアプリケーションのデザイナーがおろそかにしがちなのは、いわゆる「パスワードを忘れた場合」ページ。
こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示される。
こうした情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまう。

これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示するようにする。
さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHAの入力をユーザーに義務付けるようにする。
もちろん、この程度では自動化された総当たり攻撃プログラムからの攻撃から完全に逃れることはできない。
こうしたプログラムは送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるから。
しかしこの対策は攻撃に対するある程度の防御になることも確か。

アカウントのハイジャック

多くのWebアプリケーションでは、ユーザーアカウントを簡単にハイジャックできてしまう。

パスワード

攻撃者が、盗み出されたユーザーセッションcookieを手に入れ、それによってWebアプリケーションが標的ユーザーとの間で共用可能になった状態を考えてみる場合。
パスワードが簡単に変更できる画面設計(古いパスワードの入力が不要)であれば、攻撃者は数クリックするだけでアカウントをハイジャックできてしまう。
あるいは、パスワード変更画面がCSRF攻撃に対して脆弱な作りになっている場合、攻撃者は標的ユーザーを別のWebページに誘い込み、CSRFを実行するように仕込まれたimgタグを踏ませて、標的ユーザーのWebパスワードを変更する。
対応策としては、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすること。
同時に、ユーザーにパスワードを変更させる場合は、古いパスワードを必ず入力させること。

メール

しかし攻撃者は、登録されているメールアドレスを変更することでアカウントを乗っ取ろうとする可能性もある。
攻撃者は、メールアドレス変更に成功すると「パスワードを忘れた場合」ページに移動し、攻撃者の新しいメールアドレスに変更通知メールを送信する。
システムによってはこのメールに新しいパスワードが記載されていることもある。
対応策は、メールアドレスを変更する場合にもパスワード入力を必須にすること。

その他

Webアプリケーションの構成によっては、ユーザーアカウントをハイジャックする方法が他にも潜んでいる可能性がある。
多くの場合、CSRFとXSSが原因となる。
GMailのCSRF脆弱性で紹介されている例をとりあげる。
同記事の概念実証によると、この攻撃を受けた場合、標的ユーザーは攻撃者が支配するWebサイトに誘い込まれる。
そのサイトのImgタグには仕掛けがあり、GMailのフィルタ設定を変更するHTTP GETリクエストがそこから送信される。
この標的ユーザーがGMailにログインしていた場合、フィルタ設定が攻撃者によって変更され、この場合はすべてのメールが攻撃者に転送されるようになる。
この状態は、アカウント全体がハイジャックされたのと同じぐらいに有害。
対応策は、アプリケーションのロジックを見なおしてXSSやCSRF脆弱性を完全に排除すること。

CAPTCHA

CAPTCHAとは、コンピュータによる自動応答でないことを確認するためのチャレンジ-レスポンス式テスト。
コメント入力欄などで、歪んだ画像に表示されている文字を入力させることで、入力者が自動スパムボットでないことを確認する場合によく使われる。
ネガティブCAPTCHAという手法を使えば、入力者に自分が人間であることを証明させるかわりに、ボットを罠にはめて正体を暴くことができる。

CAPTCHAのAPIとしてはreCAPTCHAが有名。
これは古書から引用した単語を歪んだ画像として表示する。
初期のCAPTCHAでは背景を歪めたり文字を曲げたりしていましたが、後者は突破されたため、現在では文字の上に曲線も書き加えて強化している。
なお、reCAPTCHAは古書のデジタル化にも使える。
ReCAPTCHAはRailsのプラグインにもなっており、APIとして同じ名前が使われている。

このAPIからは公開鍵と秘密鍵の2つの鍵を受け取る。
これらはRailsの環境に置く必要がある。
それにより、ビューでrecaptcha_tagsメソッドを、コントローラではverify_recaptchaメソッドをそれぞれ利用できる。
検証に失敗するとverify_recaptchaからfalseが返される。

CAPTCHAの問題は、ユーザーエクスペリエンスを多少損ねること。
さらに、弱視など視力に問題のあるユーザーはCAPTCHAの歪んだ画像をうまく読めないこともある。
なおポジティブCAPTCHAは、ボットによるあらゆるフォーム自動送信を防ぐ優れた方法のひとつ。

ほとんどのボットは、単にWebページをクロールしてフォームを見つけてはスパム文を入力するだけのお粗末なもの。
ネガティブCAPTCHAではこれを逆手に取り、フォームに「ハニーポット」フィールドを置いておく。
これは、CSSやJavaScriptを用いて人間には表示されないように設定されたダミーのフィールド。

ネガティブCAPTCHAが効果を発揮するのはWebをクロールする自動ボットからの保護のみであり、重要なサイトに狙いを定めるボットを防ぐのには不向き。
しかしネガティブCAPTCHAとポジティブCAPTCHAをうまく組み合わせればパフォーマンスを改善できることがある。
たとえば「ハニーポット」フィールドに何か入力された(=ボットが検出された)場合はポジティブCAPTCHAの検証は不要になり、レスポンス処理の前にGoogle ReCapchaにHTTPSリクエストを送信せずに済む。

JavaScriptやCSSを用いてハニーポットフィールドを人間から隠す方法。

ハニーポットフィールドを画面の外に追いやってユーザーから見えないようにする
フィールドを目に見えないくらい小さくしたり、背景と同じ色にしたりする
ハニーポットフィールドをあえて隠さず、「このフィールドには何も入力しないでください」と表示する
最もシンプルなネガティブCAPTCHAは、「ハニーポット」フィールドを1つ使う。
このフィールドはサーバー側でチェックする。
フィールドに何か書き込まれていれば、入力者はボットであると判定できる。
後はフォームの内容を無視するなり、通常通りメッセージを表示する(データベースには保存しない)などすればよい。
通常のメッセージをもっともらしく表示しておけば、ボットは書き込み失敗に気が付かないまま満足して次の獲物を探す。

Ned Batchelderのブログ記事には、さらに洗練されたネガティブCAPTCHA手法がいくつか紹介されている。

現在のUTCタイムスタンプを含めたフィールドをフォームに含めておき、サーバー側でこのフィールドをチェックする。
フィールドの時刻が遠い過去や未来の時刻であれば、そのフォームは無効。
フィールド名をランダムに変更します
送信ボタンを含むあらゆる型の数だけハニーポットフィールドを複数用意。
この方法で防御できるのは自動ボットだけであり、狙いを定めて特別に仕立てられたボットは防げない。
つまり、ネガティブキャプチャはログインフォームの保護には必ずしも向いているとは限らない。

ログ出力

パスワードをRailsのログに出力しないこと。

デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力される。
しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれていることがあるため、重大なセキュリティ問題の原因になることがある。
Webアプリケーションのセキュリティコンセプトを設計するときには、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合のことも必ず考慮に含めておく必要がある。
パスワードや機密情報をログファイルに平文のまま出力してしまうと、データベース上でこれらの情報を暗号化する意味がなくなってしまう。
Railsアプリケーションの設定ファイル config.filter_parameters に特定のリクエストパラメータをログ出力時にフィルタする設定を追加できる。
フィルタされたパラメータはログ内で[FILTERED]という文字に置き換えられる。

config.filter_parameters << :password

指定したパラメータは正規表現の「部分マッチ」によって除外される。
Railsはデフォルトで:passwordを適切なイニシャライザ(initializers/filter_parameter_logging.rb)に追加し、アプリケーションの典型的なpasswordパラメータやpassword_confirmationパラメータに配慮する。

正規表現

Rubyの正規表現で落とし穴になりやすいのは、より安全な\Aや\zがあることを知らずに危険な^や$を使ってしまうこと。

Rubyの正規表現では、文字列の冒頭や末尾にマッチさせる方法が他の言語と若干異なる。
このため、多くのRuby本やRails本でもこの点について間違った記載がある。
たとえば、URL形式になっているかどうかをざっくりと検証するために、以下のような単純な正規表現を使ったとする。

/^https?:\/\/[^\n]+$/i

これは一部の言語では正常に動作する。
しかし、Rubyでは^や$は、入力全体の冒頭と末尾ではなく、「 行の」冒頭と末尾にマッチしてしまう。
従って、この場合以下のような毒入りURLはフィルタを通過してしまう。

javascript:exploit_code();/*
http://hi.com
*/

上のURLがフィルタに引っかからないのは、入力の2行目にマッチしてしまうため。
従って、1行目と3行目にどんな文字列があってもフィルタを通過してしまう。
フィルタをすり抜けてしまったURLが、今度はビューの以下の箇所で表示されたとする。

link_to "Homepage", @user.homepage

表示されるリンクは一見無害に見えますが、クリックすると、攻撃者が送り込んだ邪悪なJavaScript関数を初めとするJavaScriptコードが実行されてしまう。

これらの正規表現に含まれる危険な^や$は、安全な\Aや\zに置き換える必要がある。

/\Ahttps?:\/\/[^\n]+\z/i

^や$をうっかり使ってしまうミスが頻発したため、Railsのフォーマットバリデータ(validates_format_of) では、正規表現の冒頭の^や末尾の$に対して例外を発生するようになった。
めったにないと思われるが、\Aや\zの代りに^や$をどうしても使いたい場合は、:multilineオプションをtrueに設定することもできる。

# この文字列のどの行にも"Meanwhile"という文字が含まれている必要がある
validates :content, format: { with: /^Meanwhile$/, multiline: true }

この機能は、フォーマットバリデータ利用時に起きがちなミスから保護するだけのものであり、それ以上のものではない点にご注意。
^や$はRubyでは 1つの行 に対してマッチし、文字列全体にはマッチしないということを開発者が十分理解しておくことが重要。

権限昇格

パラメータが1つ変更されただけでも、ユーザーが不正な権限でアクセスできるようになってしまうことがある。
パラメータは、たとえどれほど難読化し、隠蔽したとしても、変更される可能性が常にあることを肝に銘じる。

改ざんされる可能性が高いパラメータといえばid。http://www.domain.com/project/1の1がid。
このidはコントローラのparamsを経由して取得できる。
コントローラ内では多くの場合、次のようなコードが使われている可能性がある。

@project = Project.find(params[:id])

このコードで問題がないWebアプリケーションもあるにはあるが、そのユーザーがすべてのビューを参照する権限を持っていない場合には問題となる。
このユーザーがURLのidを42に変更し、本来のidでは表示できないページを表示できてしまうため。
このようなことにならないよう、ユーザーのアクセス権も必ずクエリに含める。

@project = @current_user.projects.find(params[:id])

Webアプリケーションによっては、ユーザーが改ざん可能なパラメータが他にも潜んでいる可能性がある。
要するに、安全確認が終わっていないユーザー入力が安全である可能性はゼロであり、ユーザーから送信されるいかなるパラメータであっても、何らかの操作が加えられている可能性が常にあるということ。

難読化とJavaScriptによる検証のセキュリティだけでお茶を濁してはいけない。
ブラウザのWeb Developer Toolbarを使えば、フォームの隠しフィールドを見つけて変更することもできる。
JavaScriptを使ってユーザーの入力データを検証することはできても、攻撃者が想定外の値を与えて邪悪なリクエストを送信することは阻止しようがない。
Mozilla Firefox用のFirebugアドオンを使えば、すべてのリクエストをログに記録して、リクエストを繰り返し送信することも、リクエストを変更することもできてしまう。
さらに、JavaScriptによる検証はブラウザのJavaScriptをオフにするだけで簡単にバイパスできてしまう。
さらに、クライアントやインターネットのあらゆるリクエストやレスポンスを密かに傍受するプロキシがクライアント側に潜んでいる可能性すらある。

インジェクション

インジェクション (注入) とは、Webアプリケーションに邪悪なコードやパラメータを導入して、そのときのセキュリティ権限で実行させること。
XSS (クロスサイトスクリプティング) やSQLインジェクションはインジェクションの顕著な例。

インジェクションによって注入されるコードやパラメータは、あるコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害。
その意味で、インジェクションは非常にトリッキーであると言える。
ここでいうコンテキストとは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどがある。

ホワイトリスト方式とブラックブラックリスト方式

通常、サニタイズや保護や検証では、ブラックリスト方式よりもホワイトリスト方式が望ましい方法。

ブラックリストに使われるのは、有害なメールアドレス、publicでないアクション、邪悪なHTMLタグなど。
ホワイトリストはこれと真逆で、有害ではないメールアドレス、publicなアクション、無害なHTMLタグなどがホワイトリストになる。
スパムフィルタなど、対象によってはホワイトリストを作成しようがないこともあるが、基本的にホワイトリスト方式を使う。

セキュリティに関連するbefore_actionでは、except: [...]ではなくonly: [...]を使う。
なぜなら将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済むため。
クロスサイトスクリプティング (XSS) 対策として」という文字列の攻撃能力は失われていない。
だからこそ、ホワイトリストを用いるフィルタリングをおすすめする。
ホワイトリストによるフィルタは、Rails 2でアップデートされたsanitize()メソッドで使われている。

tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))

この方法なら指定されたタグのみが許可されるため、あらゆる攻撃方法や邪悪なタグに対してフィルタが健全に機能する。

第2段階として、Webアプリケーションからの出力をもれなくエスケープすることが優れた対策。これは特に、ユーザー入力の段階でフィルタされなかった文字列がWeb画面に再表示されてしまうようなことがあった場合に有効。escapeHTML() (または別名のh()) メソッドを用いて、HTML入力文字「&」「"」「<」「>」を、無害なHTML表現形式(&、"、<、>) に置き換える。

攻撃の難読化とエンコーディングインジェクション

従来のネットワークトラフィックは西欧文化圏のアルファベットがほとんどであったが、それ以外の言語を伝えるためにUnicodeなどの新しいエンコード方式が使われるようになってきた。
しかしこれはWebアプリケーションにとっては新たな脅威となるかもしれない。
異なるコードでエンコードされた中に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜んでいるかもしれないため。UTF-8による攻撃方法の例。

<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

上の例を実行するとメッセージボックスが表示される。
なお、これは上のsanitize()フィルタで認識される。
Hackvertorは文字列の難読化とエンコードを行なう優れたツールであり、「敵を知る」のに最適。
Railsのsanitize()メソッドは、このようなエンコーディング攻撃をかわす。

CSSインジェクション

CSSインジェクションは実際にはJavaScriptのインジェクション。

MySpace Samyワームは、攻撃者であるSamyのプロファイルページを開くだけで自動的にSamyに友達リクエストを送信するというもの。

MySpaceでは多くのタグをブロックしていたが、CSSについては禁止していなかったため、ワームの作者はCSSに以下のようなJavaScriptを仕込んだ。

<div style="background:url('javascript:alert(1)')">

ここでスクリプトの正味の部分(ペイロード)はstyle属性に置かれる。
一重引用符と二重引用符が既に両方使われているので、このペイロードでは引用符を使えない。
しかしJavaScriptにはどんな文字列もコードとして実行できてしまう便利なeval()関数がある。
この関数は強力だが危険。

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

eval()関数はブラックリスト方式の入力フィルタを実装した開発者にとってはまさに悪夢。
この関数を使われてしまうと、たとえば以下のように「innerHTML」という単語をstyle属性に隠しておくことができてしまうため。

alert(eval('document.body.inne' + 'rHTML'));

次は、MySpaceは"javascript"という単語をフィルタしていたにもかかわらず、「javascript」と書くことでこのフィルタを突破された。

<div id="mycode" expr="alert('hah!')" style="background:url('java
 script:eval(document.all.mycode.expr)')">

さらに次は、ワームの作者がCSRFセキュリティトークンを利用していた。
ワームの作者は、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを手に入れていた。

最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入。

moz-bindingというCSSプロパティは、FirefoxなどのGeckoベースのブラウザではCSS経由でJavaScriptを注入する手段に使われる可能性があることが判明。

対応策

ブラックリストによる完璧なフィルタは決して作れません。
しかしWebアプリケーションでカスタムCSSを使える機能はめったにないため、これを効果的にフィルタできるホワイトリストCSSフィルタを見つけるのは難しい。
Webアプリケーションの色や画像をカスタマイズできるようにしたいのであれば、ユーザーに色や画像を選ばせ、Webアプリケーションの側でCSSをビルドするようにする。
ユーザーがCSSを直接カスタマイズできるような作りにはしない。
どうしても必要であれば、ホワイトリストベースのCSSフィルタとしてRailsのsanitize()メソッドを使う。

テキスタイルインジェクション(Textile Injection)

セキュリティ上の理由からHTML以外のテキストフォーマット機能を提供するのであれば、何らかのマークアップ言語を採用し、それをサーバー側でHTMLに変換するようにする。
RedClothはRuby用に開発されたマークアップ言語の一種だが、注意して使わないとXSSに対しても脆弱になる。

対応策

RedClothは必ずホワイトリストフィルタと組み合わせて使う。

Ajaxインクジェクション

Ajaxでも、通常のWebアプリケーション開発上で必要となるセキュリティ上の注意と同様の注意が必要。
1つ例外がある。
ページヘの出力は、アクションがビューをレンダリングしない場合であってもエスケープが必要。

in_place_editorプラグインや、ビューをレンダリングする代りに文字列を返すようなアクションを使う場合は、アクションで返される値を確実にエスケープする必要がある。
もしXSSで汚染された文字列が戻り値に含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまう。
入力値はすべてh()メソッドでエスケープする。

コマンドラインインクジェクション

ユーザーが入力したデータをコマンドラインのオプションに使う場合は十分に注意が必要。

Webアプリケーションが背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド)、syscall(コマンド)、system(コマンド)、そしてバッククォート記法という方法が用意されている。
特に、これらのコマンド全体または一部を入力できる可能性に注意が必要。
ほとんどのシェルでは、コマンドにセミコロン;や垂直バー|を追加して別のコマンドを簡単に結合できてしまう。

対応策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド, パラメータ)メソッドを使うこと。

system("/bin/echo","hello; rm *")
# "hello; rm *"を実行してもファイルは削除されない

ヘッダーインクジェクション

HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがある。
これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性がある。

HTTPリクエストヘッダで使われているフィールドの中にはReferer、User-Agent (クライアント側ソフトウェア)、Cookieフィールドがありまる。
Responseヘッダーには、たとえばステータスコード、Cookieフィールド、Locationフィールド (リダイレクト先を表す) がある。
これらのフィールド情報はユーザー側から提供されるものであり、さほど手間をかけずに操作できてしまう。
これらのフィールドもエスケープする。
エスケープが必要になるのは、管理画面でUser-Agentヘッダを表示する場合などが考えられる。

さらに、ユーザー入力の一部を取り入れたレスポンスヘッダを生成する場合は、何が行われているのかを正確に把握することが重要。
たとえば、ユーザーを特定のページにリダイレクトしてから元のページに戻したいとする。
このとき、refererフィールドをフォームに導入して、指定のアドレスにリダイレクトしたとする。

redirect_to params[:referer]

このとき、Railsはその文字列をLocationヘッダフィールドに入れて302(リダイレクト)ステータスをブラウザに送信する。
悪意のあるユーザーがこのとき最初に行なうのは、以下のような操作。

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

Rails 2.1.2より前のバージョン(およびRuby)に含まれるバグが原因で、ハッカーが以下のように任意のヘッダを注入できてしまう可能性がある。

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld

上のURLにおける%0d%0aは\r\nがURLエンコードされたものであり、RubyにおけるCRLF文字。
2番目の例では2つ目のLocationヘッダーフィールドが1つ目のものを上書きするため、以下のようなHTTPヘッダーが生成される。

HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld

ヘッダーインジェクションにおける攻撃方法とは、ヘッダーにCRLF文字を注入すること。
攻撃者は偽のリダイレクトでどんなことができてしまうのか。
攻撃者は、ユーザーをフィッシングサイトにリダイレクトし(フィッシングサイトの見た目は本物そっくりに作っておきます)、ユーザーを再度ログインさせてそのログイン情報を攻撃者に送信する可能性がある。
あるいは、フィッシングサイトからブラウザのセキュリティホールを経由して邪悪なソフトウェアを注入するかもしれない。
Rails 2.1.2ではredirect_toメソッドのLocationフィールドからこれらの文字をエスケープするようになった。
ユーザー入力を用いて通常以外のヘッダーフィールドを作成する場合には、CRLFのエスケープを必ず自分で実装する。

レスポンス分割

ヘッダーインジェクションが実行可能になってしまっている場合、レスポンス分割(response splitting)攻撃も同様に実行可能になっている可能性がある。
HTTPのヘッダーブロックの後ろには2つのCRLFが置かれてヘッダーブロックの終了を示し、その後ろに実際のデータ(通常はHTML)が置かれる。
レスポンス分割とは、ヘッダーフィールドに2つのCRLFを注入し、その後ろに邪悪なHTMLを配置するという手法。
このときのレスポンスは以下のようになります。

HTTP/1.1 302 Found [最初は通常の302レスポンス]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:
Content-Type: text/html


HTTP/1.1 200 OK [ここより下は攻撃者によって作成された次の新しいレスポンス]
Content-Type: text/html


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [任意の邪悪な入力が
Keep-Alive: timeout=15, max=100         リダイレクト先のページとして表示される]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

特定の条件下で、この邪悪なHTMLが標的ユーザーのブラウザで表示されることがある。
ただし、おそらくKeep-Alive接続が有効になっていないとこの攻撃は効かない。
多くのブラウザはワンタイム接続を使っているため。
かといって、Keep-Aliveが無効になっていることを当てにするわけにはいかない。
これはいずれにしろ重大なバグであり、ヘッダーインジェクションとレスポンス分割の可能性を排除するため、Railsを2.0.5または2.1.2にアップグレードする必要がある。

安全ではないクエリ生成

Rackがクエリパラメータを解析(parse)する方法とActive Recordがパラメータを解釈する方法の組み合わせに問題があり、where句がIS NULLのデータベースクエリを本来の意図に反して生成することが可能になってしまう。
(CVE-2012-2660、CVE-2012-2694 および CVE-2013-0155) のセキュリティ問題への対応として、Railsの動作をデフォルトでセキュアにするためにdeep_mungeメソッドが導入された。

以下は、deep_mungeが実行されなかった場合に攻撃者に利用される可能性のある脆弱なコードの例。

unless params[:token].nil?
  user = User.find_by_token(params[:token])
  user.reset_password!
end

params[:token]が[nil]、[nil, nil, ...]、['foo', nil]のいずれかの場合、nilチェックをパスするにもかかわらず、where句がIS NULLまたはIN ('foo', NULL)になってSQLクエリに追加されてしまう。

Railsをデフォルトでセキュアにするために、deep_mungeメソッドは一部の値をnilに置き換える。
リクエストで送信されたJSONベースのパラメータがどのように見えるかを以下に表示。

JSON    Parameters
{ "person": null }  { :person => nil }
{ "person": [] }    { :person => [] }
{ "person": [null] }    { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }

リスクと取扱い上の注意を十分理解している場合に限り、deep_mungeをオフにしてアプリケーションを従来の動作に戻すことができる。

config.action_dispatch.perform_deep_munge = false

デフォルトのヘッダー

Railsアプリケーションから受け取るすべてのHTTPレスポンスには、以下のセキュリティヘッダーがデフォルトで含まれている。

config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '1; mode=block',
  'X-Content-Type-Options' => 'nosniff',
  'X-Download-Options' => 'noopen',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin'
}

デフォルトのヘッダー設定はconfig/application.rbで変更できる。

config.action_dispatch.default_headers = {
  'Header-Name' => 'Header-Value',
  'X-Frame-Options' => 'DENY'
}

以下のようにヘッダーを除去することもできる。

config.action_dispatch.default_headers.clear

よく使われるヘッダーのリストを以下に示す。

X-Frame-Options: Railsではデフォルトで'SAMEORIGIN'が指定される。
このヘッダーは、同一ドメインでのフレーミングを許可。
'DENY'を指定するとすべてのフレーミングが不許可になる。
すべてのWebサイトについてフレーミングを許可するには'ALLOWALL'を指定。
X-XSS-Protection: Railsではデフォルトで'1; mode=block'が指定される。
XSS攻撃が検出された場合は、XSS Auditorとブロックページを使う。
XSS Auditorをオフにしたい場合は'0;'を指定します(レスポンスがリクエストパラメータからのスクリプトを含んでいる場合に便利)。
X-Content-Type-Options: 'nosniff'はRailsではデフォルト。
このヘッダーは、ブラウザがファイルのMIMEタイプを推測しないようにする。
X-Content-Security-Policy: このヘッダーは、コンテンツタイプを読み込む元のサイトを制御するための強力なメカニズム。
Access-Control-Allow-Origin: このヘッダーは、同一生成元ポリシーのバイパスとクロスオリジン(cross-origin)リクエストをサイトごとに許可する。
Strict-Transport-Security: このヘッダーは、ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定。

Content Security Policy(CSP)

Railsでは、アプリケーションでContent Security Policy(CSP)を設定するためのDSLが提供されている。
グローバルなデフォルトポリシーを設定し、それをリソースごとにオーバーライドすることも、lambdaを用いてリクエストごとに値をヘッダーに注入することもできる(マルチテナントのアプリケーションにおけるアカウントのサブドメインなど)。

以下はグローバルなポリシーの例。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  # 違反レポートの対象URIを指定する
  policy.report_uri "/csp-violation-report-endpoint"
end

以下はコントローラでオーバーライドするコード例。

# ポリシーをインラインでオーバーライドする場合
class PostsController < ApplicationController
  content_security_policy do |p|
    p.upgrade_insecure_requests true
  end
end
# リテラル値を使う場合
class PostsController < ApplicationController
  content_security_policy do |p|
    p.base_uri "https://www.example.com"
  end
end
# 静的値と動的値を両方使う場合
class PostsController < ApplicationController
  content_security_policy do |p|
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end
# グローバルCSPをオフにする場合
class LegacyPagesController < ApplicationController
  content_security_policy false, only: :index
end

レガシーなコンテンツを移行するときにコンテンツの違反だけをレポートしたい場合は、設定でcontent_security_policy_report_only属性を用いてContent-Security-Policy-Report-Onlyを設定。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_report_only = true
# コントローラでオーバーライドする場合
class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end

以下の方法でnonceの自動生成を有効にできる。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

後は以下のようにhtml_optionsの中でnonce: trueを渡せばnonce値が自動的に追加される。

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

javascript_include_tagでも同じことができる。

<%= javascript_include_tag "script", nonce: true %>

セッションごとにインライン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】iframeタグを使って外部コンテンツが埋め込めない

iframeで外部ページを埋め込む時の問題

ページに組み込んだiframeが表示されない問題についての対処法です。
ブラウザのコンソールをチェックしてみると、以下のエラーが表示されている場合があるかもしれません。

error
Refused to display xxxxx in a frame because it set 'X-Frame-Options' to SAMEORIGIN'.

これはRailsでデフォルトのセキュリティヘッダーのX-Frame-Options がSAMEORIGINになっているからです。

X-Frame-Options

X-Frame-Options は HTTP のレスポンスヘッダーで、ブラウザーがページを frame, iframe, embed, object の中に表示することを許可するかどうかを示すために使用されます。サイトはコンテンツが他のサイトに埋め込まれないよう保証することで、クリックジャッキング攻撃を防ぐために使用することができます。

参照:X-Frame-Options

 response.headers['X-Frame-Options'] = 'SAMEORIGIN'

SAMEORIGINではオリジンページに含まれているページのみフレームで表示することが可能という意味です。DENYにするとフレームにページが読み込まれないようになります。
ALLOW-FROM (uri)を使うと、指定されたuriのみがフレーム内で表示することが可能になります。

 response.headers['X-Frame-Options'] = 'DENY'
 response.headers['X-Frame-Options'] = 'ALLOW-FROM https://example.com'

しかしALLOW-FROMはブラウザ側の未サポートやバグがあり、非推奨となっています。chromeやFireFoxなどのメインブラウザで無効なので使わない方が良さそうです。
またALLOWALLというディレクティブがあるという記事もあります。(参照:Rails4: Allow your site to be iframed by another site.

 response.headers['X-Frame-Options'] = 'ALLOWALL'

しかし試したところ無効でした。MDNで確認したところ有効なディレクティブは現在SAMEORIGINDENYだけみたいです。

X-Frame-Optionsのまとめ

  • X-Frame-Options
    • DENY - ページをフレーム内に表示できなくなる
    • SAMEORIGIN - ページのドメインとフレームのドメインが同じ場合にのみ、ページがフレーム内で表示される。
    • ALLOW-FROM http://example.com (非推奨) - 指定されたURIのページのみ、フレーム内で表示される。
    • ALLOWALL (非推奨/無効) - どのページもフレーム内で表示される。

Content-Security-Policy (CSP)

コンテンツセキュリティポリシー (CSP) は、クロスサイトスクリプティング (XSS) やデータインジェクション攻撃などのような、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。これらの攻撃はデータの窃取からサイトの改ざん、マルウェアの拡散に至るまで、様々な目的に用いられます。

CSP を有効にするには、ウェブサーバーから Content-Security-Policy HTTP ヘッダーを返すように設定する必要があります (X-Content-Security-Policy ヘッダーに関する記述が時々ありますが、これは古いバージョンのものであり、今日このヘッダーを指定する必要はありません)。

X-Frame-Optionsの代替方法

X-Frame-OptionsでALLOW-FROM uriALLOWALLを使えないので、その代替としてContent-Security-Policy(CSP)を使うことができそうです。CSPのナビゲーションディレクティブを用いると埋め込み先を管理することができます。
frame-ancestorsというディレクティブでによって埋め込み先のドメインを限定化し、そのドメインでのみの表示を許可することができます。

response.headers['Content-Security-Policy'] = "frame-ancestors https://example.com"

また例えばstaging環境を許容したいという場合は以下のような感じで書くことができます。

url = Rails.env.production? ? "https://example.com" : "https://staging.example.com"
response.headers['Content-Security-Policy'] = "frame-ancestors 'self' #{url}"

これはこのように書き換えることも可能です。

response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com"

よって埋め込み元のcontrollerには以下のように記述すればOKです。

blogs_controller.rb
class BlogsController < ApplicationController
  after_action :allow_iframe, only: [:iframe]

  def iframe
    @blog = Blog.new
  end

  def allow_iframe
     response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com"
  end
end

CSP frame-ancestorsの書き方まとめ

  • 埋め込み先を自身のドメイン (サブドメインを除く) に限定させたい場合。

例えばhttps://example.com/blogsページにhttps://example.com/articlesページを埋め込みたい時、親がhttps://example.comで同じなのでblogs(埋め込み元)のコントローラーに以下の処理を書けば親が共通のページはフレームで表示することができます。

blogs_controller.rb
response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
  • 自身のドメインとそのサブドメインへの埋め込みを許可したい場合。

例えばhttps://example.com/blogsと同じ親を持つhttps://example.com/articlesページと、サブドメインhttps://staging.example.comを親に持つhttps://staging.example.com/articlesページに埋め込みたい時、blogs(埋め込み元)のコントローラーに以下の処理を書けば埋め込み先のフレーム内に表示されます。

blogs_controller.rb
response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com"

'self' はメインドメインを親に持つページ内への埋め込みを許可するという意味であり、https://*.example.comはサブドメイン(https://staging.example.com)を親とするページ内への埋め込みを許可するという意味になります。

CSPのまとめ

  • CSP
    • frame-ancestors - <frame>, <iframe>, <object>, <embed>, <applet>などの要素によって埋め込まれるページの親を指定し、埋め込みを有効にする。

X-Frame-Optionsのエラーは出るものの変更する必要はなく(というか使えるディレクティブがなく)、その代わりにCSPを指定する必要があったというのが学びでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6のActionTextでransackがうまく使えないので検索できるようにする

これはなに?

Rails6で導入されたActionTextにより簡単にブログ機能を実装できるようになりました。
大変ありがたいお話なのですが、検索機能を定番のgemであるransackを使って実装しようとしたらエラーになり使えませんでした。

その時の対処法を記載します。

どうすればいいのか?

結論から言うと、ransack使わないですね。タイトルと若干逸脱してる気もしますが、動けばいいと思います。

手順

こっから具体的な解決までのプロセスです。

事象

まずransack使うとどうなるかって話ですね。下記のようなエラーになります。

Completed 500 Internal Server Error in 42ms (ActiveRecord: 1.4ms | Allocations: 5041)
05:42:11 web.1       | 
05:42:11 web.1       | 
05:42:11 web.1       |   
05:42:11 web.1       | ActionView::Template::Error (undefined method `body_cont' for #<Ransack::Search:0x00007fc354bc9ac0>):
05:42:11 web.1       |     1: <h1>Blogs</h1>
05:42:11 web.1       |     2: 
05:42:11 web.1       |     3: <%= search_form_for @q do |f| %>
05:42:11 web.1       |     4:   <%= f.search_field :body_cont %>
05:42:11 web.1       |     5: 
05:42:11 web.1       |     6:   <%= f.submit class: "btn btn-outline-primary" %>
05:42:11 web.1       |     7: <% end %>

undefined methodで500エラーですね。とても残念です。
因みに、title_contだとうまくいきました。

原因調査

まず、該当モデルの構成はこんな感じです。title_contだとうまく検索できたので、ActionText使ってるとうまく検索できないんだろうなというのは容易に想像できます。

class Blog < ApplicationRecord
  belongs_to :user
  has_many :comments
  has_many :favorites
  has_rich_text :body

  validates :title, presence: true
  validates :body, presence: true
end

DB構成

blogstitleと言うカラムしかもってなくて、ブログのコンテンツであるbodyaction_text_rich_textsが管理してます。

なので、bodyblogsはカラムとして保持していないためメソッドが生成されてなくて、undefined methodで落ちていたのでした。

create_table "action_text_rich_texts", force: :cascade do |t|
    t.string "name", null: false
    t.text "body"
    t.string "record_type", null: false
    t.bigint "record_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
  end

create_table "blogs", force: :cascade do |t|
    t.string "title"
    t.bigint "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_blogs_on_user_id"
  end

解決方法

原因がわかったので、内部結合していい感じにransackで取得できないかと思い、READMEを見てみる。

https://github.com/activerecord-hackery/ransack

んー、なんかなさそう。頑張ればいけるのか。頑張りたくないからgem使ってるんだが。

Google先生に聞いてみよう

gemの使い方を熟読するのは面倒だったのでstackoverflowを見てみると、ドンピシャの質問がありました。

https://stackoverflow.com/questions/59316606/how-ransack-can-search-for-rails6-action-text-content/60696169#60696169

淡白すぎない?
しかも掲示してるコードだと動かないですね。ガッデム。

自分で頑張ろう

調べてもransackでの解決方法が分からなかったので、scopeを定義してクエリで解決することにしました。

app/models/blog.rb
class Blog < ApplicationRecord
  belongs_to :user
  has_many :comments
  has_many :favorites
  has_rich_text :body

  validates :title, presence: true
  validates :body, presence: true

  # このscopeです
  scope :search, -> (search_param = nil) {
    return if search_param.blank?
    joins("INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog'")
    .where("action_text_rich_texts.body LIKE ? OR blogs.title LIKE ? ", "%#{search_param}%", "%#{search_param}%")
  }
end

ちょっと詳しく解説すると、action_text_rich_textsと内部結合してbodyの中身をLIKE句で検索してます。
結合条件は、record_idrecord_typeのそれぞれ2つです。それぞれ結合先のモデル名と該当レコードのIDですね。

ViewとControllerはとてもシンプルです

app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
  def index
    @blogs = Blog.search(params["q"])
  end
end

app/views/blogs/index.html.erb
<h1>Blogs</h1>

<%= form_tag blogs_path, method: :get do %>
  <%= text_field_tag :q %>

  <%= submit_tag "Search", class: "btn btn-outline-info btn-sm" %>
<% end %>

<% @blogs.each do |blog| %>
  <div class="my-3">
    <h2>
      <%= link_to blog.title, blog %>
      <small class="pl-1">
        by <em><%= blog.user.name %></em>
      </small>
    </h2>
  </div>
<% end %>

動かしてみる

でっきるかなあ? でっきるかなあ?

Started GET "/blogs?q=world&commit=Search" for 172.21.0.1 at 2020-05-16 06:47:08 +0000
06:47:08 web.1       | Processing by BlogsController#index as HTML
06:47:08 web.1       |   Parameters: {"q"=>"world", "commit"=>"Search"}
06:47:08 web.1       |   User Load (1.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
06:47:08 web.1       |   Rendering blogs/index.html.erb within layouts/application
06:47:08 web.1       |   Blog Load (2.8ms)  SELECT "blogs".* FROM "blogs" INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog' WHERE (action_text_rich_texts.body LIKE '%world%' OR blogs.title LIKE '%world%' )
06:47:08 web.1       |   ↳ app/views/blogs/index.html.erb:9
06:47:08 web.1       |   User Load (1.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
06:47:08 web.1       |   ↳ app/views/blogs/index.html.erb:14
06:47:08 web.1       |   Rendered blogs/index.html.erb within layouts/application (Duration: 39.4ms | Allocations: 1560)
06:47:08 web.1       | Completed 200 OK in 100ms (Views: 79.1ms | ActiveRecord: 5.9ms | Allocations: 4093)

できたっぽい

まとめ

シンプルにテーブルのカラムを条件に検索したいならransack使うのが良さげですが、ちょっと複雑な条件での検索は難しそうです。僕がちゃんと検索できてないだけな気もしますが。

ransackだとformから世話してくれるので、titleだけの検索なら速攻で実装できて快適でした。

でもまあ、自前で検索機能作ってもそんなに手間じゃないので、Module化して各モデルでincludeする形でもいいかと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで7つの基本アクション以外の定義

基本アクションのおさらい

以下がRailsの標準アクションです
スクリーンショット 2020-05-16 15.12.28.png

自分でアクションを定義する

上記の基本アクション以外の処理を行いたい場合は自身で定義することができます。

その際のルーティングの定義方法にはcollectionmemberが使えます

Rails.application.routes.draw do
  resources :hoges do
    collection do
      HTTPメソッド 'オリジナルのメソッド名'
    end
  end
end
Rails.application.routes.draw do
  resources :hoges do
    member do
      HTTPメソッド 'オリジナルのメソッド名'
    end
  end
end

違いとしては、生成されるルーティングにidが付くか、付か無いかです。

・collection → :idなし
・member → :idあり

特定のページへ遷移する必要がある場合などは、memberを使うといった感じです。

そして、重要なのは、どこにメソッドの内容を記述するかです。

一般的に、開発現場などでも、テーブル(DB)とのやりとりに関するメソッドはモデルに記載するのが通例らしいです。

例えば、検索機能を実装したい時なんかはその処理を行うメソッドをモデルに書き、コントローラーで呼び出します(viewの検索フォームなどの記述は省略します)

使用例

routes.rb
 resources :tweets do
    collection do
      get 'search'
    end
  end
tweet.rb
class Tweet < ApplicationRecord
  #省略

  def self.search(search)
    return Tweet.all unless search
    Tweet.where('text LIKE(?)', "%#{search}%")
  end
end
tweets_controller.rb
class TweetsController < ApplicationController

  #省略


  def search
    @tweets = Tweet.search(params[:keyword])
  end

end

それぞれを説明すると、

まず、searchアクションのルーティングを設定します。検索結果を表示するには、詳細ページに行く必要がなく、そのため、collectionを使っています。

formでユーザーが検索を行うと、controllerでsearchアクションからモデルに記述したsearchメソッドを呼び出します。その際、引数として検索結果を渡しています(params[:keyword])

検索結果はモデルのsearchメソッドの中で変数searchに代入されメソッド内で使用できるようになります。

処理の内容は、searchの中身が空なら全ての投稿を取得し、値が入っているならwhereメソッドの中身の条件式に一致した投稿を取得します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails】RSpecによる単体テスト(コントローラ)

準備

Gemrails-controller-testingをインストールする。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  ...
  gem 'rails-controller-testing' #追記 
end
terminal
$ bundle install

次のディレクトリを作成する。
 spec/controllers

次のファイルを作成する。
 spec/controllers/tweets_controller_spec.rb

spec/controllers/tweets_controller_spec.rb
require 'rails_helper'

describe TweetsController do

end

specファイルを特定してテストを実行する。

terminal
$ bundle exec rspec spec/controllers/tweets_controller_spec.rb

Finished in ~

0 examples, 0 failures

正常に実行できることを確認した。

テストコード

newアクション

spec/controllers/tweets_controller_spec.rb
require 'rails_helper'

describe TweetsController do
  # describe 'http method' #action do
  describe 'GET' #new do
    it 'new.html.erbに遷移すること' do
      get :new
      expect(response).to render_template :new
    end
  end
end
response

example内で行われるリクエストの後の遷移先のビューの情報を持つインスタンス。

render_templateマッチャ

引数にシンボル型のアクションを指定することで、そのアクションがリクエストされた時に自動的に遷移するビューを返す。

テストを実行する。

terminal
$ bundle exec rspec spec/controllers/tweets_controller_spec.rb

TweetsController
  GET #new
    new.html.erbに遷移すること

Finished in ~
1 example, 0 failures

editアクション

コントローラ確認

app/controllers/tweets_controller.rb
  def edit
    @tweet = Tweet.find(params[:id])
  end

editアクションでは、@tweetインスタンスを定義している。

spec/controllers/tweets_controller_spec.rb
require 'rails_helper'

describe TweetsController do
  # describe 'http method' #action do
  describe 'GET' #new do
    ...

  describe 'GET #edit' do
    it "@tweetに正しい値が入っていること" do
      # factory_botで定義するインスタンスを一時的にDBに登録して取得する
      # 登録したデータはテスト完了時に削除される
      tweet = create(:tweet)
      # 作成したインスタンスのidをセットする
      get :edit, params: { id: tweet }
      #editアクションで取得するtweetが、テストで作成したtweetと同じであることを確認する
      expect(assigns(:tweet)).to eq tweet
    end

    it "edit.html.erbに遷移すること" do
      tweet = create(:tweet)
      get :edit, params: { id: tweet }
      expect(response).to render_template :edit
    end
  end
end
assignsメソッド

コントローラのテスト時に、アクションで定義するインスタンス変数をテストするためのメソッド。
直前で定義したアクションの中で定義されるインスタンス変数を、シンボル型でとる。
@tweet -> :tweet)

factory_botでtweetインスタンスを定義する。

spec/factories/tweets.rb
FactoryBot.define do
  factory :tweet do
    text {"hello!"}
    image {"hoge.png"}
    # created_atをランダムに生成
    created_at { Faker::Time.between(from: DateTime.now - 2, to: DateTime.now ) }
    user
  end
end

登録日カラムにて利用するFakerの説明は最後に。

indexアクション

indexアクションを確認する。

app/controllers/tweets_controller.rb
  ...
  def index
    @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  end
  ...
spec/controllers/tweets_controller_spec.rb
describe 'GET #index' do
  it "@tweetに正しい値が入っていること"
    #tweetレコードを3つ保存して、変数tweetsにインスタンスに取得
    tweets = create_list(:tweet, 3)
    #indexアクションへの擬似的なリクエスト
    get :index
    # created_atを基準にして、降順にtweetsを並び替える処理
    expect(assigns(:tweets)).to match(tweets.sort{ |a, b| b.created_at <=> a.created_at })
  end
  it "index.html.erbに遷移すること"
    get :index
    expect(response).to render_template :index
  end
end
matchマッチャ

引数に配列クラスのインスタンスをとり、expectの引数と比較する。
配列の中身の順番までチェックする。

factory_botでのuserインスタンス定義する。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              {"abe"}
    # email                 {"kkk@gmail.com"}
    password              {"00000000"}
    password_confirmation {"00000000"}
    sequence(:email) {Faker::Internet.email}
  end
end
Faker

今回、tweetインスタンスを複数作ることになるが、emailの値が重複するとバリデーションによりエラーが起こる。
そのため、Fakerで動的にダミーデータを生成する。

Gemfile
group :test do
  ...
  gem 'faker', "~> 2.8"
end
terminal
$ bundle install

テストの実行

terminal
$ bundle exec rspec spec/controllers/tweets_controller_spec.rb  
2020-05-16 16:26:37 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

TweetsController
  GET #new
    new.html.erbに遷移すること
  GET #edit
    @tweetに正しい値が入っていること
    edit.html.erbに遷移すること
  GET #index
    @tweetに正しい値が入っていること
    index.html.erbに遷移にすること

Finished in 0.7887 seconds (files took 3.68 seconds to load)
5 examples, 0 failures

予期する通り動作することを確認できた。

RSpec参考書籍

https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeでRails5.2+MySQL開発環境

(社内の特殊な環境を移行した際のメモです)

rubyのイメージでうまく環境を構築できなかった(=既存のrails環境のGemfileと一致するバージョンのgemが揃わなかった)ため、Ubuntuのイメージから出発してRailsの環境を構築した際の設定です。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./tmp/mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secret
  web:
    build: .
    command: "rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - ./:/railsapp
    ports:
      - "3000:3000"
    depends_on:
      - db
Dockerfile
FROM ubuntu:18.04

RUN apt-get update && apt-get upgrade && apt-get install -y \
  ruby-dev libmysqlclient-dev libmagick++-dev libcurl4-openssl-dev libssl-dev nodejs \
  && gem install rails -v "~> 5.2" -N && gem install bundler -v "~> 1.16"

RUN mkdir /railsapp
WORKDIR /railsapp
COPY ./ /railsapp
RUN bundle install
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

情報セキュリティについてのまとめ

情報セキュリティ

情報セキュリティとは、WEBサービスにおいてのセキュリティのことを指します。
情報漏洩や不正なアクセスを防ぎつつ権限のあるユーザーの利便性を高めるのが理想です。

下記の3つを保持することがWEBサービスの使命です。
1.機密性
-権限のない人が情報資産を見たり使用したりできないようにする
2.完全性
-権限のない人が情報を消したり書き換えたりできないようにする
3.可用性
-権限のある人(ユーザー)がサービスをいつでも利用できるようにする

全てにおいてのセキュリティをおびやかす欠陥や問題点のことを脆弱性と言います。
また、脆弱性は開発者のチェック不足やバグによって生まれます。

脆弱性の具体例は以下です
-個人情報を勝手に閲覧される(機密性の侵害)
-WEBページの内容が改ざんされる(完全性の侵害)
-WEBページの利用ができなくなる(可用性の侵害)

ユーザーへの金銭的補填、開発者の信頼の失墜、機会損失などの被害が生まれてしまうため、脆弱性への対策はしっかり行わなければいけません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyとは?Railsとは?

記事の概要

Ruby/Ruby on Railsとは何か分からない人が少し理解できるようになります。

Rubyとは

Rubyとはプログラミング言語の一つです。
小さいプログラムから大きいWebアプリケーションまでを実用的に作成することができます。

Rubyの特徴

・簡潔な文法で記述することができる
・コードが読みやすい
・プログラムを記述してすぐに実行することができる

Rubyを使用しているサイト

・Twitter
・hulu
・クックパッド
・食べログ
・楽天市場
・Airbnb

Railsとは

Railsとは、Ruby on Railsの略称です。Railsと言われることが多いです。
RailsはWebアプリケーションフレームワークの1つで、最も多く使われています。
Rubyという言語を使ってWebアプリケーションを作っていきます。

Webアプリケーションフレームワークとは?

Webアプリケーションフレームワークとは、Webアプリケーションを簡単に作るための骨組みのことです。
これを使うことによってより少ない労力で開発することができます。

この記事を読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングのネスト

ルーティングのネストとは

通常のルーティングの記述は

Rails.application.routes.draw do
  resources :親となるコントローラー 
  resources :親となるコントローラー 
  resources :親となるコントローラー  ,,,,,
end

という感じでそれぞれ独立した形でコントローラーへのルーティングを生成していますが、

ルーティングのネストをすると、あるコントローラーのルーティング内に、別のコントローラーのルーティングを記述することができます

Rails.application.routes.draw do
  resources :親となるコントローラー do
    resources :子となるコントローラー           ←階層を下げ、do,,,endで囲む
  end
end

使用するメリット

例えば、インスタグラムやツイッターなどにはコメント機能があります。

そして、そのコメントは、必ず投稿先が存在しています。

それでは、ネストをしないでルーティングを設定した場合と、ネストをした場合の生成されるルーティングの違いを見てみます。

ネストなし

Rails.application.routes.draw do
  #省略

  resources :tweets
  resources :comments, only: :create
end
Prefix Verb     URI Pattern           Controller#Action
#省略

comments POST   /comments(.:format)   comments#create

ネストあり

Rails.application.routes.draw do
  #省略

  resources :tweets do
    resources :comments, only: :create
  end
end
Prefix Verb           URI Pattern                            Controller#Action
#省略
tweet_comments POST   /tweets/:tweet_id/comments(.:format)   comments#create

URIに注目して下さい。コメントには投稿先が必ずあるのにも関わらず、ネストをしない場合のルーティングは、どの投稿先のコメントなのかを示す情報がありません。

それに対し、ネストをした場合は、tweet_idの箇所にツイートのid番号が入ります。それにより、どのツイートに対するコメントなのかというのがURIから判断できるようになります。

まとめ

・ネストをすることで関係性のあるもの(アソシエーション先)のid情報が取得できます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】remote:true形式でAjax通信を行う(ブックマーク機能のajax化)

Ajaxとは

Ajaxとは、Webブラウザ上で非同期通信を行い、ページ全体の再読み込み無しにページを更新する方法のことです。

同期通信について

同期通信では、クライアントはwebページ全体の情報(HTMLとそれに紐づくcss,js,imageなどのアセット)をサーバーから受け取って、ページを一から作り直します。
例えばページの一部を変更するだけなのに、他の部分も組み立て直すってことはその分ページの表示に時間がかかっちゃいます。(サーバー側の処理を待つことになる)

しかも、このリクエスト〜レスポンスの処理を行っている間は、他の処理を行わずにサーバーからレスポンスが返ってくるのを待ち続ける必要があります(よくあるのが画面が真っ白になって何もできない状態)。

そこでAjaxのような非同期通信を使用すれば、ページ遷移無しに、高速で更新処理を行い、尚且つ、リクエスト〜レスポンスの処理を行っている間も他の処理が行えます

非同期通信の方法は2種類

この便利なAjaxによる非同期通信を行う方法としては、
①remote:true形式
②ajax関数を使った形式

の2パターンが存在しますが、今回はremote:true形式について以下に記していきます。

仕組みだけ知りたいよって方は、コードの説明は読み飛ばしちゃっても大丈夫です。

コードの説明

今回作るもの

掲示板のブックマーク(いいね)ボタンを押した時に、ブックマークの登録、解除を行うという仕組みをajax化させていきます。

ルーティングの設定

ブックマークの登録、削除を行うために必要なルーティングの設定を行う。

config/routes.rb
resources :boards do
  resources :bookmarks, only: %i[create destroy], shallow: true
end

モデルの設定

モデルでUsersテーブル、Boardsテーブル、Bookmarksテーブルの関連付を行う。

簡素なER図を書くとこんな感じ。
スクリーンショット 2020-05-16 14.13.29.png

app/models/user.rb
class User < ApplicationRecord
  has_many :boards, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :bookmark_boards, through: :bookmarks, source: :board

# ブックマーク関連のインスタンスメソッド
  # ブックマークをする
  def bookmark(board)
    bookmark_boards << board
  end

  #  ブックマークを解除する
  def unbookmark(board)
    bookmark_boards.destroy(board)
  end

  # ブックマークをしているかどうかを判定する
  def bookmark?(board)
    bookmarks.where(board_id: board.id).exists?
  end
end
app/models/board.rb
class Board < ApplicationRecord
  belongs_to :user
  has_many :bookmarks, dependent: :destroy
end
app/models/bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :user_id, uniqueness: { scope: :board_id }
end

コントローラの設定

AjaxによるHTTP通信を行うには、formにremote:trueオプションを設定する必要がある。

  • form_withメソッドでAjax通信を利用しない場合(local: trueオプション)
    bookmarksコントローラのcreateアクション実行の際に、bookmarks/create.html.erbというファイルをレンダリングしようとするため、別のページへリダイレクトさせていた。

  • Ajax通信を利用する場合(remote: trueオプション)
    remote: trueの記述によって、AjaxでHTTPリクエストを送信するように設定される。
    更に、html.erbファイルではなくjs.erbファイルをレンダリングしてくれる。そして、このjs.erbファイルをjsのコードに変換した文字列が、レスポンスボディとしてブラウザに返される(詳細は後述)。

app/controllers/bookmarks_controller.rb
class BookmarksController < ApplicationController
  # js.erbファイルで変数を使用するため、インスタンス変数を設定
  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
  end

  def destroy
    @board = current_user.bookmark_boards.find(params[:id])
    current_user.unbookmark(@board)
  end
end

ブックマークボタンを切り替えるためのビュー

bookmarks/_bookmark_area.html.erbファイルで、ログイン中のユーザーが掲示板をブックマークしているかどうかによって呼び出すテンプレートを分ける。

  • ブックマークしていない場合は_bookmark.html.erbを呼び出す。
    • ブックマークボタンは色無しの状態
    • ブックマークする機能
  • ブックマークしている場合は_unbookmark.html.erbを呼び出す。
    • ブックマークボタンは色付きの状態
    • ブックマークを削除する機能
app/views/bookmarks/_bookmark_area.html.erb
<% if current_user.bookmark?(board) %>
  <%= render 'bookmarks/unbookmark', { board: board } %>
<% else %>
  <%= render 'bookmarks/bookmark', { board: board } %>
<% end %>

ブックマークしていない場合のボタンを実装

_bookmark.html.erbファイルを作成
- ブックマークするので、HTTPメソッドはpost。対応するコントローラがcreate.js.erbを呼び出す。
- id属性を付与(どのボタンをクリックしたか判別するため、各レコードのidを使用し、一意性を保つ)
- remote: trueオプションを付与。

app/views/bookmarks/_bookmark.html.erb
<%= link_to board_bookmarks_path(board),
            id: "js-bookmark-button-for-board-#{board.id}",
            method: :post,
            remote: true do %>
  <%= icon 'far', 'star' %>
<% end %>

ブックマークしている場合のボタンを実装

_unbookmark.html.erbファイルを作成
- ブックマークを削除するので、HTTPメソッドはdeletedestroy.js.erbを呼び出す。
- id属性を付与。
- remote: trueオプションを付与。

app/views/bookmarks/_unbookmark.html.erb
<%= link_to bookmark_path(board),
          id: "js-bookmark-button-for-board-#{board.id}",
          method: :delete,
          remote: true do %>
  <%= icon 'fas', 'star' %>
<% end %>

js.erbファイルを作成

js.erbファイルは以下2つの記述が可能。

1. jsの処理
2. rubyの記述(erbファイルだから)

以下のjs.erbファイルによって、画面上に表示するブックマークボタンをAjax通信で切り替えられるようにします。

create.js.erbファイルを作成】
create.js.erbでhtml()メソッドを用い、指定したセレクタのhtml部分(指定したid属性を持つ部分)を置き換える。_unbookmark.html.erbに置き換えるよう記述。

app/views/bookmarks/create.js.erb
$("#btn-bookmark-<%= @board.id %>").html("<%= j(render('boards/unbookmark', board: @board)) %>");

destroy.js.erbファイルを作成】
create.js.erbと逆の内容を記述する。

app/views/bookmarks/destroy.js.erb
$("#btn-bookmark-<%= @board.id %>").html("<%= j(render('boards/bookmark', board: @board)) %>");

ここまでがコードの細かい話!!

ブックマークボタンを押した時のHTTPレスポンスについて

上記の実装によって、なぜブックマークボタンをAjax通信で切り替えられるのか、その仕組みについて以下で説明します。
先に結論を述べると、それはサーバーからレスポンスボディとしてJavaScriptのコードを返し、そのコードに対する処理をクライアント側が実行してくれているからです。

HTTPレスポンスの中身とクライアントの処理は?

  • ブックマークボタンを押した時のHTTPレスポンスの中身はどうなっているのか?
  • それに対してクライアント(ブラウザ)側はどのような処理を行うのか?
    の2点を押さえれば、ブックマークボタンをAjax通信で切り替えられる仕組みを理解できるはずです。
ブックマークボタンを押した時のHTTPレスポンスの中身は?

erbファイルをJS形式のコード(この段階ではただの文字列!)に変換したものが、レスポンスボディとしてクライアントに返されます。

つまり、erbファイルをそのままクライアントに返すのではなく、サーバー側でjs.erbファイルのrubyの記述(j renderとか@boardとか)を事前に実行し、HTMLのコードとして展開した結果を、クライアント側に返しているのです。
一言で表すなら、クライアント側が読める内容に変換してから返している、ということです。

レスポンスに対するクライアント側の処理

これに対し、クライアントはサーバーから返ってきたレスポンスボディを見て、「これはjs形式のものだな」と判断し、そこでようやくレスポンスボディの文字列に対してJavaScriptを実行してくれる、といった感じです。

検証ツールのネットワークタブを確認

  • HTTPレスポンスのContent-Type(どういうコンテンツの種類か)が text/javascriptになっている。
    →ajax通信に設定しているから、RailsがJS形式でレスポンスを送ってくれている。
    →クライアント側はContent-typeを見て「JSで処理するんだな」と判断している。

スクリーンショット 2020-05-03 22.11.16.png

  • レスポンスボディにjs形式のコードが入っている。
    スクリーンショット 2020-05-03 22.10.46.png

  • レスポンスボディの詳細

$("#js-bookmark-button-for-board-152").replaceWith("<a id=\"js-bookmark-button-for-bo
ard-152\" data-remote=\"true\" rel=\"nofollow\" data-method=\"delete\" href=\"/bookma
rks/152\">\n  <i class=\"fas fa-star\"><\/i>\n<\/a>");

おわりに

以上でremote:true形式でAjax通信を行う方法の説明を追えます。
なにか説明部分について誤りがございましたら、ご指摘頂きたく思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Day13-14 Rails 検索機能を実装

1.検索フォーム作成

app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

2. ルーティング

collection:ルーティングに:idがつかない
member:ルーティングに:idがつく
→検索機能は:id取得する必要がないので、collenction

config/routes.rb
collection do
  get 'search'
end

3. searchメソッドをTweetモデルに定義

ビジネスロジック(プログラムの処理の流れ)は、メソッドとして、モデルにまとめて定義する。別の場所でも使うことができるため。
また、テーブルとのやりとりに関するメソッドはモデルに置く。

app/models/tweet.rb
  def self.search(search)
    return Tweet.all unless search
    Tweet.where('text LIKE(?)', "%#{search}%")
  end
もしくは
  def self.search(search)
    if search
      Tweet.where('text LIKE(?)', "%#{search}%")
    else
      Tweet.all
    end
  end

4. searchアクションをコントローラーに定義

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show, :search]

def search
  @tweets = Tweet.search(params[:keyword])
end

5.検索結果のビュー作成

app/views/tweets/search.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
</div>

別解(7つのアクションを使った方法)

1. 検索フォーム作成

app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

2. tweets::searchesコントローラー作成

$ rails g controller tweets::searches

tweetsディレクトリ配下にsearches_controller.rbを作成することで、Tweets::という名前空間が名付けられ、tweetを検索する機能とわかる。

3. ルーティング

config/routes.rb
  namespace :tweets do
    resources :searches, only: :index
  end

namespace :ディレクトリ名 do ~ endと囲んでルーティングすると、そのディレクトリ内のコントローラーアクションを指定できる。

4. 検索フォームの送信先設定

app/views/tweets/index.html.erb
<%= form_with(url: tweets_searches_path, local: true, method: :get, class: "search-form") do |form| %>

上記ルーティング通り、tweets_search_pathに合わせる

5. indexアクションをコントローラーに追加

app/controllers/tweets/searches_controller.rb
class Tweets::SearchesController < ApplicationController
  def index
    @tweets = Tweet.search(params[:keyword])
  end
end

6. 検索結果表示ビューの作成

app/views/tweets/searches/index.html.erb
<%= form_with(url: tweets_searches_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweets/tweet", locals: { tweet: tweet } %>
  <% end %>
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6+Reactで付箋アプリっぽいページを作ってみた。4 (UI作成編2)

記事について

前回まででUIの作成を行いましたが、見た目があまりに寂しいので、
スタイルシートを使って、それっぽく見せてみます。

関連する記事

書いているうちに分量がすごくなって記事を分割したので、リンク先をまとめておきます。
その1(環境構築〜モデル作成編)
その2(API作成編)
その3(UI作成編1)
その4(UI作成編2)
おまけ(モデルのテスト編)

スタイルシート

white_board/main.htmlで使われるwhite_board.scssを編集して、各DOM要素の見た目を目ます。
全体的なレイアウトにはflexレイアウト。
付箋の中身は段組が指定しやすかったのでgridレイアウトを使用しています。

app/assets/stylesheets/white_board.scss
// タイトル要素
div#WhiteBoardTitle {
  font-weight: bold;
  font-family: Monospace;
  font-size: 24px;
  border-collapse: collapse;
}

// ホワイトボード全体
// "display: flex"でflexレイアウトを指定しています。
// 左上からピチっと並べてもらいます。
div#WhiteBoard {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-flow: row wrap;
  justify-content: flex-start;
  align-items: flex-start;
  align-content: flex-start;
}

// ユーザ毎の箱
div.UserBox {
  margin: 1px;
  width: 45vw;
  height: 45vh;
  border: 1px solid;
  border-collapse: collapse;
}

// ユーザ名を表示する箱
div.UserName {
  width: 100%;
  height: 25px;
  font-weight: bold;
  font-size: 18px;
  font-family: Monospace;
  text-align: center;
  background-color: #FFEEAA;
  border-bottom: 1px solid;
}

// 付箋表示エリア
// ここもflexレイアウト
div.TaskArea {
  width: 100%;
  height: 100%;
  display: flex;
  flex-flow: row wrap;
  align-items: flex-start;
  align-content: flex-start;
}

// 付箋本体
// 影をつけてそれっぽく
div.Sticky {
  display: grid;
  padding: 1px;
  margin: 5px;
  width: 100px;
  height: 80px;
  font-size: 10px;
  font-family: monospace;
  text-align: left;
  border: 1px #FFFF00 solid;
  background-color: #FFFF00;
  box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);

}

// タスクのタイトル
div.TaskTitle {
  grid-row: 1;
  grid-column: 1 / 3;
  font-size: 12px;
  font-weight: bold;
  font-family: monospace;
  border-bottom: #FF0000 1px solid;

}

// タスクの本文
div.TaskDescription {
  grid-row: 2 / 6;
  grid-column: 1 / 3;
  font-size: 10px;
  font-family: monospace;
}

// タスクの期日
div TaskDueDate {
  grid-row: 7 / 8;
  grid-column: 1 / 3;
  font-size:8px;
  font-family: monospace;
}  

完成イメージ

こんな感じになると思います。
これで少しはましになったかなぁ。
スクリーンショット 2020-05-16 9.07.50.png

スタイルの変更後もテストだ。

まずは、これまで作ったテストを流してみます。
(間違ってvisible: hiddenになってしまったりなどの影響がないことが確認できます。)

で、狙ったスタイルが適用されているかもテストしてみます。

test/system/whiteboards_test.rb
  test "style check" do
    # white_board/mainを開く。
    visit white_board_main_url;

    # タイトルはfont-weigt: bold, font-size: 24pxのはず。
    # font-weightをマッチする時はboldでなく、数値(700)でマッチします。
    find("div#WhiteBoardTitle").assert_matches_style("font-weight" => "700", "font-size" => "24px");
    find("div#WhiteBoard").assert_matches_style("display" => "flex", "flex-flow" => "row wrap", "align-content" => "flex-start", "align-items" => "flex-start");

    # ユーザ名部分
    username_divs = all("div", class: "UserName");

    username_divs.each do | username_div |
      username_div.assert_matches_style("font-weight" => "700", "font-size" => "18px");
    end

    # 付箋
    # 色はrgba(赤, 緑, 青, 透過率)で比較するようです。
    stickies = all("div", class: "Sticky");

    stickies.each do | sticky |
      sticky.assert_matches_style("background-color" => "rgba(255, 255, 0, 1)", "width" => "100px", "display" => "grid");
    end

  end

こういうテストを作っておくとスタイルシートの書き間違いなどで、狙ったスタイルが適用されてないことを検出することができます。
さらに、期日が近くなったら背景を赤くするなどの仕組みを取り入れたら、そういうのも自動でテストできるようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】gem ' active_hash' プルダウンメニュー作成

手順

1:gem導入
2:手動でモデルを作成
3:データを引っ張る
4:アソシエーションを定義
5:実装中のエラー

1:gem 導入

Gemfail.
gem 'active_hash'

bundle installする。

2:モデル作成

※モデルは手動で作成

model/Shipping_fee.rb
class ShippingFee < ActiveHash::Base
  self.data = [
      {id: 1, name: '送料込み(出品者負担)'}, {id: 2, name: '着払い(購入者負担)'}
  ]
end

ターミナルで確認

[2] pry(main)> ShippingFee.find(1)
=> #<ShippingFee:0x00007fa012181140 @attributes={:id=>1, :name=>"送料込み(出品者負担)"}>

3:プルダウンメニューにデーターを引っ張る

new.html.haml
.new-contents__box__title
  発送量の負担
  %supan{class: "required"} 必須
= f.collection_select :shipping_fee_id, ShippingFee.all, :id, :name, {prompt: "選択してください"}, {class: ""}

ShippingFee.allでモデルからデータを取得。

4:アソシエーションを定義

item.rb
extend ActiveHash::Associations::ActiveRecordExtensions
belongs_to_active_hash :shipping_fee

active_hashにはbelongs_to_active_hashメソッドが用意されているため、
親になるモデルのみにアソシエーションを定義する。

アソシエーションの確認:

[5] pry(main)> item = Item.find(1)
[6] pry(main)> item.shipping_fee
=> #<ShippingFee:0x00007fa012181140 @attributes={:id=>1, :name=>"送料込み(出品者負担)"}>

5:実装中のエラー

LoadError Unable to autoload constant ShippingFee
原因:作成したモデルのクラス名

model/Shipping_fee.rb
#修正前
class Shipping_fee < ActiveHash::Base

#修正後
class ShippingFee < ActiveHash::Base
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】テーブルを分けて複数の画像をアップロードする

複数枚の写真を一度に保存する機能の実装において、
【itemテーブル】と【item_imageテーブル】の二つに
分けて複数枚の写真を保存する機能を実装した際の手順をまとめたのでご紹介します。

完成形

○ 商品写真は最大で10枚まで投稿可能。
○ 一つのファイルフィールドに一つの写真でアップロードしていく。
○ 5枚投稿時点で下段に切り替わる。
Image from Gyazo

1. HTMLの用意

sample.haml
~ 省略 ~

.image-container
  .image-container_box
    .form-title
      %span.box-form-explanaion
        商品画像
      %span.indispensable
        必須
    %p.image-container_box_message
      最大10枚までアップロードできます
    .image-container_box_alart-10
      ※ 1枚目は必須です
  .image-container_unit-man    【写真が順にプレビューされていく箱】
    .item-image-container__unit.preview-0  【写真一枚が入る箱 ※投稿ごとに生成されていく】
      = f.fields_for :item_images do |i|
        .image-container__unit--guide
          = i.file_field :image, class: 'img-man', id:"image-label-0",type: 'file'
          .have-image
            %i.fas.fa-camera

~ 省略 ~

◉【写真一枚が入る箱】に2つクラスがあるのは、2枚目以降では毎回クラス名を
変えていくためです。
◉この仕様では、1つのinputに写真は1つなので、multiple属性は付けていません。

2. CSSの用意

sample.scss
  .image-container {
    padding: 40px;
    border-bottom: solid 1px rgb(235, 235, 235);
    &_box {
      &_message {
        height: 19px;
        margin: 16px 0 5px;
        font-size: 14px;
        line-height: 1.4em;
        display: block;
      }
      &_alart-10 {
        margin-bottom: 4px;
        font-size: 14px;
        font-weight: bold;
        color:red;
      }
    }
    &_unit-man {
      min-height: 152px;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      .item-image-container__unit {
        align-content: center;
        align-items: center;
        background-color: rgb(245, 245, 245);
        display: flex;
        justify-content: flex-start;
        height: 150px;
        width: 118px;
        margin-left: 5px;
        margin-bottom: 2px;
        justify-content: center;
        position: relative;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;

        .have-image {
          position: absolute;
            left: 32px;
            top: 40px;
          z-index: 0;
          cursor: pointer;
          font-size: 16px;
          .fas.fa-camera {
            margin-left: 16px;
            font-size: 1.2rem;
          }
          .fas.fa-camera:hover {
            cursor: pointer;
            transform: scale(1.3, 1.3);
            transition: all 0.1s ease 0s;
          }
        }
        .item-image-container__unit__image {
          z-index: 1;
          height: 145px;
          width: 100%;
          margin: 0 3px;
          background-color: #ffffff;
          position: relative;
          img {
            width: 100%;
            height: auto;
          }
          .image-option__list--tag {
            width: 100%;
            background-color: lightblue;
            text-align: center;
            position: absolute;
              bottom: 0;
              left: 0;
          }
          .image-option__list--tag:hover {
            cursor: pointer;
            transform: scale(1.0, 1.0);
            transition: all 0.1s ease 0s;
            background-color: #ea352d;
            color:#ffffff;
          }

        }
      }
      .item-image-container__unit {
        input{
          display: none;
        }
      }
    }
  }

[flex-direction: row;]と[flex-wrap: wrap;]により、
写真が既定の幅まで投稿されたら下段に折り返してくれます。

3. JSでプレビューさせる

sample.js
$(function(){


~ 省略 ~


  var dataBox = new DataTransfer();  //データ用の箱を作る
  $(document).on('change', '.img-man', function(){    //inputの中身が変化したら発火する
    $.each(this.files, function(i, file){
      var fileReader = new FileReader();
      dataBox.items.add(file)    
      fileReader.readAsDataURL(file);     //ファイルの読み込み
      fileReader.onloadend = function() {     //読み込み完了すると発火
        var num = $('.item-image-container__unit').length  //写真の枚数をnumに代入
        var src = fileReader.result   //写真のデータをsrcに代入
        var html =  `<div class="item-image-container__unit__image">
                        <img src="${src}">
                      <div class="image-option__list--tag btn-${num}">削除</div>
                    </div>`

        var field = `<div class="item-image-container__unit preview-${num}">
                      <div class="image-container__unit--guide">
                        <label for="image-label-${num}">
                          <input class="img-man" id="image-label-${num}" type="file" name="item[item_images_attributes][${num}][image]">
                          <div class="have-image">
                            <i class="fas fa-camera"></i>
                          </div>
                        </label>
                      </div>
                    </div>`
        $(html).appendTo(`.preview-${num - 1}`)  //枚数で該当するクラスに写真を追加する
                      
        if (num < 10 ) {     //10枚分まで新しいinputの生成を行う
          $(field).appendTo('.image-container_unit-man')
        }
      };
    });
  });
  //削除機能 
  $(document).on("click", ".image-option__list--tag", function(){  //削除ボタンクリックで発火
    var num = $('.item-image-container__unit').length
    var field_2 = `<div class="item-image-container__unit preview-0">
                    <div class="image-container__unit--guide">
                      <label for="image-label">
                        <input class="img-man" id="image-label-0" type="file">
                        <div class="have-image">
                          <i class="fas fa-camera"></i>
                        </div>
                      </label>
                    </div>
                  </div>`
    $(this).parent().parent().remove();  //写真が入っている箱ごと削除
    $(".item-image-container__unit").removeClass(`preview-${num - 1}`)
    $(".item-image-container__unit").addClass(`preview-${num - 2}`)
    if(num == 1) {   //全部削除した時に新たに1つフィールドを生成する
      $(field_2).appendTo('.image-container_unit-man')
    }
  });

~ 省略 ~

◉変数【num】は、写真の読み込みごとに代入され、その度に横に生成される
【preview-${num}】クラスが書き換わって横に生成されるようになっています。

◉ 下部の10枚制限の記述によって、10枚までアップされるとinputの生成が止まります。
また、multiple属性が付いていないので写真の一括選択はできなくなります。

◉削除機能に関しても、クラス名の書き換えを行う必要があります。


登録枚数に関しては、モデルにも別でバリーデーションは書いてあります。

sample.rb
  def  item_images_number
    errors.add(:item_images, "は10枚までです") if item_images.size > 10
  end

以上で終了です。
ご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails Ⅱ

投稿一覧ページ

データベースをHTMLに直接書く

投稿に関するページ
postsコントローラ
indexアクション名

コマンド
「rails generate controller posts index」は「rails g controller posts index」と省略可
作成された「views/posts/index.html.erb」に投稿一覧ページ用のHTMLを書いていく

「config/routes.rb」を開いて、
get "posts/index" => "posts#index"
というルーティング

「app/controllers/posts_controller.rb」を開いて、
indexアクションを持ったpostsコントローラ

「app/views/posts/index.html.erb」を開いて、ビューファイルが作成されている
HTMLファイルを張り付け



今日からProgateでRailsの勉強するよー!
投稿一覧ページ作成中!

「app/assets/stylesheets/posts.scss」
CSS
/* posts/index ================================ */
.posts-index-item {
padding: 20px 30px;
background-color: white;
overflow: hidden;
box-shadow: 0 2px 6px #c1ced7;
}

.post-left img {
width: 50px;
height: 50px;
border-radius: 40%;
box-shadow: 0 2px 6px #c1ced7;
object-fit: cover;
}

.post-user-name a {
font-weight: 600;
}

.post-user-name a:hover {
color: #3ecdc6;
}

.post-left {
float: left;
width: 10%;
}

.post-right {
float: left;
width: 90%;
padding-left: 25px;
text-align: left;
}

/* posts/show ================================ */
.posts-show form {
display: inline;
}

.posts-show-item {
padding: 30px;
background-color: white;
box-shadow: 0 2px 6px #c1ced7;
overflow: hidden;
}

.posts-show-item img {
width: 60px;
height: 60px;
border-radius: 40%;
box-shadow: 0 2px 6px #c1ced7;
vertical-align: middle;
object-fit: cover;
}

.posts-show-item .post-user-name a {
vertical-align: middle;
font-size: 24px;
margin-left: 15px;
}

.posts-show-item p {
font-size: 24px;
margin: 20px 0;
}

.post-time {
color: #8899a6;
margin-bottom: 10px;
}

.post-menus {
float: right;
}

.post-menus a, .post-menus input {
color: #8899a6;
text-decoration: underline;
font-size: 14px;
}

/* posts/new ================================ */
.posts-new textarea {
font-size: 20px;
padding: 10px;
min-height: 140px;
border: 1px solid rgb(216, 218, 223);
resize: none;
}

.posts-new textarea::-webkit-input-placeholder {
font-size: 24px;
opacity: 0.5;
}

viewファイル内で変数を定義

index.html.erbのようなerbという形式のファイルでは、以下の図のように<% %>で囲むことで、HTMLファイルの中にRubyのコードを記述することができます。
「erb」とは「Embedded Ruby(埋め込みRuby)」の略
埋め込むRubyコードをブラウザに表示したい場合には、以下の図のように
<% %>ではなく、<%= %>を用いま
<% %> 変数
<%= %> 表示

<% post1 = "今日からProgateでRailsの勉強するよー!" %>
<!-- ここに変数post2を定義して、指定された投稿内容を代入してください -->
<% post2 = "投稿一覧ページ作成中!" %>

each文で表示

<%
posts = [
"今日からProgateでRailsの勉強するよー!",
"投稿一覧ページ作成中!"
]
%>

アクションで変数を定義

Rails ではビューではなく、アクションで定義することが一般的

@変数

変数名を「@」から始めることでこの変数は特殊な変数となり、ビューファイルでも使用することができる
.rb ビューで使える変数
def index
@post1 = "にんじゃわんこ"
end

.erb アクションで定義した変数の呼び出し
<%= @post1 %>

データベースを用意

マイグレーションファイルと呼ばれる、データベースに変更を指示するためのファイルを作成
「rails g model Post content:text」というコマンドで作成する
generate Postsテーブル カラム名 データ型
db/migrateフォルダの下にマイグレーションファイルが作成

テーブルを用意

「rails db:migrate」を実行
マイグレーションファイルを作成した場合は必ず「rails db:migrate」を実行する必要がある

モデルを確認

テーブルを操作するためのモデルと呼ばれる特殊なクラス
「rails g model」コマンドでposts テーブルを操作するための Post モデルがすでに生成
「post.rb」が、app/modelsフォルダの中に作成されていま
・app/modelsフォルダにモデルが定義されたファイル
・db/migrateフォルダにマイグレーションファイル

rails consoleを使ってみよう

ターミナル上で「rails console」と入力し実行(Enter)することで、コンソールを起動することができます。コンソールを起動した状態で、「1+1」を実行すると、その実行結果が表示されます。
また、「quit」を実行すると、コンソールを終了することができる

テーブルに投稿データを保存しよう

posts テーブルにデータを追加するには右の図のように、
① new メソッドで Post モデルのインスタンスを作成
② posts テーブルに保存

newメソッド
saveメソッド

post1 = Post.new(content: " ")
post1.save

テーブルからデータを取り出そう

# テーブルから1つのデータを取り出す

「Post.first」とすることで、 posts テーブルにある最初のデータを取得する

contentカラムの値を取り出そう

「post.content」とすることで「Post.first」で取得したデータから投稿内容を取得することができる

テーブルから全てのデータを取り出そう

全ての投稿を取り出す posts = Post.all
投稿の配列から1つのデータを取り出す Post.all [0]
配列のデータから投稿内容を取り出す Post.all[0].content

データベースのデータを表示しよう

rails consoleのまとめ
データの作成 (new,save)
データの取得 (Post.all,post.content)

# 全ての投稿を表示する
class PostsController < ApplicationController

def index
@posts = Post.all
end
end

共通のレイアウトをまとめよう

Railsでは、「views/layouts/application.html.erb」に共通のHTMLを書いておくことができる

link_toメソッド

    Rails ではlink_toというメソッドを使うと<a>タグを作成することができるぞ。 link_to メソッドは Ruby のコードなので、「<%=%>」で囲む
第一引数に表示する文字を、第二引数に URLを書くことでリンクが作成される




<!-- 以下のリンクをlink_toメソッドを用いて変更してください -->
<%= link_to("TweetApp","/") %>



  • <!-- 以下のリンクをlink_toメソッドを用いて変更してください -->
    <%= link_to("TweetAppとは","/about") %>


  • <!-- ここにlink_toメソッドを用いて投稿一覧ページへのリンクを作成してください -->
    <%= link_to("投稿一覧","/posts/index") %>



<%= yield %>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MiniMagick(imagemagick)で複数のフォントファイルを使って絵文字に対応する

ちょっと前にimagemagickで絵文字を含むテキストを合成する時に結構ハマったのでなんとなく備忘録を。
imagemagickはフォントが1つしか指定できないので、大抵の場合絵文字があると文字化けしてしまいます。
そこで、pangoを使って複数フォントに対応します。

(以下、dockerでrailsを動かす前提で進めます。)

フォントを置く

まずは使いたいフォントをDLしてコンテナのfontsに置きます。
(apt-getで手に入るならそっちのほうがいいと思います)
今回はNoto Sans CJK JPとNoto Color EmojiをDLして/assets/fontsに置きました。

# Dockerfile
COPY /assets/fonts /usr/share/fonts

memo: Noto Color Emojiについて

Noto Color Emojiは定番のフォントだと思いますが、入手先が3つあってそれぞれ対応しているUnicodeのバージョンが違います。
- 公式サイト → Unicode10
- apt-get → Unicode11
- Github → Unicode12(最新)
Github以外は更新が止まってるようなので、Unicodeが更新される度にGithubからDLして上書きする必要があります。

フォントの設定を変える

Noto Color Emojiはビットマップフォントですが、設定によってはこれがデフォルトで無効になってることがあるので書き換えます。

# Dockerfile
RUN rm /etc/fonts/conf.d/70-no-bitmaps.conf
RUN ln -s /etc/fonts/conf.avail/70-yes-bitmaps.conf /etc/fonts/conf.d/

ビットマップフォントを無効にする設定ファイルを消して、conf.availから有効にする設定をコピーしてます。

次にNoto Color Emojiの優先度を上げるため、設定ファイルを作って/etc/fonts/に置きます。(/usr/share/fontsではないので注意)

local.conf
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
  <alias>
    <family>sans-serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>monospace</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
</fontconfig>
# Dockerfile
COPY /config/local.conf /etc/fonts/
RUN fc-cache -f

最後に念の為fc-cache -fでキャッシュを消して設定を読み込ませます。
これで、フォントの設定は完了です。

pangoで画像を生成する

image.rb
MiniMagick::Tool::Convert.new do |convert|
  convert.size "600x200" 
  convert.pango("<span font='Noto Sans CJK JP' size='36864'>#{title}</span>")
  convert << "image.png"
end

2行目で指定したサイズに合わせてよしなに改行してくれます。
pangoはフォールバックフォントに対応しているので、Noto Sans CJK JPで表示できない文字が来た場合は先程設定したfontconfigに基づいてNoto Color Emojiで表示されるという仕組みです。
sizeはフォントサイズに1024掛けた数値を指定します。

pangoは他にも色々リッチなテキストを生成できるので、興味ある方は公式のdocを御覧ください。
https://www.imagemagick.org/Usage/text/#pango

絵文字がモノクロになる

出力された画像を見たら絵文字がモノクロだった…なんて時はOSのバージョンが古いかもです。
linuxの場合はUbuntu 18.04以降でないとカラーになりません。
自分はdebianのdockerコンテナ使ってたので、バージョンをbusterに変えて無事カラーになりました。

# Dockerfile
FROM ruby:2.6.5-buster

おわりに

こうしてまとめると大したことやってないですが、これに辿り着くまでにえらい時間かかりました…

あと例えば画像サイズに収まるように文字数をカウントしてトリミングしたい時は絵文字の扱いに注意が必要です。
サロゲートペアとか、肌の色を表す文字とか、複数の絵文字を合成してたりとか、ややこしい仕様が色々あります…
その辺の仕組みは↓の記事に詳しく書かれてます。
https://qiita.com/_sobataro/items/47989ee4b573e0c2adfc
https://tech.drecom.co.jp/count-length-of-string-including-pictogram/

絵文字、知れば知るほど難しい……:sob:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

貧乏環境(t3.small)でFWのベンチマーク Rails6.x vs SpringBoot2.3.x vs Vert.x3.9.0 vs actix-web1.0.x (2020/05現在)

最近はVert.x推し。でもだれでも使えるSpringBootは実際どれくらいなの?ってことを知りたかった。
CPUが潤沢な場合にパフォーマンスが出るのはすごいよ。すごい。
でも貧乏人には大きなサーバーは無理。だから小さめの環境で効率が良くさばける?という観点でベンチマークする。
有名なベンチマークのアレではPlay Frameworkはそんなスコア出てない。
Vert.x vs Play となってPlayはScalaで脳死するのでJavaが使えるVert.xが勝つ、っていう私の前提。
プロジェクトでみんなが使えるか、って視点。

 環境

  • EC2のt3.small
  • EC2にSSHして内部からabコマンド実行(堪忍して、傾向はわかるはず)
  • SpringBootとVert.xはKotlinで書いたけどまぁ、この程度のプログラムでそんな関係あるはずない(はず)
  • Vert.xはwebプラグイン付き。流石に生は辛い

プログラム

簡単なJSONを吐き出すエンドポイントを実装(DBとかしらん、FWの単純性能JSOシリアライズってやつ?)

ベンチマーク

同時接続100の世界

普通にやっとくべきベンチマークの世界

Rails6 => 230.17[#/sec]

$ ab -n 1000 -c 100 http://127.0.0.1:3000/test
=> Requests per second:    230.17 [#/sec] (mean)

Railsに温まるという概念はなかった。何回やってもこれぐらい

SpringBoot => 3196.47 #/sec

$ ab -n 1000 -c 100 http://127.0.0.1:8081/test
=> Requests per second:    3196.47 [#/sec] (mean)

叩くたびに温まって3000ぐらいで落ち着く感じ

Vert.x => 5473.96 #/sec

$ ab -n 1000 -c 100 http://127.0.0.1:8889/test
=> Requests per second:    5473.96 [#/sec] (mean)

叩くたびに温まって5400ぐらいで落ち着く感じ

actix-web => 5800.87 #/sec

$ ab -n 1000 -c 100 http://127.0.0.1:8082/test
=> Requests per second:    5800.87 [#/sec] (mean)

速い。

同時接続1000の世界

Yahoo砲ぐらい

Rails6 => 87.49 #/sec

$ ab -n 5000 -c 1000 http://127.0.0.1:3000/test
=> Requests per second:    87.49 [#/sec] (mean)

負けてる。複数台アプリを横に並べるのを検討しなくてはならない

SpringBoot => 2819.57 #/sec

$ ab -n 5000 -c 1000 http://127.0.0.1:8081/test
=> Requests per second:    2819.57 [#/sec] (mean)

負け始めてるけどこれだけ出てればまぁ、サーバー増やさなくても良さそう

Vert.x => 5045.04 #/sec

$ ab -n 5000 -c 1000 http://127.0.0.1:8889/test
=> Requests per second:    5045.04 [#/sec] (mean)

負け始めてるけどこれだけ出てればまぁ、サーバー増やさなくても良さそう

actix-web => 5593.91 #/sec

$ ab -n 5000 -c 1000 http://127.0.0.1:8082/test
=> Requests per second:    5593.91 [#/sec] (mean)

速い。

同時接続10000の世界

C10Kって言われてるアレ

Rails6 => テスト完走できず

$ ab -n 20000 -c 10000 http://127.0.0.1:3000/test
=> テスト完走できず

負けてる。複数台アプリを横に並べるのを検討しなくてはならない

SpringBoot => 完走不可、500ぐらい、3000 #/sec

$ ab -n 2000 -c 10000 http://127.0.0.1:8081/test
=> 完走不可、500ぐらい、3000 [#/sec] (mean)

もはや信頼できない感じ

Vert.x => 4714.83 #/sec

$ ab -n 20000 -c 10000 http://127.0.0.1:8889/test
=> Requests per second:   4714.83 [#/sec] (mean)

信頼して4500ぐらい稼げる。信頼ができる

actix-web => 1160.18 #/sec

$ ab -n 20000 -c 10000 http://127.0.0.1:8082/test
=> Requests per second:    1160.18 [#/sec] (mean)

あれ?だめじゃん。

感想

感想1

Rails6

Railsはスパゲティーになるし、早期にパフォーマンスの問題にぶち当たって苦しむことがわかる。
2020年現在では魅力的とは言い難い構成でしょう。
きっとLaravelも同様。スクリプト言語の辛み
LAはめちゃくちゃあがって怖くなる

Spring Boot

頑張ってる。誰でも見通しの良いプロジェクト構成にできるメリット。IDEのサポートも最高レベルなのが良い。
ただし、C10Kぐらいまでサービスが育ってしまうと信頼性がおちる。
同時接続1000ぐらいに抑えられるようにサーバーを横に並べる必要がありそう。
あとLAがかなり上がるが、まぁ、こんなものかレベル。

Vert.x

さすがC10K問題に対応している。
見通しの良いプロジェクト構成のベストプラクティスをこれを書いた人は知らないのでそのへんは試行錯誤が必要そう。
ということは開発に参加するたびそのサービスの構成の文化を知ることから始める必要がありそうなのかも。
IDEのサポートは最高レベルなのが良い。
LAは一番低かった。

actix-web

すくないアクセス数では最速。
だけどC10Kになると死。Rustの書きづらさを考えるとこれは痛い。
開発環境はVSCodeなので開発も一歩Kotlinに劣る。コンパイル鬼遅い。
クロスコンパイルは結局成功せず。Linuxへ持っていってコンパイルした。これもつらい。
バイナリ一発でサーバーにrustの環境がなくても動くのはネイティブバイナリの強み。
静的リンクしてくれるようにコンパイルしたので更に可搬性は優れる。
私がRsut慣れしてないもあるが開発は辛い。デファクトのIDEがないのが辛みかな。

感想2

SpringBootはやはり鉄板。便利機能てんこ盛りできっと開発体験は最高。
C10Kになっちゃうことなんて基本ないのでぱっぱり鉄板SpringBootなんでしょう。(Playどうなんでしょ?)
もはやRailsは情弱なのかも(あ、ごめんなさい、こういう強い否定言葉は良くないですね)
プログラミングを楽しみたい人はVert.xを使えばよいのでは、って感じ。
Vert.xは肌触りSinatraです。ってことはプロジェクト構成大変そうでしょ?
私は楽しみたいのでRustのWAFが非同期周りの仕様追加を受けて落ち着くまではVert.xで楽しむつもり
actix-webは今回は残念な結果になった。actix-webの知識がなさすぎるのでなにか間違っているのかもしれない。RustのFWは非同期まわりの仕様追加したてなので、これからどんどん強くなっていくとおもうので将来に期待ですね。

感想3

Railsは比較するとデプロイがめんどい。
SpringBootとVert.xはjar一個にまとまるのでデプロイめっちゃ楽。
こういうところも大事

感想4

Rails系はテストコード書きやすいよね。そこは高評価。
Rustもテスト書きやすい(テスト前提の感じすらある)

感想5

貧乏環境で楽したいならSpringBoot、C10Kまでなにも心配したくないならVet.x
JVM強い。

感想6

Kotlin歴3日ぐらいだけどJava書ける人は何も勉強しなくてもKotlinできるはず。なぜならJavaのソースをIDEに貼り付けると自動で変換してくれるのよ。Kotlin良いよKotlin。

感想7

h2oは最近動きないので

ELB(SSL終端) -> nginx(不要かも) -> Vet.x

が貧乏人には「さいつよ」とおもいました。

以上です。

  • TODO: Playどうなの?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

before_action

before_actionとは

railsのコントローラーのアクションを実行する前に処理を行いたいときや、同じ記述の処理をまとめたい時に使います。

基本的な書き方は以下です

hoges_controller.rb
   before_action :処理させたいメソッド名

この場合だと、同じコントローラーに定義されている各メソッドの実行の前に、指定したメソッドによる処理が行われます。

もしくはresourcesメソッドと同じようにonlyやexceptなどでアクションの限定をすることもできます。

hoges_controller.rb
before_action :処理させたいメソッド名, only: [:アクション1,:アクション2]

この場合はアクション1と2が実行される前にのみ、指定した「処理したいメソッド」が実行されます。

使用例

コントローラーに下記の記述があったとします。

tweets_controller.rb
class TweetsController < ApplicationController

  def index
    @tweets = Tweet.all
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
    @tweet = Tweet.find(params[:id])    ←処理が被ってる
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
    @tweet = Tweet.find(params[:id])       ←処理が被ってる
  end

  private
  def tweet_params
    params.require(:tweet).permit(:name,:text)
  end
end

みてもらうと分かりますが、editとshowにおいて記述が被っています

そのため、上記のコードをリファクタリングし、

tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]

  def index
    @tweets = Tweet.all
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
  end

  private
  def tweet_params
    params.require(:tweet).permit(:name,:text)
  end

  def set_tweet                          ←共通の処理をまとめている
    @tweet = Tweet.find(params[:id]) 
  end
end

共通の処理をset_tweetというメソッドでまとめ、showアクションとeditアクションの実行の際にbefore_actionで呼び出しています。

もっと分かりやすいイメージ

before_action :authenticate_user!

上記はdeviseを導入した際に使えるようになるメソッドである「authenticate_user!」を指定しています。ログインしていない場合はログイン画面に遷移させるメソッドです。

よく、Amazonなどの通販サイトなどを使用していると分かりますが、商品の閲覧はログインしていなくても出来るけど、購入に進むとログインを求められますよね。

このように何らかのアクションを実行する前に行なって欲しい共通の処理をbefore_actionでは指定します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]ユーザー情報にenumを使った実装

はじめに

初めまして、Railsを勉強中のものです。
現在SNSアプリを開発中です。

よくある投稿機能や出品機能などに持たせるカラムとして、あらかじめ用意した値の中から選択させる属性を扱う場合enumが便利だと色んな記事で見てたのですが、
思っていたよりすんなり実装できなかったので初めてenumを使う方の参考になればと思い記録しておきます。

あくまで私が実装した内容を載せるだけなので、細かい説明は参考にしたリンクを貼っておきますのでそちらをご参照ください。

実装する内容

ユーザー情報に持たせるカラムとして、性別と年齢をenumを使って実装していきます。
Railsのバージョンは5.2.4です。今回はhamlで書いてます。

それではやってみましょう。

enumで値を定義する

まずはマイグレーションファイルから。
enumを使うカラムはinteger型で用意します。

db/migrate/xxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|

      t.string :nickname,            null: false
      t.string :email,               null: false, default: ""
      t.string :encrypted_password,  null: false, default: ""
      t.string :profile,             null: false
      # 型はintegerにする
      t.integer :gender,             default: 0
      t.integer :age,                default: 0

      # .省略
  end
end

初めオプションはnull :falseにしてました。今回はそれでも問題はなかったのですが、他の記事を見るとどれもdefault: 0で設定してたのと、公式ではこちらが推奨されてるみたいなので直しました。

次にモデルで実際にデータを用意します。

app/models/user.rb
class User < ApplicationRecord
  enum gender: { gender_private: 0, male: 1, female: 2, others: 3 }
  enum age: { age_private: 0, teens: 1, twenties: 2, thirties: 3, forties: 4, fifties: 5, over_sixties: 6 }
end

配列で定義してる方もいました。

app/models/user.rb
class User < ApplicationRecord
  enum gender: [ :gender_private, :male, :female, :others ]
  enum age: [ :age_private, :teens, :twenties, :thirties, :forties, :fifties, :over_sixties ]
end

どちらでも大丈夫ですが、配列の場合対応する数字が自動で0から順になるということだけ注意が必要です。
他のカラムと区別したり、わかりやすくするために1、10、20などの数字にする方もいるみたいです。その場合はハッシュで書くしかないですね。

viewでフォームを実装する

次にフォームの実装です。
私はセレクトボックス で用意しました。

app/views/devise/registrations/new.html.haml
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|

  .field
    = f.label :gender
    %br/
    = f.select :gender, options_for_select_from_enum(User, :gender)
  .field
    = f.label :age
    %br/
    = f.select :age, options_for_select_from_enum(User, :age)

フォームの実装はいろいろな書き方があると思いますが、私の場合
ArgumentError ('2' is not a valid gender):
というエラーが出てしまい、色々試した結果最終的に上の書き方になりました。
同じエラーに当たった方は、こちらの記事が詳しく書いてありました。

enumを使うとArgumentError in TasksController#create => '2' is not a valid status_id

これでenumを使って性別と年齢を登録できるようになりましたが、現状セレクトボックスに表示されるのは英語のままになっています(age_private、teens、twenties ...)

プルダウンの表示を日本語化する

こちらもいくつかやり方があるみたいです。
gemを使う方法もあるみたいですが今回は別の方法で実装します。

【Rails】Enumってどんな子?使えるの?

こちらの記事を参考にさせてもらいました。
というかまんまこの通りやりました。

ちなみにgemを使う方法はこちらに書いてあります。
【初心者向け】i18nを利用して、enumのf.selectオプションを日本語化する[Rails]

ja.ymlファイルの作成

まず、i18機能で日本語設定をしていない場合はファイルを用意する必要があります。

Gemfile
#rails5系
gem 'rails-i18n', '~> 5.1'

#rails4系
gem 'rails-i18n', '~> 4.0' 

こちらのgemを導入、もしくは

svenfuchs/rails-i18n
こちらのファイルをコピーして、config/locales/ja.ymlファイルを作ります。

そのファイルにenumで定義した値を日本語に変換させるのための記述をします。

config/locales/ya.yml
:ja
  #省略
  enums:
    user:
      gender:
        gender_private: "非公開"
        male: "男性"
        female: "女性"
        others: "他"
        #省略

デフォルトの言語を日本語化

次にapplication.rbでデフォルトの言語を日本語化するための設定です。

config/application.rb
module SnsApp
  class Application < Rails::Application
    #省略

    #追記
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]

   #省略
  end
end

これで先程のja.ymlファイルが読み込まれます。

そしてもう一つファイルを作ります。

Helperメソッドを用意する

app/helpersの下にファイルを作ります。

app/helpers/enum_helper.rb
module EnumHelper
  # クラスオブジェクトとカラム名を引数として呼ばれるコールバック関数です
  def options_for_select_from_enum(klass,column)
    #該当クラスのEnum型リストをハッシュで取得
    enum_list = klass.send(column.to_s.pluralize)
    #Enum型のハッシュリストに対して、nameと日本語化文字列の配列を取得(valueは使わないため_)
    enum_list.map do | name , _value |
      # selectで使うための組み合わせ順は[ 表示値, value値 ]のため以下の通り設定
      [ t("enums.#{klass.to_s.downcase}.#{column}.#{name}") , name ]
    end
  end
end

上の記事から丸ごと頂きました。

これでプルダウンの日本語も完了です!

最後に

今回はenumを使った実装方法を書きましたが、他に都道府県のカラムも欲しかったのでそちらはactive_hashで実装しました。

使い勝手も実装のしやすさもactive_hashの方が楽なのですが、性別など2個か3個しかない値のためにモデルをいっぱい作るのも無駄なような感じがしたのと、
大体皆さん性別はenumで実装してる方が多いみたいなので今回使ってみました。
実際覚えてしまえば使う場面はいっぱいあると思うので、必要に応じて積極的に使っていこうと思います。

私が書いたやり方でうまくいかなかった方は、今回参考にした記事をまとめておきますので色々見て見てください!

また、無駄な記述や間違った認識などがありましたらご指摘いただけるとありがたいです!

参考にした記事

【Rails】Enumってどんな子?使えるの?

Ruby on Rails:ModelのプロパティにEnumを使う※おまけ:ラジオボタンでEnumを扱う

enumを使うとArgumentError in TasksController#create => '2' is not a valid status_id

【初心者向け】i18nを利用して、enumのf.selectオプションを日本語化する[Rails]

[初学者]Railsのi18nによる日本語化対応

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む