changeset 0:8f811f47ac60 draft

RCS-revision 1.1 date: 2003/12/29 16:05:57; author: yuuji; state: Exp; Initial revision =============================================================================
author HIROSE Yuuji <yuuji@gentei.org>
date Mon, 29 Dec 2003 16:05:57 +0859
parents
children 8145b15d3d6f
files after5.rb
diffstat 1 files changed, 2760 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/after5.rb	Mon Dec 29 16:05:57 2003 +0859
@@ -0,0 +1,2760 @@
+#!/usr/local/bin/ruby
+#
+# Associative Scheduling Table - after5
+# (C)2003 by HIROSE Yuuji [yuuji@gentei.org]
+# $Id: after5.rb,v 1.1 2003/12/29 16:05:57 yuuji Exp $
+# Last modified Tue Dec 30 00:46:49 2003 on firestorm
+# See http://www.gentei.org/~yuuji/software/after5/
+# このスクリプトはEUCで保存してください。
+
+require 'kconv'
+
+$charset = 'EUC-JP'
+
+class HTMLout
+  def contenttype(type = "text/html", charset = $charset)
+    sprintf "Content-type: %s; charset=%s\n\n", type, charset
+  end
+  def initialize(title = "Document")
+    @title = title
+    @eltstack = []
+  end
+  def resetstack()
+    @eltstack = []
+  end
+  def head(title = @title, css = "style.css")
+    sprintf <<__EOS__, title, css
+<html>
+<head>
+<title>%s</title>
+<link rel="stylesheet" type="text/css" href="%s">
+</head>
+__EOS__
+  end
+
+  def startelement(elt, attrs = {}, nl = true)
+    attr = ""
+    if attrs.is_a?(Hash)
+      for k in attrs.keys
+	attr += " %s=\"%s\"" % [k, attrs[k]]
+      end
+    end
+    @eltstack.push(elt)
+    sprintf "<%s%s>%s", elt, attr, nl ? "\n" : ""
+  end
+  def endelement(elt = nil, nl = true)
+    if elt
+      x = elt
+      @eltstack.pop
+    else
+      x = @eltstack.pop
+    end
+    sprintf "</%s>%s", x, nl ? "\n" : ""
+  end
+  def element(elt, attrs = nil, nl = nil)
+    attr = ""
+    lf = nl ? "\n" : ""
+    if attrs.is_a?(Hash)
+      for k in attrs.keys
+	attr += " %s=\"%s\"" % [k, attrs[k]]
+      end
+    end
+    body = yield
+    sprintf "<%s%s>%s%s%s</%s>%s", elt, attr, lf, body, lf, elt, lf
+  end
+  def elementln(elt, attr=nil)
+    body = yield
+    element(elt, attr, true){body}
+  end
+  def a(href, anchor = nil, attrs = {})
+    attr = attrs
+    attr['href'] = href
+    element("a", attr){
+      anchor or href
+    }
+  end
+  def p(msg, attrs=nil)
+    element("p", attrs){msg}
+  end
+  def text(name, value='', size=nil, maxlength=nil)
+    sprintf "<input type=\"text\" name=\"%s\" value=\"%s\"%s%s>",
+      name, value,
+      size ? " size=\"%s\""%size.to_s : '',
+      maxlength ? " maxlength=\"%s\""%maxlength.to_s : ''
+  end
+  def hidden(name, value='')
+    sprintf "<input type=\"hidden\" name=\"%s\" value=\"%s\">", name, value
+  end
+  def radio(name, value, text='', checked=nil)
+    sprintf "<input type=\"radio\" name=\"%s\" value=\"%s\"%s>%s",
+      name, value, checked ? " checked" : "", text
+  end
+  def checkbox(name, value, text='', checked=nil)
+    sprintf "<input type=\"checkbox\" name=\"%s\" value=\"%s\"%s>%s",
+      name, value, checked ? " checked" : "", text
+  end
+  def submit(name, value, text='')
+    sprintf "<input type=\"submit\" name=\"%s\" value=\"%s\">%s\n",
+      name, value, text
+  end
+  def reset(name, value, text='')
+    sprintf "<input type=\"reset\" name=\"%s\" value=\"%s\">\n",
+      name, value, text
+  end
+  def submit_reset(name)
+    submit(name, "GO")+reset(name, "Reset")
+  end
+
+  def select_integer(name, b, e, selected=nil)
+    start = (b<e ? b : e)
+    last  = (b>e ? b : e)
+    "<select name=\"#{name}\">\n" + \
+    (start..last).collect{|i|
+      sprintf "<option%s>%d%s",
+	(selected.to_s==i.to_s) ? " selected" : "", 
+	i,
+	i%6==0 ? "\n" : ''
+    }.join + \
+    "\n</select>\n"
+  end
+end
+
+class PasswdMgr
+  def initialize(name, mode=0640)
+    require 'dbm'
+    @pdb = DBM.open(name, mode)
+  end
+  def checkpasswd(user, passwd)
+    if @pdb[user] then
+      @pdb[user] == passwd.crypt(@pdb[user])
+    end
+  end
+  def setpasswd(user, passwd)
+    salt = [rand(64),rand(64)].pack("C*").tr("\x00-\x3f","A-Za-z0-9./")
+    @pdb[user] = passwd.crypt(salt)
+  end
+  def userexist?(user)
+    @pdb[user] ? true : false
+  end
+  def getpasswd(user)
+    @pdb[user]
+  end
+  def delete(user)
+    @pdb.delete(user)
+  end
+  def close()
+    @pdb.close()
+  end
+  def newpasswd(length)
+    srand(Time.now.to_i)
+    left	= "qazxswedcvfrtgb12345"
+    right	= "yhnmjuik.lop;/67890-"
+    array	= [left, right]
+    (1..length).collect{|i|
+      a = array[i%array.length]
+      a[rand(a.length), 1]
+    }.join('')
+  end
+  def users()
+    @pdb.keys
+  end
+  private :newpasswd
+  def setnewpasswd(user, length=8)
+    newp = newpasswd(length)
+    setpasswd(user, newp)
+    newp
+  end
+end
+  
+class ScheduleDir
+  def initialize(dir = "s")
+    @dir = dir
+    @schedulefile = "sched"
+    @usermapdir = File.join(@dir, "usermap")
+    @usermap = mkusermap()
+    @groupmapdir = File.join(@dir, "groupmap")
+    @groupmap = mkgroupmap()
+    @crondir = File.join(@dir, "crondir")
+    
+  end
+  def mkusermap()
+    map = {}
+    unless test(?d, @usermapdir)
+      mkdir_p(@usermapdir)
+    end
+    Dir.foreach(@usermapdir){|u|
+      next if /^\./ =~ u
+      newu = ''
+      u.split('').each{|c|	# for security wrapping
+	newu << c[0].chr if /[-A-Z_.@]/i =~ c
+      }
+      u = newu
+      map[u] = {}
+      d = File.join(@usermapdir, u)
+      next unless test(?d, d)
+      Dir.foreach(d){|attr|
+	next if /^\./ =~ attr
+	attr.untaint if /^[A-Za-z]+$/ =~ attr
+	file = File.join(@usermapdir, u, attr)
+	next unless test(?s, file) && test(?r, file)
+	map[u][attr] = IO.readlines(file).join().strip
+      }
+    }
+    map
+  end
+  def putuserattr(user, attr, text)
+    # if text==nil, remove it
+    d = File.join(@usermapdir, user)
+    Dir.mkdir(d) unless test(?d, d)
+    file = File.join(d, attr)
+    begin
+      @usermap[user][attr] = text
+      if text==nil
+	File.unlink(file)
+      else
+	open(file, "w"){|w| w.puts @usermap[user][attr]}
+      end
+    rescue
+      return nil
+    end
+    return {attr => text}
+  end
+  def getuserattr(user, attr)
+    # Should we distinguish between attribute is nil and "" ?
+    if @usermap.has_key?(user) && @usermap[user][attr].is_a?(String) &&
+	@usermap[user][attr] > ''
+      return @usermap[user][attr]
+    else
+      return nil
+    end
+  end
+
+  def nickname(user)
+    if @usermap.has_key?(user) && @usermap[user]['name'].is_a?(String) &&
+	@usermap[user]['name'] > ''
+      return @usermap[user]['name']
+    else
+      return user.sub(/@.*/, '')
+    end
+  end
+  def setnickname(user, nickname)
+    putuserattr(user, 'name', nickname)
+  end
+
+  #
+  # make group map
+  def collectmembers(gname)
+    @visitedgroup=[] unless @visitedgroup
+    return [] unless @visitedgroup.grep(gname).empty?
+    @visitedgroup.push(gname)
+    mdir = File.join(@groupmapdir, gname, 'members')
+    return [] unless test(?d, mdir)
+    members = []
+    Dir.foreach(mdir){|item|
+      next if /^\./ =~ item
+      item.untaint
+      next unless test(?f, File.join(mdir, item))
+      if /.+@.+/ =~ item
+	members << item
+      else
+	members += collectmembers(item)
+      end
+    }
+    @visitedgroup.pop
+    members
+  end
+  def mkgroupmap()
+    map = {}
+    return map unless test(?d, @groupmapdir)
+    @visitedgroup = []
+    Dir.foreach(@groupmapdir){|g|
+      next if /^\./ =~ g
+      newg = ''
+      next unless /^[-a-z0-9_.]+$/i =~ g
+      #g.untaint ## untaintじゃだめだ。map{g} のkeyがtaintedになっちゃうよ
+      gg = ''			# for security wrapping
+      g.split('').each{|c| gg << c[0].chr if c != '`'}
+      g = gg
+      map[gg] = {}
+      d = File.join(@groupmapdir, g)
+      next unless test(?d, d)
+      # get group name
+      gnf = File.join(d, 'name')
+      if test(?r, gnf) && test(?s, gnf)
+	n = IO.readlines(gnf)[0].to_s.strip
+	map[g]['name'] = if n > '' then n else g end
+      else
+	map[g]['name'] = g
+      end
+      # get administrators
+      #
+      gad = File.join(d, 'admin')
+      map[g]['admin'] = []
+      if test(?d, gad)
+	Dir.foreach(gad){|a|
+	  # administrator should be a person (not group)
+	  next unless /@/ =~ a
+	  map[g]['admin'] << a
+	}
+      end
+      # collect members
+      #map[g]['members'] = collectmembers(g)
+      memd = File.join(d, 'members')
+      map[g]['members'] = []
+      if test(?d, memd)
+	Dir.foreach(memd){|a|
+	  next if /^\./ =~ a
+	  map[g]['members'] << a
+	}
+      end
+    }
+    map
+  end
+  def groupmap()
+    @groupmap
+  end
+  def groups()
+    @groupmap.keys
+  end
+  def addgroup(grp, users, remove=nil, role='members')
+    return nil unless @groupmap[grp]
+    for u in users
+      next unless account_exists(u)
+      mdir = File.join(@groupmapdir, grp, role)
+      file = File.join(mdir, u)
+      if remove
+	@groupmap[grp][role].delete(u)
+	File.unlink(file) if test(?e, file)
+      else
+	@groupmap[grp][role] << u
+	@groupmap[grp][role].uniq
+	Dir.mkdir(file) unless test(?d, mdir)
+	open(file, "w"){|x|}	# Touch it
+      end
+    end
+    grp
+  end
+  def setgroupname(grp, name)
+    return nil unless @groupmap[grp]
+    mdir = File.join(@groupmapdir, grp)
+    nfile = File.join(mdir, 'name')
+    @groupmap[grp]['name'] = name
+    if grp == name
+      # remove the name file because it is default name
+      File.unlink(nfile) if test(?e, nfile)
+    else
+      Dir.mkdir(mdir) unless test(?d, mdir)
+      open(nfile, "w"){|n| n.puts name.to_s.strip}
+    end
+    name
+  end
+  def creategroup(grp, grpname="", admin=[])
+    return nil unless /^[-A-Z0-9._:!$%,]+$/i =~ grp
+    grp.untaint
+    gdir = File.join(@groupmapdir, grp)
+    mkdir_p(gdir)		# Should not care errors here
+    Dir.mkdir(File.join(gdir, "admin"))
+    Dir.mkdir(File.join(gdir, "members"))
+    @groupmap[grp] = {}
+    if grpname == ''
+      @groupmap[grp]['name'] = grp
+    else
+      setgroupname(grp, grpname)
+    end
+    @groupmap[grp]['members'] = []
+    @groupmap[grp]['admin'] = []
+    addgroup(grp, admin)
+    addgroup(grp, admin, nil, 'admin')
+    return @groupmap[grp]
+  end
+  def createuser(user, email = nil)
+    return nil unless /@/ =~ user
+    return nil if %r@[\/()\;|,$\%^!\#&\'\"]@ =~ user
+    email = email || user
+    @usermap[user] = {}
+    Dir.mkdir(File.join(@usermapdir, user))
+    putuserattr(user, 'email', email)
+  end
+  def deleteuser(user)
+    return nil unless @usermap[user]
+    begin
+      @usermap[user]		# return value
+    ensure
+      @usermap.delete(user)
+      rm_rf(File.join(@usermapdir, user))
+      rm_rf(File.join(@groupmapdir, "*/members/#{user}"))
+      rm_rf(File.join(@crondir, "[1-9]*-*-*/#{user}"))
+      rm_rf(File.join(@dir, "[1-9]*/[0-9][0-9]/[0-9][0-9]/[0-9]???/#{user}"))
+    end
+  end
+  def destroygroup(grp)
+    return nil unless @groupmap[grp]
+    begin
+      @groupmap[grp]		# return value
+    ensure
+      @groupmap.delete(grp)
+      rm_rf(File.join(@groupmapdir, grp))
+      rm_rf(File.join(@groupmapdir, "*/members/#{grp}"))
+      rm_rf(File.join(@crondir, "[1-9]*-*-*/#{grp}"))
+      rm_rf(File.join(@dir, "[1-9]*/[0-9][0-9]/[0-9][0-9]/[0-9]???/#{grp}"))
+    end
+  end
+  def rm_rf(path)
+    if (list = Dir.glob(path))[0]
+      for p in list
+	system "/bin/rm -rf \"#{p}\""
+      end
+      cleanup_files(list[0])
+    end
+  end
+  def account_exists(instance)
+    if /@/ =~ instance
+      true
+    else
+      ! @groupmap.select{|k, v| k==instance}.empty?
+    end
+  end
+  def ismember(user, grouporuser)
+    return true if user==grouporuser
+    if @groupmap[grouporuser]
+      @groupmap[grouporuser]['members'].grep(user)[0]
+    end
+  end
+  def isgroup(grp)
+    @groupmap[grp]
+  end
+  def isadmin(user, group)
+    @groupmap[group] and @groupmap[group]['admin'].grep(user)[0]
+  end
+  def members(grp)
+    @groupmap[grp] and ####################@groupmap[grp]['members']
+      collectmembers(grp)
+  end
+  def admins(grp)
+    @groupmap[grp] and @groupmap[grp]['admin']
+  end
+  def groupname(grp)
+    @groupmap[grp] && @groupmap[grp]['name']
+  end
+  def day_all(d, user=nil)
+    y, m, d = d.scan(%r,(\d\d\d\d+)/(\d+)/(\d+),)[0]
+    #daydir = File.join(@dir, "%04d"%y.to_i, "%02d"%m.to_i, "%02d"%d.to_i)
+    daydir = File.join("s", "%04d"%y.to_i, "%02d"%m.to_i, "%02d"%d.to_i)
+    sched = {}
+    return sched unless test(?d, daydir)
+    Dir.foreach(daydir) {|time|
+      next if /^\./ =~ time
+      next unless /^\d\d\d\d$/ =~ time
+      time.untaint
+      t = File.join(daydir, time)
+      next unless test(?d, t)
+      sched[time] = {}
+      Dir.foreach(t){|who|
+	next if /^\./ =~ who
+
+	visible = false
+	#next unless /@/ =~ who	# user must be as user@do.ma.in
+	next unless account_exists(who)
+	who.untaint
+	dir = File.join(t, who)
+	next unless test(?d, dir) && test(?x, dir)
+	pub = File.join(dir, 'pub')
+	if test(?f, pub) && test(?r, pub) && test(?s, pub)
+	  if IO.readlines(pub)[0].to_i > 0
+	    visible = true
+	  end
+	end
+
+      
+	if ismember(user, who) || visible
+	  sched[time][who] = {}
+	  file = File.join(dir, @schedulefile)
+	  if test(?s, file) && test(?r, file) && test(?s, file)
+	    sched[time][who]['sched'] = IO.readlines(file).join().chomp!
+	  end
+	  sched[time][who]['pub'] = visible
+	end
+      } #|who|
+      sched.delete(time) if sched[time].empty?
+    }
+    sched
+  end
+
+  def scheduledir(user, y, m, d, time)
+    sprintf("%s/%04d/%02d/%02d/%04d/%s",
+	    @dir, y.to_i, m.to_i, d.to_i, time.to_i, user)
+  end
+  def schedulefile(user, y, m, d, time)
+    File.join(scheduledir(user, y, m, d, time), @schedulefile)
+  end
+  def mkdir_p(path, mode=0777)
+    # Do not mkdir `path' for
+    #	absolute paths
+    #   those paths which contains `../'
+    # for the sake of security reason
+    return false if %r,\.\./|^/, =~ path
+    p = 0
+    i=0
+    while p=path.index("/", p)
+      dir = path[0..p].chop
+      p += 1
+      break if i > 10	# overprotecting
+      next if test(?d, dir)
+      Dir.mkdir(dir, mode)
+      i += 1
+    end
+    Dir.mkdir(path, mode) unless test(?d, path)
+  end
+
+  #
+  # register schedule for user
+  #
+  def register(user, year, month, day, time, text, replace=nil)
+    # return code: 0 = succesfull new registration
+    #              1 = succesfull appending registration
+    dir = scheduledir(user, year, month, day, time)
+    file = schedulefile(user, year, month, day, time)
+    ret = 0
+    um = File.umask(027)
+    begin
+      if !replace && test(?s, file)
+	ret = 1
+      else
+	mkdir_p(dir, 0777)
+      end
+    ensure
+      File.umask(um)
+    end
+    open(file, replace ? "w" : "a"){|out|out.print text}
+    return ret
+  end
+  def getschedule(user, year, month, day, time)
+    file = schedulefile(user, year, month, day, time)
+    if test(?r, file) && test(?s, file)
+      return IO.readlines(file).join
+    end
+    return nil
+  end
+  def remove(user, year, month, day, time)
+    file = schedulefile(user, year, month, day, time)
+    dir = File.dirname(file)
+    if test(?r, file) && test(?s, file)
+      File.unlink(file)
+    end
+    for f in Dir.glob(File.join(dir, "*"))
+      f.untaint
+      File.unlink(f)
+    end
+    Dir.rmdir(dir) if test(?d, dir)
+    begin
+      Dir.rmdir(File.dirname(dir))
+    rescue
+    end
+  end
+  #
+  # register file
+  #
+  def putfile(user, year, month, day, time, file, contents)
+    scback = @schedulefile
+    begin
+      @schedulefile = File.basename(file)
+      register(user, year, month, day, time, contents, true)
+    ensure
+      @schedulefile = scback
+    end
+  end
+  def getfile(user, year, month, day, time, file)
+    scback = @schedulefile
+    begin
+      @schedulefile = File.basename(file)
+      getschedule(user, year, month, day, time)
+    ensure
+      @schedulefile = scback
+    end
+  end
+  def removefile(user, year, month, day, time, file)
+    dir = scheduledir(user, year, month, day, time)
+    file = File.join(dir, file)
+    if test(?e, file)
+      File.unlink(file)
+    end
+  end
+  #
+  # registration to crondir
+  #
+  def cronlink_file(nt_time, user, y, m, d, time)
+    subdir = nt_time.strftime("%Y-%m-%d-%H%M/#{user}")
+    cdir = File.join(@crondir, subdir)
+    File.join(cdir, sprintf("%04d-%02d-%02d-%04d", y, m, d, time))
+  end
+  def register_crondir(nt_time, user, y, m, d, time)
+    linkfile = cronlink_file(nt_time, user, y, m, d, time)
+    mkdir_p(File.dirname(linkfile))
+    scfile = schedulefile(user, y, m, d, time)
+    if test(?s, scfile)
+      sclink = File.join("../../..", scfile.sub!(Regexp.quote(@dir+'/'), ''))
+      File.symlink(sclink, linkfile) unless test(?e, linkfile)
+      return linkfile
+    end
+    return false
+  end
+  def remove_crondir(nt_time, user, y, m, d, time)
+    linkfile = cronlink_file(nt_time, user, y, m, d, time)
+    scfile = schedulefile(user, y, m, d, time)
+    if test(?e, linkfile)
+      File.unlink(linkfile)
+      begin
+	dir = linkfile
+	2.times {|x|
+	  dir = File.dirname(dir)
+	  if Dir.open(dir).collect.length <= 2  # is empty dir
+	    Dir.rmdir(dir)
+	  else
+	    break
+	  end
+	}
+      rescue
+      end
+      return linkfile
+    end
+    return false
+  end
+
+  #
+  # return the Hash of crondir {user => files}
+  def notify_list(asof)
+    slack = 5*60
+    gomifiles = []
+    ntl = {}
+    return ntl unless test(?d, @crondir)
+    Dir.foreach(@crondir){|datedir|
+      dd = File.join(@crondir, datedir)
+      next unless test(?d, dd)
+      next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ dd
+      y, m, d, hm = $1.to_i, $2.to_i, $3.to_i, $4.to_i
+      hh = hm/100 % 60
+      mm = (hm%100) % 60
+      t = Time.mktime(y, m, d, hh, mm)
+      next if t-slack > asof
+      #
+      # collect them
+      Dir.foreach(dd){|user|
+	next unless /@/ =~ user || isgroup(user)
+	ud = File.join(dd, user)
+	next unless test(?d, ud)
+	ntl[user] = {}
+	Dir.foreach(ud){|date|
+	  next if /^\./ =~ date
+	  unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ date
+	    gomifiles << File.join(ud, date)
+	    next
+	  end
+	  f = File.join(ud, date)
+	  if test(?s, f)
+	    ntl[user][date] = {}
+	    ntl[user][date]['file'] = f
+	    ntl[user][date]['text'] = IO.readlines(f)
+	  else
+	    File.unlink(f)	# symlink points to nonexistent file
+	  end
+	}
+	if ntl[user].empty?
+	  # if ud does not contain valid cron symlinks,
+	  # ud had been left badly.  Remove it.
+	  ntl.delete(user) 
+	  cleanup_files(gomifiles)
+	end
+      }
+    }
+    ntl
+  end
+  # 
+  # cleanup file and directories
+  def cleanup_crondir(time)
+    Dir.foreach(@crnondir){|datedir|
+      dd = File.join(@crondir, datedir)
+      next unless test(?d, dd)
+      next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ dd
+      y, m, d, hm = $1.to_i, $2.to_i, $3.to_i, $4.to_i
+      hh = hm/100 % 60
+      mm = (hm%100) % 60
+      t = Time.mktime(y, m, d, hh, mm)
+      if t < time
+	system "rm -rf #{dd}"
+      end
+    }
+  end
+  #
+  # remove files in FILES, and remove parent directory if possible
+  def cleanup_files(files)
+    sentinel = File.stat(@dir).ino
+    for f in files
+      File.unlink(f) if test(?e, f)
+      d = f
+      loop {
+	d = File.dirname(d)
+	break if d.length < 2
+	break if File.stat(d).ino == sentinel
+	begin
+	  puts "rmdir #{d}" if $DEBUG
+	  Dir.rmdir(d)
+	rescue
+	  break
+	end
+      }
+    end
+  end
+end
+
+class StringIO<IO
+  def initialize()
+    @str=""
+  end
+  def foo=(str)
+    @str = str
+  end
+  def print(str)
+    @str << str
+  end
+  def puts(str)
+    @str << str+"\n"
+  end
+  def printf(*args)
+    @str << sprintf(*args)
+  end
+  def write(bytes)
+    print(bytes)
+  end
+  def gets()
+    return nil if @str == ''
+    p = @str.index(?\n)
+    if p
+      r = @str[0..p]
+      @str=@str[p+1..-1]
+    else
+      r = @str
+    end
+    return r
+  end
+  def readline()
+    this.gets()
+  end
+  def readlines()
+    r = @str
+    @str=''
+    r
+  end
+    
+  def p(*obj)
+    STDOUT.p(*obj)
+  end
+end
+
+class CMDTimeout < Exception
+  def initialize()
+    @pw = IO.pipe
+    @pr = IO.pipe
+    @pe = IO.pipe
+    @timeout = false
+  end
+  def start(cmd, timeout, mixstderr=false)
+    if @pid=fork
+      @pw[0].close
+      @pr[1].close
+      @pe[1].close
+      # puts "parent!"
+      if @tk=fork
+	# main
+      else
+	@pw[1].close
+	@pr[0].close
+	@pe[0].close
+	trap(:INT){exit 0}
+	sleep timeout
+	begin
+	  @timeout = true
+	  STDERR.puts "TIMEOUT"
+	  Process.kill :INT, @pid
+	rescue
+	  #puts "Already done"
+	end
+	exit 0
+      end
+    else
+      # Running this block with pid=@pid
+      trap(:INT){@timeout = true; exit 0}
+      @pw[1].close
+      STDIN.reopen(@pw[0])
+      @pw[0].close
+
+      @pr[0].close
+      STDOUT.reopen(@pr[1])
+      if mixstderr
+	STDERR.reopen(@pr[1])
+      else
+	STDERR.reopen(@pe[1])
+      end
+      @pr[1].close
+      @pe[0].close
+      @pe[1].close
+
+      exec *cmd
+      exit 0
+    end
+    return [@pw[1], @pr[0], @pe[0]]
+  end
+  def wait()
+    Process.waitpid(@pid, nil)
+  end
+  def close()
+    @pr.each{|p| p.close unless p.closed?}
+    @pw.each{|p| p.close unless p.closed?}
+    @pe.each{|p| p.close unless p.closed?}
+    begin
+      Process.kill :INT, @tk
+    rescue
+    end
+  end
+  def timeout()
+    @timeout
+  end
+end
+
+class Holiday
+  def initialize(dir = ".")
+    @@dir = dir
+    defined?(@@holiday) || setupHoliday
+  end
+  def setupHoliday(file = "holiday")
+    @@holiday = {}
+    return unless test(?f, file) && test(?s, file)
+    IO.foreach(file){|line|
+      line.strip
+      next if /^#/ =~ line
+      date, what = line.scan(/(\S+)\s+(.*)/)[0]
+      if %r,(\d+)/(\d+)/(\d+), =~ date
+	cdate = sprintf("%d/%d/%d", $1.to_i, $2.to_i, $3.to_i)
+	@@holiday[cdate] || @@holiday[cdate] = []
+	@@holiday[cdate] << what
+      elsif %r,(\d+)/(\d+), =~ date
+	cdate = sprintf("%d/%d", $1.to_i, $2.to_i)
+	@@holiday[cdate] || @@holiday[cdate] = []
+	@@holiday[cdate] << what
+      elsif %r,(\d+)/(\w+), =~ date
+	cdate = sprintf("%d/%s", $1.to_i, $2.downcase)
+	@@holiday[cdate] || @@holiday[cdate] = []
+	@@holiday[cdate] << what
+      end
+    }
+  end
+  def isHoliday(y, m, d, wday=nil)
+    y, m, d = y.to_i, m.to_i, d.to_i
+    wname = %w[sun mon tue wed thu fri sat]
+    holiday = @@holiday[sprintf("%d/%d/%d", y, m, d)] ||
+      @@holiday[sprintf("%d/%d", m, d)]
+    unless holiday
+      wday = wname[wday || Time.mktime(y, m, d).wday]
+      nthweek = (d-1)/7+1
+      holiday = @@holiday[sprintf("%d/w%d%s", m, nthweek, wday)]
+    end
+    holiday
+  end
+  def holidays()
+    @@holiday
+  end
+end
+
+class After5
+  def initialize()
+    @me = File.expand_path($0)
+    @mydir, @myname = File.dirname(@me), File.basename(@me)
+    @mydir.untaint
+    Dir.chdir @mydir
+    # @mybase = @myname.sub(/\.\w+$/, '')
+    @mybase = "after5" ########################################### secure?
+    @myname='a5.cgi' # if test(?f, File.join(@mydir, "a5.cgi"))
+    @conf = nil
+    @schedulearea = {'rows'=>'4', 'cols'=>'60', 'name'=>'schedule'}
+    @oldagent = (%r,Mozilla/4, =~ ENV['HTTP_USER_AGENT'])
+    @opt = {
+      'conf'		=> @mybase+".cf",
+      'css'		=> @mybase+".css",
+      'logfile'		=> @mybase+".log",
+      "sendmail"	=> "/usr/sbin/sendmail",
+      'hostcmd'		=> '/usr/bin/host',
+      'nslookup'	=> '/usr/sbin/nsookup',
+      'bg'		=> 'ivory',
+      'at_bsd'		=> '%H:%M %b %d %Y',
+      'at_solaris'	=> '%H:%M %b %d,%Y',
+      'schedir'		=> 's',
+      'tdskip'		=> '<br>',
+      'forgot'		=> 'wasureta',
+      'size'		=> @oldagent ? '15' : '40',
+      'morning'		=> '6',
+      'night'		=> '22',
+      'alldaydir'	=> '3000',
+      'pswdlen'		=> 4,
+      'pswddb'		=> 'a5pswd',
+    }
+    @ntlist = [
+      ['nt10m', "10"+msg('minutes', 'before')],
+      ['nt30m',	"30"+msg('minutes', 'before')],
+      ['nt60m',	"60"+msg('minutes', 'before')],
+      ['nttoday', msg('theday')],
+      ['nt1d',	"1"+msg('days', 'before')],
+      ['nt2d',	"2"+msg('days', 'before')],
+      ['nt3d',	"3"+msg('days', 'before')],
+      ['nt7d',	"7"+msg('days', 'before')],
+      ['nt30d',	"30"+msg('days', 'before')],
+    ]
+    ##@job = "today"
+    @job = "login"
+    @sc = ScheduleDir.new
+    @O = StringIO.new
+    @H = HTMLout.new()
+    @umback = File.umask
+    @author = 'yuuji@gentei.org'
+    @after5url = 'http://www.gentei.org/~yuuji/software/after5/'
+    File.umask(007)
+  end
+  def doit()
+    @params = getarg()
+    @cookie = getcookie()
+    p @cookie if $DEBUG
+    p @params if $DEBUG
+
+    @O.print @H.contenttype()
+    @O.print @H.head("After 5", @opt['css'])
+    @O.print @H.startelement("body", true)
+
+    ######### @O.puts @H.p(@cookie.inspect)  #cookie check!
+
+    ## x = {"align"=>'center'}
+    ## @H.element("p", x, "hoge", nil)
+    ## @H.element("p", nil, "buha", nil)
+
+    if !@params['passwd'] && @cookie['passwd']
+      @params['passwd'] = @cookie['passwd']
+    end
+    if !@params['user'] && @cookie['user']
+      @params['user'] = @cookie['user']
+    end
+    @params['user'] = safecopy(@params['user'])
+    eval @job
+    # @job.call
+    @O.print @H.endelement(nil, true) # body
+    @O.print "</html>\n"	# html
+    setcookie()
+
+    print @O.readlines
+  end
+  def msg(*keyword)
+    unless defined?(@msg)
+      @msg = {
+	'title'		=> ['みんなの予定表 <img src="after5.png" alt="「アフター5」" width="107" height="53">', 'Schedule table for us all <img src="after5.png" alt="After 5" width="107" height="53">'],
+	'login'	=> ['ログイン', 'Login'],
+	'loginfirst'	=> ['最初にログインすべし', 'Login first'],
+	'autherror'	=> ['認証エラーがあったと管理者に伝えてくれっす',
+	  'Unexpected authentication error. Please tell this to the administrator'],
+	'yourmail'	=> ['あなたのメイルアドレス', 'Your email address'],
+	'passwd'	=> ['パスワード<br>(初回時は空欄)',
+	  'Passowrd<br>Left blank, first time'],
+	'error'		=> ['エラー:', 'Error: '],
+	'mailerror'	=> ['メイルアドレスが違います', 'Invalid email address'],
+	'pswderror'	=> ['パスワードが違います', 'Password incorrect'],
+	'fmtdaysschedule'=> ['%sの予定', 'Schedule on %s'],
+	'noplan'	=> ['登録されている予定はありません', 'No plans'],
+	'allday'	=> ['全日', 'whole day'],
+	'addsched'	=> ['新規予定項目の登録', 'Register new schedule'],
+	'defthisday'	=> ['デフォルトの日付はこの日になってま', ''],
+	'24hour'	=> ['24時間制4桁でね<br>(0000〜2359)<br>%sは時刻指定なし', 'in 24-hour<br>(0000-2359)<br>%s for whole day'],
+	'reqnotify'	=> ['通知メイルいるけ?', 'Previous notification'],
+	'rightnow'	=> ['登録時にすぐ', 'Right now on registration'],
+	'immediatenote'	=> ['に以下の予定を登録しました',
+	  ", Your schedule has been registered as follows;"],
+	'registerer_is'	=> ['登録名義: ', 'Register as '],
+	'registerer'	=> ['登録者: ', 'registerer: '],
+	'about'		=> ['約', 'about'],
+	'minutes'	=> ['分', 'minutes'],
+	'hours'		=> ['時間', 'hour(s)'],
+	'days'		=> ['日', 'day(s)'],
+	'before'	=> ['前', 'before'],
+	'theday'	=> ['当日朝', "the day's morning"],
+	'night'		=> ['(夜)', '(night)'],
+	'publicok'	=> ['メンバーに<br>見せてもええね?',
+	  'visible from other members?'],
+	'public'	=> ['公', 'pub'],
+	'nonpublic'	=> ['非', 'sec'],
+	'yes'		=> ['はいな', 'yes'],
+	'no'		=> ['やだ', 'nope'],
+	'invaliddate'	=> ['日付指定が変みたい', 'Invalid time string'],
+	'past'		=> ['それはもう過去の話ね', 'It had Pasted'],
+	'putsomething'	=> ['何か書こうや', 'Write some message please'],
+	'appended'	=> ['既存の予定に追加しました', 'Appended'],
+	'append'	=> ['追加', 'append'],
+	'join'		=> ['参加', 'join'],
+	'regist'	=> ['登録', 'register'],
+	'remove'	=> ['削除', 'remove'],
+	'deletion'	=> ['完全消去', 'deletion'],
+	'deletionwarn'	=> ['OK押したら即消去。確認とらないぞ',
+	  'Hitting OK immediately delets this group, be carefully!'],
+	'deluser'	=> ['%s ユーザ消してええかの?', "Delete the user `%s'"],
+	'delgroup'	=> ['%s グループ消してええかの?', "Delete the group `%s'"],
+	'really?'	=> ['ほんまにええけ?', 'Really?'],
+	'chicken'	=> ['ふっ、腰抜けめ', 'Hey, chicken boy'],
+	'modify'	=> ['修正', 'modify'],
+	'done'		=> ['完了', 'done'],
+	'success'	=> ['成功', 'success'],
+	'failure'	=> ['失敗', 'failure'],
+	'tomonthlist'	=> ['%s の一覧', 'all %s table'],
+	'notifysubj'	=> @mybase+"'s reminder for your plan",
+	'introduce'	=> ['はいこんにちは、'+@mybase+'ですよ〜。',
+	  "Hi, this is #{@mybase}'s notification."],
+	'notifymail'	=> ['こんな予定がありまっせ。',
+	  "You have some eschedule below;"],
+	'notification'	=> ['の通知', 'notification'],
+	'newaccount'	=> ["新しいアカウントを作りました。\n"+
+      			   "パスワードは %s さん宛に送信しておきました。\n",
+	  "You got new account for #{@mybase}\n" +
+	  "Password was sent to %s.\nThank you.\n"],
+	'accessfrom'	=> ["%s からのアクセスによる送信\n",
+	  "This mail was sent by the access from %s\n"],
+	'newpassword'	=> ["%s さんのパスワードは %s です。\n",
+	  "The password of %s is %s\n"],
+	'mischief'	=> ["身に覚えのない場合はいたずらです。どうしましょ。",
+	  'If you have no idea for getting this message, '+
+	  'it is mischief by someone else'],
+	'user'		=> ['ユーザ', 'user'],
+	'group'		=> ['グループ', 'group'],
+	'personal'	=> ['個人で', 'personal'],
+	'registas'	=> ['グループ予定として登録', 'Register as group'],
+	'joinquit'	=> ['入退', 'joining/quiting'],
+	'of'		=> ['の', "'s"],
+	'id'		=> ['ID(ローマ字1単語空白なしで)', 'ID(without spaces)'],
+	'name'		=> ['名前', 'name'],
+	'anystring'	=> ['(日本語OK)', '(any length, any characters)'],
+	'setto'		=> ['を設定 → ', 'set to '],
+	'management'	=> ['管理', 'management'],
+	'administrator'	=> ['管理者', 'Administrator'],
+	'newgroup'	=> ['新規グループ作成', 'Create new group'],
+	'adminop'	=> ['管理<br>操作', "Administrative<br>operation"],
+	'member'	=> ['メンバー', 'Member'],
+	'addedtogroup'	=> ['をグループに追加 →', 'added to the group:'],
+	'removedfromgp'	=> ['をグループから削除:', 'removed from the group:'],
+	'soleadmin'	=> ['%s は %s の唯一の管理者なのでやめられないのだ',
+	  "%s is sole administrator of %s.  Cannot retire."],
+	'recursewarn'	=> ['個人では加入してないが、別の加入グループがこのグループに入っているので実質参加していることになっている。',
+	  'Though this member does not join to this group, it is assumed to be joining this group because other group where one joins is joined to this group.'],
+	'regaddress'	=> ['登録アカウント名', 'Account id'],
+	'existent'	=> ['既にあるんすよ → ', 'Already exists: '],
+	'mailaddress'	=> ['通知送付先アドレス', 'Notification email address'],
+	'weburl'	=> ['ゲストブックとかURL<br><small>(予定への反応を書いて欲しい場所)</small>', 'Your guest book URL'],
+	'usermodwarn'	=> ['いちいち yes/no とか確認取らないから押したら最後、気いつけて。',
+	  'This is the final decision.  Make sure and sure.'],
+	'joinmyself'	=> ['自分自身が既存のグループに対して入る(IN)か出る(OUT)かを決めるのがここ。自分管理のグループに誰かを足すなら「管理操作」、新たにグループを作るなら',
+	  'In this page, you can decide put yourself IN or OUT of the existing groups.  If you want to manage the member of your own group, go to'],
+	'groupwarn'	=> ['自分が参加してないグループAに、自分が参加しているグループBが含まれている場合、グループAにも加入していると見なされるので気をつけよう。管理者はグループのニックネームを変えられるよ。',
+	  'Though you are not member of group A, you are treated as a member of A, if you join to the group B, which is a member of A.  Think the nesting of groups carefully, please.  Group administrator can change the group nickname.'],
+	'wholemembers'	=> ['グループ内グループを考慮した上で、現在グループ %s への通知は以下のメンバーに送られる。',
+	  "Consiering the groups registered in another group, notification to the group `%s' is send to members as follows."],
+	'noadmingroup'	=> ['管理できるグループはないっす',
+	 "'There's no groups under your administration."],
+	'multiplemail'	=> ['複数の宛先に通知したいなら..',
+	  'Wanna send notify to multiple address...'],
+	'nickname'	=> ['ニックネーム', 'nickname'],
+	'shortnameplz'	=> ['表が崩れるほど長すぎるニックネームは嫌われるよ。短めにね。',
+	  'Because nickname is displayed many times in table, shorter name is prefered.'],
+	'nicknamenote'	=> ['ニックネームを消去するとデフォルト名になりんす.',
+	  'Default name is displayed if you remove nickname.'],
+	'nothingtodo'	=> ['って何もやることあらへんかったで',
+	  'Nothing to do for this transaction.']
+      }
+    end
+    lang=0
+    keyword.collect{|k|
+      if @msg[k].is_a?(Array)
+	@msg[k][lang]
+      elsif @msg[k].is_a?(String)
+	@msg[k]
+      else
+	''
+      end
+    }.join(['', ' '][lang])
+  end
+
+  def setcookie()
+    cookievals = %w[user passwd ^nt]
+    p = {}
+    @params.keys.grep(/^(user$|passwd$|nt)/){|v|
+      p[v] = @params[v].to_s.strip
+    }
+    c = gencookie(p, 3600*6*1)
+    printf "Set-Cookie: %s\n", c if c
+  end
+
+  def encode(string)		# borrowed from cgi.rb
+    string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
+      '%' + $1.unpack('H2' * $1.size).join('%').upcase
+    end.tr(' ', '+')
+  end
+  def decode!(string)
+    string.gsub!(/\+/, ' ')
+    string.gsub!(/%(..)/){[$1.hex].pack("c")}
+  end
+
+  def gencookie(a, expire)
+    x = a.collect{|k, v|
+      sprintf("%s=%s", k, encode(v)) if v
+    }
+    x.delete(nil)
+    return nil if x.empty?
+    str = x.join('&')
+    ex = (Time.new+expire).to_s
+    sprintf "value=%s; expires=%s", encode(str), ex
+  end
+
+  def login()
+    @O.print @H.elementln("h1", nil){msg('title')}
+    @O.print @H.elementln("h2", nil){msg('login')}
+    format = {'method'=>'POST', 'action'=>@myname+"?-today"}
+    @O.print @H.elementln("form", format){
+      @H.elementln("table", nil){
+	@H.elementln("tr", nil){
+	  @H.element("td", nil){msg('yourmail')} + \
+	  @H.element("td", nil){
+	    sprintf '<input type="text" size="%s" name="user">', @opt['size']
+	  }
+	} + \
+	@H.elementln("tr", nil){
+	  @H.element("td", nil){msg('passwd')} + \
+	  @H.element("td", nil){
+	    sprintf '<input type="password" size="%s" name="passwd">', @opt['size']
+	  }
+	}
+      } + '<input type="submit" value="LOGIN">'
+    }
+    @O.print footer2()
+  end
+  def open_pm()
+    begin
+      PasswdMgr.new(@opt['pswddb'])
+    rescue
+      STDERR.printf "Cannot open pswd file [%s]\n", @opt['pswddb']
+      STDERR.printf "euid=#{Process.euid}, uid=#{Process.uid}\n", @opt['pswddb']
+      nil
+    end
+  end
+  def outputError(*msg)
+    @O.print @H.p(msg('error')+sprintf(*msg))
+  end
+  def mailaddress(user)
+    email = @sc.getuserattr(user, "email")
+    email || user
+  end
+  def webpage(user)
+    @sc.getuserattr(user, "webpage")
+  end
+  def checkauth()
+    auth = catch (:auth) {
+      unless @params['user']
+	outputError(@H.a(@myname, msg('loginfirst')))
+	throw :auth, nil 
+      end
+      unless pm=open_pm()
+	outputError(msg('autherror'))
+	throw :auth, nil
+      end	
+      user, passwd = @params['user'], @params['passwd']
+      email = mailaddress(user)
+      if !checkmail(user)
+	outputError(msg('mailerror'))
+	throw :auth, nil
+      end
+      if pm.userexist?(user)
+	if pm.checkpasswd(user, passwd)
+	  throw :auth, true
+	elsif passwd == @opt['forgot']
+	  newp = pm.setnewpasswd(user, @opt['pswdlen'])
+	  sendMail(email, "#{@mybase} password",
+		   "(#{ENV['REMOTE_ADDR']} からのアクセスによる送信)\n" +
+		   "#{@mybase} 用の #{user} さんのパスワードは\n" +
+		   (newp || "未定義") + "\nです。\n")
+	  @O.print @H.p("#{email} 宛に送信しておきました")
+	  throw :auth, nil
+	else
+	  outputError(msg('pswderror'))
+	  throw :auth, nil
+	end
+      else
+	newp = pm.setnewpasswd(user, @opt['pswdlen'])
+	@sc.createuser(user, user)
+	sendMail(email, "#{@mybase} new account",
+		 sprintf(msg('accessfrom'), ENV['REMOTE_ADDR']) +
+		 sprintf(msg('newpassword'), user, newp) +
+		 sprintf(msg('mischief')))
+	@O.print @H.p(sprintf(msg('newaccount'), user))
+	@O.print @H.p(@H.a(@myname, msg('login')))
+	throw :auth, nil
+      end
+    }
+    if auth
+      return true
+    else
+      return false
+    end
+  end
+  def safecopy(string)
+    return nil unless string
+    if $SAFE > 0
+      cpy=''
+      string.split('').each{|c|
+	cpy << c[0].chr if c[0] != ?`  # `
+      }
+      cpy
+    else
+      string
+    end
+  end
+  def checkmail(mail)
+    account, domain = mail.scan(/(.*)@(.*)/)[0]
+    return false unless account != nil && domain != nil
+    return false unless /^[-0-9a-z_.]+$/oi =~ domain
+    domain = safecopy(domain)
+    require 'socket'
+    begin
+      TCPSocket.gethostbyname(domain)
+      return true
+    rescue
+      if test(?x, @opt["hostcmd"])
+	open("| #{@opt['hostcmd']} -t mx #{domain}.", "r") {|ns|
+	  #p ns.readlines.grep(/\d,\s*mail exchanger/)
+	  return ! ns.readlines.grep(/is handled .*(by |=)\d+/).empty?
+	}
+      elsif test(?x, @opt["nslookup"])
+	open("| #{@opt['nslookup']} -type=mx #{domain}.", "r") {|ns|
+	  #p ns.readlines.grep(/\d,\s*mail exchanger/)
+	  return ! ns.readlines.grep(/\d,\s*mail exchanger/).empty?
+	}
+      end
+      return false
+    end
+  end # checkmail
+
+  # Logging
+  #
+  def putLog(msg)
+    msg += "\n" unless /\n/ =~ msg
+    open(@opt["logfile"], "a+") {|lp|
+      lp.print Time.now.to_s + " " + msg
+    }
+  end
+
+  def sendnotify(whom, subj, body)
+    users = users()
+    if grepgroup(whom)
+      recipients = @sc.members(whom)
+    else
+      recipients=[whom]
+    end
+    for u in recipients
+      if users.grep(u)[0]
+	sendMail(mailaddress(u), subj, body)
+      end
+    end
+  end
+
+  def sendMail(to, subject, body)
+    body = Kconv::tojis(body)
+    subject = Kconv.tojis(subject)
+    to = safecopy(to)		# cleanup tainted address
+    if /\e/ =~ subject		# If contains JIS chars...
+      subject = subject.split(//,1).pack('m')
+      subject = "=?iso-2022-jp?B?#{subject}?="
+    end
+    subject.gsub!(/\n/, '')
+    begin
+      if (m=open("|-", "w"))
+	m.print "To: #{to}\n"
+	m.print "Subject: #{subject}\n"
+	m.print "Mime-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Type: Text/Plain; charset=iso-2022-jp
+
+"
+	m.print body, "\n"
+	m.close
+      else
+	# exec(@attr['mail'], "-s", subject, to)
+	exec(@opt['sendmail'], to)
+	exit 0;
+      end
+      putLog("Sent '#{subject}' to #{to}\n")
+      return true
+    rescue
+      putLog("FAILED! - Sent '#{subject}' to #{to}\n")
+      return nil
+    end
+  end # sendMail
+
+  def today()
+    today = Time.now
+    showtable(today)
+  end
+  def isleap?(y)
+    if y%400 == 0
+      true
+    elsif y%100 == 0 || y%4 != 0
+      false
+    else
+      true
+    end
+  end
+  def daysofmonth(year, month)
+    dl = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+    if month != 2 || !isleap?(year)
+      dl[month-1]
+    else
+      29
+    end
+  end
+  #
+  # Return the Time object at the last day of last month
+  def lastmonth(today)
+    Time.at(Time.mktime(today.year, today.month)-3600*24)
+  end
+  #
+  # Return the Time object at the first day of next month
+  def nextmonth(today)
+    y, m = today.year, today.month
+    Time.at(Time.mktime(y, m, daysofmonth(y, m))+3600*24)
+  end
+
+  def month(month)
+    y, m = month.scan(%r,(\d\d\d\d+)/(\d+),)[0]
+    if y && m
+      showtable(Time.mktime(y, m, 1))
+    else
+      outputError "%s %s", msg('invaliddate'), month
+      return nil
+    end
+  end
+  def footer1()
+    "<br>" + \
+    @H.element("p"){
+      me = @myname+"?-"; delim = " / "
+      @H.a(me+'userman', msg('user', 'management')) + delim + \
+      @H.a(me+'groupman', msg('group', 'management'))
+    }
+  end
+
+  def footer2()
+    "<hr>" + \
+    @H.element("code") {
+      "This " + \
+      @H.a(@after5url, "After5") + \
+      " board is maintained by " + \
+      @opt['maintainer'].sub('@', "&#x40;") + "."
+    }
+  end
+  def footer()
+    footer1+footer2
+  end
+  def nickname(userORgroup)
+    if grepgroup(userORgroup)
+      @sc.groupname(userORgroup)
+    else
+      @sc.nickname(userORgroup)
+    end
+  end
+  #
+  # show specified month's calendar
+  def showtable(day)
+    if !checkauth
+      return nil
+    end
+    
+    month = day.month.to_s
+    first = Time.mktime(day.year, day.month, 1)
+    last  = daysofmonth(day.year, day.month)
+    wday1 = first.wday
+    start = 1-wday1
+    wname = %w[sun mon tue wed thu fri sat]
+    today = Time.now
+    todaym = today.month
+    todayd = today.day
+    tdclass = {}
+    tdclass["width"] = "64px" if @oldagent # workaround for NN4
+
+    holiday = Holiday.new
+    # create dayofweek header
+    @O.print @H.elementln("h1", nil){sprintf "%d/%d", day.year, day.month}
+    @O.print @H.startelement("table", {'border'=>"1", 'class'=>'main'})
+
+    # day of week
+    @O.print @H.startelement("tr")
+    for w in wname
+      @O.print @H.element("th", {'class'=>w}){w.capitalize}
+    end
+    @O.print "\n"+@H.endelement(nil, true)
+
+    # create day table
+    column = start
+    ## p day, last
+    while column <= last
+      @O.print @H.elementln("tr", nil){
+	(column..column+6).collect{|d|
+	  todayp = (day.month==todaym && d==todayd)
+	  wd=d-column
+	  hd = holiday.isHoliday(day.year, day.month, d, wd)
+	  tdclass['class'] = (hd ? 'holiday' : wname[wd])
+	  @H.element("td", tdclass){
+	    if d>0 && d <= last
+	      date = "%d/%d/%d"%[day.year, day.month, d]
+	      @H.element("p", {'class'=>todayp ? 'todayline' : 'dayline'}){
+		@H.a(@myname+"?-show+"+date, d.to_s)
+	      } + \
+	      # isHoliday?
+	      if hd
+		@H.element("small"){hd.join("<br>")}
+	      end.to_s + \
+	      @H.element("p", {'class'=>'topic'}){
+		s = @sc.day_all(date, @params['user'])
+		if !s.empty?
+		  s.keys.sort.collect{|time|
+		    s[time].keys.sort.collect{|who|
+		      sprintf "%04s:%s",
+			time == @opt['alldaydir'] ? msg('allday') : time,
+			nickname(who)
+		    }.join
+		  }.join("<br>")
+		else
+		  @opt['tdskip']
+		end
+	      }
+	    else
+	      @opt['tdskip']
+	    end
+	  }
+	}.join
+      }
+      # ここ活かしてない
+      @H.elementln("tr", nil){
+	(column..column+6).collect{|d|
+	  wd=d%7
+	  @H.element("td", {'class'=>wname[wd]}){
+	    @H.element("div", {'class'=>'scline'}){
+	      if d>0 && d <= last
+		s = @sc.day_all("%d/%d/%d"%[day.year, day.month, d])
+		unless s.empty?
+		  s.keys.sort.collect{|time|
+		    s[time].keys.sort.collect{|who|
+		      sprintf "%4s:%s", time, who.sub(/@.*/, '')
+		    }
+		  }.join("<br>")
+		else
+		  @opt['tdskip']
+		end
+	      else 
+		@opt['tdskip']
+	      end
+	    }
+	  }
+	}.join
+      }
+      column += 7
+    end
+
+    # month-link
+    @O.print @H.elementln("tr", {'class'=>'monthlink'}){
+      lm1 = lastmonth(day)
+      lm2 = lastmonth(lm1)
+      lm3 = lastmonth(lm2)
+      nm1 = nextmonth(day)
+      nm2 = nextmonth(nm1)
+      nm3 = nextmonth(nm2)
+      [lm3, lm2, lm1, nil, nm1, nm2, nm3].collect{|t|
+	@H.element("td"){
+	  if t.is_a?(Time)
+	    ym = sprintf("%d/%d", t.year, t.month)
+	    @H.a(sprintf("%s?-month+%s", @myname, ym), ym)
+	  else
+	    sprintf "%d/%d", day.year, day.month
+	  end
+	}
+      }.join("\n")
+    }
+    @O.print "\n"+@H.endelement(nil, true)
+    
+    @O.print "showtable" if @params['user'] == @author
+    @O.print footer
+    ##schedule.day_all("2003/12/22")
+    # @O.print @H.endelement()
+  end
+
+  #
+  # Put carrying values
+  def hiddenvalues()
+    h = %w[user].collect{|v|
+      if @params[v]
+	sprintf "<input type=\"hidden\" name=\"%s\" value=\"%s\">\n",
+	  v, @params[v]
+      end
+    }
+    h.delete(nil)
+    h.join
+  end
+  #
+  # Return the string of table
+  def dayTableString(s, user, date)
+    r = ''
+    r << @H.startelement("table", {'border'=>'1'}, true)
+    r << @H.element("tr", nil){
+      @H.element("th", {'class'=>'time'}){'TIME'} + \
+      @H.element("th", nil){'Schedule'}
+    }
+    for time in s.keys
+      tstr = case time
+	     when @opt['alldaydir']
+	       msg('allday')
+	     else
+	       sprintf "%02d:%02d", time.to_i/100, time.to_i%100
+	     end
+      r << @H.startelement("tr", nil, true)
+      r << @H.element("th", {'class'=>'time'}){tstr}
+      r << @H.element("td"){
+	@H.elementln("table"){
+	  s[time].keys.collect{|who|
+	    editable = (user==who || @sc.ismember(user, who))
+	    groupp   = grepgroup(who)
+	    @H.element("tr"){
+	      @H.element("td", {'class'=>groupp ? 'group' : 'who'}){
+		if !groupp && webpage(who)
+		  @H.a(webpage(who), nickname(who))
+		else
+		  nickname(who)
+		end
+	      } + \
+	      @H.element("td"){
+		if editable
+		  s[time][who]['pub'] ? msg('public') :
+		    msg('nonpublic')
+		else
+		  @opt['tdskip']
+		end
+	      } + \
+	      @H.element("td"){
+		if editable
+		  @H.a(@myname+"?-modify+#{date}/#{time}/#{who}",
+		       msg('modify'))
+		else
+		  @opt['tdskip']
+		end
+	      } + \
+	      @H.element("td"){
+		if editable
+		  @H.a(@myname+"?-remove+#{date}/#{time}/#{who}",
+		       msg('remove'))
+		else
+		  @opt['tdskip']
+		end
+	      } + \
+	      @H.element("td"){s[time][who]['sched']}
+	    }
+	  }.join("\n")
+	}
+      }
+      r << @H.endelement()
+    end
+    r << @H.endelement()
+    r
+  end
+  #
+  # show the schedule list of specified date
+  #
+  def show(date)
+    if !checkauth
+      return nil
+    end
+    border1 = {'border'=>'1'}
+    user = safecopy(@params['user'])
+    s = @sc.day_all(date, user)
+    mygroup = @sc.groups().select{|g|@sc.ismember(user, g)}
+
+    @O.print @H.element("h1", nil){
+      sprintf msg('fmtdaysschedule'), date
+    }
+    if s.empty?
+      @O.print @H.p(msg('noplan'))
+    else
+      @O.print dayTableString(s, user, date)
+    end #is_empty?
+    thisyear, thismonth, thisday = date.scan(%r,(\d\d\d\d+)/(\d+)/(\d+),)[0]
+    mstr = sprintf "%04d/%02d", thisyear.to_i, thismonth.to_i
+    @O.print @H.a(@myname+"?-month+"+mstr,
+		  sprintf(msg('tomonthlist'), mstr))
+    #
+    # Link button to add new plan
+    #now = Time.now+3600*24
+    now = Time.mktime(thisyear, thismonth, thisday.to_i, Time.now.hour)
+    y, m, d, h, min = now.year, now.month, now.day, now.hour, now.min
+    @O.print @H.element('h2', nil, true){msg('addsched')}
+    @O.print @H.element('p', nil){msg('defthisday')}
+    @O.print @H.element("form", {'action'=>@myname+"?-addsched", 'method'=>'POST'}){
+      @H.elementln('table', border1){
+	@H.elementln('tr'){
+	  @H.element('th'){'Name'} + \
+	  @H.element('td'){
+	    hiddenvalues() + @sc.nickname(user)
+	  }
+	} + \
+	@H.elementln('tr'){
+	  @H.element('th'){'Year'} + \
+	  @H.element('td'){@H.select_integer("year", y, y+5, y)}
+	} + \
+	@H.elementln('tr'){
+	  @H.element('th'){'Month'} + \
+	  @H.element('td'){@H.select_integer("month", 1, 12, m)}
+	} + \
+	@H.elementln('tr'){
+	  @H.element('th'){'Day'} + \
+	  @H.element('td'){@H.select_integer("day", 1, 31, d)}
+	} + \
+	@H.elementln('tr'){
+	  @H.element('th'){'Time<br>'+ \
+	    sprintf(msg('24hour'), @opt['alldaydir'])} + \
+	  @H.element('td'){
+	    '<input type=text name="time" value="3000" size=8 maxlength="4">'
+	  }
+	} + \
+	@H.elementln('tr'){
+	  @H.element('th'){msg('publicok')} + \
+	  @H.element('td'){
+	    @H.radio('pub', 'yes', msg('yes')+'<br>', true) + \
+	    @H.radio('pub', 'no', msg('no'))
+	  }
+	}
+	## table
+      } + \
+      @H.elementln("p"){	# put notify mail checkbox
+	msg('reqnotify') + '<br>' + \
+	@ntlist.collect{|n, v|
+	  @H.checkbox(n, 'yes', v, @params[n])
+	}.join("\n") + \
+	" " + @H.checkbox('rightnow', 'yes', msg('rightnow'), true) + \
+	"\n"
+      } + \
+      if mygroup[0]
+	@H.elementln("p"){	# put "register as"
+	  msg('registas') + "<br>\n" + \
+	  mygroup.collect{|g|
+	    @H.radio('registas', g, @sc.groupname(g))
+	  }.join(' ') + "\n/ " + \
+	  @H.radio('registas', 'no', msg('personal'))
+	}
+      end.to_s + "\n" + \
+      @H.element("textarea", @schedulearea){} + "<br>\n" + \
+      @H.submit_reset("GO")
+    } #form
+    @O.print "show" if user == @author
+  end
+
+  #
+  # call process
+  def call_process(cmd, input=nil, timeout=10)
+    prc = CMDTimeout.new
+    fds = prc.start(cmd, timeout, true)
+    if input
+      Thread.start {
+	fds[0].sync = true
+	fds[0].print.input
+	fds[0]
+      }
+    end
+    begin
+      fds[1].readlines
+    ensure
+      prc.close()
+    end
+  end
+  #
+  # notification registerer
+  def notify_time(year, month, day, time, symbol)
+    if (t = time.to_i) > 2359
+      hh = mm = 0
+    else
+      hh, mm = t/100, t%100
+    end
+    base = Time.mktime(year.to_i, month.to_i, day.to_i, hh, mm)
+    if /nt(\d+)([mh])$/ =~ symbol
+      return nil if t > 2359
+      num, unit = $1.to_i, $2.downcase
+      rate = {'h'=>3600, 'm'=>60}[unit] || 3600
+      return Time.at(base-rate*num)
+    elsif /nt(\d+)d/ =~ symbol
+      seconds = $1.to_i*3600*24
+      targetday= Time.at(base-seconds).to_a
+      targetnight =
+	Time.mktime(*(targetday.indexes(5,4,3)+[@opt['night'].to_i]))
+    elsif "nttoday" == symbol
+      Time.mktime(year.to_i, month.to_i, day.to_i, @opt['morning'])
+    end
+  end
+  def reg_notify(user, year, month, day, time, text, cancelall = nil)
+    threshold = 5*60		# Omit notifycation within 30min future
+
+    y, m, d, t, = year.to_i, month.to_i, day.to_i, time.to_i
+    if t > 2359
+      hh = mm = 0
+    else
+      hh = t/100
+      mm = t%100
+    end
+    now = Time.now
+    
+    filearg = [user, year, month, day, t]
+    @ntlist.each{|k, v|
+      nt_time = notify_time(year, month, day, t, k)
+      if !nt_time
+	# do nothing for allday schedule's notification before some minutes
+      elsif cancelall || nt_time < now+threshold ||
+	  /yes|on|true|1/ !~ @params[k] || !@params[k]
+	# cancel
+	uf = @sc.remove_crondir(nt_time, user, year, month, day, t)
+	@sc.removefile(*(filearg+[k]))
+      else
+	# register
+	lf = @sc.register_crondir(nt_time, user, year, month, day, t)
+	@sc.putfile(*(filearg+[k, lf]))
+      end
+    }
+  end
+  def cancel_notify(user, year, month, day, time)
+    reg_notify(user, year, month, day, time, 'dummy', true)
+  end
+  #
+  # add or remove a schedule
+  #
+  def add_remove(remove = nil)
+    if !checkauth
+      return nil
+    end
+    user = registerer = @params['user']
+    as = @params['registas']
+    if as && as > '' && /^no$/ !~ as && @sc.ismember(user, as)
+      registerer = as
+    end
+    now = Time.now
+    y, m, d, h, min = now.year, now.month, now.day, now.hour, now.min
+
+    $KCODE='e' if $DEBUG
+    @O.print @params.inspect if $DEBUG
+    #
+    # Check the validity of specified time
+    sy = @params['year'].to_i
+    sm = @params['month'].to_i
+    sd = @params['day'].to_i
+    tm = @params['time'].to_i
+    if tm > 2399
+      timedir=@opt['alldaydir']
+      sh, smin = 23, 59
+      tmstr = msg('allday')
+    else
+      sh = (tm/100).to_i
+      smin = (tm%100).to_i
+      timedir = sprintf("%04d", tm)
+      tmstr = sprintf("%d:%02d", sh, smin)
+    end
+    time = nil
+    begin
+      time = Time.mktime(sy, sm, sd, sh, smin)
+    rescue
+      outputError "%s<br>\nyear=%s<br>month=%s<br>day=%s<br>time=%s\n",
+	msg('invaliddate'),
+	@params['year'], @params['month'], @params['day'], @params['time']
+      return nil
+    end
+    unless @params['schedule'] && @params['schedule'].strip > ''
+      outputError msg('putsomething')
+      return nil
+    end
+
+    # do remove or addition
+    if remove
+      cancel_notify(registerer, sy, sm, sd, timedir)
+      begin
+	@sc.remove(registerer, sy, sm, sd, timedir)
+	#########@O.print @H.p(msg('remove')+msg('done'))
+      rescue
+	outputError("Failed"+$!)
+      end
+    else
+      if time < now
+	outputError(msg('past'))
+	return nil
+      end
+      begin
+	(text = @params['schedule'].strip.gsub(/\r+\n/, $/)) << "\n"
+	replace = (/modify/i =~ @params['editmode'])
+	rc = @sc.register(registerer, sy, sm, sd, timedir, text, replace)
+	if @params['pub'] && /yes/ =~ @params['pub']
+	  @sc.putfile(registerer, sy, sm, sd, timedir, 'pub', "1\n")
+	else
+	  @sc.removefile(registerer, sy, sm, sd, timedir, 'pub')
+	end
+	########  @O.print @H.p(msg('appended')) if rc == 1
+      rescue
+	outputError("Failed"+$!)
+      end
+      text = @sc.getschedule(registerer, sy, sm, sd, timedir)
+      reg_notify(registerer, sy, sm, sd, timedir, text)
+      if @params['rightnow'] && /yes/i =~ @params['rightnow']
+	header = sprintf("%s/%s/%s %s %s\n%s%s%s\n%s\n",
+			 sy, sm, sd, tmstr, msg('immediatenote'),
+			 msg('registerer_is'), nickname(registerer),
+			 if user!=registerer
+			   sprintf(" (%s%s)",
+				   msg('registerer'), nickname(user))
+			 else
+			   ""
+			 end,
+			 "-"*70)
+	sendnotify(registerer, "Registration completed", header+text)
+      end
+    end
+    show(sprintf("%04d/%02d/%02d", sy, sm, sd))
+    @O.print "add_remove" if user == @author
+  end
+
+  # add
+  def addsched()
+    add_remove(/remove/i =~ @params['editmode'])
+  end
+
+  #
+  # Display remove or modify screen
+  def remove_modify(datetime, remove)
+    if !checkauth
+      return nil
+    end
+
+    user = @params['user']
+    y, m, d, time, dummy, as =
+      datetime.scan(%r,(\d\d\d\d+)/(\d+)/(\d+)/(\d+)(/(.+))?,)[0]
+    # datetime always contains trailing slash generated by parsedate
+    # but if the trailing part is a user(not a group), it is removed
+    # because it filtered out by grepgroup() function
+    if ! (y && m && d && time)
+      outputError "Invalid time specification"
+      return nil
+    elsif as && as > ''
+      unless @sc.ismember(user, as)
+	outputError "You have no permission to edit group %s's schedule", as
+	return nil
+      end
+      user = as
+    end
+    unless text=@sc.getschedule(user, y, m, d, time)
+      outputError "%s %s", datetime, msg('noplan')
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      sprintf "%s %s", datetime, remove ? msg('remove') : msg('modify')
+    }
+    @O.print @H.elementln("form", {'action'=>@myname+"?-addsched", 'method'=>'POST'}){
+      pubp=(@sc.getfile(user, y, m, d, time, 'pub').to_i > 0)
+      if as
+	@H.hidden("registas", as)
+      end.to_s + \
+      "<input type=\"hidden\" name=\"year\" value=\"%04d\">\n" % y.to_i + \
+      "<input type=\"hidden\" name=\"month\" value=\"%02d\">\n" % m.to_i + \
+      "<input type=\"hidden\" name=\"day\" value=\"%02d\">\n" % d.to_i + \
+      "<input type=\"hidden\" name=\"time\" value=\"%04d\">\n" % time.to_i + \
+      msg('reqnotify') + "<br>\n" + \
+      @ntlist.collect{|nt, v|
+	cronp = @sc.getfile(user, y, m, d, time, nt)
+	sprintf "<input type=\"checkbox\" name=\"%s\"%s>%s \n",
+	  nt, (cronp ? " checked" : ""), v
+      }.join + "<br>" + \
+      @H.element("textarea", @schedulearea) {text} + "<br>" + \
+      @H.radio("editmode", "append", msg('append')) + ' / ' + \
+      @H.radio("editmode", "modify", msg('modify'), !remove) + ' / ' + \
+      @H.radio("editmode", "remove", msg('remove'), remove) + ' / ' + \
+      "<br>\n" + \
+      msg('publicok') + \
+      @H.radio("pub", "yes", msg('yes'), pubp) + \
+      @H.radio("pub", "no", msg('no'), !pubp) + \
+      '<br>' + \
+      @H.submit_reset("GO")
+    }
+    @O.print "remove_modify" if user == @author
+  end
+  def remove(datetime)
+    remove_modify(datetime, true)
+  end
+  def modify(datetime)
+    remove_modify(datetime, false)
+  end
+
+  def prohibitviahttp()
+    %w[REMOTE_ADDR REMOTE_HOST SERVER_NAME].each{|v|
+      if ENV[v]
+	print "Content-type: text/plain\n\n"
+	print "Do not call this via CGI"
+	exit 0
+      end
+    }
+  end
+  #
+  # notify: call via cron
+  def notify()
+    prohibitviahttp()
+    unless @opt['maintainer']
+      STDERR.printf "Set maintainer(email-address) in %s\n", @opt['conf']
+      STDERR.print "(ex)  maintainer=yuuji@gentei.org\n"
+      exit 0
+    end
+    Dir.chdir @mydir
+    line = "-"*25
+    indent = "    "
+    now = Time.now
+    p "notifylist", @sc.notify_list(now) if $DEBUG
+    @sc.notify_list(now).each{|u, datehash|
+      dellist = []
+      content = datehash.sort.collect{|date, filehash|
+	next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ date
+	y, m, d, t = $1.to_i, $2.to_i, $3.to_i, $4.to_i
+	if t > 2359
+	  hhmm = msg('allday')
+	  comment = msg('theday')
+	else
+	  hhmm = sprintf "%02d:%02d", t/100, t%100
+	  diff = Time.mktime(y, m, d, t/100, 5%100) - now
+	  if diff < 7200
+	    comment = "%d%s" % [diff/60, msg('minutes', 'before')]
+	  elsif (ddiff=(Time.mktime(y, m, d)-Time.mktime(now.year, now.month, now.day))/3600/24) == 0
+	    comment = "%s%d%s" %
+	      [msg('about'), diff/3600, msg('hours', 'before')]
+	  else
+	    comment = "%d%s" % [ddiff, msg('days', 'before')]
+	  end
+	end
+	dellist << filehash['file']
+	sprintf("%s[[[%d/%d/%d %s]]]%s\n", line, y, m, d, hhmm, line) + \
+	sprintf("(%s %s)\n", comment, msg('notification')) + \
+	indent+filehash['text'].join(indent) + "\n\n"
+      }
+      # content.delete(nil)
+      if content
+	if $DEBUG
+	  print content
+	else
+	  content.unshift(msg('introduce')+"\n"+msg('notifymail')+"\n")
+	  content.unshift(@opt['url'].to_s+"\n")
+	  if sendnotify(u, msg('notifysubj'), content.join)
+	    # send mail completed
+	    begin
+	      @sc.cleanup_files(dellist)
+	    rescue
+	    end
+	  end
+	end
+      end
+    }
+    if !(list=@sc.notify_list(now)).empty?
+      subj = @mybase+": Undeleted old cron files detected"
+      files = list.collect{|who, whash|
+	whash.sort.collect{|date, fhash| fhash['file']}.join("\n")
+      }.join("\n")
+      sendMail(@opt['maintainer'], subj,
+	       "This is `#{@mybase}' in #{@mydir}\n" +
+	       "You'd better check and remove these files.\n\n"+files)
+    end
+    
+    exit 0
+  end
+
+  #
+  # user management
+  def userman()
+    if !checkauth
+      return nil
+    end
+    user=@params['user']
+    nickname = @sc.nickname(user)
+    tdclass = {}
+    tdclass["width"] = "80px" if @oldagent # workaround for NN4
+
+    @O.print @H.elementln("h1"){
+      @mybase+' '+msg('user', 'management')
+    }
+    @O.print @H.p(@sc.mkusermap.inspect) if $DEBUG
+    @O.print @H.p(msg('usermodwarn'))
+    @O.print \
+    @H.elementln("form", {'action'=>@myname+"?-usermod", 'method'=>'POST'}){
+      @H.elementln("table"){
+	@H.elementln("tr"){
+	  @H.element("td", tdclass) {msg('regaddress')} + \
+	  @H.element("td") {
+	    @H.element("code"){user}
+	  }
+	} + \
+	@H.elementln("tr"){
+	  @H.element("td", tdclass) {msg('mailaddress')} + \
+	  @H.element("td") {
+	    @H.text("newmail", mailaddress(user), @opt['size'], 80)
+	  }
+	} + \
+	@H.elementln("tr"){
+	  @H.element("td", tdclass) {msg('weburl')} + \
+	  @H.element("td") {
+	    @H.text("webpage", webpage(user), @opt['size'], 80)
+	  }
+	} + \
+	@H.elementln("tr"){
+	  @H.element("td") {msg('nickname')} + \
+	  @H.element("td") {
+	    @H.text("nickname", nickname, @opt['size'], 10)
+	  }
+	}
+      } + \
+      @H.elementln("p"){
+	msg('shortnameplz') + "<br>\n" + \
+	@H.a(@after5url+"multiplenotify.html", msg('multiplemail'))
+      } + \
+      '<br>' + \
+      @H.submit_reset("GO")
+    } # form
+
+    #
+    # Next section, REMOVE USER!
+    @O.print @H.elementln("h2"){
+      sprintf "%s %s %s", msg('user'), user, msg('deletion')
+    }
+    @O.print @H.p(msg('deletionwarn'))+"\n"
+    @O.print @H.elementln("form", {'action'=>@myname+"?-delusersub+#{user}", 'method'=>'POST'}){
+      @H.hidden("user", user) + "\n" + \
+      @H.elementln("table"){
+	@H.elementln("tr"){
+	  @H.elementln("td"){
+	    sprintf msg('deluser'), user
+	  } + \
+	  @H.elementln("td"){
+	    @H.radio("delete", "yes", msg('yes')) + ' ' + \
+	    @H.radio("delete", "no", msg('no'), true)
+	  }
+	} + \
+	@H.elementln("tr"){
+	  @H.elementln("td"){
+	    sprintf msg('really?'), user
+	  } + \
+	  @H.elementln("td"){
+	    @H.radio("delete2", "yes", msg('yes')) + ' ' + \
+	    @H.radio("delete2", "no", msg('no'), true)
+	  }
+	}
+      } + \
+      "<br>\n" + @H.submit_reset("GO")
+    }
+
+
+  end
+  def usermod()
+    if !checkauth
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      msg('user', 'management')+" "+msg('done')
+    }
+    user=@params['user']
+    email = mailaddress(user)
+    newmail = @params['newmail']
+    nickname = @sc.nickname(user)
+    newnn = @params['nickname'].to_s.strip
+    webpage = webpage(user)
+    newweb  = @params['webpage']
+    if email != newmail
+      # change user's address
+      if newmail == user
+	newvalue = nil
+      elsif checkmail(newmail)
+	newvalue = newmail
+      else
+	@O.print @H.elementln("pre"){"Invalid mail address"}
+      end
+      @O.print @H.elementln("pre"){
+	if @sc.putuserattr(user, 'email', newvalue)
+	  sprintf "new mail address=\"%s\"", mailaddress(user)
+	else
+	  sprintf "Setting new mail address to \"%s\" failed", newvalue
+	end
+      }
+    end
+    if nickname != newnn
+      if @sc.setnickname(user, newnn)
+	@O.print @H.p(msg('success'))
+	@O.print @H.elementln("pre"){
+	  sprintf "user=\"%s\"\nnickname=\"%s\"", user, @sc.nickname(user)
+	}
+	@O.print @H.p(msg('nicknamenote')) if newnn == ''
+      else
+	@O.print @H.p(msg('failure'))
+      end
+    end
+    if newweb > '' && webpage != newweb
+      if @sc.putuserattr(user, "webpage", newweb)
+	@O.print @H.p(msg('success'))
+	@O.print @H.elementln("pre"){
+	  sprintf "user=\"%s\"\nwebpage=\"%s\"", user, webpage(user)
+	}
+      else
+	@O.print @H.p("Update webpage"+msg('failure'))
+      end
+    end
+  end
+  #
+  # Display form of group management
+  def groupman()
+    if !checkauth
+      return nil
+    end
+    user=@params['user']
+    nickname = @sc.nickname(user)
+    tdclass = {}
+    tdclass["width"] = "80px" if @oldagent # workaround for NN4
+    admclass = {'class'=>'admin'}
+    grmap = @sc.groupmap
+
+    @O.print @H.elementln("h1"){
+      @mybase+' '+msg('group', 'management')
+    }
+    $KCODE='e' if $DEBUG
+    @O.print grmap.inspect if $DEBUG
+    @O.print @H.p(msg('joinmyself')+
+		  @H.a(@myname+"?-newgroup", msg('newgroup')))
+    @O.print @H.p(msg('usermodwarn'))
+    @O.print \
+    @H.elementln("form", {'action'=>@myname+"?-groupmod", 'method'=>'POST'}){
+      @H.elementln("table", {'border'=>'1', 'vertical-align'=>'top'}){
+	grmap.collect{|g, ghash|
+	  @H.elementln("tr"){
+	    @H.element("td", @sc.isadmin(user, g) ? admclass : nil){
+	      g
+	    } + \
+	    @H.element("td"){
+	      @H.element("div", {'class'=>'c'}) {
+		if @sc.isadmin(user, g)
+		  @H.a(@myname+"?-admgroup+#{g}", msg('adminop'))
+		else
+		  '--'
+		end
+	      }
+	    } + \
+	    @H.element("td"){
+	      memberp = @sc.ismember(user, g)
+	      if ghash['admin'].grep(user)[0]
+		@H.text("groupname-#{g}", ghash['name'], nil, 20)
+	      else
+		ghash['name']
+	      end + '<br>' + \
+	      @H.radio("groupadd-#{g}", "yes", "IN", memberp) + " / " + \
+	      @H.radio("groupadd-#{g}", "no", "OUT", !memberp)
+	    } + \
+	    @H.element("td"){
+	      ghash['members'].collect{|u|
+		@sc.nickname(u)
+	      }.join(", ")
+	    }
+	  }
+	}
+      } + \
+      '' + \
+      @H.p(msg('groupwarn', 'shortnameplz')) + \
+      @H.submit_reset("GO")
+    } # form
+  end
+  def groupmod()
+    if !checkauth
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      msg('group', 'management')+" "+msg('done')
+    }
+    user=@params['user']
+    @O.print @params.inspect if $DEBUG
+
+    for grp in @sc.groups()
+      #
+      # as a member, participate or retire
+      key = "groupadd-#{grp}"
+      removep = (/no/i =~ @params[key])
+      memberp = @sc.ismember(user, grp)
+      if @params[key]
+	if (!removep) ^ memberp
+	  @sc.addgroup(grp, [user], removep)
+	  @O.print @H.elementln("p"){
+	    sprintf "%s [%s] %s %s", msg('user'), user,
+	      removep ? msg('removedfromgp') : msg('addedtogroup'), grp
+	  }
+	end
+      end
+      #
+      # as a owner, change the name of group
+      if @sc.isadmin(user, grp) &&
+	  (newname = @params["groupname-#{grp}"]) &&
+	  @sc.groupname(grp) != newname
+	@sc.setgroupname(grp, newname)
+	@O.print @H.elementln("p"){
+	  sprintf "%s %s%s %s",
+	    msg('group'), grp, msg('of', 'name', 'setto'), newname
+	}
+      end
+    end
+  end
+  def users()
+    unless pm=open_pm()
+      outputError(msg('autherror'))
+      return nil
+    end	
+    pm.users
+  end
+  def grepgroup(gname)
+    gr = @sc.groups.grep(gname)[0]
+  end
+  def admgroup(group = nil)
+    # if group==nil, create new
+    if !checkauth
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      msg('group', 'management')
+    }
+    user=@params['user']
+
+    # Check the existent group's validity
+    if group
+      unless (gr=grepgroup(group))
+      @O.print @H.p("No such group #{group}")
+	return nil
+      end
+      group = gr
+      unless @sc.isadmin(user, group)
+	@O.print @H.p("You are not administrator of #{group}.")
+	return nil
+      end
+      @O.print @H.elementln("h2"){
+	msg('group')+" #{group}" +
+	  if group != @sc.groupname(group)
+	    " (#{@sc.groupname(group)})"
+	  end.to_s
+      }
+      actionmethod={'action'=>@myname+"?-admgroupsub", 'method'=>'POST'}
+    else
+      # New group creation
+      @O.print @H.elementln("h2"){
+	msg('newgroup')
+      }
+      actionmethod={'action'=>@myname+"?-newgroupsub", 'method'=>'POST'}
+    end
+
+    userlist = ([user] + users()).uniq
+    myselfclass = {'class'=>'admin'}
+    colspan2 = {'colspan'=>'2'}
+    warnclass = {'class'=>'warn'}
+    warnp = nil
+
+    @O.print @H.elementln("form", actionmethod){
+      @H.hidden('group', group) + "\n" + \
+      if group
+	""
+      else
+	# new group creation
+	grps = @sc.groups()
+	i=1
+	defname = "group%03d"%i
+	while grps.grep(defname)[0]
+	  defname = "group%03d"%(i+=1)
+	end
+	@H.element("pre"){
+	  msg('group', 'of', 'id')+"\n"+@H.text("group", defname) + "\n" + \
+	  msg('group', 'of', 'name', 'anystring')+"\n"+ \
+	  @H.text("gname", '') + "\n"
+	}
+      end + \
+      @H.elementln("table", {'border'=>'1'}){
+	@H.elementln("tr"){
+	  @H.element("th"){msg('join')} + \
+	  @H.element("th"){msg('administrator')} + \
+	  @H.element("th"){msg('member')}
+	} + \
+	userlist.collect{|u|
+	  recursememp = nil
+	  if group
+	    memberp = (@sc.ismember(u, group) && true)
+	    adminp = (@sc.isadmin(u, group) && true)
+	    if !memberp && @sc.members(group).grep(u)[0]
+	      recursememp = true
+	    end
+	  else
+	    memberp = adminp = (u == user)
+	  end
+	  @H.elementln("tr", (u==user ? myselfclass : nil)){
+	    @H.element("td"){
+	      @H.radio('mem-'+u, 'yes', 'YES / ', memberp) + \
+	      @H.radio('mem-'+u, 'no', 'NO', !memberp)
+	    } + \
+	    @H.element("td"){
+	      @H.radio('adm-'+u, 'yes', 'YES / ', adminp) + \
+	      @H.radio('adm-'+u, 'no', 'NO', !adminp)
+	    } + \
+	    @H.element("td"){
+	      @sc.nickname(u) + \
+	      if recursememp
+		warnp = true
+		@H.element("span", warnclass){"(*)"}
+	      end.to_s
+	    }
+	  }
+	}.join + \
+	# group names
+	@H.elementln("tr"){
+	  @H.element("th", colspan2){msg('join')} + \
+	  @H.element("th"){msg('group')}
+	} + \
+	@sc.groups().collect{|g|
+	  next if group == g
+	  memberp = @sc.ismember(g, group)
+	  @H.element("tr"){
+	    @H.element("td", colspan2){
+	      @H.radio('mem-'+g, 'yes', 'YES / ', memberp) + \
+	      @H.radio('mem-'+g, 'no', 'NO', !memberp)
+	    } + \
+	    @H.element("td"){
+	      if @sc.isadmin(user, g)
+		@H.a(@myname+"?-admgroup+#{g}", @sc.groupname(g))
+	      else
+		@sc.groupname(g)
+	      end
+	    }
+	  }
+	}.join
+      } + "<br>\n" + \
+      @H.submit_reset("GO")
+    } # form
+    @O.print @H.p(@H.element("span", warnclass){"(*)"}+
+		  msg('recursewarn')) if warnp
+    if group
+      @O.print @H.p(sprintf(msg('wholemembers'), group))
+      @O.print @H.elementln("p", {'class'=>'listup'}){
+	@sc.members(group).collect{|u|@sc.nickname(u)}.join(", ")}
+    end
+
+    #
+    # Next section, REMOVE GROUP!
+    return nil unless group
+    @O.print @H.elementln("h2"){
+      sprintf "%s %s %s", msg('group'), group, msg('deletion')
+    }
+    @O.print @H.p(msg('deletionwarn'))+"\n"
+    @O.print @H.elementln("form", {'action'=>@myname+"?-delgroupsub+#{group}", 'method'=>'POST'}){
+      @H.hidden("group", group) + "\n" + \
+      @H.elementln("table"){
+	@H.elementln("tr"){
+	  @H.elementln("td"){
+	    sprintf msg('delgroup'), group
+	  } + \
+	  @H.elementln("td"){
+	    @H.radio("delete", "yes", msg('yes')) + ' ' + \
+	    @H.radio("delete", "no", msg('no'), true)
+	  }
+	} + \
+	@H.elementln("tr"){
+	  @H.elementln("td"){
+	    sprintf msg('really?'), group
+	  } + \
+	  @H.elementln("td"){
+	    @H.radio("delete2", "yes", msg('yes')) + ' ' + \
+	    @H.radio("delete2", "no", msg('no'), true)
+	  }
+	}
+      } + \
+      "<br>\n" + @H.submit_reset("GO")
+    }
+
+    @O.print footer()
+  end
+  def newgroup()
+    admgroup(nil)
+  end
+
+  def delgroupsub(group)
+    if !checkauth
+      return nil
+    end
+    user = @params['user']
+    if group != @params['group']
+      @O.print @H.p("Group mismatch")
+      return nil
+    end
+    unless (gr=grepgroup(group))
+      @O.print @H.p("No such group #{group}")
+      return nil
+    end
+    group = gr
+    unless @sc.isadmin(user, group)
+      @O.print @H.p("You are not administrator of #{group}.")
+      return nil
+    end
+    unless @params['delete'] && /yes/i =~ @params['delete'] \
+      && @params['delete2'] && /yes/i =~ @params['delete2']
+      @O.print @H.p(msg('chicken'))
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      msg('group')+" #{group} "+msg('deletion')
+    }
+    @O.print @H.p(@sc.destroygroup(group) ? msg("done") : msg("failure"))
+
+    @O.print footer()
+  end
+
+  def deleteuser(user)
+    @sc.deleteuser(user) &&
+      begin
+	pm = open_pm
+	pm.delete(user)
+	pm.close()
+	true
+      rescue
+	nil
+      end
+  end
+  def delusersub(user)
+    if !checkauth
+      return nil
+    end
+    user = @params['user']
+    if user != @params['user']
+      @O.print @H.p("User mismatch")
+      return nil
+    end
+    unless (us=users().grep(user)[0])
+      @O.print @H.p("No such user #{user}")
+      return nil
+    end
+    user = us
+    unless @params['delete'] && /yes/i =~ @params['delete'] \
+      && @params['delete2'] && /yes/i =~ @params['delete2']
+      @O.print @H.p(msg('chicken'))
+      return nil
+    end
+    @O.print @H.elementln("h1"){
+      msg('user')+" #{user} "+msg('deletion')
+    }
+    @O.print @H.p(deleteuser(user) ? msg("done") : msg("failure"))
+
+    @O.print footer()
+  end
+
+  def admgroupsub()
+    if !checkauth
+      return nil
+    end
+    user = @params['user']
+    group = @params['group']
+    unless (gr=grepgroup(group))
+      @O.print @H.element("pre"){"No such group #{group.inspect}"}
+      return nil
+    end
+    unless @sc.isadmin(user, group)
+      @O.print @H.p("You are not administrator of #{group}.")
+      return nil
+    end
+    gorup = gr
+    @O.print @H.elementln("h1"){
+      msg('group', 'management', 'done')
+    }
+    @O.print @H.elementln("h2"){
+      msg('group')+" #{group}" +
+	if group != @sc.groupname(group)
+	  " (#{@sc.groupname(group)})"
+	end.to_s
+    }
+    somethingdone = nil
+    for u in users()
+      for var, kind in {
+	  "mem"=>['members', 'member'], 'adm'=>['admin', 'administrator']}
+	memv = "#{var}-#{u}"
+	if @params[memv]
+	  joinp = ((/^yes/i =~ @params[memv]) && true)
+	  membp = if var=='mem'
+		    @sc.ismember(u, group)
+		  else		# admin
+		    @sc.isadmin(u, group)
+		  end && true
+	  if var=='adm' && @sc.admins(group).length == 1 && membp && !joinp
+	    @O.print @H.p(sprintf(msg('soleadmin'), u, group))
+	  elsif joinp ^ membp
+	    somethingdone = true
+	    @sc.addgroup(group, [u], !joinp, kind[0])
+	    @O.print @H.elementln("p"){
+	      sprintf "%s [%s](%s) %s %s", msg('user'), u,
+		msg(kind[1]),
+		joinp ?  msg('addedtogroup'): msg('removedfromgp'), group
+	    }
+	  end
+	end
+      end
+    end # users()
+
+    # add or remove for group in groups
+    for g in @sc.groups()
+      next if g == group
+      memv = "mem-#{g}"
+      if @params[memv]
+	joinp = ((/^yes/i =~ @params[memv]) && true)
+	membp = (@sc.ismember(g, group) && true)
+	if joinp ^ membp
+	  somethingdone = true
+	  @sc.addgroup(group, [g], !joinp)
+	  @O.print @H.elementln("p"){
+	    sprintf "%s [%s] %s %s", msg('group'), g,
+	      joinp ?  msg('addedtogroup'): msg('removedfromgp'), group
+	  }
+	end
+      end
+    end # groups
+    unless somethingdone
+      # @O.print @H.p(msg('nothingtodo'))
+    end
+    @O.print footer()
+  end
+  def newgroupsub()
+    if !checkauth
+      nil
+    end
+    user = @params['user']
+    newgroup = @params['group']
+    newgname = @params['gname']
+
+    
+    if @sc.groups.grep(newgroup)[0]
+      @O.print @H.p(msg('existent')+newgroup)
+      return nil
+    end
+    @sc.creategroup(newgroup, newgname, [user])
+    admgroup(newgroup)
+  end
+
+  def setpasswd(user)
+    prohibitviahttp()
+    pm = open_pm()
+    exit 1 unless pm
+    if pm.userexist?(user) then
+      begin
+	system "stty -echo"
+	STDERR.print "New passwd: "
+	p1 = STDIN.gets
+	STDERR.print "\nAgain: "
+	p2 = STDIN.gets
+      ensure
+	system "stty echo"
+      end
+      if (p1 == p2) then
+	pm.setpasswd(user, p1.chop!)
+      end
+      STDERR.print "New password for #{user} set successfully\n"
+    else
+      STDERR.print "User #{user} not found\n"
+    end
+    pm.close()
+    exit 0
+  end
+  def adduser(user)
+    prohibitviahttp()
+    pm = open_pm()
+    exit 1 unless pm
+    newpwd = pm.setnewpasswd(user, 4)
+    print "#{user}'s password is #{newpwd}\n"
+    pm.close()
+    exit 0
+  end
+  def deluser(user)
+    prohibitviahttp()
+    pm = open_pm()
+    exit 1 unless pm
+    pm.delete(user)
+    pm.close()
+    exit 0
+  end
+
+  # read configuratoin file
+  def readconf(conf)
+    cf =  "after5.cf" #conf # || @opt['conf']
+    cf = File.join(@mydir, cf) unless test(?s, cf)
+    cf = File.join(ENV["HOME"], cf) unless test(?s, cf)
+    return unless test(?s, cf)
+    begin
+      IO.foreach(cf){|line|
+	next if /^\s *#/ =~ line
+	line.chop!
+	line.sub!(/^\s*#.*/, '')
+	next if /^$/ =~ line
+	case line
+	  # title, type = line.split(/\t+/)
+	when /^([a-z]+)=(.*)/oi
+	  key, value = $1, $2
+	  case value
+	  when /^(no|none|null|nil)$/io
+	    @opt[key] = nil
+	  else
+	    @opt[key] = value
+	  end
+	  print "#{key} set to #{value}\n" if $DEBUG
+	end
+      }
+    rescue
+      STDERR.print "Configuration file %s not readable\n", cf
+    end
+  end
+
+  def parsedate(string)
+    if %r,^(\d\d\d\d+)/(\d+)/(\d+)/(\d+)/([^/]+)$, =~ string
+      sprintf "%04d/%02d/%02d/%04d/%s", $1.to_i, $2.to_i, $3.to_i, $4.to_i,
+	grepgroup($5)
+    elsif %r,^(\d\d\d\d+)/(\d+)/(\d+)/(\d+), =~ string
+      sprintf "%04d/%02d/%02d/%04d", $1.to_i, $2.to_i, $3.to_i, $4.to_i
+    elsif %r,^(\d\d\d\d+)/(\d+)/(\d+), =~ string
+      sprintf "%04d/%02d/%02d", $1.to_i, $2.to_i, $3.to_i
+    elsif %r,^(\d\d\d\d+)/(\d+), =~ string
+      sprintf "%04d/%02d", $1.to_i, $2.to_i
+    elsif %r,^(\d\d\d\d+)/(\d+), =~ string
+      sprintf "%04d", $1.to_i
+    end
+  end
+
+  def getarg()
+    argument = {}
+
+    while /^-/ =~ ARGV[0]
+      case ARGV[0]
+      when '-f'
+	conf = ARGV[1]
+	ARGV.shift
+      when "-d"
+	$DEBUG = true
+      when "-install"
+      when "-addsched"
+	@job = "addsched"
+      when "-today"
+	@job = "today"
+      when "-remove"
+	ARGV.shift
+	@job = 'remove "'+parsedate(ARGV[0])+'"'
+      when "-modify"
+	ARGV.shift
+	@job = 'modify "'+parsedate(ARGV[0])+'"'
+      when "-month"
+	ARGV.shift
+	@job = 'month "'+parsedate(ARGV[0])+'"'
+      when "-show"
+	ARGV.shift
+	# @job = "show '"+ARGV[0]+"'"
+	@job = "show '"+parsedate(ARGV[0])+"'"
+      when "-login"
+	@job = "login"
+      when "-userman"
+	@job = "userman"
+      when "-usermod"
+	@job = "usermod"
+      when "-groupinout"
+	@job = "groupinout"
+      when "-groupsubmit"
+	@job = "groupsubmit"
+      when "-groupman"
+	@job = "groupman"
+      when "-groupmod"
+	@job = "groupmod"
+      when "-notify"
+	@job = 'notify'	# + exit
+      when "-newgroup"
+	@job = 'newgroup'
+      when "-admgroup"
+	ARGV.shift
+	gr = safecopy(grepgroup(ARGV[0]))
+	##gr.untaint
+	@job = 'admgroup "'+gr+'"'
+      when "-admgroupsub"
+	@job = 'admgroupsub'
+      when "-newgroupsub"
+	@job = 'newgroupsub'
+      when "-delusersub"
+	ARGV.shift
+	usr = users().grep(ARGV[0])[0]
+	@job = 'delusersub "'+usr+'"'
+      when "-delgroupsub"
+	ARGV.shift
+	gr = grepgroup(ARGV[0])
+	@job = 'delgroupsub "'+gr+'"'
+      when /-(setpasswd|deluser|adduser)$/
+	ARGV.shift
+	@job = $1+ " '#{ARGV[0]}'" # + exit
+      when ""
+      end
+      ARGV.shift
+    end
+
+    readconf(@conf)
+
+    query = ''
+    method = ENV["REQUEST_METHOD"]
+    if /POST/i =~ method then
+      length = ENV["CONTENT_LENGTH"].to_i
+      query = STDIN.read(length)
+    elsif /GET/i =~ method then
+      query = ENV["QUERY_STRING"]
+    else                            # executed from command line
+      query = ARGV.join("&")
+    end
+
+    for unit in query.split(/\&/)
+      if /^([a-z][-_0-9@%a-z.]*)=(.*)/i =~ unit
+	key, value = $1, $2
+	#value.gsub!(/%(..)/){[$1.hex].pack("c")} # これでURLデコードが一発
+	decode!(value)
+	decode!(key)
+	value = Kconv::toeuc(value) # EUCに変換
+	printf "[%s] = %s\n", key, value if $DEBUG
+	argument[key] = value
+      end
+    end
+    argument
+  end
+  def getcookie()
+    cookie = {}
+    if /value=(.*)/ =~ ENV['HTTP_COOKIE']
+      # value=$1.gsub!(/%(..)/){[$1.hex].pack("c")}
+      value=decode!($1)
+      for line in value.split("&")
+	if /(\w+)=(.*)/ =~ line
+	  key, value = $1, $2
+	  #value.gsub!(/%(..)/){[$1.hex].pack("c")} # これでURLデコードが一発
+	  decode!(value)
+	  value = Kconv::toeuc(value) # EUCに変換
+	  printf "cookie[%s] = %s\n", key, value if $DEBUG
+	  cookie[key] = value
+	end
+      end
+    end
+    cookie
+  end
+end
+
+After5.new.doit
+
+if __FILE__ == $0
+end
+
+
+# Local variables:
+# buffer-file-coding-system: euc-jp
+# End:

yatex.org