Download as pdf or txt
Download as pdf or txt
You are on page 1of 33

<delete dir="output\bin" verbose="false"/>

<delete dir="output\classes" verbose="false"/>


<delete dir="output\src" verbose="false"/>
<mkdir dir="output\bin"/>
<mkdir dir="output\classes"/>
<mkdir dir="output\src"/>

.cldc.version" value="1.0"/>

Real Life Rails

an Developer eBook
contents
[ ] Real Life Rails

2 Develop with NetBeans,


Deploy on Linux
Mark Watson
9
9 Build a Wiki System with Rails
Anil Hemrajani

20 Build Robust Security into a


Rails-Based Wiki System
Anil Hemrajani

20

Real Life Rails Copyright 2008, Jupitermedia Corp.

1
[ Real Life Rails ]

Develop with NetBeans, Deploy on Linux


By Mark Watson

This article assumes you have some experience with

T
he Ruby language's conciseness makes it great for
development—fewer lines of code means reduced Rails development (in particular, creating a Rails appli-
development time and maintenance costs. Rails, the cation and using the Rails command-line tools) and ver-
Ruby-based Web application framework, can be equally sion control (in particular, installing and using the com-
developer-friendly, but some developers have encountered mand-line SVN tools — see the sidebar. It also assumes
scaling limitations when deploying large-scale Rails Web you know how to set up a Linux server for production
applications. While scaling Rails (with Ruby, Gem, and Rails
applications over many servers to installed). It will not provide
handle many thousands of concur- instructions for setting these up.
rent users can indeed lead to prob- To get the most out of this article,
lems, with the proper tools and you will need the following tech-
techniques developers can deploy nologies in addition to Rails and
small- and medium-scale Rails your Linux server: NetBeans 6.0,
applications effectively. the nginx HTTP and proxy server,
the Mongrel server, and the
I'm going to explain how to do memcached system.
just that using my preferred Ruby
and Rails development IDE Leveraging NetBeans
NetBeans 6.0, some open source
server and caching systems, and
6.0 Ruby and Rails
the typical Rails application I work Support
with: running on a single server The first step toward using
and needing to support only 100 Jupiterimages
NetBeans 6.0 for your Ruby and
or so concurrent users. Let's start Rails development is installing
by walking you through how to set up NetBeans 6.0 for NetBeans and the Ruby plugins. Download the Ruby
Ruby and Rails development, and then moves on to NetBeans version 6.0 bundle I use at
techniques for deploying Rails applications efficiently. http://download.netbeans.org/netbeans/6.0/final/. Add


Scaling Rails applications over many servers to handle many thousands
of concurrent users can indeed lead to problems.

2 ” Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Figure 1 Figure 2

any optional Ruby and Rails plugins (e.g., if you use


JRuby and want to add GlassFish plugins) by running
the menu item Tools Plugins.

Although I do use JRuby for projects that use existing


Java libraries (for me, this is mostly artificial intelligence
development where I need Java libraries for machine
learning, reasoning, the semantic Web, etc.), I still use
the C version of Ruby for almost all Rails development
and deployment. By default the Ruby NetBeans 6.0
bundle sets JRuby as the Ruby system. Change this set-
You should now have NetBeans and the Ruby plugins
ting by using the menu item NetBeans Preferences
installed and ready to go.
Ruby (Figure 1).
If you are new to NetBeans, it may take you a while to
Next, you will use the Rails Generator in NetBeans to
get used to the environment. To shorten that process,
make your development environment as convenient as
use the following usage tips:
possible. Figure 2 shows my Rails CookingSpace.com
project (and a few other projects that are collapsed
1. Check out the NetBeans Web site's one-page
from view while I work on CookingSpace.com). When I
summary of keyboard shortcuts at
right-click on the top-level project (or control-click on a
http://wiki.netbeans.org/wiki/view/RubyShortcuts
Mac), NetBeans displays a popup tools menu with the
standard NetBeans tools on the bottom and the Ruby-
2. Make use of the Ruby and Rails plugins' menu
and Rails-specific tools added to the top. The top
options for running tests inside the IDE, which I find
option (Generate...) launches Rails Generator.
more convenient than using the command line rake
tools (see Figure 4).
I find this pop-up tools menu more convenient than
keeping a shell window open to use the Rails com-
3. One of the most useful techniques for interactive-
mand line utilities. Figure 3 shows the Rails Generator
ly developing a Rails Web application using
dialog box.
NetBeans 6.0 is to run both a test server (WEBrick
or Mongrel) and a Rails console at the same time.

3 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Figure 3
As you edit and save code you see the
changes testing in a Web browser on the test
server and you can test snippets of code
before adding them to your models or con-
trollers. Meanwhile, the console enables you
to inspect live data. See Figure 5 for an
example of this setup where the panel in the
lower right corner has both server and con-
sole (currently selected) tabs.

Before proceeding with this article, now would


be a good time to either create a trivial test
Rails application using your new NetBeans
development setup or import an existing Rails
application that you have already written. You
can import an existing Rails Web application by
Figure 4
using the menu File New Project Ruby Ruby on
Rails Project with Existing Sources.

Techniques for Efficient


Rails App Deployment
You have many good options for deploying
Rails Web applications. I will discuss only one: a
deployment setup that works well on a single
server but supports adding additional servers as
needed. The setup is composed of the nginx
server, the Mongrel server, and the memcached
system, which provides a high-performance,
distributed memory object caching system that
you can use to cache both database requests
and Web services requests. Rails applications
can scale, but as with most Web applications,
they might have only a few concurrent users. If
Figure 5
you add memcached to Mongrel, which is fast
by itself, you might be able to put off a more
complex deployment until you have more con-
current users.

You should have your Rails application's devel-


opment settings set for your development PC
and the production settings for your Linux serv-
er. This is important because this discussion
assumes (because you are not using Capistrano)
that to deploy a new version of your Web appli-
cation you can simply do an svn update in your
server's deployment directory.

4 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Using memcached is as easy as installing a gem:

sudo gem install memcache-client

sudo gem install memcache-client

And adding two lines to your production.rb file:

config.action_controller.fragment_cache_store = :mem_cache_store
config.action_controller.session_store = :mem_cache_store

I do not usually add these to my development.rb file.

Deploying a Cluster on Mongrels on a Linux Server


With Ruby, Gem, and Rails installed on your Linux server (using the same version numbers as on your development
PC), you can deploy a Mongrel cluster on it. I will demonstrate how using my CookingSpace.com project as the
example. I start by creating a new unprivileged account with the same name as my Rails application. I prefer to
name the account after the Web application because later I might want to use the nginx Web server to service
several virtual domain names and I would like the Mongrel cluster for each Web application to run under its own
account.

Next, I create a deployment directory, create the account, and set ownership of the deployment directory:

sudo mkdir /var/mongrel


sudo mkdir /var/mongrel/cookingspace
sudo /usr/sbin/groupadd cookingspace
sudo /usr/sbin/adduser -r cookingspace -g cookingspace
sudo chown -R cookingspace /var/mongrel/cookingspace
sudo chgrp -R cookingspace /var/mongrel/cookingspace

Now, I use SSH to remotely login to my server using the account cookingspace and pull the latest version of my
Web application from my subversion server:

cd /var/mongrel/cookingspace/
svn co svn+ssh://MY_ACCOUNT@MY_SERVER.com/home/svn/svn-repos/cookingspace .

I am using MySQL for my Web application so, on my Linux server, I need to set up the databases:

mysqladmin create cookingspace_production --user=root -p


mysqladmin create cookingspace_development --user=root -p
mysqladmin create cookingspace_test --user=root -p
export RAILS_ENV=production
rake db:migrate

Before going any further, it is a good idea to test the Web application running a single Mongrel, but in production
mode:

cd /var/mongrel/cookingspace/
script/server --environment=production

5 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Assuming that this works, we will now install the mongrel_clusters Ruby Gem:

sudo gem install mongrel_cluster

When logged in as user cookingspace, configure three Mongrels and start and stop the cluster to test it:

cd /var/mongrel/cookingspace/
mongrel_rails cluster::configure -e production -p 3001 -n 3 -a 127.0.0.1 --user cookingspace --group cookingspace
mongrel_rails cluster::start
mongrel_rails cluster::stop

Installing and Configuring the nginx Proxy and Web Server


On your server, download the latest stable version of nginx and install it:

cd nginx-0.5.35
./configure
make
sudo make install

This installs nginx in /usr/local/nginx, and you need to edit the file /usr/local/nginx/conf/nginx.conf to look like this:

worker_processes 3;

error_log logs/error.log notice; Using Subversion


error_log logs/warning.log warning;

pid logs/nginx.pid;
for Version Control
While NetBeans supports Subversion (svn), I pre-
events { fer to use the command line svn tools because I
worker_connections 1024; am so used to running svn on remote servers in a
} SSH shell. I strongly recommend that you start
using svn immediately after using the rails com-
http { mand to create an empty Rails project. After per-
include conf/mime.types; forming an svn import on your new empty Rails
default_type application/octet-stream; project, do a fresh checkout of your project and

tcp_nopush on;
let svn know what to ignore:
keepalive_timeout 65; svn propset svn:ignore "*.log" log
tcp_nodelay on; svn propset svn:ignore "schema.rb" db
svn revert public/index.html
upstream mongrel { rm public/index.html
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
For large teams, you should set up Subversion
repositories to use separate directories for trunk,
}
tags, and branches. If you are deploying to an
inexpensive virtual private server (VPS), I assume
gzip on;
that your Rails project is small- or medium-scale
gzip_min_length 1100; with just one or two developers. In these cases,
gzip_buffers 4 8k; simply develop and deploy from trunk.

6 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
gzip_types text/plain;

server {
listen 80;
server_name cookingspace.com;
root /var/mongrel/cookingspace/public;

access_log off;
rewrite_log on;

location / {
proxy_pass http://mongrel;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

}
}
To test, make sure that your Mongrel cluster has started and run nginx manually:

cd /usr/local/nginx/
sbin/nginx -c conf/nginx.conf

Starting Your Mongrel Cluster and Nginx Automatically


This was a tough section for me to write because different Linux distributions have slightly different tools for man-
aging processes at startup. That said, even though it is a little "old school," I think that most Linux distributions
(and also FreeBSD, etc.) support putting startup commands in /etc/rc.local. So that is what I will show here. I tested
this on only one Linux server:

# I made sure that /usr/local installs are visible:


export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
export PATH=$PATH:/usr/local/bin

# start memcached using a non-privileged account:


/usr/local/bin/memcached -d -m 16384 -l 127.0.0.1 -p 11211 -u cookingspace >
/home/cookingspace/memcached.log &

# start the mongrel cluster and nginx using non-privileged accounts:


(cd /var/mongrel/cookingspace/ ; su cookingspace -m -c "nohup
/usr/local/bin/mongrel_rails cluster::restart >
/home/cookingspace/mongrel_rails.log &" )
(cd /usr/local/nginx/ ; sbin/nginx -c conf/nginx.conf)
# note: nginx spawns worker processes using a non-privileged account.

This is a little simplistic, but it has the advantage of likely working on most systems.

7 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
A Time-Saver
After working with server-side Java for over 10 years, I
What If I Need to Scale?
find writing Rails applications to be a lot of fun. It If your system gets so many users that you need to
seems to take much less time to get projects done. I scale to multiple servers, here are a couple of rela-
can spend more time solving real problems and less tively simple things that you can do:
time dealing with infrastructure software. I will still look
to server-side Java for projects that must scale and that • Move your database to another server and
require large development teams—the Java features increase its memory allocation.
that slow down development (static typing, verbose but
readable code) also make Java better for very large • Run your Mongrel cluster across multiple servers.
development teams. That said, most projects are small
enough to be quickly written by a few developers, and If these options are not enough, buy faster servers
Ruby and Rails are a great fit for small and medium- with more memory.
sized projects (the sidebar What If I Need to Scale?
offers some suggestions for adding additional servers
to handle increased user loads).

Deploying Rails applications to servers, however, has a


reputation for being tricky—and if you are not familiar
with Linux system administration it probably is. I hope
that this article saves you time and aggravation in your
deployments. I

8 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]

Build a Wiki System with Rails


By Anil Hemrajani

In order to build and run the example wiki, you will

T
he Ruby language and the Ruby on Rails (Rails for
short) Web application framework have created a lot need to have three core pieces of software installed:
of buzz in the software programming industry and for
good reason. While Ruby has been around for more than a • Ruby interpreter
decade, the Rails framework is relatively new but it is • RubyGems packaging system
becoming a darling of many programmers who are tired of • Rails framework
the overly complicated world of
Web frameworks. In addition, you will need a
Ruby gem named RedCloth as
This article demonstrates just the text markup engine. The
how easy it is to write a simple following is a list of the software
Wiki application using Ruby and versions used for this article
and Rails together. While Rails (note: this software must be
is used mostly to build data- installed in the order shown):
base applications, this article
goes the file-based persistence 1. Ruby interpreter (I used ver-
route because I personally like sion 1.8.5.)
using Wiki systems that are file 2. RubyGems packaging system
based and coincidentally they (I used version 0.9.2.)
are easy to set up. In addition, 3. Gems:
very few resources on the Web • Ruby on Rails (rails-1.2.2)
discuss developing non-data- Alternatively, you can simply
base applications using Rails. Jupiterimages type gem install rails -- include-
Still, switching to a database using the Rails database dependencies.
support should be relatively painless if you choose to • RedCloth textile markup language - (3.0.4) using
go that route. the gem install redcloth command.


While Rails is used mostly to build database applications, this article
goes the file-based persistence route because I personally like using Wiki
systems that are file based and coincidentally they are easy to set up.

9 ” Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Assuming you have everything installed, you can test Figure 1
your configuration using the following commands:

1. ruby --help
2. gem --help (or better yet, gem list --local)
3. rails –help

Features of the Wiki System


Before jumping into the code, let's review some fea-
tures that the Wiki system, I have named RailsWiki,
needs to provide. Wiki systems provide simple ways of
adding, editing, and removing content using the Web.
So, the RailsWiki naturally should enable the user to do
Figure 2
at least the following:

• Create a new Wiki document.


• Edit an existing Wiki document, with the ability to
use a text markup language for formatting the con-
tent.
• View (open) an existing Wiki document.
• Print an existing Wiki document.
• Delete an existing Wiki document.
• List all previously created Wiki documents (for view-
ing or deleting purposes). Figure 3

Next, let's review UI mockups that implement these


high-level feature requests, also known as user stories.
Figures 1, 2, 3, and 4 show screenshots for a wiki's
home page, view feature, edit feature, and print fea-
ture, respectively. These figures reflect most of the fea-
tures you need to implement RailsWiki.

The System Design


Now that you have a fairly good idea of how the UI will
look, let's look at the more technical aspects, such as
the basic design of the application. Figure 5 shows the
design of the RailsWiki application, which uses a typical
model-view-controller (MVC) architecture with an atypi-
cal persistence solution: a file system. As previously
Figure 4
mentioned, I intentionally chose a file-based persist-
ence scheme to keep the Wiki system as simple as
possible. After all, as the inventor of Wiki, Ward
Cunningham, has himself said about the Wiki concept:
"What's the simplest thing that could possibly work?"

Coding The Application


Assuming your software is properly installed, you are
ready to begin coding using Ruby and Rails.

10 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Figure 5

One of the beautiful things about Rails is that it gener- The generate command will create several files, some
ates a lot of code, directories, and files in order to give of which include the following:
you a jumpstart for developing a new Web application.
In order to generate files for RailsWiki, the very first • app/controllers/wiki_controller.rb
command you will type is the rails command from the • app/views/layouts/application.rhtml
top-level directory, under which you want your applica- • app/views/wiki/edit.rhtml
tion's parent and sub-directories created (for example, • app/views/wiki/help.rhtml
/users/anil/dev/ or c:\anil\dev), as shown here: • app/views/wiki/index.rhtml
• app/views/wiki/print.rhtml
> rails railswiki • app/views/wiki/view.rhtml

If the rails command worked properly, you will see a At this point, you already have a working Web applica-
bunch of directories and files created for you. Some of tion. To test your stub application, start the bundled
the directories you will work with in this article include WEBrick HTTP server by typing the following com-
the following: mand:

• app/controllers/ ruby script/server


• app/models/
• app/views/ Once the Web server start has started, open
• app/helpers/ http://localhost:3000/wiki/ in your Web browser and
• public/stylesheets/ you should see a screen similar to the one shown in
• test/unit/ Figure 6.
• config/
• script/ Before adding custom code in the generated controller
and view stub files, you will write a model class first to
Author's Note Regarding Relative Command and serve as your wiki engine.
Directory Path Names: From this point forward, any
commands and directories listed will be relative to the
top-level directory of the RailsWiki application. For Figure 6
example, app/views/ in my case would be located
under c:\anil\dev\railswiki\app\views\.

Generating Controller and


View Stub Files
The first few files you will develop (or rather, generate)
include the controller and corresponding view files. So
start by typing the following command from the top-
level directory of your Rails application:

ruby script/generate controller Wiki


index view edit print help

11 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Developing the "Model" Class @@basedir.

Since Rails is typically used to develop database- • The find method reads in an entire Wiki file and
backed Web applications, most developers working returns it as a string to the caller. (While find as a
with such applications typically use the script/generate method name might sound a bit misleading since it
command to generate the model and related files. reads in a file, I chose this name to stay consistent
However, since this example uses file-based persist- with the Rails naming convention. By following similar
ence, you will write your model class from scratch (see naming conventions, you should be able to switch to
Listing 1) while still following as many of the Rails con- a database-based persistence model easily using the
ventions as possible. For example, you will place your Rails model support.)
model class, Wiki, in the app/models directory (as
app/models/wiki.rb) where Rails would normally place • The find_wikis method returns an array of file
generated model files. Furthermore, the method nam- names found in basedir ending with the value of the
ing conventions are similar to those found in the class variable @@extension, which in this case hap-
ActiveRecord::Base class provided by Rails. Listing 1 pens to default to ".Wiki".
shows the complete Wiki model class.
• The save method simply opens a file for writing and
A few of the notable methods in this class include writes the entire contents of the method parameter
basedir, find, find_wikis, and save, so let's take a closer content to it.
look at these methods:
The other notable public methods include delete,
• The basedir method allows you to set a static direc- exists?, and attributes. The delete method deletes a
tory name where all the Wiki files will be stored. If Wiki file, the exists? method indicates whether a Wiki
this method is not called, then the current directory is file exists or not, and the attributes method provides
used by default as specified in the class variable attributes of a file (such as modified date and file size).

Listing 1: Wiki Model Class


This listing shows the entire contents of your Wiki model class, which is located in app/models/wiki.rb.
require 'find'

# This is the core/manager class for the Railswiki application


# Author: Anil Hemrajani
# Date: Feb, 2007

class Wiki
@@basedir = "."
@@extension = "wiki"

def self.basedir(basedir)
@@basedir = basedir
end

# check if file exists


def self.exists?(basefilename)
File.file?(getfullpath(basefilename))
end

# get file stat continued

12 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Unit Testing the Model Code
Before plugging the model code into your controller class, you should unit test it to ensure all its methods work as
advertised. Generally, using small unit tests that test individual methods is advisable, but I chose to put multiple
tests in a single method named test_wiki in the unit test file test/unit/wiki_test.rb. The test_wiki method tests the
save, find, delete and exists? methods in the Wiki model class, as shown in the following code excerpt from the
wiki_test.rb file:

# Ensure file doesn't exist (yet)


assert !File.exists?(getfullpath(@filename))

# Test save and find


Wiki.save @filename, DEFAULT_TEXT
assert File.exists?(getfullpath(@filename))
text = Wiki.find(@filename)
assert_equal DEFAULT_TEXT, text

# Test delete
Wiki.delete @filename

# Test exists?
assert !Wiki.exists?(@filename)

You should be aware of a couple of problems—and their corresponding solutions—related to testing database-less
rails applications. You typically would unit test the model classes by typing the command rake test:units (rake
test_units in previous versions of rails) from the top-level directory of a Rails application. However, since Rails tries
to connect to the database by default and the RailsWiki application isn't using a database, you need to make two

Listing 1: Wiki Model Class continued

def self.attributes(basefilename)
File.stat(getfullpath(basefilename))
end

# delete existing wiki file


def self.delete(basefilename)
File.delete(getfullpath(basefilename))
end

def self.file_extension
@@extension
end

# save raw wiki file contents


def self.save(basefilename, content)
mkdir

file = File.open(getfullpath(basefilename), 'w')


begin
file.print content
ensure continued

13 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
changes to get this to work (thanks to the "Ruby on Rails Unit Tests" blog by Jay Fields).

First, you need to include a file under lib/tasks ending with the extension .rake. Accordingly, in the downloadable
source code for this article, you will find a file named testing.rake. This file must contain the following line of Ruby
code:

Rake::Task[:'test:units'].prerequisites.clear

Secondly, you need to fix the test/test_helper.rb file by replacing its code with the following:

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) \
+ "/../config/environment")
require 'application'
require 'test/unit'
require 'action_controller/test_process'
require 'breakpoint'

