#!/usr/bin/ruby1.8
#!/usr/bin/env ruby
#
# wfo - Wiki Frontend at Offline
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#require 'wfo/main'
# wfo/main.rb - wfo main routine and subcommands
#
# Copyright (C) 2006,2007 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#require 'mconv'
# mconv.rb - character code conversion library using iconv
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#  1. Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#  3. The name of the author may not be used to endorse or promote products
#     derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

require 'iconv'

module Mconv
  def Mconv.setup(internal_mime_charset)
    internal_mime_charset = internal_mime_charset.downcase
    case internal_mime_charset
    when 'euc-jp'
      kcode = 'e'
    when 'euc-kr'
      kcode = 'e'
    when 'shift_jis'
      kcode = 's'
    when 'iso-8859-1'
      kcode = 'n'
    when 'utf-8'
      kcode = 'u'
    else
      raise "unexpected MIME charset: #{internal_mime_charset}"
    end
    @internal_mime_charset = internal_mime_charset
    $KCODE = kcode
  end

  # xxx: euc-kr
  case $KCODE
  when /\Ae/i; @internal_mime_charset = 'euc-jp'
  when /\As/i; @internal_mime_charset = 'shift_jis'
  when /\Au/i; @internal_mime_charset = 'utf-8'
  when /\An/i; @internal_mime_charset = 'iso-8859-1'
  else
    raise "unknown $KCODE: #{$KCODE.inspect}"
  end

  def Mconv.setup_locale_charset
    Mconv.setup(Mconv.locale_charset)
  end

  def Mconv.locale_charset
    codeset = Mconv.locale_codeset
    case codeset
    when /\A(?:euc-jp|eucjp|ujis)\z/i
      'euc-jp'
    when /\A(?:euc-kr|euckr)\z/i
      'euc-kr'
    when /\A(?:shift_jis|sjis)\z/i
      'shift_jis'
    when /\A(?:utf-8|utf8)\z/i
      'utf-8'
    when /\A(?:iso-8859-1|iso8859-1|us-ascii|ANSI_X3.4-1968)\z/i
      'iso-8859-1'
    else
      'utf-8'
    end
  end

  def Mconv.locale_codeset
    codeset = `locale charmap`.chomp
    status = $?
    if status.to_i == 0 && !codeset.empty?
      codeset
    else
      nil
    end
  end

  def Mconv.internal_mime_charset
    @internal_mime_charset.dup
  end

  def Mconv.valid_charset?(str)
    /\A(us-ascii|iso-2022-jp|euc-jp|shift_jis|utf-8|iso-8859-1)\z/i =~ str
  end

  def Mconv.conv(str, to, from)
    ic = Iconv.new(to, from)

    result = ''
    rest = str

    begin
      result << ic.iconv(rest)
    rescue Iconv::Failure
      result << $!.success

      rest = $!.failed

      # following processing should be customizable by block?
      result << '?'
      rest = rest[1..-1]

      retry
    end

    result << ic.close

    result
  end

  CharsetTable = {
    'us-ascii' => /\A[\s\x21-\x7e]*\z/,
    'euc-jp' =>
      /\A(?:\s                               (?# white space character)
         | [\x21-\x7e]                       (?# ASCII)
         | [\xa1-\xfe][\xa1-\xfe]            (?# JIS X 0208)
         | \x8e(?:([\xa1-\xdf])              (?# JIS X 0201 Katakana)
                 |([\xe0-\xfe]))             (?# There is no character in E0 to FE)
         | \x8f[\xa1-\xfe][\xa1-\xfe]        (?# JIS X 0212)
         )*\z/nx,
    "iso-2022-jp" => # with katakana
      /\A[\s\x21-\x7e]*                      (?# initial ascii )
         (\e\(B[\s\x21-\x7e]*                (?# ascii )
         |\e\(J[\s\x21-\x7e]*                (?# JIS X 0201 latin )
         |\e\(I[\s\x21-\x7e]*                (?# JIS X 0201 katakana )
         |\e\$@(?:[\x21-\x7e][\x21-\x7e])*   (?# JIS X 0201 )
         |\e\$B(?:[\x21-\x7e][\x21-\x7e])*   (?# JIS X 0201 )
         )*\z/nx,
    'shift_jis' =>
      /\A(?:\s                               (?# white space character)
         | [\x21-\x7e]                       (?# JIS X 0201 Latin)
         | ([\xa1-\xdf])                     (?# JIS X 0201 Katakana)
         | [\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc]      (?# JIS X 0208)
         | ([\xf0-\xfc][\x40-\x7e\x80-\xfc]) (?# extended area)
         )*\z/nx,
    'utf-8' =>
      /\A(?:\s
         | [\x21-\x7e]
         | [\xc0-\xdf][\x80-\xbf]
         | [\xe0-\xef][\x80-\xbf][\x80-\xbf]
         | [\xf0-\xf7][\x80-\xbf][\x80-\xbf][\x80-\xbf]
         | [\xf8-\xfb][\x80-\xbf][\x80-\xbf][\x80-\xbf][\x80-\xbf]
         | [\xfc-\xfd][\x80-\xbf][\x80-\xbf][\x80-\xbf][\x80-\xbf][\x80-\xbf]
         )*\z/nx
  }
  Preference = ['us-ascii', "iso-2022-jp", 'euc-jp', 'utf-8', 'shift_jis']

  def Mconv.guess_charset(str)
    guess_charset_list(str).first
  end

  def Mconv.guess_charset_list(str)
    case str
    when /\A\xff\xfe/; return ['utf-16le']
    when /\A\xfe\xff/; return ['utf-16be']
    end
    count = {}
    CharsetTable.each {|name, regexp|
      count[name] = 0
    }
    str.scan(/\S+/n) {|fragment|
      CharsetTable.each {|name, regexp|
        count[name] += 1 if regexp =~ fragment
      }
    }
    max = count.values.max
    count.reject! {|k, v| v != max }
    return count.keys if count.size == 1
    return ['us-ascii'] if count['us-ascii']
    
    # xxx: needs more accurate guess
    Preference.reject {|name| !count[name] }
  end

  def Mconv.minimize_charset(charset, string)
    # shortcut
    if /\A(?:euc-jp|utf-8|iso-8859-1)\z/i =~ charset
      if /\A[\x00-\x7f]*\z/ =~ string
        return 'us-ascii'
      else
        return charset
      end
    end

    charset2 = 'us-ascii'
    begin
      # round trip?
      s2 = Iconv.conv(charset, charset2, Iconv.conv(charset2, charset, string))
      return charset2 if string == s2
    rescue Iconv::Failure
    end
    charset
  end
end

class String
  def decode_charset(charset)
    Mconv.conv(self, Mconv.internal_mime_charset, charset)
  end

  def encode_charset(charset)
    Mconv.conv(self, charset, Mconv.internal_mime_charset)
  end

  def decode_charset_exc(charset)
    Iconv.conv(Mconv.internal_mime_charset, charset, self)
  end

  def encode_charset_exc(charset)
    Iconv.conv(charset, Mconv.internal_mime_charset, self)
  end

  def encode_charset_exactly(charset)
    result = Iconv.conv(charset, Mconv.internal_mime_charset, self)
    round_trip = Iconv.conv(Mconv.internal_mime_charset, charset, result)
    if self != round_trip
      raise ArgumentError, "not round trip"
    end
    result
  end

  def guess_charset
    Mconv.guess_charset(self)
  end

  def guess_charset_list
    Mconv.guess_charset_list(self)
  end

  def decode_charset_guess
    decode_charset(guess_charset)
  end
end

Mconv.setup_locale_charset

require 'optparse'
require 'open-uri'
require 'pathname'
require 'tempfile'

module WFO
end

#require 'wfo/missing'
# wfo/missng.rb - complement missing features
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'open-uri'

unless OpenURI::Meta.instance_methods.include? "last_request_uri"
  module OpenURI::Meta
    def last_request_uri
      @base_uri
    end

    undef base_uri
    def base_uri
      if content_location = self.meta['content-location']
        u = URI(content_location)
        u = @base_uri + u if u.relative? && @base_uri
        u
      else
        @base_uri
      end
    end
  end
end

require 'htree'

unless HTree::Doc::Trav.instance_methods.include? "base_uri"
  module HTree::Doc::Trav
    attr_accessor :base_uri
  end

  alias HTree_old HTree
  def HTree(html_string=nil, &block)
    if block
      HTree_old(html_string, &block)
    else
      result = HTree_old(html_string)
      result.instance_eval {
        if html_string.respond_to? :base_uri
          @request_uri = html_string.last_request_uri
          @protocol_base_uri = html_string.base_uri
        else
          @request_uri = nil
          @protocol_base_uri = nil
        end
      }
      result
    end
  end

  module HTree::Doc::Trav
    attr_reader :request_uri

    undef base_uri
    def base_uri
      return @base_uri if defined? @base_uri
      traverse_element('{http://www.w3.org/1999/xhtml}base') {|elem|
        base_uri = URI(elem.get_attr('href'))
        base_uri = @protocol_base_uri + base_uri if @protocol_base_uri
        @base_uri = base_uri
      }
      @base_uri = @request_uri unless defined? @base_uri
      return @base_uri
    end

    def traverse_html_form(orig_charset=nil)
      traverse_element('{http://www.w3.org/1999/xhtml}form') {|form|
        yield WFO::Form.make(form, self.base_uri, @request_uri, orig_charset)
      }
      nil
    end
  end
end
#require 'wfo/pat'
# wfo/pat.rb - pattern library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

module WFO
end

module WFO::Pat
  # RFC 2616
  HTTP_Token = /[!#-'*+\-.0-9A-Z^-z|~]*/n
  HTTP_QuotedString = /"((?:[\t\r\n !#-\[\]-~]|\\[\000-\177])*)"/n

  # RFC 2617
  HTTP_AuthParam = /(#{HTTP_Token})=(#{HTTP_Token}|#{HTTP_QuotedString})/
  HTTP_Challenge = /(#{HTTP_Token})\s+(#{HTTP_AuthParam}(?:\s*,\s*#{HTTP_AuthParam})*)/n
end

#require 'wfo/workarea'
# wfo/workarea.rb - local workarea library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'zlib'

class WFO::WorkArea
  def self.has?(filename)
    n = Pathname.new(filename)
    info_path = n.dirname + '.wfo' + "i_#{n.basename}.gz"
    info_path.exist?
  end

  def self.each_filename(dir=Pathname.new('.'))
    (dir + '.wfo').each_entry {|n|
      if /\Ai_(.*)\.gz\z/ =~ n.basename.to_s
        yield dir + $1
      end
    }
  end

  def initialize(filename, repository_type=nil, url=nil, form=nil, textarea_name=nil)
    @filename = Pathname.new(filename)
    @info_path = @filename.dirname + '.wfo' + "i_#{@filename.basename}.gz"
    if url
      raise "alread exists : #{@info_path}" if @info_path.exist?
      @url = url.dup
      @info = {}
      @info['URL'] = @url
      @info['repository_type'] = repository_type.dup
      @info['form'] = form
      @info['textarea_name'] = textarea_name
    else
      raise "not exists : #{@info_path}" if !@info_path.exist?
      Zlib::GzipReader.open(@info_path.to_s) {|f|
        @info = Marshal.load(f)
      }
      @url = @info['URL']
    end
  end
  attr_reader :filename, :url

  def make_accessor
    WFO::Repo.fetch_class(@info['repository_type']).make_accessor(@info['URL'])
  end

  def store
    store_info
    store_text
  end

  def store_info
    @info_path.dirname.mkpath
    Zlib::GzipWriter.open(@info_path.to_s) {|f|
      Marshal.dump(@info, f)
    }
  end

  def store_text
    @filename.open('wb') {|f|
      f.write self.original_text
    }
  end

  def original_text
    @info['form'].fetch(@info['textarea_name'])
  end

  def original_text=(text)
    @info['form'].set(@info['textarea_name'], text)
  end

  def local_text
    @filename.open('rb') {|f| f.read }
  end

  def local_text=(text)
    @filename.open('wb') {|f| f.write text }
  end

  def make_backup(text)
    backup_filename = @filename.dirname + (".~" + @filename.basename.to_s)
    backup_filename.open("wb") {|f| f.write text }
    backup_filename
  end

  def modified?
    self.original_text != self.local_text
  end

end
#require 'wfo/webclient'
# wfo/webclient.rb - stateful web client library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'net/https'
#require 'wfo/form'
# wfo/form.rb - HTML form handling library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#require 'escape'
# escape.rb - escape/unescape library for several formats
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#  1. Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#  3. The name of the author may not be used to endorse or promote products
#     derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

# Escape module provides several escape functions.
# * URI
# * HTML
# * shell command
module Escape
  module_function

  # Escape.shell_command composes
  # a sequence of words to
  # a single shell command line.
  # All shell meta characters are quoted and
  # the words are concatenated with interleaving space.
  #
  #  Escape.shell_command(["ls", "/"]) #=> "ls /"
  #  Escape.shell_command(["echo", "*"]) #=> "echo '*'"
  #
  # Note that system(*command) and
  # system(Escape.shell_command(command)) is roughly same.
  # There are two exception as follows.
  # * The first is that the later may invokes /bin/sh.
  # * The second is an interpretation of an array with only one element: 
  #   the element is parsed by the shell with the former but
  #   it is recognized as single word with the later.
  #   For example, system(*["echo foo"]) invokes echo command with an argument "foo".
  #   But system(Escape.shell_command(["echo foo"])) invokes "echo foo" command without arguments (and it probably fails).
  def shell_command(command)
    command.map {|word| shell_single_word(word) }.join(' ')
  end

  # Escape.shell_single_word quotes shell meta characters.
  #
  # The result string is always single shell word, even if
  # the argument is "".
  # Escape.shell_single_word("") returns "''".
  #
  #  Escape.shell_single_word("") #=> "''"
  #  Escape.shell_single_word("foo") #=> "foo"
  #  Escape.shell_single_word("*") #=> "'*'"
  def shell_single_word(str)
    if str.empty?
      "''"
    elsif %r{\A[0-9A-Za-z+,./:=@_-]+\z} =~ str
      str
    else
      result = ''
      str.scan(/('+)|[^']+/) {
        if $1
          result << %q{\'} * $1.length
        else
          result << "'#{$&}'"
        end
      }
      result
    end
  end

  # Escape.uri_segment escapes URI segment using percent-encoding.
  #
  #  Escape.uri_segment("a/b") #=> "a%2Fb"
  #
  # The segment is "/"-splitted element after authority before query in URI, as follows.
  #
  #   scheme://authority/segment1/segment2/.../segmentN?query#fragment
  #
  # See RFC 3986 for details of URI.
  def uri_segment(str)
    # pchar - pct-encoded = unreserved / sub-delims / ":" / "@"
    # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
    # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
    str.gsub(%r{[^A-Za-z0-9\-._~!$&'()*+,;=:@]}n) {
      '%' + $&.unpack("H2")[0].upcase
    }
  end

  # Escape.uri_path escapes URI path using percent-encoding.
  # The given path should be a sequence of (non-escaped) segments separated by "/".
  # The segments cannot contains "/".
  #
  #  Escape.uri_path("a/b/c") #=> "a/b/c"
  #  Escape.uri_path("a?b/c?d/e?f") #=> "a%3Fb/c%3Fd/e%3Ff"
  #
  # The path is the part after authority before query in URI, as follows.
  #
  #   scheme://authority/path#fragment
  #
  # See RFC 3986 for details of URI.
  #
  # Note that this function is not appropriate to convert OS path to URI.
  def uri_path(str)
    str.gsub(%r{[^/]+}n) { uri_segment($&) }
  end

  # :stopdoc:
  def html_form_fast(pairs, sep=';')
    pairs.map {|k, v|
      # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
      #   unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
      # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
      # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
      # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
      # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
      # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
      k = k.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
        '%' + $&.unpack("H2")[0].upcase
      }
      v = v.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
        '%' + $&.unpack("H2")[0].upcase
      }
      "#{k}=#{v}"
    }.join(sep)
  end
  # :startdoc:

  # Escape.html_form composes HTML form key-value pairs as a x-www-form-urlencoded encoded string.
  #
  # Escape.html_form takes an array of pair of strings or
  # an hash from string to string.
  #
  #  Escape.html_form([["a","b"], ["c","d"]]) #=> "a=b&c=d"
  #  Escape.html_form({"a"=>"b", "c"=>"d"}) #=> "a=b&c=d"
  #
  # In the array form, it is possible to use same key more than once.
  # (It is required for a HTML form which contains
  # checkboxes and select element with multiple attribute.)
  #
  #  Escape.html_form([["k","1"], ["k","2"]]) #=> "k=1&k=2"
  #
  # If the strings contains characters which must be escaped in x-www-form-urlencoded,
  # they are escaped using %-encoding.
  #
  #  Escape.html_form([["k=","&;="]]) #=> "k%3D=%26%3B%3D"
  #
  # The separator can be specified by the optional second argument.
  #
  #  Escape.html_form([["a","b"], ["c","d"]], ";") #=> "a=b;c=d"
  #
  # See HTML 4.01 for details.
  def html_form(pairs, sep='&')
    r = ''
    first = true
    pairs.each {|k, v|
      # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
      #   unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
      # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
      # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
      # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
      # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
      # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
      r << sep if !first
      first = false
      k.each_byte {|byte|
        ch = byte.chr
        if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
          r << "%" << ch.unpack("H2")[0].upcase
        else
          r << ch
        end
      }
      r << '='
      v.each_byte {|byte|
        ch = byte.chr
        if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
          r << "%" << ch.unpack("H2")[0].upcase
        else
          r << ch
        end
      }
    }
    r
  end

  # :stopdoc:
  HTML_TEXT_ESCAPE_HASH = {
    '&' => '&amp;',
    '<' => '&lt;',
    '>' => '&gt;',
  }
  # :startdoc:

  # Escape.html_text escapes a string appropriate for HTML text using character references.
  #
  # It escapes 3 characters:
  # * '&' to '&amp;'
  # * '<' to '&lt;'
  # * '>' to '&gt;'
  #
  #  Escape.html_text("abc") #=> "abc"
  #  Escape.html_text("a & b < c > d") #=> "a &amp; b &lt; c &gt; d"
  #
  # This function is not appropriate for escaping HTML element attribute
  # because quotes are not escaped.
  def html_text(str)
    str.gsub(/[&<>]/) {|ch| HTML_TEXT_ESCAPE_HASH[ch] }
  end

  # :stopdoc:
  HTML_ATTR_ESCAPE_HASH = {
    '&' => '&amp;',
    '<' => '&lt;',
    '>' => '&gt;',
    '"' => '&quot;',
  }
  # :startdoc:

  # Escape.html_attribute_content escapes a string appropriate for an HTML attribute which is quoted
  # by double-quote.
  #
  # It escapes 4 characters:
  # * '&' to '&amp;'
  # * '<' to '&lt;'
  # * '>' to '&gt;'
  # * '"' to '&quot;'
  #
  # This function is not appropriate for an attribute which is quoted by single-quote.
  def html_attribute_content(str)
    str.gsub(/[&<>"]/) {|ch| HTML_ATTR_ESCAPE_HASH[ch] }
  end

  # Escape.html_attr encodes a string as a double-quoted HTML attribute using character references.
  #
  #  Escape.html_attr("abc") #=> "\"abc\""
  #  Escape.html_attr("a&b") #=> "\"a&amp;b\""
  #
  def html_attr(str)
    '"' + Escape.html_attribute_content(str) + '"'
  end
end
require 'net/https'

module WFO
end

class WFO::Form
  def self.make(form_tree, base_uri, referer_uri=nil, orig_charset=nil)
    action_uri = base_uri + form_tree.get_attr('action')
    method = form_tree.get_attr('method')
    enctype = form_tree.get_attr('enctype')
    accept_charset = form_tree.get_attr('accept-charset')
    form = self.new(action_uri, method, enctype, accept_charset, referer_uri, orig_charset)
    form_tree.traverse_element(
      '{http://www.w3.org/1999/xhtml}input',
      '{http://www.w3.org/1999/xhtml}button',
      '{http://www.w3.org/1999/xhtml}select',
      '{http://www.w3.org/1999/xhtml}textarea') {|control|
      name = control.get_attr('name')
      next if !name
      case control.name
      when '{http://www.w3.org/1999/xhtml}input'
        next if control.get_attr('disabled')
        type = control.get_attr('type')
        type = type ? type.downcase : 'text'
        case type
        when 'text'
          form.add_text(name, control.get_attr('value').to_s)
        when 'hidden'
          form.add_hidden(name, control.get_attr('value').to_s)
        when 'password'
          form.add_password(name, control.get_attr('value').to_s)
        when 'submit'
          form.add_submit_button(name, control.get_attr('value').to_s)
        when 'checkbox'
          checked = control.get_attr('checked') ? :checked : nil
          form.add_checkbox(name, control.get_attr('value').to_s, checked)
        when 'radio'
          checked = control.get_attr('checked') ? :checked : nil
          form.add_radio(name, control.get_attr('value').to_s, checked)
        when 'file'
          form.add_file(name)
        else
          raise "unexpected input type : #{type}"
        end
      when '{http://www.w3.org/1999/xhtml}button'
        next if control.get_attr('disabled')
        raise "unexpected control : #{control.name}"
      when '{http://www.w3.org/1999/xhtml}select'
        next if control.get_attr('disabled')
        multiple = control.get_attr('multiple') ? :multiple : nil
        options = []
        control.traverse_element('{http://www.w3.org/1999/xhtml}option') {|option|
          next if option.get_attr('disabled')
          selected = option.get_attr('selected') ? :selected : nil
          options << [option.get_attr('value'), selected]
        }
        form.add_select(name, multiple, options)
      when '{http://www.w3.org/1999/xhtml}textarea'
        next if control.get_attr('disabled')
        form.add_textarea(name, control.extract_text.to_s)
      else
        raise "unexpected control : #{control.name}"
      end
    }
    form
  end

  def initialize(action_uri, method=nil, enctype=nil, accept_charset=nil, referer_uri=nil, orig_charset=nil)
    @action_uri = action_uri
    method ||= 'get'
    @method = method.downcase
    enctype ||= 'application/x-www-form-urlencoded'
    @enctype = enctype.downcase
    if accept_charset
      @accept_charset = accept_charset.downcase.split(/\s+/)
    elsif orig_charset
      @accept_charset = [orig_charset]
    else
      @accept_charset = ['utf-8']
    end
    @accept_charset.map! {|charset| charset.downcase }
    @controls = []
    @referer_uri = referer_uri
    @orig_charset = orig_charset
  end
  attr_reader :action_uri, :referer_uri

  def add_text(name, value)
    @controls << [name, value, :text]
  end

  def add_hidden(name, value)
    @controls << [name, value, :hidden]
  end

  def add_password(name, value)
    @controls << [name, value, :password]
  end

  def add_submit_button(name, value)
    @controls << [name, value, :submit_button]
  end

  def add_checkbox(name, value, checked)
    @controls << [name, value, :checkbox, checked]
  end

  def add_radio(name, value, checked)
    @controls << [name, value, :radio, checked]
  end

  def add_file(name)
    @controls << [name, nil, :file]
  end

  def add_select(name, multiple, options)
    @controls << [name, options, :select, multiple]
  end

  def add_textarea(name, value)
    @controls << [name, value, :textarea]
  end

  def set(name, value)
    c = @controls.assoc(name)
    raise IndexError, "no control : #{name}" if !c
    c[1] = value.to_str
  end

  def fetch(name)
    c = @controls.assoc(name)
    raise IndexError, "no control : #{name}" if !c
    return c[1]
  end

  def input_type(name)
    c = @controls.assoc(name)
    return nil if !c
    return c[2]
  end

  def get(name)
    c = @controls.assoc(name)
    return nil if !c
    return c[1]
  end

  def has?(name)
    c = @controls.assoc(name)
    !!c
  end

  def each_textarea
    @controls.each {|name, value, type|
      if type == :textarea
        yield name, value
      end
    }
  end

  def make_request(submit_name=nil)
    secrets = []
    case @method
    when 'get'
      case @enctype
      when 'application/x-www-form-urlencoded'
        query = encode_application_x_www_form_urlencoded(submit_name)
        secrets << query
        request_uri = @action_uri.request_uri + "?"
        request_uri += query
        secrets << request_uri
        uri = @action_uri.dup
        if uri.query
          uri.query << '?' << query
        else
          uri.query = query
        end
        req = WFO::ReqHTTP.get(uri)
      else
        raise "unexpected enctype: #{@enctype}"
      end
    when 'post'
      case @enctype
      when 'application/x-www-form-urlencoded'
        query = encode_application_x_www_form_urlencoded(submit_name)
        secrets << query
        req = WFO::ReqHTTP.post(@action_uri, 'application/x-www-form-urlencoded', query)
      else
        raise "unexpected enctype: #{@enctype}"
      end
    else
      raise "unexpected method: #{@method}"
    end
    if @referer_uri
      req['Referer'] = @referer_uri.to_s
    end
    if block_given?
      begin
        yield req
      ensure
        secrets.each {|s|
          KeyRing.vanish!(s)
        }
      end
    else
      req
    end
  end

  def encode_application_x_www_form_urlencoded(submit_name=nil)
    successful = []
    has_submit = false
    @controls.each {|name, value, type, *rest|
      case type
      when :submit_button
        if !has_submit && name == submit_name
          successful << [name, value]
          has_submit = true
        end
      when :checkbox, :radio
        checked = rest[0]
        successful << [name, value] if checked
      when :text, :textarea, :password, :hidden
        successful << [name, value]
      when :select
        selected_options = []
        value.each {|option, selected| selected_options << option if selected }
        selected_options.each {|option| successful << [name, option] }
      else
        raise "unexpected control type: #{type}"
      end
    }
    accept_charset = @accept_charset.dup
    charset = accept_charset.shift
    begin
      encoded_successful = successful.map {|name, value|
        [name.encode_charset_exactly(charset), value.encode_charset_exactly(charset)]
      }
    rescue Iconv::Failure
      if charset = accept_charset.shift
        retry
      else
        encoded_successful = successful
      end
    end
    Escape.html_form(encoded_successful)
  end
end
#require 'wfo/cookie'
# wfo/cookie - HTTP cookie handling library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

module WFO
end

class WFO::Cookie
  AttrPat = /[^=;,]+/
  QuotedStringPat = /"[\r\n\t !#-\377]*"/
  ObsValuePat = /[A-Za-z]{3}, \d\d-[A-Za-z]{3}-\d\d(?:\d\d+)? \d\d:\d\d:\d\d GMT/
  ValuePat = /#{ObsValuePat}|#{QuotedStringPat}|[^;,]*/

  def self.parse(request_uri, field_value)
    self.split(field_value).map {|pairs|
      self.new(request_uri, pairs)
    }

  end

  def self.split(field_value)
    cookies = [[]]
    field_value.scan(/(#{AttrPat})\s*(?:=\s*(#{ValuePat})\s*)?([;,])?/) {|attr, value, term|
      attr = attr.strip
      cookies.last << [attr, value]
      if term == ','
        cookies << []
      end
    }
    cookies.pop if cookies.last == []
    cookies
  end

  def initialize(request_uri, pairs)
    @request_uri = request_uri
    @pairs = pairs
    pair = @pairs.find {|k, v| /\Adomain\z/i =~ k }
    if !pair || /\A\d+(?:\.\d+)+\z/ =~ request_uri.host
      @domain = request_uri.host
      @domain_pat = /\A#{Regexp.quote @domain}\z/i
    else
      cookie_domain = pair[1]
      if /\A\./ !~ cookie_domain
        raise ArgumentError, "An cookie domain not started with a dot: #{cookie_domain}"
      end
      if /\..*\./ !~ cookie_domain
        raise ArgumentError, "An cookie domain needs more dots: #{cookie_domain}"
      end
      if /#{Regexp.quote cookie_domain}\z/ !~ request_uri.host
        raise ArgumentError, "An cookie domain is not match: #{cookie_domain} is not suffix of #{request_uri.host}"
      end
      @domain = cookie_domain
      @domain_pat = /#{Regexp.quote cookie_domain}\z/i
    end
    pair = @pairs.find {|k, v| /\Apath\z/i =~ k }
    if !pair
      @path = request_uri.path.sub(%r{[^/]*\z}, '')
      @path_pat = /\A#{Regexp.quote @path}/
    else
      cookie_path = pair[1]
      sep = %r{/\z} =~ cookie_path ? "" : '(\z|/)'
      if %r{\A#{Regexp.quote cookie_path}#{sep}} !~ request_uri.path
        raise ArgumentError, "An cookie path is not match: #{cookie_path} is not prefix of #{request_uri.path}"
      end
      @path = cookie_path
      @path_pat = /\A#{Regexp.quote cookie_path}#{sep}/
    end
  end
  attr_reader :domain, :path

  def match?(uri)
    return false if @domain_pat !~ uri.host
    return false if @path_pat !~ uri.path
    return false if @pairs.find {|k, v| /\Asecure\z/ =~ k } && uri.scheme != 'https'
    true
  end

  def name
    @pairs[0][0]
  end

  def value
    @pairs[0][1]
  end

  def encode_cookie_field
    name, value = @pairs[0]
    "#{name}=#{value}"
  end
end
#require 'wfo/auth'
# wfo/auth.rb - authentication library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

module WFO::Auth
  @reqauth_checker = []
  @auth_handler = []

  def self.added(name)
    name = name.to_s
    if /_reqauth_checker\z/ =~ name
      @reqauth_checker << method(name)
    elsif /_auth_handler\z/ =~ name
      @auth_handler << method(name)
    end
  end
end

class << WFO::Auth
  attr_reader :reqauth_checker
  attr_reader :auth_handler

  def singleton_method_added(name)
    WFO::Auth.added(name)
  end
end

module WFO::Auth
  def self.codeblog_auth_handler(webclient, response)
    uri = response.uri
    unless response.code == '403' &&
           uri.scheme == 'https' &&
           uri.host == 'www.codeblog.org' &&
           uri.port == 443
      return nil
    end
    apache_authtypekey_handler(webclient, response)
  end

  def self.apache_authtypekey_handler(webclient, response)
    uri = response.uri
    errpage = response.body
    return nil if />Log in via TypeKey</ !~ errpage
    # It seems a login page generated by login.pl in Apache-AuthTypeKey.
    typekey_uri = nil
    HTree(errpage).traverse_element("{http://www.w3.org/1999/xhtml}a") {|e|
      if href = e.get_attr('href')
        href = URI(href)
        if href.host == 'www.typekey.com'
          typekey_uri = href
          break
        end
      end
    }
    return nil if !typekey_uri

    response = typekey_login(webclient, typekey_uri)
    return nil if response.code != '302'
    #destination_uri = URI(resp['Location'])

    # use uri instead of destination_uri because www.codeblog.org's login.pl
    # had a URI escaping problem.

    return WFO::ReqHTTP.get(uri)
  end

  def self.typekey_login(webclient, typekey_uri)
    typekey_login_form = nil
    HTree(typekey_uri).traverse_element('{http://www.w3.org/1999/xhtml}form') {|form|
      form = WFO::Form.make(form, typekey_uri)
      if form.has?('username') && form.has?('password')
        typekey_login_form = form
        break
      end
    }
    return nil if !typekey_login_form
    resp = nil
    KeyRing.with_authinfo(KeyRing.typekey_protection_domain) {|username, password|
      typekey_login_form.set('username', username)
      typekey_login_form.set('password', password)
      typekey_login_form.make_request {|req|
        resp = webclient.do_request_state(req)
      }
    }
    # The password vanishing is not perfect, unfortunately.
    # arr = []; ObjectSpace.each_object(String) {|s| arr << s }; arr.each {|v| p v }

    if resp.code == '200' # send email address or not?
      email_form = nil
      HTree(resp.body).traverse_element('{http://www.w3.org/1999/xhtml}form') {|form|
        email_form = WFO::Form.make(form, typekey_login_form.action_uri)
        break
      }
      req = email_form.make_request
      resp = webclient.do_request_state(req)
    end

    return nil if resp.code != '302'
    return_uri = URI(resp['Location'])

    webclient.do_request_state(WFO::ReqHTTP.get(return_uri))
  end
end

module WFO
  def Auth.http_basic_auth_handler(webclient, response)
    uri = response.uri
    unless response.code == '401' &&
           response['www-authenticate'] &&
           response['www-authenticate'] =~ /\A\s*#{Pat::HTTP_Challenge}s*\z/n
      return nil
    end
    auth_scheme = $1
    rest = $2
    params = []
    while /\A#{Pat::HTTP_AuthParam}(?:(?:\s*,\s*)|\s*\z)/ =~ rest
      rest = $'
      k = $1
      v = $3 ? $3.gsub(/\\([\000-\377])/) { $1 } : $2
      params << [k, v]
    end
    return nil if /\Abasic\z/i !~ auth_scheme
    return nil if params.length != 1
    k, v = params[0]
    return nil if /\Arealm\z/i !~ k
    realm = v
    protection_domain = KeyRing.http_protection_domain(uri, 'basic', realm)
    canonical_root_url = protection_domain[0]
    KeyRing.with_authinfo(protection_domain) {|username, password|
      user_pass = "#{username}:#{password}"
      credential = [user_pass].pack("m")
      KeyRing.vanish!(user_pass)
      credential.gsub!(/\s+/, '')
      path_pat = /\A#{uri.path.sub(%r{[^/]*\z}, '')}/
      webclient.add_basic_credential(canonical_root_url, realm, path_pat, credential)
    }
    webclient.make_request_basic_authenticated(response.request) # xxx: update req destructively.
    return response.request
  end
end
#require 'keyring'
# keyring.rb - password storage library
#
# Copyright (C) 2006,2007 Tanaka Akira  <akr@fsij.org>
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#  1. Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#  3. The name of the author may not be used to endorse or promote products
#     derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

require 'pathname'
require 'digest/sha2'
#require 'escape'
autoload :Etc, 'etc'

# = keyring - manage encrypted storage for authentication information
#
# The keyring library stores authentication information such as username and
# passwords in a keyring directory in encrypted form.
#
# The keyring directory is ~/.keyring by default.
#
# gpg is used for the encryption.
#
# You needs your public and secret key for the encryption.
# (Use "gpg --gen-key" if you don't have one yet.)
#
# == How to specify your authentication information in the keyring.
#
# The keyring library uses ASCII armored gpg encrypted file to
# store passwords and related data.
#
# Comment field is used to select the file. 
#
# ~/.keyring/foobar.asc :
#   -----BEGIN PGP MESSAGE-----
#   Version: GnuPG v1.4.5 (GNU/Linux)
#   Comment: non-encrypted-prefix-of-the-strings
#
#   ... encrypted-sequence-of-strings ...
#   -----END PGP MESSAGE-----
#
# === Example 1.  TypeKey
#
# The following example stores a username and password for TypeKey.
# <http://www.sixapart.jp/typekey/>
#
#  % mkdir ~/.keyring
#  % cd ~/.keyring
#  % echo TypeKey typekey-username typekey-password |
#    gpg --comment TypeKey -e -a --default-recipient-self > typekey.asc
#
# It creates a file ~/.keyring/typekey.asc as follows.
#
#   -----BEGIN PGP MESSAGE-----
#   Version: GnuPG v1.4.5 (GNU/Linux)
#   Comment: TypeKey
#
#   ... "TypeKey typekey-username typekey-password\n" in encrypted form ...
#   -----END PGP MESSAGE-----
#
# Now, KeyRing.with_authinfo("TypeKey") {|username, password| ... }
# can be used to retrieve the typekey-username and typekey-password.
# It use gpg to decrypt the file.
# So gpg may ask you a passphrase of your key.
#
# === Example 2.  HTTP Basic Authentication
#
#  % echo http://www.example.org basic "realm" username password |
#    gpg --comment 'http://www.example.org basic "realm" username' -e -a --default-recipient-self > example-org.asc
#
# It creates a file ~/.keyring/example-org.asc as follows.
#
#   -----BEGIN PGP MESSAGE-----
#   Version: GnuPG v1.4.5 (GNU/Linux)
#   Comment: http://www.example.org basic "realm" username
#
#   ... "http://www.example.org basic "realm" username password\n" in encrypted form ...
#   -----END PGP MESSAGE-----
#
# Now, KeyRing.with_authinfo can be used to lookup username and password.
#
#   KeyRing.with_authinfo("http://www.example.org", "basic", "realm", "username") {|password| ... }
#
# It is possible to lookup username AND password as follows.
#
#   KeyRing.with_authinfo("http://www.example.org", "basic", "realm") {|username, password| ... }
#
# It is also possible to lookup realm and authentication scheme.
#
#   KeyRing.with_authinfo("http://www.example.org", "basic") {|realm, username, password| ... }
#   KeyRing.with_authinfo("http://www.example.org") {|auth_scheme, realm, username, password| ... }
#
# == Keyring Directory Layout and File Format
#
# The keyring directory is ~/.keyring.
#
# ~/.keyring may have any number of authentication information file.
# The file must be named with ".asc" suffix.
#
# The keyring library searches ~/.keyring/*.asc for authentication information.
# The filename is not important.
#
# The authentication information file should be ASCII armored gpg encrypted file as follows.
#
# ~/.keyring/foobar.asc :
#   -----BEGIN PGP MESSAGE-----
#   Version: GnuPG v1.4.5 (GNU/Linux)
#   Comment: non-encrypted-prefix
#
#   ... encrypted-sequence-of-strings ...
#   -----END PGP MESSAGE-----
#
# The file should contain Comment field and encrypted contents.
#
# The encrypted contents should be sequence of strings separated by white spaces.
# (The syntax of the strings is described later.)
#
#   Example: A B C D
#
# The Comment field should contain prefix of the sequence of strings.
#
#   Example: A B C
#   Example: A B
#   Example: A
#
# Each string in the Comment field can be a hexadecimal SHA256 hash prepended with "sha256:" prefix.
#
#   Example: sha256:559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd B
#   Example: A sha256:df7e70e5021544f4834bbee64a9e3789febc4be81470df629cad6ddb03320a5c
#   Example: sha256:559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd sha256:df7e70e5021544f4834bbee64a9e3789febc4be81470df629cad6ddb03320a5c
#
# A string contained in the Comment field and encrypted contents must be one of following forms.
#
# * A string not containing a white space and beginning with a digit or
#   alphabet.
#   /[0-9A-Za-z][!-~]*/ 
#
# * A string quoted by double quotes "...".
#   The string content may contain printable ASCII character including space
#   and escape sequences \\, \" and \xHH.
#   /"((?:[ !#-\[\]-~]|\\["\\]|\\x[0-9a-fA-F][0-9a-fA-F])*)"/
#
# * A white space is one of space, tab, newline, carriage return, form feed.
#   /\s/
#
# == Convention of Authentication Information
#
# Although the keyring library itself doesn't define the semantics of the sequence of strings, 
# it is useful to standardize the usage of the strings.
#
# So the keyring library provides convenience methods to make protection domains
# for TypeKey and HTTP Authentication.
# They returns a protection domain appropriate for an argument of KeyRing.with_authinfo.
#
# * KeyRing.typekey_protection_domain
#
#   For TypeKey authentication, protection domain is ["TypeKey"].
#   The authentication information, username and password, can be stored as follows.
#
#    % echo TypeKey typekey-username typekey-password |
#      gpg --comment TypeKey -e -a --default-recipient-self > typekey.asc
#
# * KeyRing.http_protection_domain(uri, scheme, realm)
#
#   For HTTP authentication, protection domain is [canonical-root-URL, scheme, realm].
#   In Basic authentication, "basic" is used for the scheme.
#
#   The Basic authentication information, username and password, can be stored as follows.
#
#    % echo 'canonical-root-url basic "realm" username password' |
#      gpg --comment 'canonical-root-URL basic "realm"' -e -a --default-recipient-self > service.asc
#
# === Method
#
# * KeyRing.with_authinfo(protection_domain) {|authentication_informaion| ... }
# * KeyRing.typekey_protection_domain
# * KeyRing.http_protection_domain(uri, scheme, realm)
#

class KeyRing
  # KeyRing.with_authinfo takes one or more strings as the argument.
  # protection_domain can be a string or an array of strings.
  #
  # protection_domain is compared to the Comment fields in ~/.keyring/*.asc.
  # If a matched Comment field is found, the corresponding file is decrypted to obtain
  # the authentication information represented as a sequence of strings using gpg.
  #
  # KeyRing.with_authinfo yields the sequence of strings excluded with
  # beginning words given with protection_domain.
  #
  def self.with_authinfo(protection_domain, &block) # :yields: authentication_information
    self.new.with_authinfo(protection_domain, &block)
  end

  # :stopdoc:

  def self.decrypt_file(path)
    `#{Escape.shell_command(%W[gpg -d -q #{path}])}`
  end

  def initialize(dir=nil)
    unless dir
      home = ENV['HOME'] || Etc.getpwuid.dir
      dir = "#{home}/.keyring"
    end
    @dir = Pathname.new(dir)
  end

  def with_authinfo(protection_domain) # :yield: password
    protection_domain = [protection_domain] if String === protection_domain
    path = search_encrypted_file(protection_domain)
    s = KeyRing.decrypt_file(path)
    if $? != 0
      KeyRing.vanish!(s)
      raise AuthInfoNotFound, "gpg failed with #{$?}"
    end
    begin
      authinfo = KeyRing.decode_strings_safe(s)
      KeyRing.vanish!(s)
      s = nil
      if protection_domain.length <= authinfo.length &&
         authinfo[0, protection_domain.length] == protection_domain
        authinfo[0, protection_domain.length].each {|v| KeyRing.vanish!(v) }
        authinfo[0, protection_domain.length] = []
      end
      ret = yield *authinfo
    ensure
      KeyRing.vanish!(s) if s
      authinfo.each {|v| KeyRing.vanish!(v) } if authinfo
    end
    ret
  end

  def match_protection_domain(given, spec)
    given == spec ||
    (/\Asha256:/ =~ spec && $' == Digest::SHA256.hexdigest(given))
  end

  def search_encrypted_file(protection_domain)
    paths = @dir.children.sort_by {|path| path.to_s }
    paths.each {|path|
      next if path.extname != '.asc'
      path.each_line {|line|
        break if line == "\n"
        if /^Comment:/ =~ line
          prefix = KeyRing.decode_strings($')
          next if prefix.length < protection_domain.length
          if protection_domain.zip(prefix).all? {|s, t| match_protection_domain(s, t) }
            return path
          end
        end
      }
    }
    raise AuthInfoNotFound, "authentication information not found in #{@dir}: #{KeyRing.encode_strings protection_domain}" 
  end

  # :startdoc:

  class AuthInfoNotFound < StandardError
  end

  # KeyRing.typekey_protection_domain returns ["TypeKey"].
  def self.typekey_protection_domain
    ["TypeKey"]
  end

  # KeyRing.http_protection_domain returns [canonical-root-URL-of-given-uri, scheme, realm]
  def self.http_protection_domain(uri, scheme, realm)
    uri = uri.dup
    # make it canonical root URL
    uri.path = ""
    uri.query = nil
    uri.fragment = nil
    [uri.to_s, scheme, realm]
  end

  # :stopdoc:

  def self.encode_strings(strings)
    strings.map {|s|
      if /\A[0-9A-Za-z][!-~]*\z/ =~ s
        s
      else
        '"' +
        s.gsub(/[^ !#-\[\]-~]/n) {|ch|
          case ch
          when /["\\]/
            '\\' + ch
          else
            '\x' + ch.unpack("H2")[0]
          end
        } +
        '"'
      end
    }.join(' ')
  end

  RawStrPat = /[0-9A-Za-z][!-~]*/ 
  QuotedStrPat = /"((?:[ !#-\[\]-~]|\\["\\]|\\x[0-9a-fA-F][0-9a-fA-F])*)"/
  def self.decode_strings(str)
    s = str
    r = []
    until /\A\s*\z/ =~ s
      case s
      when /\A\s*(#{RawStrPat})(?:\s+|\z)/o
        s = $'
        r << $1
      when /\A\s*(#{QuotedStrPat})(?:\s+|\z)/o
        s = $'
        r << $2.gsub(/\\(["\\])|\\x([0-9a-fA-F][0-9a-fA-F])/) { $1 || [$2].pack("H2") }
      else
        raise ArgumentError, "strings syntax error: #{str.inspect}"
      end
    end
    r
  end

  Spaces = [?\s, ?\n, ?\t]

  # KeyRing.decode_strings_safe is same as KeyRing.decode_strings except
  # it doesn't retain temporally strings which contains a part of the argument.
  # Single character strings may retains, though.
  def self.decode_strings_safe(str)
    r = []
    i = 0
    len = str.length
    while i < len
      ch = str[i]
      i += 1
      next if Spaces.include? ch
      case ch
      when ?0..?9, ?A..?Z, ?a..?z
        s = ch.chr
        r << s
        while i < len && !Spaces.include?(str[i])
          s << str[i].chr
          i += 1
        end
      when ?"
        s = ""
        r << s
        while true
          raise ArgumentError, "strings syntax error" if i == len
          ch = str[i]
          i += 1
          if ?" === ch
            break
          elsif ?\\ === ch
            if i < len
              ch = str[i]
              i += 1
              case ch
              when ?", ?\\
                s << ch.chr
              when ?x
                if i+1 < len
                  ch1 = str[i]
                  raise ArgumentError, "strings syntax error" if /\A[0-9a-fA-F]\z/n !~ ch1.chr
                  ch2 = str[i+1]
                  raise ArgumentError, "strings syntax error" if /\A[0-9a-fA-F]\z/n !~ ch2.chr
                  s << (ch1.chr.to_i(16) * 16 + ch2.chr.to_i(16)).chr
                  i += 2
                else
                  raise ArgumentError, "strings syntax error"
                end
              else
                raise ArgumentError, "strings syntax error"
              end
            else
              raise ArgumentError, "strings syntax error"
            end
          elsif /\A[\s!#-\[\]-~]\z/ =~ ch.chr
            s << ch.chr
          else
            raise ArgumentError, "strings syntax error"
          end
        end
        if i < len && !Spaces.include?(str[i])
          raise ArgumentError, "strings syntax error"
        end
      else
        raise ArgumentError, "strings syntax error"
      end
    end
    r
  ensure
    if $!
      r.each {|s| KeyRing.vanish!(s) }
    end
  end

  def self.vanish!(s)
    0.upto(s.length-1) {|i|
    s[i] = ?\0
    }
    s.replace ""
  end

  # :startdoc:
end

class WFO::WebClient
  def self.do
    webclient = self.new
    old = Thread.current[:webclient]
    begin
      Thread.current[:webclient] = webclient
      yield
    ensure
      Thread.current[:webclient] = old
    end
  end

  def self.read(uri, opts={})
    Thread.current[:webclient].read(uri, opts)
  end

  def self.read_decode(uri, opts={})
    Thread.current[:webclient].read_decode(uri, opts)
  end

  def self.read_decode_nocheck(uri, opts={})
    Thread.current[:webclient].read_decode_nocheck(uri, opts)
  end

  def self.do_request(request)
    Thread.current[:webclient].do_request(request)
  end

  def initialize
    @basic_credentials = {}
    @cookies = {}
  end

  def add_basic_credential(canonical_root_url, realm, path_pat, credential)
    @basic_credentials[canonical_root_url] ||= []
    @basic_credentials[canonical_root_url] << [realm, path_pat, credential]
  end

  def make_request_basic_authenticated(request)
    canonical_root_url = request.uri.dup
    canonical_root_url.path = ""
    canonical_root_url.query = nil
    canonical_root_url.fragment = nil
    canonical_root_url = canonical_root_url.to_s
    return if !@basic_credentials[canonical_root_url]
    path = request.uri.path
    @basic_credentials[canonical_root_url].each {|realm, path_pat, credential|
      if path_pat =~ path
        request['Authorization'] = "Basic #{credential}"
        break
      end
    }
  end

  def update_cookies(uri, set_cookie_field)
    cs = WFO::Cookie.parse(uri, set_cookie_field)
    cs.each {|c|
      key = [c.domain, c.path, c.name].freeze
      @cookies[key] = c
    }
  end

  def insert_cookie_header(request)
    cs = @cookies.reject {|(domain, path, name), c| !c.match?(request.uri) }
    if !cs.empty?
      request['Cookie'] = cs.map {|(domain, path, name), c| c.encode_cookie_field }.join('; ')
    end
  end

  def do_request(request)
    results = do_redirect_requests(request)
    results.last.last
  end

  def do_redirect_requests(request)
    results = []
    while true
      response = do_request_state(request)
      results << [request, response]
      if /\A(?:301|302|303|307)\z/ =~ response.code && response['location']
        # RFC 1945 - Hypertext Transfer Protocol -- HTTP/1.0
        #  301 Moved Permanently
        #  302 Moved Temporarily
        # RFC 2068 - Hypertext Transfer Protocol -- HTTP/1.1
        #  301 Moved Permanently
        #  302 Moved Temporarily
        #  303 See Other
        # RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1
        #  301 Moved Permanently
        #  302 Found
        #  303 See Other
        #  307 Temporary Redirect
        redirect = URI(response['location'])
        # Although it violates RFC2616, Location: field may have relative
        # URI.  It is converted to absolute URI using uri as a base URI.
        redirect = request.uri + redirect if redirect.relative?
        request = WFO::ReqHTTP.get(redirect)
      else
        break
      end
    end
    results
  end

  def do_request_state(request)
    make_request_basic_authenticated(request)
    insert_cookie_header(request)
    resp = do_request_simple(request)
    update_cookies(request.uri, resp['Set-Cookie']) if resp['Set-Cookie']
    resp
  end

  def do_request_simple(req)
    if proxy_uri = req.uri.find_proxy
      # xxx: proxy authentication
      klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port)
    else
      klass = Net::HTTP
    end
    h = klass.new(req.uri.host, req.uri.port)
    if req.uri.scheme == 'https'
      h.use_ssl = true
      h.verify_mode = OpenSSL::SSL::VERIFY_PEER
      store = OpenSSL::X509::Store.new
      store.set_default_paths
      h.cert_store = store
    end
    h.start {
      if req.uri.scheme == 'https'
        sock = h.instance_variable_get(:@socket)
        if sock.respond_to?(:io)
          sock = sock.io # 1.9
        else
          sock = sock.instance_variable_get(:@socket) # 1.8
        end
        sock.post_connection_check(req.uri.host)
      end
      req.do_http(h)
    }
  end

  def read(uri, header={})
    request = WFO::ReqHTTP.get(uri)
    header.each {|k, v| request[k] = v }

    while true
      response = do_request(request)
      break if response.code == '200' &&
               WFO::Auth.reqauth_checker.all? {|checker|
                 !checker.call(self, response)
               }
      request = nil
      WFO::Auth.auth_handler.each {|h|
        if request = h.call(self, response)
          break
        end
      }
      if request == nil
        raise "no handler for #{response.code} #{response.message} in #{response.uri}"
      end
    end

    result = response.body
    OpenURI::Meta.init result
    result.status = [response.code, response.message]
    result.base_uri = response.uri # xxx: Content-Location
    response.each {|name,value| result.meta_add_field name, value }
    result
  end

  def read_decode(uri, header={})
    page_str = self.read(uri, header)
    unless charset = page_str.charset
      charset = page_str.guess_charset
    end
    result = page_str.decode_charset(charset)
    round_trip = result.encode_charset(charset)
    if page_str != round_trip
      raise "cannot decode in round trip manner: #{uri}"
    end
    OpenURI::Meta.init result, page_str
    return result, charset
  end

  def read_decode_nocheck(uri, header={})
    page_str = self.read(uri, header)
    unless charset = page_str.charset
      charset = page_str.guess_charset
    end
    result = page_str.decode_charset(charset)
    OpenURI::Meta.init result, page_str
    return result, charset
  end
end

module WFO
  class ReqHTTP
    def self.get(uri)
      self.new('GET', uri)
    end

    def self.post(uri, content_type, query)
      self.new('POST', uri, {'Content-Type'=>content_type}, query)
    end

    def initialize(method, uri, header={}, body=nil)
      @method = method.upcase
      @uri = uri
      @header = header
      @body = body
    end
    attr_reader :uri

    def []=(field_name, field_value)
      @header[field_name] = field_value
    end

    def do_http(http)
      case @method
      when "GET"
        req = Net::HTTP::Get.new(@uri.request_uri)
        @header.each {|field_name, field_value| req[field_name] = field_value }
        resp = http.request(req)
        result = WFO::RespHTTP.new(self, resp)
      when "POST"
        req = Net::HTTP::Post.new(@uri.request_uri)
        @header.each {|field_name, field_value| req[field_name] = field_value }
        resp = http.request(req, @body)
        result = WFO::RespHTTP.new(self, resp)
      else
        raise ArgumentError, "unexpected method: #{@method}"
      end
      result
    end
  end

  class RespHTTP
    def initialize(request, resp)
      @request = request
      @resp = resp
    end
    attr_reader :request

    def uri
      @request.uri
    end

    def code
      @resp.code
    end

    def message
      @resp.message
    end

    def [](field_name)
      @resp[field_name]
    end

    def each
      @resp.each {|field_name, field_value|
        yield field_name, field_value
      }
      nil
    end

    def body
      @resp.body
    end
  end
end

#require 'wfo/repo'
# wfo/repo.rb - repository framework
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

class WFO::Repo
  @repo_classes = []

  def self.inherited(subclass)
    @repo_classes << subclass
  end

  def self.repo_classes
    @repo_classes
  end

  def self.type
    self.to_s.sub(/\A.*::/, '').downcase
  end

  def self.available_types
    @repo_classes.map {|c| c.type }
  end

  def self.fetch_class(type)
    @repo_classes.each {|c|
      return c if c.type == type
    }
    raise "repository class not found: #{type}"
  end

  def self.find_class_and_stable_uri(url, type=nil)
    page = WFO::WebClient.read(url)
    if type
      c = fetch_class(type)
      stable_uri = c.find_stable_uri(page)
      return c, stable_uri
    else
      @repo_classes.each {|c|
        if c.applicable?(page)
          stable_uri = c.find_stable_uri(page)
          return c, stable_uri
        end
      }
    end
    raise "unknown repository type : #{url}"
  end

  def self.make_accessor(url, type=nil)
    c, stable_uri = find_class_and_stable_uri(url, type)
    return c.make_accessor(stable_uri)
  end
end

module WFO::RepoTextArea
  def initialize(form, uri, textarea_name, submit_name)
    @form = form
    @uri = uri
    @textarea_name = textarea_name
    @submit_name = submit_name
  end
  attr_reader :form, :textarea_name

  def current_text
    @form.fetch(@textarea_name).dup
  end

  def replace_text(text)
    @form.set(@textarea_name, text)
  end

  def commit
    req = @form.make_request(@submit_name)
    resp = WFO::WebClient.do_request(req)
    return if resp.code == '200'
    raise "HTTP POST error: #{resp.code} #{resp.message}"
  end

  def reload
    self.class.make_accessor(@uri)
  end
end

#require 'wfo/repo/tdiary'
# wfo/repo/tdiary.rb - tDiary repository library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'htree'

class WFO::TDiary < WFO::Repo
  def self.applicable?(page)
    /<meta name="generator" content="tDiary/ =~ page
  end

  def self.find_stable_uri(page)
    if /<span class="adminmenu"><a href="(update.rb\?edit=true;year=\d+;month=\d+;day=\d+)">/ =~ page
      page.base_uri + $1 # it assumes base element not exist.
    elsif /<span class="adminmenu"><a href="update.rb">/ =~ page
      now = Time.now
      page.base_uri + "update.rb?edit=true;year=#{now.year};month=#{now.month};day=#{now.day}"
    else
      raise "update link not found in tDiary page : #{page.last_request_uri}"
    end
  end

  def self.make_accessor(uri)
    page_str, orig_charset = WFO::WebClient.read_decode_nocheck(uri)
    page_tree = HTree(page_str)
    if page_str.last_request_uri != uri
      raise "tDiary update page redirected"
    end
    form, textarea_name, submit_name = find_replace_form(page_tree, orig_charset)
    self.new(form, uri, textarea_name, submit_name)
  end

  def self.find_replace_form(page, orig_charset)
    page.traverse_html_form(orig_charset) {|form|
      next unless form.input_type('replace') == :submit_button
      form.each_textarea {|name, value| return form, name, 'replace' }
    }
    raise "replace form not found in #{page.request_uri}"
  end

  def recommended_filename
    y = @form.fetch('year')
    m = @form.fetch('month')
    d = @form.fetch('day')
    "%d-%02d-%02d" % [y, m, d]
  end

  include WFO::RepoTextArea
end
#require 'wfo/repo/qwik'
# wfo/repo/qwik.rb - qwikWeb repository library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'htree'

class WFO::Qwik < WFO::Repo
  def self.applicable?(page)
    %r{>powered by <a href="http://qwik.jp/"\n>qwikWeb</a} =~ page
  end

  def self.find_stable_uri(page)
    last_request_uri = page.last_request_uri.to_s
    if /\.html\z/ =~ last_request_uri
      return URI(last_request_uri.sub(/\.html\z/, '.edit'))
    else
      tree = HTree(page)
      tree.traverse_element("{http://www.w3.org/1999/xhtml}a") {|e|
        href = e.get_attr('href')
        if href && /\.edit\z/ =~ href
          return page.last_request_uri + href
        end
      }
    end
    raise "edit page could not find : #{last_request_uri}"
  end

  def self.make_accessor(uri)
    page_str, orig_charset = WFO::WebClient.read_decode(uri)
    page_tree = HTree(page_str)
    if page_str.last_request_uri != uri
      raise "qwikWeb edit page redirected"
    end
    form, textarea_name, submit_name = find_textarea_form(page_tree, orig_charset)
    self.new(form, uri, textarea_name, submit_name)
  end

  def self.find_textarea_form(page, orig_charset)
    page.traverse_html_form(orig_charset) {|form|
      next unless form.input_type('save') == :submit_button
      form.each_textarea {|name, value| return form, name, 'save' }
    }
    raise "textarea not found in #{page.request_uri}"
  end

  def recommended_filename
    @form.action_uri.to_s.sub(%r{\A.*/}, '').sub(/\.save\z/, '')
  end

  include WFO::RepoTextArea
end

module WFO::Auth
  def self.qwik_reqauth_checker(webclient, resp)
    %r{<a href=".login"\n>Login</a\n>} =~ resp.body
  end

  def self.qwik_auth_handler(webclient, resp)
    qwik_auth_handler_typekey(webclient, resp)
  end

  def self.qwik_auth_handler_typekey(webclient, resp)
    uri = resp.uri
    unless %r{>powered by <a href="http://qwik.jp/"\n>qwikWeb</a} =~ resp.body
      return nil
    end
    unless %r{<a href="\.login"\n>Login</a\n>} =~ resp.body
      return nil
    end
    qwik_login_uri = uri + ".login"
    resp = webclient.do_request_state(WFO::ReqHTTP.get(qwik_login_uri))
    if resp.code == '200'
      qwik_typekey_uri = nil
      HTree(resp.body).traverse_element("{http://www.w3.org/1999/xhtml}a") {|e|
        if e.extract_text.to_s == "Login by TypeKey"
          qwik_typekey_uri = qwik_login_uri + e.get_attr('href')
        end
      }
      return nil if !qwik_typekey_uri
    elsif resp.code == '302' && %r{/\.typekey\z} =~ resp['Location']
      # "https://www.codeblog.org/.typekey"
      # "https://www.codeblog.org/wg-chairs/.typekey"
      qwik_typekey_uri = URI(resp['Location'])
    else
      return nil
    end

    resp = webclient.do_request_state(WFO::ReqHTTP.get(qwik_typekey_uri))
    return nil if resp.code != '302'
    typekey_uri = URI(resp['Location'])

    resp = WFO::Auth.typekey_login(webclient, typekey_uri)

    if resp.code == '302' # codeblog
      codeblog_uri = URI(resp['Location'])
      resp = webclient.do_request_state(WFO::ReqHTTP.get(codeblog_uri))
    end

    return nil if resp.code != '200'

    return WFO::ReqHTTP.get(uri)
  end
end
#require 'wfo/repo/trac'
# wfo/repo/trac.rb - Trac's Wiki repository library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'htree'

class WFO::Trac < WFO::Repo
  def self.applicable?(page)
    %r{<a id="tracpowered" href="http://trac.edgewall.com/">} =~ page
  end

  def self.find_stable_uri(page)
    u = page.last_request_uri.dup
    u.query = 'action=edit'
    u
  end

  def self.make_accessor(uri)
    page_str, orig_charset = WFO::WebClient.read_decode(uri)
    page_tree = HTree(page_str)
    if page_str.last_request_uri != uri
      raise "Trac edit page redirected"
    end
    form, textarea_name, submit_name = find_textarea_form(page_tree, orig_charset)
    self.new(form, uri, textarea_name, submit_name)
  end

  def self.find_textarea_form(page, orig_charset)
    page.traverse_html_form(orig_charset) {|form|
      next unless form.input_type('save') == :submit_button
      form.each_textarea {|name, value| return form, name, 'save' }
    }
    raise "textarea not found in #{page.request_uri}"
  end

  def recommended_filename
    @form.action_uri.to_s.sub(%r{\A.*/}, '')
  end

  include WFO::RepoTextArea
end

module WFO::Auth
  def self.trac_auth_handler(webclient, resp)
    uri = resp.uri

    unless %r{<a id="tracpowered" href="http://trac.edgewall.com/">} =~ resp.body
      return nil
    end
    if resp.code != '403'
      return nil
    end

    trac_login_uri = nil
    HTree(resp.body).traverse_element("{http://www.w3.org/1999/xhtml}a") {|e|
      if e.extract_text.to_s == "Login"
        trac_login_uri = uri + e.get_attr('href')
        break
      end
    }
    return nil if !trac_login_uri
    webclient.read(trac_login_uri)

    return WFO::ReqHTTP.get(uri)
  end
end
#require 'wfo/repo/pukiwiki'
# wfo/repo/pukiwiki.rb - PukiWiki repository library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'htree'

class WFO::PukiWiki < WFO::Repo
  def self.applicable?(page)
    %r{Based on "PukiWiki"} =~ page
  end

  def self.find_stable_uri(page)
    tree = HTree(page)
    tree.traverse_element("{http://www.w3.org/1999/xhtml}a") {|e|
      href = e.get_attr('href')
      if href && /\?cmd=edit&/ =~ href
        return page.last_request_uri + href
      end
    }
    raise "edit page could not find : #{last_request_uri}"
  end

  def self.make_accessor(uri)
    page_str, orig_charset = WFO::WebClient.read_decode(uri)
    page_tree = HTree(page_str)
    if page_str.last_request_uri != uri
      raise "edit page redirected"
    end
    form, textarea_name, submit_name = find_textarea_form(page_tree, orig_charset)
    self.new(form, uri, textarea_name, submit_name)
  end

  def self.find_textarea_form(page, orig_charset)
    page.traverse_html_form(orig_charset) {|form|
      next unless form.input_type('write') == :submit_button
      form.each_textarea {|name, value| return form, name, 'write' }
    }
    raise "textarea not found in #{page.request_uri}"
  end

  def recommended_filename
    @uri.query.sub(/\A.*&page=/, '')
  end

  include WFO::RepoTextArea
end

#require 'wfo/repo/textarea'
# wfo/repo/textarea.rb - simple HTML textarea repository library
#
# Copyright (C) 2006 Tanaka Akira  <akr@fsij.org>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'htree'

class WFO::TextArea < WFO::Repo
  def self.applicable?(page)
    %r{<textarea}i =~ page
  end

  def self.find_stable_uri(page)
    u = page.last_request_uri
  end

  def self.make_accessor(uri)
    page_str, orig_charset = WFO::WebClient.read_decode(uri)
    page_tree = HTree(page_str)
    form, textarea_name = find_textarea_form(page_tree, orig_charset)
    self.new(form, uri, textarea_name, nil)
  end

  def self.find_textarea_form(page, orig_charset)
    page.traverse_html_form(orig_charset) {|form|
      form.each_textarea {|name, value| return form, name }
    }
    raise "textarea not found in #{page.request_uri}"
  end

  def recommended_filename
    @form.action_uri.to_s.sub(%r{\A.*/}, '')
  end

  include WFO::RepoTextArea
end

module WFO
  module_function

  def err(msg)
    STDERR.puts msg
    exit 1
  end

  def main
    subcommand = ARGV.shift
    case subcommand
    when 'help', '-h'
      do_help(true)
    when 'checkout', 'co'
      do_checkout ARGV
    when 'status', 'stat', 'st'
      do_status ARGV
    when 'update', 'up'
      do_update ARGV
    when 'commit', 'checkin', 'ci'
      do_commit ARGV
    when 'diff', 'di'
      do_diff ARGV
    when 'workdump'
      do_workdump ARGV
    else
      puts "unknown subcommand : #{subcommand}"
      do_help(false)
    end
  end

  def do_help(status)
    puts <<'End'
usage:
  wfo help
  wfo checkout [-t repo_type] URL [local-filename][.ext]
  wfo status [-u] [local-filename...]
  wfo update [local-filename...]
  wfo commit [local-filename...]
  wfo diff [-u] [local-filename...]
  wfo workdump [local-filename...]
End
    exit status
  end

  def do_checkout(argv)
    opt = OptionParser.new
    opt.banner = 'Usage: wfo checkout [-t repo_type] URL [local-filename][.ext]'
    opt_t = nil; opt.def_option('-t repo_type', "repository type (#{Repo.available_types})") {|v|
      opt_t = v
    }
    opt.def_option('-h', 'help') { puts opt; exit 0 }
    opt.parse!(argv)
    WebClient.do {
      url = URI(argv.shift)
      local_filename_arg = argv.shift
      if !local_filename_arg
        extname = '.txt'
      elsif /^\./ =~ local_filename_arg
        extname = local_filename_arg
      else
        if /\./ =~ local_filename_arg
          local_filename = local_filename_arg
        else
          local_filename = local_filename_arg + '.txt'
        end
        if WorkArea.has?(local_filename)
          err "local file already exists : #{local_filename.inspect}"
        end
      end
      repo_class, stable_uri = Repo.find_class_and_stable_uri(url, opt_t)
      accessor = repo_class.make_accessor(stable_uri)

      if !local_filename
        local_filename = make_local_filename(accessor.recommended_filename, extname)
      end
      workarea = WorkArea.new(local_filename, accessor.class.type, stable_uri, accessor.form, accessor.textarea_name)
      workarea.store
      puts local_filename
    }
  end

  def make_local_filename(recommended_basename, extname)
    if %r{/} =~ recommended_basename ||
      recommended_basename = File.basename(recommended_basename)
    end
    if recommended_basename.empty?
      recommended_basename = "empty-filename"
    end
    tmp = "#{recommended_basename}#{extname}"
    if !WorkArea.has?(tmp)
      local_filename = tmp
    else
      n = 1
      begin
        tmp = "#{recommended_basename}_#{n}#{extname}"
        n += 1
      end while WorkArea.has?(tmp)
      local_filename = tmp
    end
    local_filename
  end

  def do_status(argv)
    opt = OptionParser.new
    opt.banner = 'Usage: wfo status [options] [local-filename...]'
    opt_u = false; opt.def_option('-u', 'update check') { opt_u = true }
    opt.def_option('-h', 'help') { puts opt; exit 0 }
    opt.parse!(argv)
    WebClient.do {
      ws = argv_to_workareas(argv)
      if opt_u
        ws.each {|w|
          accessor = w.make_accessor
          remote_text = accessor.current_text
          local_text = w.local_text
          original_text = w.original_text
          if original_text == local_text
            if original_text == remote_text
              # not interesting.
            else
              puts "#{w.filename}: needs-update"
            end
          else
            if original_text == remote_text
              puts "#{w.filename}: localy-modified"
            else
              puts "#{w.filename}: needs-merge"
            end
          end
        }
      else
        ws.each {|w|
          local_text = w.local_text
          original_text = w.original_text
          if original_text != local_text
            puts "#{w.filename}: localy-modified"
          end
        }
      end
    }
  end

  def do_update(argv)
    WebClient.do {
      ws = argv_to_workareas(argv)
      ws.each {|w|
        accessor = w.make_accessor
        remote_text = accessor.current_text
        local_text = w.local_text
        original_text = w.original_text
        if original_text != remote_text
          if original_text == local_text
            w.local_text = remote_text
            w.original_text = remote_text
            w.store_info
            puts "#{w.filename}: updated"
          else
            merged, conflict = merge(local_text, original_text, remote_text)
            backup_path = w.make_backup(local_text)
            w.local_text = merged
            w.original_text = remote_text
            w.store_info
            if conflict
              puts "#{w.filename}: conflict (backup: #{backup_path})"
            else
              puts "#{w.filename}: merged (backup: #{backup_path})"
            end
          end
        end
      }
    }
  end

  def merge(local_text, original_text, remote_text)
    original_file = tempfile("wfo.original", original_text)
    local_file = tempfile("wfo.local", local_text)
    remote_file = tempfile("wfo.remote", remote_text)
    command = ['diff3', '-mE',
      '-L', 'edited by you',
      '-L', 'before edited',
      '-L', 'edited by others',
      local_file.path,
      original_file.path,
      remote_file.path]
    merged = IO.popen(Escape.shell_command(command), 'r') {|f|
      f.read
    }
    status = $?
    unless status.exited?
      raise "[bug] unexpected diff3 failure: #{status.inspect}"
    end
    case status.exitstatus
    when 0
      conflict = false
    when 1
      conflict = true
    when 2
      raise "diff3 failed"
    else
      raise "[bug] unexpected diff3 status: #{status.inspect}"
    end
    return merged, conflict
  end

  def do_commit(argv)
    WebClient.do {
      ws = argv_to_workareas(argv)
      ws.reject! {|w| !w.modified? }
      up_to_date = true
      as = []
      ws.each {|w|
        accessor = w.make_accessor
        remote_text = accessor.current_text
        local_text = w.local_text
        original_text = w.original_text
        if remote_text != original_text
          puts "not up-to-date : #{w.filename}"
          up_to_date = false
        end
        as << [w, accessor, local_text]
      }
      exit 1 if !up_to_date
      as.each {|w, accessor, local_text|
        accessor.replace_text local_text
        accessor.commit
        accessor2 = accessor.reload
        if accessor2.current_text != local_text
          backup_filename = w.make_backup(local_text)
          puts "commited not exactly.  local file backup: #{backup_filename}"
          w.local_text = accessor2.current_text
          w.original_text = accessor2.current_text
          w.store
        else
          w.original_text = local_text
          w.store_info
        end
        puts w.filename
      }
    }
  end

  def do_diff(argv)
    opt = OptionParser.new
    opt.banner = 'Usage: wfo diff [options] [local-filename...]'
    opt_u = false; opt.def_option('-u', 'update check') { opt_u = true }
    opt.def_option('-h', 'help') { puts opt; exit 0 }
    opt.parse!(argv)
    WebClient.do {
      ws = argv_to_workareas(argv)
      no_diff = true
      ws.each {|w|
        local_text = w.local_text
        if opt_u
          accessor = w.make_accessor
          other_text = accessor.current_text
          other_label = "#{w.filename} (remote)"
        else
          other_text = w.original_text
          other_label = "#{w.filename} (original)"
        end
        if other_text != local_text
          no_diff = false
          other_file = tempfile("wfo.other", other_text)
          local_file = tempfile("wfo.local", local_text)
          command = ['diff', '-u',
            "--label=#{other_label}", other_file.path,
            "--label=#{w.filename}", local_file.path]
          system(Escape.shell_command(command))
        end
      }
      exit no_diff
    }
  end

  def do_workdump(argv)
    argv.each {|n|
      puts "#{n} :"
      pp WorkArea.new(n).instance_eval { @info }
    }
  end

  def argv_to_workareas(argv)
    ws = []
    if argv.empty?
      WorkArea.each_filename {|n|
        ws << WorkArea.new(n)
      }
    else
      ws = argv.map {|n| WorkArea.new(n) }
    end
    ws
  end

  def tempfile(basename, content)
    t = Tempfile.new(basename)
    t.write content
    t.flush
    t
  end

end

WFO.main
