File: Cgi

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.

- set `AllowOverride FileInfo` and `Options ExecCGI` to some +cgi-bin+ Directory directive in httpd.conf
- put bin/amshandler to that directory
- put .htaccess to that directory

        AddHandler amrita-script ams
        Action amrita-script /amrita/cgi-bin/amshandler

## 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