Now running the command rake test:units will generate output similar to this:

Started
.
Finished in 0.094 seconds.

1 tests, 5 assertions, 0 failures, 0 errors

Listing 1: Wiki Model Class continued

file.close
end
end

# return array of wiki file names in basedir


# get raw wiki file contents
def self.find(basefilename)
#unless (FileTest.file?(basefilename)) return "File not found."
getline = ""
File.open(getfullpath(basefilename), 'r') do |f1|
while line = f1.gets
getline = getline + line
end
end

getline
end

# Get list of file names ending with @@extension


def self.find_wikis()
files = []
Find.find(@@basedir) do |path| continued

14 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Integrating the Model, View, and Controller Code
Now that you have coded and tested the Wiki model class, you can begin integrating your model, view, and con-
troller code. Listing 2 shows the complete code for your WikiController class, found under
app/controllers/wiki_controller.rb.

Recall that when you ran the Rails script/generate controller command earlier, it generated a few stub files.
However, I added code to the generated methods and also added some additional methods that are not tied to
any view but serve as specific actions used by other views. The list of public methods in WikiController includes:
initialize, index, edit, view, print, help, create, delete, and save. Let's take a closer look at some of these methods:

• The initialize method sets the name of the directory where your Wiki files will be stored. This name is config-
ured in the config/environment.rb file via the WIKI_DIR property—this is the only configuration you will need to
change to store your Wiki files under a more appropriate directory, if needed:

