--- /dev/null
+#!/usr/bin/env ruby
+# encoding: utf-8
+# Copyright (C) 2016 Ian Kelling
+
+# 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, see <http://www.gnu.org/licenses/>.
+
+# debian sets LANG=C when starting apache2.
+# the envoding comment above fixes the internal encoding afaik,
+# Found this at
+# https://stackoverflow.com/questions/20521371/set-utf-8-as-default-for-ruby-1-9-3
+# also note man ruby's -E arg.
+Encoding.default_external = Encoding::UTF_8
+
+require 'cgi'
+require 'fileutils'
+require 'time'
+require 'sqlite3'
+
+Dir.chdir(File.join(File.dirname(__FILE__), '..'))
+
+require '../b'
+include B
+
+
+# constanty things
+DEBUG = true
+CAPTCHA = -> {
+ c = []
+ x = (<<EOF).split("\n")
+What does a dog wag?
+tail
+Would you sleep better on a bed or a keyboard?
+bed
+Are there more or less than one million people on the Earth?
+more
+Which of Lilly and Robert is more commonly a woman’s name?
+lilly
+Which word has fewer letters, adorable or fox?
+fox
+What is the normal color of milk?
+white
+Which of Iceland and Turkey is an island nation?
+iceland
+Is a filesystem like a tree or a rose?
+tree
+What character is a tilde?
+~
+Which is brighter, the moon or the sun?
+(the )?sun
+Which is closer, the moon or the sun?
+(the )?moon
+What language is this sentence written in?
+english
+What animal says "meow" and catches mice?
+cat
+What animal quacks and has webbed feet?
+duck
+Which are better fliers: worms or birds?
+birds
+Which typically runs first: a kernel or a web browser?
+kernel
+EOF
+ while x.length > 0
+ c << x.pop(2)
+ end
+ c
+}[]
+
+def do_captcha
+ captcha_q = CAPTCHA.sample[0]
+ puts "Content-type: text/html\n\n"
+ puts skel("#{DN}/captcha", <<EOF, header: ' / <a href="/blog.html">blog</a> / captcha')
+<p>Hello friend. I haven't read a post from #{IP}, and I only remember for a few months, so:</p>
+<p>#{captcha_q}</p>
+
+<form action="/cgi/comment" method="post">
+ <input class="misc" type="text" name="url">
+ <input name="goto" type="hidden" value="#{GOTO}">
+ <input name="question" type="hidden" value="#{captcha_q}">
+ <input name="answer">
+<br>Your comment:
+ <textarea rows="10" name="comment" maxlength="1000">#{COMMENT_TXT}</textarea>
+ <input type="submit" value="Submit">
+</form>
+EOF
+ exit 0
+end
+
+
+def fail(msg)
+ if DEBUG and msg
+ puts "Content-type: text/plain\n\n"
+ puts msg
+ else
+ redir
+ end
+ exit 0
+end
+
+def redir
+ puts 'Status: 302 Found'
+ puts "Location: #{GOTO}#comment-section\n\n"
+ exit(0)
+end
+
+
+def bn(*args)
+ File.basename *args
+end
+
+
+###### begin error checking & arg parsing ######
+cgi = CGI.new
+IP = cgi.remote_addr
+
+if cgi.has_key?('goto')
+ GOTO = cgi['goto']
+else
+ GOTO = '/'
+ fail('redir to /')
+end
+
+if (cgi.has_key?('url') && cgi['url'] != "") || ! cgi.has_key?('comment')
+ fail("comment not in form or url in form. cgi.params: #{cgi.params}")
+end
+
+COMMENT_TXT = cgi["comment"]
+
+
+if COMMENT_TXT.length > 1000 or GOTO.length > 150
+ fail('length of comment or goto is too great')
+end
+
+
+captchad = false
+if cgi.has_key?('answer') && cgi.has_key?('question')
+ if cgi['answer'].downcase !~ /^#{CAPTCHA.to_h[cgi['question']]}$/
+ do_captcha
+ end
+ captchad = true
+end
+
+
+-> {
+ found = false
+ Dir.foreach('blog') do |entry|
+ next if ['.','..'].any? { |f| f == entry }
+ if GOTO == '/blog/' + entry
+ found = true
+ break
+ end
+ end
+ fail('goto entry not found') unless found
+}[]
+######### end error checking & arg parsing ########
+
+
+$db = db_init
+state = nil
+WHITELIST_CUTOFF = NOW - 4*DAY
+
+
+####### begin: state for ips we've seen before #######
+[[5, 60], # 1 min
+ [10, 60*5], # 5 min
+ [20, 60*60], # 60 min
+ [30, 60*60*24], # 1 day
+ [60, 60*60*24*7]] # 1 week
+ .each do |max_posts, date|
+
+ if $db.execute(<<-SQL, [NOW - date])[0][0] > max_posts
+ select count(*) from c
+ where date > ? and ip = '#{IP}'
+SQL
+ state = 'rate_limited'
+ end
+end
+
+state ||= 'suspect' if $db.execute(<<-SQL)[0][0] > 0
+ select count(*) from c
+ where ip = '#{IP}' and (
+state = 'banned' or
+state = 'rate_limited')
+SQL
+
+unless state
+ older_date = NOW - DAY*2
+ last_moderated = $db.execute(<<-SQL, [older_date])[-1]
+ select date from c
+ where ip = '#{IP}' and (
+ state = 'moderated' or
+ (date < ? and (state = 'timed' or state = 'known')))
+SQL
+ last_moderated = last_moderated[0] if last_moderated
+ last_good = $db.execute(<<-SQL, [older_date])[-1]
+ select date from c
+ where ip = '#{IP}' and (
+ state = 'picked' or
+ (date < ? and (state = 'timed' or state = 'known')))
+SQL
+ last_good = last_good[0] if last_good
+ if last_moderated && last_good
+ if last_good > last_moderated
+ state = 'known'
+ else
+ # these 2 waiting conditions are not actually needed,
+ # since waiting is the default, but meh.
+ state = 'waiting'
+ end
+ elsif last_moderated
+ state = 'waiting'
+ elsif last_good
+ state = 'known'
+ end
+end
+####### end: state for ips we've seen before #######
+
+####### begin: whitelist checking #########
+glob = "../blog/#{'?'*'YYYY-MM-DD-'.length}#{bn GOTO, '.*'}.md"
+md_file = Dir[glob][0]
+unless state
+ b = bn(md_file,'.*')
+ post_date = Time.parse(b[0..DATE_LEN]).to_i
+ if post_date > WHITELIST_CUTOFF
+ state = 'timed'
+ end
+end
+###### end: whitelist checking ########
+
+state ||= 'waiting'
+
+if state != 'known' && ! captchad
+ do_captcha
+end
+
+
+# states:
+# timed
+# # was posted a whitelist period, so automatically posted.
+# # whitelist periods are per page times when legit comments are
+# # much more likely than spam, so we automatically let comments through.
+
+# known
+# # ip posted good comment before: either, one in picked state, or
+# # a timed/known comment which is over 2 days old (I saw it and didn't remove
+# # it)
+
+# picked
+# # manually marked as a good comment, so publish it.
+
+# rate_limited
+# # posting too much, consider them a spammer.
+
+# moderated
+# # bad comment, but don't ban them
+
+# banned
+# # all comments from this ip dead, new comment's dont even go into the db.
+
+# waiting
+# # waiting for manual moderation, get's posted automatically if there
+# # is none in 24 hours
+
+# suspect
+# # had a bad post in the past. does not
+# # automatically get posted in time without moderation.
+
+
+# any of the manual states
+date = $db.execute(<<-SQL)[0][0]
+select max(date) from c where
+state = 'moderated' or
+state = 'banned' or
+state = 'picked'
+SQL
+
+# not the bad automatic states
+query = <<-SQL
+select count(*) from c where
+state != 'rate_limited' and
+state != 'suspect'
+SQL
+
+
+if date
+ new_count = $db.execute(query + 'and date > ?',date)
+else
+ new_count = $db.execute(query)
+end
+
+if new_count == 1
+ require 'net/smtp'
+ def send_email(opts={})
+ opts[:to] ||= ENV['USER']
+ opts[:server] ||= 'localhost'
+ opts[:from] ||= ENV['USER']
+ opts[:from_alias] ||= ENV['USER']
+ opts[:subject] ||= "test subject"
+ opts[:body] ||= ""
+
+ msg = <<END_OF_MESSAGE
+From: #{opts[:from_alias]} <#{opts[:from]}>
+To: <#{opts[:to]}>
+Subject: #{opts[:subject]}
+
+#{opts[:body]}
+END_OF_MESSAGE
+
+ Net::SMTP.start(opts[:server]) do |smtp|
+ smtp.send_message msg, opts[:from], opts[:to]
+ end
+ end
+ send_email :subject => 'new comments on iankelling.org'
+end
+
+$db.execute('insert into c values (NULL, ?, ?, ?, ?, ?)',
+ [state,
+ IP,
+ NOW,
+ GOTO,
+ COMMENT_TXT])
+
+post(md_file)
+
+redir