#!/usr/bin/python3
"""
A feedback cgi to go along with (in particular) pelican.

This will send mail to (as set in the environment) CONTACT_ADDRESS.

For more details see https://blog.tfiu.de/a-feedback-form-in-pelican.html

Distributed under CC-0.

Use this with a template like:

<aside id="feedback-form">
<h1>Reply or Comment</h1>
<form action="/bin/feedback" method="post">
	<label>Sender (will not be published; I only need that if you
		want a response):<br/>
	<input type="text" name="sender" class="feedback-sender" size="50"/>
	</label><br/>

	<label>Message:
	<textarea name="message" class="feedback-message" cols="60" rows="6"
	></textarea></label></br>

	<label><input type="checkbox" name="publishable" checked="checked"/>
		Feel free to publish</label>
	<br/>
	<input type="hidden" name="origin" value="{{ "/"+article.url }}">

	<input type="text" name="textcha" size="7" placeholder="textcha"/>
	<input type="submit" value="Send" class="feedback-submit"/>

	<p class="feedback-hint">Spam protechtion (textcha): Type “josua” into
		the text field next to the submit button.</p>
</form>
</aside>

Suggested CSS:

#feedback-form {
	display: block;
	width: 70%;
	background-color: #e0e0d5;
	border-left: 1% solid #e0e0d5;
	border-right: 1% solid #e0e0d5;
	padding: 0.5ex 4% 0.5ex 4%;
	margin: 3ex auto 1ex auto;
}

#feedback-form h1 {
	font-size: 16pt;
}

#feedback-form label {
	display: block;
	font-style: italic;
}

.feedback-sender {
	width: 90%;
	margin-left: 10%;
}

.feedback-message {
	width: 90%;
	margin-left: 10%;
}

div.parallel {
  display: flex;
  flex-flow: row nowrap;
  width: 100%;
}

.parallel > p {
	width: 48%;
  padding: 0.5rem;
}
"""

import cgi
import os
import re
import subprocess
import sys
import time

from email import charset
from email import utils as emailutils
from email.header import Header
from email.parser import Parser
from email.mime.nonmultipart import MIMENonMultipart


TEXTCHA_SOLUTION = "josua"

# **SECURITY**: Do *not* insert remotely controlled content into
# headers here!

MAIL_TEMPLATE = """From: blog <no-reply@localhost>
Subject: Blog Feedback
To: {recipient}

Publishable: {publishable}
Sender: {sender}
Origin URL: {origin}

{message}
"""

SENDMAIL = ["sendmail", "-t"]


################ Micro templating start
def escapePCDATA(val):
	if val is None:
		return ""

	return str(val
		).replace("&", "&amp;"
		).replace('<', '&lt;'
		).replace('>', '&gt;'
		).replace("\0", "&x00;")


def escapeAttrVal(val):
	"""returns val with escapes for double-quoted attribute values.
	"""
	if val is None:
		return '""'
	return escapePCDATA(val).replace('"', '&quot;')


class Template(object):
	"""a *very* basic and ad-hoc template engine.

	It works on HTML strings, with the following constructs expanded:

	* $[key] -- value for key, escaped for double-quoted att values
	* $(key) -- value for key, escaped for PCDATA
	* $|func| -- replace with the value of func(vars)
	* $!raw! -- value for key, non-escaped (other template ops are expanded)
	* $$ -- a $ char.

	Use either unicode strings or plain ASCII
	"""
	def __init__(self, source):
		self.source = str(source)

	def render(self, vars):
		"""returns a string with the template filled using vars.

		vars is a dictionary mapping keys to unicode-able objects.

		You'll get back a unicode string that you must encode before
		spitting it out to the web.
		"""
		return re.sub(r"\$\$", "$",
			re.sub(r"\$\|([a-zA-Z0-9_]+)\|", 
				lambda mat: globals()[mat.group(1)](vars),
			re.sub(r"\$\(([a-zA-Z0-9_]+)\)", 
				lambda mat: escapePCDATA(vars.get(mat.group(1), "")),
			re.sub(r"\$\[([a-zA-Z0-9_]+)\]", 
				lambda mat: escapeAttrVal(vars.get(mat.group(1), "")),
			re.sub(r"\$!([a-zA-ZÄÖÜäöüß0-9_]+)!", 
				lambda mat: str(vars.get(mat.group(1), "")),
			self.source)))))

	def serve(self, vars):
		"""emits a basic CGI response for this template.
		"""
		payload = self.render(vars
			).replace("\n", "\r\n").encode("utf-8")

		sys.stdout.buffer.write((
			"content-type: text/html;charset=utf-8\r\n"
			"content-length: %d\r\n\r\n"%len(payload)).encode("ascii"))
		sys.stdout.buffer.write(payload)