WIKI_DIR = "/tmp/railswiki"

• The index method calls Wiki.find_wikis to generate a list of existing Wiki file names and passes it onto the
index.rhtml view using the instance variable @filelist. This view loops through the array and populates a HTML
<SELECT> drop down box:

<select name="f">
<% @filelist.each do |file| %>
<option><%= file %></option>
<% end %>
</select>

Listing 1: Wiki Model Class continued

if File.file?(path) && path =~ /.#{@@extension}$/


files << File.basename(path, ".#{@@extension}")
end
end

files
end

private
def self.mkdir()
Dir.mkdir(@@basedir) if !File.exist?(@@basedir)
end

def self.getfullpath(basefilename)
@@basedir + "/" + getfullname(basefilename)
end

def self.getfullname(basefilename)
basefilename + "." + @@extension
end
end

15 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
• The edit, view, and print methods simply call an internal (private) method named get_content, which reads in
the request file and passes on the file attributes and its contents to the corresponding views via the @filestat and
@content instance variables, respectively. The following code shows how:

def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end

Apart from reading in the contents and attributes of the requested file, the get_content method also redirects the
page to your index page, along with an error message in case of an exception. The error message is set using the
Rails flash feature (essentially, a hash containing application errors).

Listing 2: WikiController Class


This listing shows the entire contents of your WikiController class, which is located in
app/controllers/wiki_controller.rb.
# This is the front/main controller for the Wiki application
# Author: Anil Hemrajani
# Date: Feb, 2007
class WikiController < ApplicationController
layout "wiki" , :except => [ :index, :print ]

