Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsの技: liquidタグで動的にユーザーコンテンツを表示する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Railsの技: liquidタグで動的にユーザーコンテンツを表示する(翻訳)

ユーザー側で生成したコンテンツを受け取る機能を構築する場合、ユーザーの指定に応じてコンテンツを動的に表示する必要が生じることがあります。たとえば、ユーザーが誰かを自分のアカウントに招待するとアプリケーションがウェルカムメッセージを表示するようになっていて、そのメッセージをユーザーがカスタマイズできるようにしたいとします。

Railsプログラマーは、動的なテキストを用いるコンテンツの記述に慣れ親しんでいます(ビューテンプレートを書くときは常にそうします)。しかし、ユーザーが書いたERBやHAMLをアプリで実行するような真似は許したくないものです。これは大きなセキュリティリスクであると同時に、テキストを変更するのにプログラミング言語を丸ごと学ぶことを強いるのは、あまりにもユーザーに不親切です。

$NAME*|EMAIL|*といった特殊な文字列を値と置き換える、何らかの「マジック文字列」を使う方法もないわけではありません(これは「マージタグ」と呼ばれることもあります)。しかしこの方法で実装しようとすると、たいていの場合gsubや正規表現といった厄介なエッジケースにまみれてしまいます。

この機能を実装する別のよい方法は、Liquidテンプレートを使うことです。Liquidは極めてシンプルなテンプレート言語で、Shopifyはもっぱらこれを用いて、eコマースストアのオーナーが自分のショップをカスタマイズできるようにしています。

Liquidテンプレートは上述の問題をすべて解決してくれます。

レンダリングで使われるコンテキストをプログラマーが制御できるので、セキュリティを確保できます(Liquidテンプレートでは、明示的に渡されたデータのみアクセスが許されます)。

Liquidの構文は最小限に抑えられていて、「デフォルト値」「大文字小文字の変換」「データの書式設定」といったよく使われる操作については組み込み関数が用意されています。

さらにこのライブラリは、入力を解析するときに危なっかしい正規表現に依存していません。忘れた頃に壊れたりせず、無効な構文をキャッチして適切に扱ってくれます。

使い方

プロジェクトにliquid gemを追加します。

Shopify/liquid - GitHub

基本操作は以下の2つのステップで行います。

# ユーザー入力からテンプレートを作成する
template = Liquid::Template.parse("Hi {{ customer.name }}!")

# 動的なデータを与えてテンプレートをレンダリングする
template.render({"customer": {"name": "Matt"}})
#=> "Hi Matt!"

実際には、Railsビューヘルパーを自作するときに取り入れたい便利機能がいくつもあります。

  • Liquidでは動的データハッシュに文字列キーを使う必要がありますが、Railsアプリでは多くの場合ハッシュにシンボルを使っています。Railsのdeep_stringify_keysメソッドを呼び出すことでハッシュを変換できます。
  • renderの代わりにrender!を呼び出すと失敗時に例外を発生するようになるので、生のユーザー入力を返す形でフォールバックできます。
  • Liquidのstrict_variablesオプションとstrict_filtersオプションは、未定義の変数やフィルタをエラーにできます。これらのオプションを両方ともtrueにしておけば、空のコンテンツをエラーなしで表示する代わりに、構文エラーをユーザーに表示するようになります。

私のプロジェクトでは、以下のヘルパーメソッドを追加してあります。

# app/helpers/liquid_helper.rb
module LiquidHelper
  def liquid(text, context: {})
    template = Liquid::Template.parse(text)
    template.render!(context.deep_stringify_keys, {
      strict_variables: true,
      strict_filters: true
    })
  rescue Liquid::Error
    text.to_s
  end
end

これで、Railsアプリの任意のビュー(およびメイラー)でliquidヘルパーを呼び出して、ユーザーが生成した動的なコンテンツを表示できます。

@campaign = Campaign.create!(subject: "Welcome {{ customer.name }}!")
<%= liquid(@campaign.subject, context: { customer: { name: @customer.name } }) %>

注: Action Textのリッチテキスト機能を使っている場合は、以下のようにLiquidの式展開に続けてhtml_safeを呼び出す必要があります。理由は、出力がraw HTMLであるためです。

<%= liquid(@campaign.message, context: { customer: { name: @customer.name } }).html_safe %>

このヘルパーは、エラー時に元の入力をそのまま返してくれます。

liquid("Hi {{ missing_value }}", context: {})
#=> "Hi \{\{ missing_value }}"

liquid("Hi {{ foo", context: {})
#=> "Hi \{\{ foo"

また、コンテキストハッシュを毎回ビルドする代わりにLiquidのレンダリングコンテキストで使う場合は、以下のようにモデルに便利メソッドを追加してもよいでしょう。

class Customer < ApplicationRecord
  belongs_to :organization

  def to_liquid
    # Liquidテンプレートで利用可能にしたいフィールドを公開する
    {
      name: name,
      email: email,
      company_name: organization.name
    }
  end
end
liquid("New sign up from {{ customer.company_name}}. Say hi to {{ customer.name }} <{{ customer.email }}>!", context: customer.to_liquid)
#=> New sign up from Arrows. Say hi to Matt <matt@arrows.to>!

実地で培われた高度な機能

以下のようなアプリケーション固有の機能をユーザー向けに提供したい場合は、独自のフィルタを登録できます。

  • {{ customer | avatar_url }}
  • {{ task.due_date | next_business_day }}
  • {{ '#7ab55c' | color_to_rgb }}

また、resource_limitsを指定することで、式展開が極端に遅くなることを回避できます。

これらについては本記事の範疇を超えるので、自分で調べてみてください。

参考資料

関連記事

Railsの技: StimulusJSコントローラからRailsの環境変数にアクセスする(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。