ERROR_TEMPLATE = Template("""<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Failed</title>
<link rel="stylesheet" href="/theme/css/style.css"/>
</head>
<body>
<h1>Sorry</h1>
<p>Whatever you tried, it didn't work out:</p>
<p class="errmsg">$(msg)</p>
</body>
</html>""")


RESPONSE_TEMPLATE = Template("""<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Report</title>
<link rel="stylesheet" href="/theme/css/style.css"/>
</head>
<body>
<div class="parallel">
<p xml:lang="en">Thanks for your feedback.  If it is publishable, it may
take a while before it shows up in the article.
<a href="$(origin)">Back to original article</a>.</p>

<p xml:lang="de">Danke fürs Feedback.  Auch wenn ich es veröffenlichen
darf, wird es einige Zeit dauern, bis es im Artikel erscheint.
<a href="$(origin)">Zurück zum Artikel</a>.</p>
</div>
</body>
""")


def show_error(msg):
	ERROR_TEMPLATE.serve({"msg": msg})


def format_mail(mail_text):
	"""returns a mail with headers and content properly formatted as
	a bytestring and MIME.

	mail_text must be a unicode instance or pure ASCII
	"""
	rawHeaders, rawBody = mail_text.split("\n\n", 1)
	cs = charset.Charset("utf-8")
	cs.body_encoding = charset.QP
	cs.header_encoding = charset.QP
	# they've botched MIMEText so bad it can't really generate 
	# quoted-printable UTF-8 any more.  So, let's forget MIMEText:
	msg = MIMENonMultipart("text", "plain", charset="utf-8")
	msg.set_payload(rawBody, charset=cs)

	for key, value in list(Parser().parsestr(rawHeaders).items()):
		if key.lower()=="date":
			continue

		if re.match("[ -~]*$", value):
			# it's plain ASCII, don't needlessly uglify output
			msg[key] = value
		else:
			msg[key] = Header(value, cs)

	msg["Date"] = emailutils.formatdate(time.time(), 
		localtime=False, usegmt=True)
	return msg.as_string()


def send_mail(mail_text):
	"""sends mail_text (which has to have all the headers) via sendmail.

	This will return True when sendmail has accepted the mail, False 
	otherwise.
	"""
	msg = format_mail(mail_text)

	pipe = subprocess.Popen(SENDMAIL,
		stdin=subprocess.PIPE)
	pipe.stdin.write(msg.encode("ascii", "ignore"))
	pipe.stdin.close()

	if pipe.wait():
		raise Exception("Sorry -- mail could not be sent."
			"  Try the contact address.")


def evaluate_form(form):
	recipient = os.environ.get("CONTACT_ADDRESS", "root")
	origin = form.getfirst("origin")
	message = form.getfirst("message")
	sender = form.getfirst("sender")
	publishable = form.getfirst("publishable")
	textcha =  form.getfirst("textcha")
	if textcha!=TEXTCHA_SOLUTION:
		raise Exception("You need to type 'josua' into the textcha field.")

	mail_text = MAIL_TEMPLATE.format(**locals())
	send_mail(mail_text)


def show_report(form):
	RESPONSE_TEMPLATE.serve({
		"origin": form.getfirst("origin")})


def main():
	try:
		form = cgi.FieldStorage()
		evaluate_form(form)
		show_report(form)
	except Exception as ex:
		import traceback; traceback.print_exc()
		show_error(str(ex))


def _test():
	from urllib import parse
	
	os.environ["QUERY_STRING"] = parse.urlencode({
		"origin": "http://foo.bar/baz",
		"message": "This is a test message. Äußerst.\nLines.\n",
		"sender": "tester@monk.org",
		"publishable": "publishable",
		"textcha": TEXTCHA_SOLUTION,
		})
	main()

if __name__=="__main__":
	if "SERVER_SOFTWARE" in os.environ:
		main()
	else:
		_test()