def initialize
Wiki.basedir WIKI_DIR
end

def index
@filelist = Wiki.find_wikis
end

def edit
get_content
end

def view
get_content
end

def print
get_content
end

def help
@filename = get_filename params[:f] # need this for other actions continued

16 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Once the controller has passed control to the view, the view simply displays contents of the instance variable
@content. For example, the view.rhtml template has a single line of code in the entire file that not only displays
the content of the Wiki file but also converts it prior to doing so using a helper method named to_html:

<%= to_html @content %>

The to_html helper method can be found in the app/helpers/wiki_helper.rb file, a Rails-generated helper module
(helper modules are a nice way to make methods available to your views). This is also where the gem named
RedCloth, discussed earlier, comes into play since it does the leg work of converting the raw Wiki file text to
HTML, as shown in this code contained in our wiki_helper.rb file:

require 'redcloth'

module WikiHelper
# parse and return data as HTML
def to_html(rawtext)
return "" if rawtext.nil?

r = RedCloth.new rawtext
r.to_html
end
end

Listing 2: WikiController Class continued


end

# create new wiki file


def create
@filename = get_filename params[:f]
if Wiki.exists?(@filename)
flash[:error] = "File '#{@filename}' already exists; use Open instead of
Create."
redirect_to :action => :index
else
Wiki.save @filename, ""
redirect_to :action => :view, :f => @filename
end
end

