Cgi
Path: docs/Cgi
Modified: Thu Nov 07 16:33:20 JST 2002

Using Amrita with cgi

summary

This document describes how to use amrita with cgi programing using series of sample code.

The sample is a web bookmark system. You can see the demo at...

  http://www.walrus-ruby.org/amrita/

The model class

At first, we make the model class.

  !/usr/bin/ruby

  require 'amrita/template'
  class Item
    include Amrita::ExpandByMember
    attr_reader :group, :name, :url

    def initialize(group, name, url)
      @group, @name, @url = group, name, url
    end
    def to_s
      %Q[#{@group}|#{@name}|#{@url}]
    end

    def link
      e(:a, :href=>@url) { @url }
    end
  end
  class BookmarkList
    attr_reader :groups

    def initialize
      @groups = {}
    end
    def load_from_file(path)
      File::open(path) do |f|
        f.each do |line|
          begin
            add_new_item(*line.chomp.split('|'))
          rescue
          end
        end
      end
    end

    def save_to_file(path)
      File::open(path, "w") do |f|
        @groups.each do |k, v|
          v.each do |data|
            f.puts data.to_s
          end
        end
      end
    end
    def add_new_item(group="", name="", url="", *x)
      item = Item.new(group, name, url)
      @groups[group] ||= []
      @groups[group] << item
    end
  end

  if __FILE__ == $0
    require 'runit/testcase'
    require 'runit/cui/testrunner'
    class TestBMModel < RUNIT::TestCase
      def test_item
        item = Item.new("aa", "bb", "http://www.xxx.com/")
        assert_equal("aa", item.group)
        assert_equal("bb", item.name)
        assert_equal("http://www.xxx.com/", item.url)
      end

      def test_bookmarkmodel
        bm = BookmarkList.new
        assert_equal(0, bm.groups.size())
        assert_equal({}, bm.groups)
        bm.add_new_item("g", "nm", "http://www.xxx.com/")
        assert_equal(1, bm.groups.size())
        assert_equal(1, bm.groups["g"].size())
        assert_equal("nm", bm.groups["g"][0].name)
        assert_equal("http://www.xxx.com/", bm.groups["g"][0].url)
      end

      def test_load
        bm = BookmarkList.new
        bm.load_from_file("bookmark.dat.sample")
        assert_equal(3, bm.groups.size())
        assert_equal(3, bm.groups["BBS"].size())
        assert_equal("2ch", bm.groups["BBS"][0].name)
        assert_equal("http://www.ruby-lang.org/", bm.groups["Script Languages"][0].url)
      end

      def test_save
        tmp = "/tmp/bmtest#{$$}"
        bm = BookmarkList.new
        bm.load_from_file("bookmark.dat.sample")
        bm.add_new_item("html", "amrita", "http://kari.to/amrita/")
        assert_equal(4, bm.groups.size())
        assert_equal(3, bm.groups["BBS"].size())
        assert_equal("2ch", bm.groups["BBS"][0].name)
        assert_equal("http://www.ruby-lang.org/", bm.groups["Script Languages"][0].url)
        assert_equal(1, bm.groups["html"].size())
        assert_equal("amrita", bm.groups["html"][0].name)

        bm.save_to_file(tmp)
        bm = BookmarkList.new
        bm.load_from_file(tmp)
        assert_equal(4, bm.groups.size())
        assert_equal(3, bm.groups["BBS"].size())
        assert_equal("2ch", bm.groups["BBS"][0].name)
        assert_equal("http://www.ruby-lang.org/", bm.groups["Script Languages"][0].url)
        assert_equal(1, bm.groups["html"].size())
        assert_equal("amrita", bm.groups["html"][0].name)
      ensure
        File::unlink tmp
      end
    end

    if ARGV.size == 0
      RUNIT::CUI::TestRunner.run(TestBMModel.suite)
    else
      ARGV.each do |method|
        RUNIT::CUI::TestRunner.run(TestBMModel.new(method))
      end
    end
  end

The class Item is the bookmark items. It has three attributes: group, name, url.

The class + BookmarkList is collaction of Item. It contains Item s separated by groups and is able to save and load the list to/from a file.

The model class has nothing to do with HTML. So, it can be unit-tested easily.

bookmark.cgi

bookmark.cgi displays bookmark list. And it accepts new bookmark entry.

html template file

bookmark.cgi uses this template.

  <html>
  <body>

  <h1>amrita bookmark sample</h1>
    <div id="groups">
      <h1 id="group_name"></h1>
      <table border="1">
        <tr><th>name</th><th>url</th></tr>
        <tr id=items>
          <td id="name"></td>
          <td id="link"></td>
        </tr>
      </table>
    </div>
    <hr>
    <form  id="form" method="post">
      <table>
        <tr>
          <th>group:</th>
          <td id=group_sel></td>
        </tr>
        <tr>
          <th>new_group:</th>
          <td><input name="group" type="text">
        </td>
        </tr>
        <tr>
          <th>title:</th>
          <td><input name="title" type="text">
        </td>
        </tr>
        <tr>
          <th>url:</th>
          <td><input name="url" type="text">
        </td>
        </tr>
          <tr><th>
          <td><input value="newitem" type="submit">
        </td>
        </tr>
      </table>
    </form>

  </body>
  </html>

code

This is the code of bookmark.cgi

  !/usr/bin/ruby

  require 'cgi'
  require 'amrita/template'
  require 'bmmodel'
  include Amrita
  DATAFILE_PATH="bookmark.dat"
  TEMPLATE_PATH="bookmark.html"
  CACHE_PATH="/tmp/bookmark"

  def make_model_data(bm, selected_group)
    groups = bm.groups.keys.sort
    data = {
      :groups => groups.collect do |k|
        {
          :group_name=>k,
          :items=>bm.groups[k]
        }
      end ,
      :form => {
        :group_sel=>e(:select, :name=>"group_sel") {
          groups.collect do |g|
            if g == selected_group
              e(:option, :value=>g, :selected=>"selected") { g }
            else
              e(:option, :value=>g) { g }
            end
          end
        },
      }
    }

    data
  end
  def generate_output(bm, group)
    Amrita::TemplateFileWithCache::set_cache_dir(CACHE_PATH)
    tmpl = Amrita::TemplateFileWithCache[TEMPLATE_PATH]
    tmpl.use_compiler = true
    tmpl.expand($stdout, make_model_data(bm,group))
  end

  def main
    bm = BookmarkList.new
    bm.load_from_file(DATAFILE_PATH)
    cgi = CGI.new
    url = cgi['url'][0]
    group = ""
    if url
      group = (cgi['group'][0]).to_s
      group = (cgi['group_sel'][0]).to_s if group == ""
      name = (cgi['title'][0]).to_s
      name = url if name == ""
      bm.add_new_item(group, name, url)
      bm.save_to_file(DATAFILE_PATH)
    end
    puts cgi.header
    generate_output(bm, group)
  end
  main

use a model class object for amrita's model data

  class Item
    include Amrita::ExpandByMember

    def link
      e(:a, :href=>url) { url } # <a href="http://www.xxx.com/">http://www.xxx.com/</a>
    end
  end

Ruby's class is an open class: it can be edited by user without modifing the original code. The class Item is defined in other source file.

We make this class include Amrita::ExpandByMember and add a method named link so that it's method can be used directly by template.

      <tr id=items>
        <td id="name"></td>
        <td id="link"></td>
      </tr>

We will provide Item objects for id items and because Item object is a Amrita::ExpandByMember object,id name and link will be used as method names.

url is a method related to MODEL so it should be defined in model class (bmmodel.rb). And link contains information about VIEW (HTML presentation) so it's better to put it to the view related source(bookmark.cgi).

make a forms element

If you add a new item, the next page displayed contains the selection of groups with default of the selected group.

The model data here....

    :form => {
      :group_sel=>e(:select, :name=>"group_sel") {
        groups.collect do |g|
          if g == selected_group
            e(:option, :value=>g, :selected=>"selected") { g }
          else
            e(:option, :value=>g) { g }
          end
        end
      },
    }

generates this html.

   <td>
     <select name="group_sel">
       <option value="BBS">BBS</option>
       <option value="Script Languages" selected="selected">Script Languages</option>
       <option value="TestXSS">TestXSS</option>
     </select>
   </td>

And this HTML is inserted to the element with id group_sel.

using the compiled code

  Amrita::TemplateFileWithCache::set_cache_dir(CACHE_PATH)
  tmpl = Amrita::TemplateFileWithCache[TEMPLATE_PATH]
  tmpl.use_compiler = true
  tmpl.expand($stdout, make_model_data(bm,group))

Amrita::TemplateFileWithCache is a kind of Amrita::TemplateFile that can reuse compiled code stored in cache file.

If there is the cache data matches to TEMPLATE_PATH in CACHE_PATH and it is younger than template itself, amrita reuse the compiled code automatically.

CAUTION: be careful to prevent users to edit the cache file.

Currently, amrita does not check the cache file weather it was created by amrita nor unmodified . So if someone can edit it, he or she can insert any dangerous code into it to be executed by amrita.

It's YOUR resposibility to protect the cache files from crackers. Don't use TemplateFileWithCache::set_cache_dir if you don't understand this.


using amrita script as cgi

This is a viewer of bookmark written in amrita-script.

  <html>
  <body>

  <amritascript> <!--
    require "bmmodel"
    include Amrita
    bm = BookmarkList.new
    bm.load_from_file("bookmark.dat")
    groups = bm.groups.keys.sort

    data = {
      :groups => groups.collect do |k|
        {
          :group_name=>k,
          :items=>bm.groups[k].collect do |item|
          {
            :name=>item.name,
            :link=>a(:href=>item.url) { item.url }
          }
        end
        }
      end
    }
  //--></amritascript>
    <div id="groups">
      <h1 id="group_name"></h1>
      <table border="1">
        <tr><th>name</th><th>url</th></tr>
        <tr id="items">
          <td id="name">name</td>
          <td><a id="link">url with link</a></td>
        </tr>
      </table>
    </div>

  </body>
  </html>

How to run in apache.


using bookmark.rb under mod_ruby

sample/cgi/bookmark.rb is a script can run under mod_ruby.

  LoadModule ruby_module /usr/lib/apache/mod_ruby.so
  RubyRequire apache/ruby-run

  Alias /amrita/cgi-bin/ /home/tnaka/cvswork/amrita/sample/cgi/
  <Location /amrita/cgi-bin>
    Options ExecCGI
    SetHandler ruby-object
    RubyHandler Apache::RubyRun.instance

    SetEnv AmritaCacheDir /tmp/bookmark # be careful
  </Location>

using amrita-script under mod_ruby

   LoadModule ruby_module /usr/lib/apache/mod_ruby.so
   Alias /amrita/cgi-bin/ /home/tnaka/cvswork/amrita/sample/cgi/
   RubyRequire amrita/handlers
   SetEnv AmritaCacheDir /tmp/bookmark
   <Files *.ams>
     Options ExecCGI
     SetHandler ruby-object
     RubyHandler Amrita::AmsHandler.instance
   </Files>

X

code and output

code:

output:

description