Recipe 5.15. Avoiding Harmful Code in Views with Liquid Templates


Problem

Contributed by: Christian Romney

You want to give your application's designers or end users the ability to design robust view templates without risking the security or integrity of your application.

Solution

Liquid templates are a popular alternative to the default ERb views with .rhtml templates. Liquid templates can't execute arbitrary code, so you can rest easy knowing your users won't accidentally destroy your database.

To install Liquid, you need the plug-in, but first you must tell Rails about its repository. From a console window in the Rails application's root directory type:

$ ruby script/plugin source svn://home.leetsoft.com/liquid/trunk $ ruby script/plugin install liquid             

Once the command has completed, you can begin creating Liquid templates. Like ERb, Liquid templates belong in the controller's folder under app/views. To create an index template for a controller named BlogController, for instance, you create a file named index.liquid in the app/views/blog folder.

Now, let's have a look at the Liquid markup syntax. To output some text, simply embed a string between a pair of curly braces:

{{ 'Hello, world!' }}

You can also pipe text through a filter using a syntax very similar to the Unix command line:

{{ 'Hello, world!' | downcase }}

All but the most trivial templates will need to include some logic, as well. Liquid includes support for conditional statements:

{% if user.last_name == 'Orsini' %}   {{ 'Welcome back, Rob.' }} {% endif %}

and for loops:

{% for line_item in order %}   {{ line_item }} {% endfor %}

Now for a complete example. Assume you've got an empty Rails application ready, with your database.yml file configured properly, and the Liquid plug-in installed as described above.

First, generate a model called Post:

$ ruby script/generate model Post             

Next, edit the migration file: 001_create_posts.rb. For this example, you want to keep things simple:

db/migrate/001_create_posts.rb:

class CreatePosts < ActiveRecord::Migration   def self.up     create_table :posts do |t|       t.column :title, :string     end   end   def self.down     drop_table :posts   end end

Now, generate the database table by running:

$ rake db:migrate             

With the posts table created, it's time to generate a controller for the application. Do this with:

$ ruby script/generate controller Posts             

Now you're ready to add Liquid support to the application. Start your preferred development server with:

$ ruby script/server -d             

Next, add some general support for rendering liquid templates within the application. Open the ApplicationController class file in your editor, and add the following render_liquid_template method:

app/controllers/application.rb:

class ApplicationController < ActionController::Base   def render_liquid_template(options={})         controller = options[:controller].to_s if options[:controller]     controller ||= request.symbolized_path_parameters[:controller]          action = options[:action].to_s if options[:action]           action ||= request.symbolized_path_parameters[:action]            locals = options[:locals] || {}         locals.each_pair do |var, obj|            assigns[var.to_s] = \                     obj.respond_to?(:to_liquid) ? obj.to_liquid : obj        end          path = "#{RAILS_ROOT}/app/views/#{controller}/#{action}.liquid"       contents = File.read(Pathname.new(path).cleanpath)          template = Liquid::Template.parse(contents)                     returning template.render(assigns, :registers => {:controller => controller}) do |result|       yield template, result if block_given?     end   end end

This method, which is partly based on code found in the excellent Mephisto publishing tool, finds the correct template to render, parses it in the context of the variables assigned, and is rendered when the application layout yields control to the index.liquid template.

To call this method, add the following index action to the PostsController:

app/controllers/posts_controller.rb:

class PostsController < ApplicationController   def index     @post = Post.new(:title => 'My First Post')     render_liquid_template :locals => {:post => @post}   end   # ... end

For convenience, add a simple to_liquid method to the Post model:

app/models/post.rb:

class Post < ActiveRecord::Base   def to_liquid     attributes.stringify_keys   end end

You're just about finished. Next, you must create an index.liquid file in the app/views/posts directory. This template simply contains:

app/views/posts/index.liquid:

<h2>{{ post.title | upcase }}</h2>

Lastly, a demonstration of how you can even mix and match RHTML templates for your layout with Liquid templates for your views:

app/views/layouts/application.rhtml:

<html>   <head>     <title>Liquid Demo</title>   </head>   <body>          <%= yield %>   </body> </html>

You're finally ready to view your application. Point your browser to /posts; e.g., http://localhost:3000 /posts.

Discussion

The main difference between Liquid and ERb is that Liquid doesn't use Ruby's Kernel#eval method when processing instructions. As a result, Liquid templates can process only data that is explicitly exposed to them, resulting in enhanced security. The Liquid templating language is also smaller than Ruby, arguably making it easier to learn in one sitting.

Liquid templates are also highly customizable. You can add your own text filters easily. Here's a simple filter that performs ROT-13 scrambling on a string:

module TextFilter   def crypt(input)     alpha = ('a'..'z').to_a.join      alpha += alpha.upcase     rot13 = ('n'..'z').to_a.join + ('a'..'m').to_a.join     rot13 += rot13.upcase     input.tr(alpha, rot13)   end end

To use this filter in your Liquid templates, create a folder called liquid_filters in the lib directory. In this new directory, add a file called text_filter.rb containing the code listed above.

Now open your environment.rb and enter:

config/environment.rb:

require 'liquid_filters/text_filter' Liquid::Template.register_filter(TextFilter)

Your template could now include a line such as this one:

{{ post.title | crypt }}

Liquid is production-ready code. Tobias Lütke created Liquid to use on Shopify.com, an e-commerce tool for nonprogrammers. It's a very flexible and elegant tool and is usable by designers and end users alike. In practice, you'll probably want to cache your processed templates, possibly in the database. For a great example of Liquid templates in action, download the code for the Mephisto blogging tool from http://mephistoblog.com.

See Also

  • For more information on Liquid, be sure to visit the wiki at http://home.leetsoft.com/liquid/wiki

  • For more information on Mephisto, check out the official site at http://www.mephistoblog.com




Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net