# delete existing wiki file


def delete
if params[:f].nil? || params[:f].empty?
flash[:error] = "File parameter not specified."
redirect_to :action => :index
else
@filename = get_filename params[:f]
Wiki.delete @filename
redirect_to :action => :index
end
end continued

17 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Other Notable Files
The app/views/layouts/application.rhtml-generated file provides an application-wide (that is, across all controllers),
consistent look-and-feel, which includes things such as the header and footer. However, you can also have con-
troller-specific layouts, so I renamed this file to wiki.rhtml to make it applicable only to your WikiController. This
wiki.rhtml file also links to the cascading style sheet (CSS) file, public/stylesheets/wiki.css, which you can in turn use
to control the aesthetics of your application such as the colors, fonts, and so on.

The one other pre-generated file, which I have tweaked slightly, is config/routes.rb. By modifying one of the map-
pings and deleting the generated public/index.html file, you can access the index page without the trailing URI
(that is, /wiki/):

map.connect '', :controller => "wiki"

In other words, a URL of http://localhost:3000/ is treated the same as http://localhost:3000/wiki/.

Listing 2: WikiController Class continued


# save existing wiki file
def save
@filename = get_filename params[:f]
content = params[:c]
p @filename
p content
Wiki.save @filename, content
redirect_to :action => :view, :f => params[:f]
end

private
def get_filename(f)
f = "untitled" if (f.nil? || f.empty?)
f
end

# load file into @content using param :f; for errors go to index page
def get_content
begin
@filename = get_filename params[:f]
@filestat = Wiki.attributes @filename
@content = Wiki.find @filename
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
end

18 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
I also added the following line to the config/routes.rb file to allow you to use clean REST (Representational State
Transfer)-like Web services URLs:

map.connect 'wiki/:action/:f', :controller => "wiki"

For example, with the above line added, a URL such as http://localhost:3000/wiki/view?f=untitled would look like
http://localhost:3000/wiki/view/untitled. This sort of URL formatting is something the Rails helper methods (such as
link_to, redirect_to, url_for, and others) automatically handle for you.

What Next?
You have successfully built a completely functional Wiki system, but you obviously can add to and enhance this
basic system. If you are interested in learning more Rails or simply building upon the downloadable application,
here are a couple of suggestions:

1. Leverage the strengths of Rails and have your application persist its Wiki pages in a database (perhaps using a
text data type for the actual Wiki text).

2. Add search capabilities to the application using an existing Ruby gem that provides indexing and searching
capabilities. For example, one such gem named Ferret can be found at ferret.davebalmain.com/trac.

3. Add support for commonly found Wiki features such as WikiWord, change history, and so on.
Apart from that, have fun coding with Ruby and Rails! I

19 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]

Build Robust Security into a


Rails-Based Wiki System
By Anil Hemrajani

The company I founded recently has been using this

