iCalでカレンダーを公開/公開中止/照会するためのCGIを書いてみた

CalDAVって、あんなにでっかいソースで何をしているんだろうか←ソースを読め
とりあえず、mod_actionsが有効ならば使えるはず。RFC2445はざっと読んだだけなので、細かいところは修正が必要かも。iCalのicsフィアルは扱えるはず。
確認したのは、Apache 2.2.8, Ruby 1.8.6, iCal 3.0.4。
calcgi.rb

#!/usr/bin/ruby
# $Id$

require "cgi"

class CalCGI < CGI
  attr("put_content")
  attr("calendar")

  def cal_initialize_query()
    if ("POST" == env_table['REQUEST_METHOD']) and
      %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n.match(env_table['CONTENT_TYPE'])
      boundary = $1.dup
      @multipart = true
      @params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
    elsif ("PUT" == env_table['REQUEST_METHOD'])
#      @put_content = read_from_cmdline
      @put_content = readlines
      if ("text/calendar" == env_table['CONTENT_TYPE'])
        @calendar = rfc2445parse(@put_content)
      end
    else
      @multipart = false
      @params = CGI::parse(
        case env_table['REQUEST_METHOD']
        when "GET", "HEAD"
          if defined?(MOD_RUBY)
            Apache::request.args or ""
          else
            env_table['QUERY_STRING'] or ""
          end
        when "POST"
          stdinput.binmode if defined? stdinput.binmode
          stdinput.read(Integer(env_table['CONTENT_LENGTH'])) or ''
        else
          read_from_cmdline
        end
      )
    end
    @cookies = CGI::Cookie::parse((env_table['HTTP_COOKIE'] or env_table['COOKIE']))
    end
  private :cal_initialize_query

  def rfc2445parse(content)
    cal_data = Hash.new([].freeze)
    target_data = Hash.new()
    stacked_data = []
    stacked_name = []
    target_name = ""

    content = connect_content_line(content)

    content.each do |line|
      key, value, param_hash = split_content_line(line)
      
      if "BEGIN" == key
        if target_name != "" # top
          stacked_data.push(target_data.dup)
          stacked_name.push(target_name)
        end

        target_data = {value => Hash.new()} 
        target_name = value
      elsif "END" == key
        if stacked_data.size > 0
          if stacked_data[-1][stacked_name[-1]].include?(value)
            stacked_data[-1][stacked_name[-1]][value].push(target_data[value].dup)
          else
            stacked_data[-1][stacked_name[-1]][value] = [target_data[value].dup]
          end  
          target_data = stacked_data.pop
          target_name = stacked_name.pop
        end
      else
        target_data[target_name][key] = [value, param_hash]
      end
    end

    cal_data = target_data
  end
  private :rfc2445parse

  def connect_content_line(content)
    connected_content = []

    content.each do |line|  #double quoteが複数にまたがったら嫌なので、先につなげておく
      line.chomp!
      if /^(\s)(.*)$/ =~ line
        connected_content[-1] +=  $2 # spaceは、一文字だけ取り除くのでstripは不可
      else
        connected_content.push(line)
      end
    end
    connected_content
  end
  private :connect_content_line

  def split_content_line(line)
    key = ""
    value = ""
    param_list = Array.new()
    param_hash = Hash.new()
    in_quote = false
    key_gotten = false
    setting_param = false

    line.split(//).each do |c|
      if key_gotten
        value += c
      elsif in_quote
        if c == '"'
          in_quote = false
        end
      else
        if c == ":"
          key_gotten = true
          next
        elsif c == '"'
          in_quote = true
        elsif c == ','
          param_list.push("")
        elsif c == ';'
          setting_param = true
          param_list.push("")
        else
          if setting_param
            param_list[-1] += c
          else  
            key += c
          end
        end
      end
    end
    param_list.each do |a_param|
      pkey, pvalue = a_param.split("=",2)
      param_hash[pkey] = pvalue
    end
    param_hash = nil if param_hash.size == 0
    [key, value, param_hash]
  end
  private :split_content_line
  def initialize()
    if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE")
      Apache.request.setup_cgi_env
    end
    extend QueryExtension
    @multipart = false
    cal_initialize_query()  # set @params, @cookies
  end
end

calendar.cgi

#!/usr/bin/ruby
# $Id$

require "calcgi"

Ok_result_header = {"nph" => false,
      "status" => "OK",
      "type" => "text/calendar",
      "charset" => "UTF-8"}
Not_found_result_header = {"nph" => false,
      "status" => "NOT_FOUND",
      "type" => "text/calendar",
      "charset" => "UTF-8"}
Base_dir = "/var/db/apache2/calendar"

def result_put(cgi) 
  file = File.open(Base_dir + ENV["PATH_INFO"], "w")
  file.puts(cgi.put_content)
  file.close
  cgi.out(Ok_result_header){""}
end
def result_get(cgi) 
  if File.exist?(Base_dir + ENV["PATH_INFO"])
    file = File.open(Base_dir + ENV["PATH_INFO"], "r")
    cgi.out(Ok_result_header) do
      file.gets(nil)
    end
    file.close
  else
    cgi.out(Not_found_result_header) {""}
  end
end
def result_delete(cgi) 
  if File.exist?(Base_dir + ENV["PATH_INFO"])
    File.delete(Base_dir + ENV["PATH_INFO"])
    cgi.out(Ok_result_header) {""}
  else
    cgi.out(Not_found_result_header) {""}
  end
end

cgi = CalCGI.new()
case cgi.request_method
when "GET"
  result_get(cgi)
when "PUT"
  result_put(cgi)
when "DELETE"
  result_delete(cgi)
end

以下、Leopardでの話なので、別環境の人は適当に読み替えて。
/etc/apache2/other/calendar.confを、以下の内容で作成。

<IfModule actions_module>
Action text/calendar cgi-bin/calendar.cgi
</IfModule>

ディレクトリ、/var/db/apache2/calendar を作成し、ownerをwww:wwwに変更。
/Library/WebServer/CGI-Executables の下に、calcgi.rb, calendar.cgi を置く。これもownerはwww:wwwで。
iCalでのカレンダーの公開では、公開先を「個人用サーバ」にして、ベース URLを http://your.web.server/cgi-bin/calendar.cgi にする。認証については、詳しくないしいい加減な答えになるかもしれないので、別の人にきいてみて。とりあえず、ローカルで使う限りは何もしなくてもいけるはず。
iCalでのカレンダーの照会は、URLを http://your.web.server/cgi-bin/calendar.cgi/calendar_name.ics にすればいけるはず。

と、こんなかんじで。