Skip to content

Commit ae99200

Browse files
committed
First public release
1 parent 2874abf commit ae99200

File tree

16 files changed

+620
-0
lines changed

16 files changed

+620
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ docs/_build/
5555

5656
# PyBuilder
5757
target/
58+
59+
lddata/*.json

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
# ldform
22
CGI form handler used for LinuxDays web forms
3+
4+
This is an quick and dirty hack to get the form data stored to JSON files.
5+
Every submission has to contain key `regid` linking to an existing directory in
6+
the data directory. All data from each submission is then saved into a JSON file
7+
named YYYYMMDD-HHMMSS-nnn.json where nnn is random nonce.
8+
9+
Request handling can be extended by creating a custom handler with some special
10+
features like sending confirmation e-mail or mailing list subscription.
11+
12+
For extracting data, there is a convinience script `export.py` which exports
13+
given form fields as a CSV file. Utility `losuj.py` can be used to draw a
14+
submission by random.

cgi-bin/ldform.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python3
2+
import os.path
3+
BASE_PATH = "/srv/www/"
4+
DATA_PATH = os.path.join(BASE_PATH, "lddata")
5+
6+
import sys
7+
sys.path[0] = BASE_PATH
8+
9+
import ldform
10+
ldform.cgi_main(DATA_PATH, traceback=True)

ldform.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<form action="/cgi-bin/ldform.py" method="post">
2+
<input type="text" name="formid" value="cfp2015">
3+
<input type="text" name="email">
4+
<input type="text" name="name">
5+
<input type="submit">
6+
</form>

ldform/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .cgi import cgi_main

ldform/cgi.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
3+
import cgi
4+
import cgitb
5+
6+
import os
7+
import datetime
8+
import random
9+
import string
10+
import json
11+
import importlib
12+
13+
# Python 3.2 compatibility
14+
if __package__ is None:
15+
__package__ = "ldform"
16+
17+
# Base path global variable
18+
BASE_PATH = ""
19+
20+
21+
class MyDict(dict):
22+
"""
23+
Dictionary with few convinience functions and
24+
a default value instead of KeyError
25+
"""
26+
def getfirst(self, key, default=None):
27+
return self.get(key, [default])[0]
28+
29+
def getlist(self, key, default=''):
30+
return ", ".join(self.get(key, [default]))
31+
32+
def __missing__(self, key):
33+
return [""]
34+
35+
36+
def _randstr(length=3):
37+
return ''.join(random.SystemRandom().choice(string.ascii_lowercase)
38+
for i in range(length))
39+
40+
41+
def save(data):
42+
global BASE_PATH
43+
outfname = "{}.json".format(data['regid'])
44+
with open(os.path.join(BASE_PATH, data['formid'], outfname),
45+
"w", encoding="utf-8") as outf:
46+
json.dump(data, outf, indent=4, sort_keys=True, ensure_ascii=False)
47+
outf.write('\n')
48+
49+
50+
def cgi_main(base_path, traceback=False):
51+
"""
52+
Serve CGI query
53+
There has to be a formid value with a corresponding data directory.
54+
A JSON file is produced in target directory with all user submitted
55+
values plus remote IP address and a nonce, which can be used to
56+
authenticate the owner of such request.
57+
Finally a notify and respond functions of module named same as formid
58+
is called. The intended purpuse is to send e-mail notification and
59+
create a HTML response output.
60+
61+
@param base_path path where to store collected form JSONs
62+
(subdirectory per formid should exist)
63+
@param traceback display fancy tracebacks on an exception
64+
"""
65+
global BASE_PATH
66+
BASE_PATH = base_path
67+
if traceback:
68+
cgitb.enable()
69+
70+
try:
71+
form = cgi.FieldStorage()
72+
formid = os.path.basename(form.getfirst('formid', 'default'))
73+
outpath = os.path.join(BASE_PATH, formid)
74+
if not os.path.isdir(outpath) or not os.access(outpath, os.W_OK):
75+
raise RuntimeError("No valid formid found.")
76+
77+
data = MyDict()
78+
for key in form.keys():
79+
data[key] = form.getlist(key)
80+
81+
regid = datetime.datetime.now().strftime("%Y%m%d-%H%M%S"
82+
"-{}".format(_randstr(3)))
83+
84+
# Make sure these are added after user-supplied input
85+
# so they can be trusted
86+
data['formid'] = formid
87+
data['regid'] = regid
88+
data['nonce'] = _randstr(10)
89+
data['ip'] = os.environ["REMOTE_ADDR"]
90+
91+
try:
92+
handler = importlib.import_module('.handlers.{}'.format(formid),
93+
__package__)
94+
except ImportError:
95+
handler = importlib.import_module('.handlers.generic',
96+
__package__)
97+
98+
handler.respond(data)
99+
save(data)
100+
101+
except RuntimeError as e:
102+
print("Content-type: text/html; charset=UTF-8")
103+
print("""
104+
<html>
105+
<body>
106+
<h1>Something went wrong</h1>
107+
<p>We were unable to process your request…</p>
108+
<pre>{}</pre>
109+
</body>
110+
</html>
111+
""".format(e))

ldform/emailer.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
3+
import email.mime.text
4+
import email.utils
5+
import email.header
6+
import smtplib
7+
import copy
8+
import sys
9+
import re
10+
import argparse
11+
12+
13+
def sendemail(textpart, subject, recipients,
14+
sender=("LinuxDays", "[email protected]"),
15+
server="localhost"):
16+
"""
17+
Send textual MIME e-mail
18+
@param textpart Textual part of e-mail
19+
@param subject Message subject
20+
@param recipients List of tuples of ("User Name", "E-mail address")
21+
@param sender Tuple of ("User Name", "E-mail address")
22+
@param server SMTP server
23+
"""
24+
25+
msgtpl = email.mime.text.MIMEText(textpart, _charset="utf-8")
26+
msgtpl['Subject'] = subject
27+
msgtpl['From'] = email.header.make_header(
28+
((sender[0], None),
29+
("<{}>".format(sender[1]), None))
30+
).encode()
31+
msgtpl['Precedence'] = 'bulk'
32+
msgtpl['Date'] = email.utils.formatdate(localtime=True)
33+
34+
smtp = smtplib.SMTP(server)
35+
for name, emailaddr in recipients:
36+
try:
37+
msg = copy.deepcopy(msgtpl)
38+
msg['Message-id'] = email.utils.make_msgid('linuxdays')
39+
msg['To'] = email.header.make_header(
40+
((name, None),
41+
("<{}>".format(emailaddr), None))
42+
).encode()
43+
# print(msg.as_string())
44+
smtp.send_message(msg)
45+
except:
46+
print("Unexpected error:", sys.exc_info()[0])
47+
smtp.quit()
48+
49+
if __name__ == '__main__':
50+
parser = argparse.ArgumentParser(description='LinuxDays spammer')
51+
parser.add_argument('--subject', "-s", help='message Subject',
52+
default='LinuxDays informace')
53+
parser.add_argument('--fromname', help='sender name', default='LinuxDays')
54+
parser.add_argument('--fromemail', '-f', help='sender e-mail',
55+
default='[email protected]')
56+
parser.add_argument('recipients', help='file with recipient list, one'
57+
'on line, formatted Jmeno Prijmeni <adresa>',)
58+
parser.add_argument('text', help='file with message body',)
59+
parser.add_argument('--notest', help='actually send the messages',
60+
action='store_true')
61+
62+
args = parser.parse_args()
63+
64+
recipients = list()
65+
with open(args.recipients) as recfile:
66+
for line in recfile:
67+
m = re.match('(.*) <([^>]*)>$', line)
68+
if m:
69+
recipients.append(m.groups())
70+
71+
print("{} recipients added...".format(len(recipients)))
72+
with open(args.text) as bodyfile:
73+
textpart = bodyfile.read()
74+
75+
if args.notest:
76+
print("Sending...")
77+
sendemail(textpart, args.subject, recipients,
78+
(args.fromname, args.fromemail))
79+
else:
80+
print(
81+
"About to send text:\n{}\nfrom:{} <{}>\n"
82+
"with subject: {}\nto addresses:{}".format(
83+
textpart, args.fromname, args.fromemail,
84+
args.subject, recipients
85+
)
86+
)

ldform/export.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import json
4+
import argparse
5+
import sys
6+
import csv
7+
8+
BASE_PATH = '/srv/www/lddata'
9+
10+
11+
def export_data(formid, *args):
12+
""" Generate list of with values for *args. """
13+
global BASE_PATH
14+
path = os.path.join(BASE_PATH, formid)
15+
for f in sorted(os.listdir(path)):
16+
if not f.endswith('.json'):
17+
continue
18+
with open(os.path.join(path, f), 'r', encoding='utf-8') as inf:
19+
data = json.load(inf)
20+
r = []
21+
for field in args:
22+
d = data.get(field, [""])
23+
if type(d) is list:
24+
r.append(", ".join(d))
25+
elif type(d) is str:
26+
r.append(d)
27+
else:
28+
r.append("")
29+
yield r
30+
31+
32+
def write_csv(outf, data, dialect='excel-tab', heading=None):
33+
writer = csv.writer(outf, dialect=dialect)
34+
if heading:
35+
writer.writerow(heading)
36+
writer.writerows(data)
37+
38+
def main():
39+
parser = argparse.ArgumentParser(description='Export form data'
40+
'to a CSV file')
41+
parser.add_argument('-o', '--outfile',
42+
help='output to a CSV file instead of stdout')
43+
parser.add_argument('-n', '--noheader', action='store_true',
44+
help='Do not output field names on the first line')
45+
parser.add_argument('-d', '--dialect', choices=csv.list_dialects(),
46+
default='excel-tab', help="CSV dialect to output "
47+
"(default %(default)s)")
48+
parser.add_argument('formid',
49+
help="id of form whose data should be extracted")
50+
parser.add_argument('field', nargs="+",
51+
help="list of fields that should be exported")
52+
args = parser.parse_args()
53+
54+
data = export_data(args.formid, *args.field)
55+
heading = None if args.noheader else args.field
56+
if args.outfile:
57+
outf = open(args.outfile, 'w', newline='', encoding='utf-8')
58+
else:
59+
outf = sys.stdout
60+
with outf:
61+
write_csv(outf, data, args.dialect, heading)
62+
63+
64+
if __name__ == '__main__':
65+
main()

ldform/handlers/__init__.py

Whitespace-only changes.

ldform/handlers/findreg.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import json
3+
import re
4+
from ..cgi import BASE_PATH
5+
from ..cgi import MyDict
6+
7+
confirmationtext = """Content-type: text/html; charset=UTF-8
8+
9+
<html>
10+
<body>
11+
<h1>Nalezení registrace</h1>
12+
<form action="/cgi-bin/ldform.py" method="get">
13+
<input type="hidden" name="formid" value="findreg">
14+
<input type="text" name="q" placeholder="Hledané jméno" value="{q}">
15+
<input type="submit">
16+
</form>
17+
<table>
18+
<tr><th>#</th><th>Jméno</th><th>regid</th></tr>
19+
{table}
20+
</table>
21+
</body>
22+
</html>
23+
"""
24+
25+
26+
27+
def respond(data):
28+
"""Find registration data and delete them."""
29+
global BASE_PATH
30+
query = data.getfirst('q', '')
31+
path = os.path.join(BASE_PATH, 'reg2015')
32+
order = 0
33+
results = []
34+
if len(query) > 2:
35+
for f in sorted(os.listdir(path)):
36+
if not f.endswith('.json'):
37+
continue
38+
with open(os.path.join(path, f), 'r', encoding='utf-8') as inf:
39+
data = MyDict(json.load(inf))
40+
order += 1
41+
name = data.getfirst('name')
42+
if not name:
43+
continue
44+
regid = data.get('regid')
45+
if re.search(query, name, re.I):
46+
results.append((order, name, regid))
47+
48+
rows = ("<tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(o, n, r)
49+
for o, n, r in results)
50+
table = "\n".join(rows)
51+
print(confirmationtext.format(q=query, table=table))

0 commit comments

Comments
 (0)