O
ne of the core advantages of wiki systems is the
ability to edit Web pages stored on a server very application for a couple of months now to manage
through a Web browser. However, this function also a dozen or so encrypted wiki pages. So this system has
allows administrators to look at the contents of your wiki practical, real world applications.
pages, particularly if your wiki uses a shared server. If you
store confidential information on Changes to the
that server, this is an especially
undesirable compromise. A sim-
Views
ple solution to this dilemma is to Adding authentication and
store the Web pages in an encryption to the RailsWiki
encrypted form and use application requires some
authentication to access them changes in the client and serv-
in a readable form. er-side code. For the client
side, you need to add a logon
Building on the wiki file-man- screen as shown in Figure 2.
agement system created in
the previous article, we're Notice that the logon screen
now going to demonstrate contains not only a user name
how to add authentication and password but also an
and encryption security fea- encryption key. (You will see
tures to the system (see the how to use this key to encrypt
sidebar for an overview of data later.) In addition to the
Jupiterimages
these security concepts). This new logon screen, you also
simple system, called RailsWiki, offers basic Web page need to modify the old "Home" screen by adding a
management features such as view, edit, print, and so checkbox for the user to choose whether a new file
on (Figure 1 presents its basic design). should be encrypted or not. Figure 3 reflects this
change.


Building on the wiki file-management system created in the previous
article, we're now going to demonstrate how to add authentication and
encryption security features to the system

20 ” Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Figure 1

Figure 2

Changes to the Model Classes


The original RailsWiki application had a single model
class called Wiki. This article splits this class into two
classes, WikiUser and WikiDocument. The split is a
clean way to have one class represent the user and
another represent the user's documents (i.e., Web
pages). Listing 1 and Listing 2 contain the complete
code for WikiUser and WikiDocument, respectively. Figure 3
Let's review the most notable methods in each of
these classes.

WikiUser
WikiUser essentially provides authentication-related
services by leveraging the Ruby Crypt library. Based on
the descriptions of the various block ciphers supported
by this library, I chose to use Blowfish.

The first interesting thing to note about this class is that


it actually enables the user to create his or her own
password file instead of requiring an administrator to
do so. The password file itself is encrypted using the
Blowfish cipher as demonstrated in the following code
excerpt for the create_account method:

def create_account(plain_password)
blowfish = Crypt::Blowfish.new(PASSWORD_KEY)
f = open_file "w"
f.print(blowfish.encrypt_string(plain_password))
f.close
end

The remaining methods in the WikiUser class essentially provide additional authentication services such as validat-
ing an existing password, determining if the user account already exists, and so on.

WikiDocument
The WikiDocument class is a bit more involved. It provides file-management services such as opening, saving, and
deleting wiki files, getting a list of wiki file names in the user's directory, etc. Let's review the methods you need to
add and change from the original RailsWiki application.

21 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
For starters, the self.save_encrypted method accepts the fully qualified path name of the wiki file to be created,
along with a key to encrypt the plain text. This method first calls the internal mkdir method to ensure that the
directory specified in the path variable exists. Then it creates the files and encrypts the plain text using the
encrypt_string method of the Crypt::Blowfish class. The following code excerpt from the WikiDocument class
demonstrates all of this:

def self.save_encrypted(path, content, key = nil)


raise "key is nil or empty" if (key.nil? || key.empty?)
mkdir path

file = File.open(path, 'wb')


begin
blowfish = Crypt::Blowfish.new(key)
file.print(blowfish.encrypt_string(content))
ensure
file.close
end
end

The other notable method in the WikiDocument class is find_encrypted. The key difference between this method
and the find method that originally was part of the Wiki class is the extra step of decrypting the encrypted text
using the decrypt_string method in the Crypt::Blowfish class, as shown in the following code excerpt:

def self.find_encrypted(path, key)


raise "key is nil or empty" if (key.nil? || key.empty?)

content = self.find(path)
return "" if content.nil?

blowfish = Crypt::Blowfish.new(key)
blowfish.decrypt_string content
end

Overview of Security Concepts


While many forms of security exist in information technology, you can boil application-level security down to
authentication, authorization, and encryption:

1. Authentication is the process of verifying that a person or software application is who it says it is. This is com-
monly done using the combination of a logon user ID and a password.

2. Authorization generally follows authentication. After the person or application has been verified, authorization
determines which role the user fits into and accordingly what the user is permitted to do in the software appli-
cation.

3. Encryption is the process of converting data to a form that cannot be understood by unauthorized people or
programs.

22 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Changes to the Controller Class
The controller class, WikiController (see Listing 3), changes significantly from the original RailsWiki application.
Let's review some of methods with the most notable changes.
The create_account method kicks things off by allowing a user to create his or her own account. If all the parame-
ters passed into this method are valid, the user's account (password file) is created using the WikiUser model class,
as demonstrated here:

wu = WikiUser.new user
wu.create_account pass
flash[:error] = "Account created; you may login now."

The login method authenticates the user using the WikiUser model class. If the user's credentials are valid, the sys-
tem establishes a session for the user and forwards him or her to the home view. Otherwise, it sends the user back
to the index view with an error message. The following code excerpt demonstrates all of this:

unless wikiuser.valid_password?(pass)
flash[:error] = "Invalid password."
redirect_to :action => :index
else
session[:user] = wikiuser
redirect_to :action => :home, :f => DEFAULT_FILENAME
end

Listing 1: WikiUser Model Class


This listing shows the entire contents of the WikiUser model class, which is located in app/models/wiki_user.rb.
require 'crypt/blowfish'

# This is the wiki user class for the RailsWiki application


# Author: Anil Hemrajani
# Date: June, 2007
class WikiUser
attr_accessor :user, :key

def initialize(user, key = nil)


@user = user
@key = key
end

# Create a password file in user's own directory


def create_account(plain_password)
blowfish = Crypt::Blowfish.new(PASSWORD_KEY)
f = open_file "w"
f.print(blowfish.encrypt_string(plain_password))
f.close
end

# Validate password against stored/encrypted password


def valid_password?(compare_password) continued

23 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
The home method simply gathers a list of wiki pages, encrypted and non-encrypted, which belong to the current
user and passes it to the home view, as demonstrated here:

@filelist = []
list1 = WikiDocument.find_wikis(get_basedir, PLAIN_EXTENSION)
list2 = WikiDocument.find_wikis(get_basedir, ENCRYPTED_EXTENSION)
@filelist.concat(list1) unless list1.nil?
@filelist.concat(list2) unless list2.nil?

The create method creates a blank wiki file with the appropriate extension, as shown here:

if (extname.nil? || (extname != ENCRYPTED_EXTENSION && extname != PLAIN_EXTENSION))


if (encrypt)
@filename = @filename + ENCRYPTED_EXTENSION
else
@filename = @filename + PLAIN_EXTENSION
end
end

The save method calls the internal/private method save_document, which in turn determines the appropriate save
method to call on the WikiDocument model class based on whether encryption is required or not, as demonstrat-
ed here:

Listing 1: WikiUser Model Class continued

f = open_file "r"
encrypted_password = f.gets(nil)
f.close

blowfish = Crypt::Blowfish.new(PASSWORD_KEY)
compare_password == blowfish.decrypt_string(encrypted_password)
end

def self.exists?(user)
File.exists?(WIKI_DIR + "/" + user + "/" + PASSWORD_FILE)
end

:private
def open_file(mode)
basedir = WIKI_DIR + "/" + @user
begin
Dir.mkdir basedir
rescue
end
File.open(basedir + "/" + PASSWORD_FILE, mode)
end
end

24 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
def save_document(filename, content, encrypt)
if (encrypt)
WikiDocument.save_encrypted((get_fullpath filename, false), content,
(get_session_object).key)
else
WikiDocument.save((get_fullpath filename, false), content)
end
end

Once a wiki file has been created, these files are read using the private method get_content, which determines
whether the file needs to be decrypted or not, as shown here:

Listing 2: WikiDocument Model Class


This listing shows the entire contents of the WikiDocument model class, which is located in
app/models/wiki_document.rb.
require 'find'
require 'crypt/blowfish'

# This is the core/manager class for the Railswiki application


# Author: Anil Hemrajani
# Date: June, 2007
class WikiDocument

# save raw wiki file contents


def self.save(path, content)
mkdir path

file = File.open(path, 'wb')


begin
file.print(content)
ensure
file.close
end
end

# save wiki file contents w/ encryption


def self.save_encrypted(path, content, key = nil)
raise "key is nil or empty" if (key.nil? || key.empty?)
mkdir path

file = File.open(path, 'wb')


begin
blowfish = Crypt::Blowfish.new(key)
file.print(blowfish.encrypt_string(content))
ensure
file.close
end
end
continued

25 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
@filestat = WikiDocument.attributes(get_fullpath(@filename))
extname = File.extname(@filename)
if (extname == ENCRYPTED_EXTENSION)
@content = WikiDocument.find_encrypted(get_fullpath(@filename),
(get_session_object).key)
else
@content = WikiDocument.find(get_fullpath(@filename))
end
One last notable method in the controller class is the authenticate method. This method is called before various
methods that require a user to be logged in, that is, they require an active session for the user. This method is

Listing 2: WikiDocument Model Class continued

# get raw wiki file contents


def self.find(path)
content = ""
File.open(path, 'rb') do |f1|
while line = f1.gets(nil)
content = content + line
end
end

(content.nil? || content.empty?) ? "" : content


end

# get decrypted wiki file contents


def self.find_encrypted(path, key)
raise "key is nil or empty" if (key.nil? || key.empty?)

content = self.find(path)
return "" if content.nil?

blowfish = Crypt::Blowfish.new(key)
blowfish.decrypt_string content
end

# Return array of file names ending with extension


def self.find_wikis(path, extension = nil)
files = []

if (extension.nil? || extension.empty?)
Find.find(path) do |f|
if FileTest.directory?(f) && (f != path)
Find.prune # Don't enter sub-directories
else
files << File.basename(f)
end
end
else
Find.find(path) do |f| continued

26 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
automatically called prior to such methods by using Rails' filters as shown in this single line of code:

before_filter :authenticate, :only => [:home, :edit, :view, :print]

Configuration File
The other file worth inspecting is the config/environment.rb file, since it contains various default settings for
RailsWiki, as shown here:

WIKI_DIR = "/users"
PLAIN_EXTENSION = ".wiki"
ENCRYPTED_EXTENSION = ".swiki"
DEFAULT_FILENAME = "untitled" + PLAIN_EXTENSION
PASSWORD_KEY = "WikiUserKey"
PASSWORD_FILE = ".railswiki_pass"
PATH_SEPARATOR = "/"

Listing 2: WikiDocument Model Class continued

if FileTest.directory?(f) && (f != path)


Find.prune # Don't enter sub-directories
else
files << File.basename(f) if File.extname(f) == extension
end
end
end

files
end

# check if file exists


def self.exists?(path)
File.file?(path)
end

# get file stat


def self.attributes(path)
File.stat(path)
end

# delete existing wiki file


def self.delete(path)
File.delete(path)
end

private
def self.mkdir(path)
dir = File.dirname(path)
Dir.mkdir(dir) if !File.exists?(dir)
end
end

27 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
The Wrap Up • Add revisions and ability to roll back to a previous
version of a wiki document
The previous article showed how to build RailsWiki, a • Add search capabilities using something like the
very bare bones but functional wiki system. This article Ferret library
added security features to it, namely authentication and
encryption. However, many robust wiki systems today If you decide to add any of the above features, be sure
tend to contain a lot more features to enable collabora- to submit your changes to the RailsWiki open source
tion. Here are some ideas for extending the RailsWiki project on RubyForge and/or notify me, as I would love
system even further: to use some of them myself! I

• Add role-based security (that is, authorization) This content was adapted from Internet.com's DevX
• Add email alerts for notifying others when changes Web site. Contributors: Mark Watson and Anil
to a file occur Hemrajani .

Listing 3: WikiController Class


This listing shows the entire contents of the WikiController class, which is located in
app/controllers/wiki_controller.rb.
# This is the front/main controller for the RailsWiki application
# Author: Anil Hemrajani
# Date: June, 2007
class WikiController < ApplicationController
layout "wiki" , :except => [ :index, :print, :help ]
before_filter :authenticate, :only => [:home, :edit, :view, :print]

# Create a user directory and password file


def create_account
user = params[:user]
pass = params[:pass]

if (user.nil? || user.empty?)
flash[:error] = "User is required."
elsif (WikiUser.exists? user)
flash[:error] = "Account already exists."
elsif (pass.nil? || pass.empty?)
flash[:error] = "Password is required."
else
wu = WikiUser.new user
wu.create_account pass
flash[:error] = "Account created; you may login now."
end

redirect_to :action => :index


end

# validate fields; if OK, forward to home page


def login
user = params[:user]
pass = params[:pass]
key = params[:key] continued

28 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Listing 3: WikiController Class continued

if (user.nil? || user.empty? || pass.nil? || pass.empty?)


flash[:error] = "User and password are required."
redirect_to :action => :index
else
wikiuser = WikiUser.new(user, key)
begin
unless wikiuser.valid_password?(pass)
flash[:error] = "Invalid password."
redirect_to :action => :index
else
session[:user] = wikiuser
redirect_to :action => :home, :f => DEFAULT_FILENAME
end
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :index
end
end
end

# Clear the session


def logout
session[:user] = nil
redirect_to :action => :index
end

# index page; forward to home if home page if logged in


def index
redirect_to :action => :home, :f => DEFAULT_FILENAME unless
(get_session_object).nil?
end

# Home page; get list of wiki files for user


def home
@key = (get_session_object).key
@encrypt = (@key.nil? && !@key.empty?)

@filelist = []
list1 = WikiDocument.find_wikis(get_basedir, PLAIN_EXTENSION)
list2 = WikiDocument.find_wikis(get_basedir, ENCRYPTED_EXTENSION)
@filelist.concat(list1) unless list1.nil?
@filelist.concat(list2) unless list2.nil?
get_content
end

# Create new wiki file; ensure proper file extension is used


def create
@filename = get_filename params[:f]
encrypt = (!params[:e].nil? && !params[:e].empty?) continued

29 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Listing 3: WikiController Class continued

extname = File.extname(@filename)

if (extname.nil? || (extname != ENCRYPTED_EXTENSION && extname !=


PLAIN_EXTENSION))
if (encrypt)
@filename = @filename + ENCRYPTED_EXTENSION
else
@filename = @filename + PLAIN_EXTENSION
end
end

if (encrypt && extname == PLAIN_EXTENSION)


flash[:error] = "Invalid file extension; use " + ENCRYPTED_EXTENSION
elsif (!encrypt && extname == ENCRYPTED_EXTENSION)
flash[:error] = "Invalid file extension; use " + PLAIN_EXTENSION
elsif WikiDocument.exists?((get_fullpath @filename, encrypt))
flash[:error] = "File '#{@filename}' already exists; use Open instead
of Create."
else
flash[:error] = "File '#{@filename}' created."
save_document(@filename, "", encrypt)
end
redirect_to :action => :edit, :f => @filename
end

# Save a wiki file


def save
@filename = get_filename params[:f]
save_document(@filename, params[:c], (File.extname(@filename) == ENCRYPT-
ED_EXTENSION))
redirect_to :action => :view, :f => params[:f]
end

# Delete existing wiki file


def delete
if params[:f].nil? || params[:f].empty?
flash[:error] = "File parameter not specified."
redirect_to :action => :index
else
@filename = get_filename params[:f]
WikiDocument.delete((get_fullpath @filename, false))
redirect_to :action => :home
end
end

# Edit page; get content


def edit
get_content continued

30 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Listing 3: WikiController Class continued

end

# View page; get content


def view
get_content
end

# Print page; get content


def print
get_content
end

private
# Save a file in either encrypted or plain form
def save_document(filename, content, encrypt)
if (encrypt)
WikiDocument.save_encrypted((get_fullpath filename, false), content,
(get_session_object).key)
else
WikiDocument.save((get_fullpath filename, false), content)
end
end

def authenticate
begin
wikiuser = get_session_object
raise "no user found in session" if wikiuser.nil?

user = wikiuser.user
raise "no user found in session" if (user.nil? || user.empty?)
rescue => exception
flash[:error] = exception
redirect_to :action => :index
end
end

# Get user session object or raise an exception if not found


def get_session_object
session[:user]
end

def get_filename(f)
f = DEFAULT_FILENAME if (f.nil? || f.empty?)
f
end

def get_fullpath(basefilename, encrypt = false)


return get_basedir + PATH_SEPARATOR + basefilename continued

31 Real Life Rails. Copyright 2008, Jupitermedia Corp.


[ Real Life Rails ]
Listing 3: WikiController Class continued

end

def get_basedir
WIKI_DIR + PATH_SEPARATOR + (get_session_object).user
end

# Load file into @content using param :f; for errors go to index page
def get_content
begin
@filename = get_filename params[:f]
if @filename == DEFAULT_FILENAME
begin
save_document(@filename, "", false) unless
WikiDocument.exists?(get_fullpath(@filename))
rescue
end
end
@filestat = WikiDocument.attributes(get_fullpath(@filename))
extname = File.extname(@filename)
if (extname == ENCRYPTED_EXTENSION)
@content = WikiDocument.find_encrypted(get_fullpath(@filename),
(get_session_object).key)
else
@content = WikiDocument.find(get_fullpath(@filename))
end
rescue Errno::ENOENT => exception
flash[:error] = exception
redirect_to :action => :home
rescue RuntimeError => exception
flash[:error] = exception
redirect_to :action => :home
end
end
end

32 Real Life Rails. Copyright 2008, Jupitermedia Corp.

You might also like