From dbe6c60e43a8d8afc987ab822b0e7ff6629e8c2b Mon Sep 17 00:00:00 2001
From: Richard Penman <26063146+richardpenman@users.noreply.github.com>
Date: Fri, 27 Mar 2015 19:28:05 +0800
Subject: [PATCH] initial commit of web interface
---
__init__.py | 1 +
controllers/ajax.py | 29 +
controllers/appadmin.py | 672 ++++++++++++++
controllers/default.py | 108 +++
models/1_db.py | 59 ++
models/2_menu.py | 24 +
models/3_cache.py | 37 +
modules/Captcha/Base.py | 127 +++
modules/Captcha/File.py | 53 ++
modules/Captcha/Visual/Backgrounds.py | 95 ++
modules/Captcha/Visual/Base.py | 69 ++
modules/Captcha/Visual/Distortions.py | 117 +++
modules/Captcha/Visual/Pictures.py | 23 +
modules/Captcha/Visual/Tests.py | 63 ++
modules/Captcha/Visual/Text.py | 100 ++
modules/Captcha/Visual/__init__.py | 14 +
modules/Captcha/Words.py | 57 ++
modules/Captcha/__init__.py | 41 +
modules/Captcha/data/fonts/vera/COPYRIGHT.TXT | 124 +++
modules/Captcha/data/fonts/vera/README.TXT | 11 +
.../Captcha/data/fonts/vera/RELEASENOTES.TXT | 162 ++++
modules/Captcha/data/fonts/vera/Vera.ttf | Bin 0 -> 65932 bytes
modules/Captcha/data/fonts/vera/VeraBI.ttf | Bin 0 -> 63208 bytes
modules/Captcha/data/fonts/vera/VeraBd.ttf | Bin 0 -> 58716 bytes
modules/Captcha/data/fonts/vera/VeraIt.ttf | Bin 0 -> 63684 bytes
modules/Captcha/data/fonts/vera/VeraMoBI.ttf | Bin 0 -> 55032 bytes
modules/Captcha/data/fonts/vera/VeraMoBd.ttf | Bin 0 -> 49052 bytes
modules/Captcha/data/fonts/vera/VeraMoIt.ttf | Bin 0 -> 54508 bytes
modules/Captcha/data/fonts/vera/VeraMono.ttf | Bin 0 -> 49224 bytes
modules/Captcha/data/fonts/vera/VeraSe.ttf | Bin 0 -> 60280 bytes
modules/Captcha/data/fonts/vera/VeraSeBd.ttf | Bin 0 -> 58736 bytes
modules/Captcha/data/fonts/vera/local.conf | 32 +
modules/Captcha/data/pictures/abstract/1.jpeg | Bin 0 -> 5547 bytes
.../Captcha/data/pictures/abstract/10.jpeg | Bin 0 -> 7099 bytes
.../Captcha/data/pictures/abstract/11.jpeg | Bin 0 -> 9778 bytes
.../Captcha/data/pictures/abstract/12.jpeg | Bin 0 -> 10036 bytes
modules/Captcha/data/pictures/abstract/2.jpeg | Bin 0 -> 4854 bytes
modules/Captcha/data/pictures/abstract/3.jpeg | Bin 0 -> 4631 bytes
modules/Captcha/data/pictures/abstract/4.jpeg | Bin 0 -> 5397 bytes
modules/Captcha/data/pictures/abstract/5.jpeg | Bin 0 -> 6557 bytes
modules/Captcha/data/pictures/abstract/6.jpeg | Bin 0 -> 5590 bytes
modules/Captcha/data/pictures/abstract/7.jpeg | Bin 0 -> 5703 bytes
modules/Captcha/data/pictures/abstract/8.jpeg | Bin 0 -> 6885 bytes
modules/Captcha/data/pictures/abstract/9.jpeg | Bin 0 -> 6842 bytes
modules/Captcha/data/pictures/abstract/README | 3 +
...aig_Barrington_ocotillo_and_mountains.jpeg | Bin 0 -> 85692 bytes
.../nature/Kerry_Carloy_Chisos_Sunset.jpeg | Bin 0 -> 34704 bytes
.../pictures/nature/Paul_Dowty_Mt_Bross.jpeg | Bin 0 -> 170781 bytes
modules/Captcha/data/pictures/nature/README | 2 +
modules/Captcha/data/words/README | 4 +
modules/Captcha/data/words/basic-english | 852 ++++++++++++++++++
modules/captcha.py | 96 ++
modules/common.py | 400 ++++++++
static/403.html | 1 +
static/404.html | 1 +
static/500.html | 1 +
static/css/bootstrap-responsive.min.css | 9 +
static/css/bootstrap.min.css | 9 +
static/css/calendar.css | 7 +
static/css/style.css | 6 +
static/css/web2py.css | 317 +++++++
static/css/web2py_bootstrap.css | 264 ++++++
static/css/web2py_bootstrap_nojs.css | 122 +++
static/images/facebook.png | Bin 0 -> 991 bytes
static/images/flags/ad.png | Bin 0 -> 697 bytes
static/images/flags/ae.png | Bin 0 -> 1061 bytes
static/images/flags/af.png | Bin 0 -> 759 bytes
static/images/flags/ag.png | Bin 0 -> 1323 bytes
static/images/flags/ai.png | Bin 0 -> 1402 bytes
static/images/flags/al.png | Bin 0 -> 1466 bytes
static/images/flags/am.png | Bin 0 -> 454 bytes
static/images/flags/an.png | Bin 0 -> 1206 bytes
static/images/flags/ao.png | Bin 0 -> 1196 bytes
static/images/flags/aq.png | Bin 0 -> 2320 bytes
static/images/flags/ar.png | Bin 0 -> 748 bytes
static/images/flags/as.png | Bin 0 -> 1508 bytes
static/images/flags/at.png | Bin 0 -> 508 bytes
static/images/flags/au.png | Bin 0 -> 751 bytes
static/images/flags/aw.png | Bin 0 -> 1188 bytes
static/images/flags/ax.png | Bin 0 -> 316 bytes
static/images/flags/az.png | Bin 0 -> 1131 bytes
static/images/flags/ba.png | Bin 0 -> 1345 bytes
static/images/flags/bb.png | Bin 0 -> 1275 bytes
static/images/flags/bd.png | Bin 0 -> 1204 bytes
static/images/flags/be.png | Bin 0 -> 682 bytes
static/images/flags/bf.png | Bin 0 -> 1042 bytes
static/images/flags/bg.png | Bin 0 -> 1005 bytes
static/images/flags/bh.png | Bin 0 -> 1192 bytes
static/images/flags/bi.png | Bin 0 -> 1457 bytes
static/images/flags/bj.png | Bin 0 -> 825 bytes
static/images/flags/bl.png | Bin 0 -> 903 bytes
static/images/flags/bm.png | Bin 0 -> 1537 bytes
static/images/flags/bn.png | Bin 0 -> 1318 bytes
static/images/flags/bo.png | Bin 0 -> 1049 bytes
static/images/flags/bq.png | Bin 0 -> 159 bytes
static/images/flags/br.png | Bin 0 -> 1407 bytes
static/images/flags/bs.png | Bin 0 -> 1125 bytes
static/images/flags/bt.png | Bin 0 -> 1431 bytes
static/images/flags/bv.png | Bin 0 -> 1313 bytes
static/images/flags/bw.png | Bin 0 -> 795 bytes
static/images/flags/by.png | Bin 0 -> 1152 bytes
static/images/flags/bz.png | Bin 0 -> 1261 bytes
static/images/flags/ca.png | Bin 0 -> 1259 bytes
static/images/flags/cc.png | Bin 0 -> 1580 bytes
static/images/flags/cd.png | Bin 0 -> 1332 bytes
static/images/flags/cf.png | Bin 0 -> 1219 bytes
static/images/flags/cg.png | Bin 0 -> 1239 bytes
static/images/flags/ch.png | Bin 0 -> 1078 bytes
static/images/flags/ci.png | Bin 0 -> 1039 bytes
static/images/flags/ck.png | Bin 0 -> 1576 bytes
static/images/flags/cl.png | Bin 0 -> 409 bytes
static/images/flags/cm.png | Bin 0 -> 1023 bytes
static/images/flags/cn.png | Bin 0 -> 1242 bytes
static/images/flags/co.png | Bin 0 -> 573 bytes
static/images/flags/cr.png | Bin 0 -> 1107 bytes
static/images/flags/cs.png | Bin 0 -> 194 bytes
static/images/flags/cu.png | Bin 0 -> 1217 bytes
static/images/flags/cv.png | Bin 0 -> 1180 bytes
static/images/flags/cw.png | Bin 0 -> 396 bytes
static/images/flags/cx.png | Bin 0 -> 1580 bytes
static/images/flags/cy.png | Bin 0 -> 1099 bytes
static/images/flags/cz.png | Bin 0 -> 1175 bytes
static/images/flags/de.png | Bin 0 -> 621 bytes
static/images/flags/dj.png | Bin 0 -> 1109 bytes
static/images/flags/dk.png | Bin 0 -> 1011 bytes
static/images/flags/dm.png | Bin 0 -> 1312 bytes
static/images/flags/do.png | Bin 0 -> 1126 bytes
static/images/flags/dz.png | Bin 0 -> 1112 bytes
static/images/flags/ec.png | Bin 0 -> 1135 bytes
static/images/flags/ee.png | Bin 0 -> 583 bytes
static/images/flags/eg.png | Bin 0 -> 891 bytes
static/images/flags/eh.png | Bin 0 -> 638 bytes
static/images/flags/er.png | Bin 0 -> 1431 bytes
static/images/flags/es.png | Bin 0 -> 1124 bytes
static/images/flags/et.png | Bin 0 -> 1277 bytes
static/images/flags/fi.png | Bin 0 -> 1272 bytes
static/images/flags/fj.png | Bin 0 -> 1501 bytes
static/images/flags/fk.png | Bin 0 -> 1393 bytes
static/images/flags/fm.png | Bin 0 -> 1385 bytes
static/images/flags/fo.png | Bin 0 -> 906 bytes
static/images/flags/fr.png | Bin 0 -> 903 bytes
static/images/flags/ga.png | Bin 0 -> 893 bytes
static/images/flags/gb.png | Bin 0 -> 1659 bytes
static/images/flags/gd.png | Bin 0 -> 1459 bytes
static/images/flags/ge.png | Bin 0 -> 781 bytes
static/images/flags/gf.png | Bin 0 -> 903 bytes
static/images/flags/gg.png | Bin 0 -> 701 bytes
static/images/flags/gh.png | Bin 0 -> 1014 bytes
static/images/flags/gi.png | Bin 0 -> 1201 bytes
static/images/flags/gl.png | Bin 0 -> 1213 bytes
static/images/flags/gm.png | Bin 0 -> 1054 bytes
static/images/flags/gn.png | Bin 0 -> 1031 bytes
static/images/flags/gp.png | Bin 0 -> 1197 bytes
static/images/flags/gq.png | Bin 0 -> 1169 bytes
static/images/flags/gr.png | Bin 0 -> 255 bytes
static/images/flags/gs.png | Bin 0 -> 1920 bytes
static/images/flags/gt.png | Bin 0 -> 1047 bytes
static/images/flags/gu.png | Bin 0 -> 1151 bytes
static/images/flags/gw.png | Bin 0 -> 1064 bytes
static/images/flags/gy.png | Bin 0 -> 1386 bytes
static/images/flags/hk.png | Bin 0 -> 1290 bytes
static/images/flags/hm.png | Bin 0 -> 751 bytes
static/images/flags/hn.png | Bin 0 -> 777 bytes
static/images/flags/hr.png | Bin 0 -> 1222 bytes
static/images/flags/ht.png | Bin 0 -> 1136 bytes
static/images/flags/hu.png | Bin 0 -> 950 bytes
static/images/flags/id.png | Bin 0 -> 488 bytes
static/images/flags/ie.png | Bin 0 -> 906 bytes
static/images/flags/il.png | Bin 0 -> 1094 bytes
static/images/flags/im.png | Bin 0 -> 962 bytes
static/images/flags/in.png | Bin 0 -> 447 bytes
static/images/flags/io.png | Bin 0 -> 1916 bytes
static/images/flags/iq.png | Bin 0 -> 1094 bytes
static/images/flags/ir.png | Bin 0 -> 1259 bytes
static/images/flags/is.png | Bin 0 -> 1254 bytes
static/images/flags/it.png | Bin 0 -> 987 bytes
static/images/flags/je.png | Bin 0 -> 1748 bytes
static/images/flags/jm.png | Bin 0 -> 1438 bytes
static/images/flags/jo.png | Bin 0 -> 1219 bytes
static/images/flags/jp.png | Bin 0 -> 1117 bytes
static/images/flags/ke.png | Bin 0 -> 1114 bytes
static/images/flags/kg.png | Bin 0 -> 1324 bytes
static/images/flags/kh.png | Bin 0 -> 1210 bytes
static/images/flags/ki.png | Bin 0 -> 1589 bytes
static/images/flags/km.png | Bin 0 -> 1214 bytes
static/images/flags/kn.png | Bin 0 -> 1378 bytes
static/images/flags/kp.png | Bin 0 -> 1229 bytes
static/images/flags/kr.png | Bin 0 -> 1389 bytes
static/images/flags/kw.png | Bin 0 -> 1032 bytes
static/images/flags/ky.png | Bin 0 -> 1425 bytes
static/images/flags/kz.png | Bin 0 -> 1331 bytes
static/images/flags/la.png | Bin 0 -> 1177 bytes
static/images/flags/lb.png | Bin 0 -> 698 bytes
static/images/flags/lc.png | Bin 0 -> 1282 bytes
static/images/flags/li.png | Bin 0 -> 1094 bytes
static/images/flags/lk.png | Bin 0 -> 1433 bytes
static/images/flags/lr.png | Bin 0 -> 1170 bytes
static/images/flags/ls.png | Bin 0 -> 1310 bytes
static/images/flags/lt.png | Bin 0 -> 981 bytes
static/images/flags/lu.png | Bin 0 -> 973 bytes
static/images/flags/lv.png | Bin 0 -> 114 bytes
static/images/flags/ly.png | Bin 0 -> 149 bytes
static/images/flags/ma.png | Bin 0 -> 1193 bytes
static/images/flags/mc.png | Bin 0 -> 488 bytes
static/images/flags/md.png | Bin 0 -> 1265 bytes
static/images/flags/me.png | Bin 0 -> 884 bytes
static/images/flags/mf.png | Bin 0 -> 903 bytes
static/images/flags/mg.png | Bin 0 -> 986 bytes
static/images/flags/mh.png | Bin 0 -> 1508 bytes
static/images/flags/mk.png | Bin 0 -> 1697 bytes
static/images/flags/ml.png | Bin 0 -> 876 bytes
static/images/flags/mm.png | Bin 0 -> 1210 bytes
static/images/flags/mn.png | Bin 0 -> 1313 bytes
static/images/flags/mo.png | Bin 0 -> 670 bytes
static/images/flags/mp.png | Bin 0 -> 1283 bytes
static/images/flags/mq.png | Bin 0 -> 1534 bytes
static/images/flags/mr.png | Bin 0 -> 1114 bytes
static/images/flags/ms.png | Bin 0 -> 1435 bytes
static/images/flags/mt.png | Bin 0 -> 1000 bytes
static/images/flags/mu.png | Bin 0 -> 1096 bytes
static/images/flags/mv.png | Bin 0 -> 1204 bytes
static/images/flags/mw.png | Bin 0 -> 1141 bytes
static/images/flags/mx.png | Bin 0 -> 1079 bytes
static/images/flags/my.png | Bin 0 -> 1340 bytes
static/images/flags/mz.png | Bin 0 -> 1196 bytes
static/images/flags/na.png | Bin 0 -> 1489 bytes
static/images/flags/nc.png | Bin 0 -> 903 bytes
static/images/flags/ne.png | Bin 0 -> 698 bytes
static/images/flags/nf.png | Bin 0 -> 1140 bytes
static/images/flags/ng.png | Bin 0 -> 810 bytes
static/images/flags/ni.png | Bin 0 -> 840 bytes
static/images/flags/nl.png | Bin 0 -> 846 bytes
static/images/flags/no.png | Bin 0 -> 1313 bytes
static/images/flags/np.png | Bin 0 -> 1144 bytes
static/images/flags/nr.png | Bin 0 -> 1071 bytes
static/images/flags/nu.png | Bin 0 -> 1141 bytes
static/images/flags/nz.png | Bin 0 -> 1440 bytes
static/images/flags/om.png | Bin 0 -> 1168 bytes
static/images/flags/pa.png | Bin 0 -> 1101 bytes
static/images/flags/pe.png | Bin 0 -> 1140 bytes
static/images/flags/pf.png | Bin 0 -> 1186 bytes
static/images/flags/pg.png | Bin 0 -> 1352 bytes
static/images/flags/ph.png | Bin 0 -> 1218 bytes
static/images/flags/pk.png | Bin 0 -> 1165 bytes
static/images/flags/pl.png | Bin 0 -> 551 bytes
static/images/flags/pm.png | Bin 0 -> 2327 bytes
static/images/flags/pn.png | Bin 0 -> 1499 bytes
static/images/flags/pr.png | Bin 0 -> 1235 bytes
static/images/flags/ps.png | Bin 0 -> 424 bytes
static/images/flags/pt.png | Bin 0 -> 1223 bytes
static/images/flags/pw.png | Bin 0 -> 1185 bytes
static/images/flags/py.png | Bin 0 -> 1055 bytes
static/images/flags/qa.png | Bin 0 -> 1120 bytes
static/images/flags/re.png | Bin 0 -> 207 bytes
static/images/flags/ro.png | Bin 0 -> 912 bytes
static/images/flags/rs.png | Bin 0 -> 834 bytes
static/images/flags/ru.png | Bin 0 -> 576 bytes
static/images/flags/rw.png | Bin 0 -> 1141 bytes
static/images/flags/sa.png | Bin 0 -> 1299 bytes
static/images/flags/sb.png | Bin 0 -> 1609 bytes
static/images/flags/sc.png | Bin 0 -> 1518 bytes
static/images/flags/sd.png | Bin 0 -> 1169 bytes
static/images/flags/se.png | Bin 0 -> 557 bytes
static/images/flags/sg.png | Bin 0 -> 1122 bytes
static/images/flags/sh.png | Bin 0 -> 1523 bytes
static/images/flags/si.png | Bin 0 -> 1121 bytes
static/images/flags/sj.png | Bin 0 -> 363 bytes
static/images/flags/sk.png | Bin 0 -> 1237 bytes
static/images/flags/sl.png | Bin 0 -> 774 bytes
static/images/flags/sm.png | Bin 0 -> 1084 bytes
static/images/flags/sn.png | Bin 0 -> 842 bytes
static/images/flags/so.png | Bin 0 -> 1137 bytes
static/images/flags/sr.png | Bin 0 -> 1175 bytes
static/images/flags/ss.png | Bin 0 -> 714 bytes
static/images/flags/st.png | Bin 0 -> 1202 bytes
static/images/flags/sv.png | Bin 0 -> 843 bytes
static/images/flags/sx.png | Bin 0 -> 851 bytes
static/images/flags/sy.png | Bin 0 -> 1039 bytes
static/images/flags/sz.png | Bin 0 -> 1383 bytes
static/images/flags/tc.png | Bin 0 -> 1404 bytes
static/images/flags/td.png | Bin 0 -> 1077 bytes
static/images/flags/tf.png | Bin 0 -> 195 bytes
static/images/flags/tg.png | Bin 0 -> 1226 bytes
static/images/flags/th.png | Bin 0 -> 1071 bytes
static/images/flags/tj.png | Bin 0 -> 1110 bytes
static/images/flags/tk.png | Bin 0 -> 1440 bytes
static/images/flags/tl.png | Bin 0 -> 886 bytes
static/images/flags/tm.png | Bin 0 -> 1421 bytes
static/images/flags/tn.png | Bin 0 -> 1210 bytes
static/images/flags/to.png | Bin 0 -> 941 bytes
static/images/flags/tr.png | Bin 0 -> 915 bytes
static/images/flags/tt.png | Bin 0 -> 1266 bytes
static/images/flags/tv.png | Bin 0 -> 1389 bytes
static/images/flags/tw.png | Bin 0 -> 1157 bytes
static/images/flags/tz.png | Bin 0 -> 1333 bytes
static/images/flags/ua.png | Bin 0 -> 711 bytes
static/images/flags/ug.png | Bin 0 -> 1097 bytes
static/images/flags/um.png | Bin 0 -> 1287 bytes
static/images/flags/us.png | Bin 0 -> 1287 bytes
static/images/flags/uy.png | Bin 0 -> 1128 bytes
static/images/flags/uz.png | Bin 0 -> 1120 bytes
static/images/flags/va.png | Bin 0 -> 1022 bytes
static/images/flags/vc.png | Bin 0 -> 1326 bytes
static/images/flags/ve.png | Bin 0 -> 1173 bytes
static/images/flags/vg.png | Bin 0 -> 1397 bytes
static/images/flags/vi.png | Bin 0 -> 1507 bytes
static/images/flags/vn.png | Bin 0 -> 1299 bytes
static/images/flags/vu.png | Bin 0 -> 1343 bytes
static/images/flags/wf.png | Bin 0 -> 1315 bytes
static/images/flags/ws.png | Bin 0 -> 1205 bytes
static/images/flags/xk.png | Bin 0 -> 812 bytes
static/images/flags/ye.png | Bin 0 -> 600 bytes
static/images/flags/yt.png | Bin 0 -> 207 bytes
static/images/flags/za.png | Bin 0 -> 1398 bytes
static/images/flags/zm.png | Bin 0 -> 1170 bytes
static/images/flags/zw.png | Bin 0 -> 1222 bytes
static/images/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes
static/images/glyphicons-halflings.png | Bin 0 -> 12799 bytes
static/images/gplus-32.png | Bin 0 -> 1513 bytes
static/images/twitter.png | Bin 0 -> 1120 bytes
static/js/analytics.min.js | 3 +
static/js/bootstrap.min.js | 6 +
static/js/calendar.js | 29 +
static/js/dd_belatedpng.js | 13 +
static/js/jquery.js | 4 +
static/js/modernizr.custom.js | 4 +
static/js/share.js | 44 +
static/js/web2py.js | 721 +++++++++++++++
static/js/web2py_bootstrap.js | 33 +
static/robots.txt | 11 +
static/sitemap.xml | 255 ++++++
views/__init__.py | 1 +
views/ajax/search.json | 1 +
views/appadmin.html | 268 ++++++
views/default/continent.html | 6 +
views/default/dynamic.html | 6 +
views/default/edit.html | 3 +
views/default/index.html | 19 +
views/default/search.html | 118 +++
views/default/sitemap.xml | 4 +
views/default/user.html | 29 +
views/default/view.html | 5 +
views/generic.html | 16 +
views/generic.ics | 17 +
views/generic.json | 1 +
views/generic.jsonp | 23 +
views/generic.load | 30 +
views/generic.map | 69 ++
views/generic.pdf | Bin 0 -> 306 bytes
views/generic.rss | 10 +
views/generic.xml | 1 +
views/layout.html | 148 +++
views/web2py_ajax.html | 16 +
353 files changed, 6026 insertions(+)
create mode 100644 __init__.py
create mode 100644 controllers/ajax.py
create mode 100644 controllers/appadmin.py
create mode 100644 controllers/default.py
create mode 100644 models/1_db.py
create mode 100644 models/2_menu.py
create mode 100644 models/3_cache.py
create mode 100644 modules/Captcha/Base.py
create mode 100644 modules/Captcha/File.py
create mode 100644 modules/Captcha/Visual/Backgrounds.py
create mode 100644 modules/Captcha/Visual/Base.py
create mode 100644 modules/Captcha/Visual/Distortions.py
create mode 100644 modules/Captcha/Visual/Pictures.py
create mode 100644 modules/Captcha/Visual/Tests.py
create mode 100644 modules/Captcha/Visual/Text.py
create mode 100644 modules/Captcha/Visual/__init__.py
create mode 100644 modules/Captcha/Words.py
create mode 100644 modules/Captcha/__init__.py
create mode 100644 modules/Captcha/data/fonts/vera/COPYRIGHT.TXT
create mode 100644 modules/Captcha/data/fonts/vera/README.TXT
create mode 100644 modules/Captcha/data/fonts/vera/RELEASENOTES.TXT
create mode 100644 modules/Captcha/data/fonts/vera/Vera.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraBI.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraBd.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraIt.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraMoBI.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraMoBd.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraMoIt.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraMono.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraSe.ttf
create mode 100644 modules/Captcha/data/fonts/vera/VeraSeBd.ttf
create mode 100644 modules/Captcha/data/fonts/vera/local.conf
create mode 100644 modules/Captcha/data/pictures/abstract/1.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/10.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/11.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/12.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/2.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/3.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/4.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/5.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/6.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/7.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/8.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/9.jpeg
create mode 100644 modules/Captcha/data/pictures/abstract/README
create mode 100644 modules/Captcha/data/pictures/nature/Craig_Barrington_ocotillo_and_mountains.jpeg
create mode 100644 modules/Captcha/data/pictures/nature/Kerry_Carloy_Chisos_Sunset.jpeg
create mode 100644 modules/Captcha/data/pictures/nature/Paul_Dowty_Mt_Bross.jpeg
create mode 100644 modules/Captcha/data/pictures/nature/README
create mode 100644 modules/Captcha/data/words/README
create mode 100644 modules/Captcha/data/words/basic-english
create mode 100644 modules/captcha.py
create mode 100644 modules/common.py
create mode 100644 static/403.html
create mode 100644 static/404.html
create mode 100644 static/500.html
create mode 100644 static/css/bootstrap-responsive.min.css
create mode 100644 static/css/bootstrap.min.css
create mode 100644 static/css/calendar.css
create mode 100644 static/css/style.css
create mode 100644 static/css/web2py.css
create mode 100644 static/css/web2py_bootstrap.css
create mode 100644 static/css/web2py_bootstrap_nojs.css
create mode 100644 static/images/facebook.png
create mode 100644 static/images/flags/ad.png
create mode 100644 static/images/flags/ae.png
create mode 100644 static/images/flags/af.png
create mode 100644 static/images/flags/ag.png
create mode 100644 static/images/flags/ai.png
create mode 100644 static/images/flags/al.png
create mode 100644 static/images/flags/am.png
create mode 100644 static/images/flags/an.png
create mode 100644 static/images/flags/ao.png
create mode 100644 static/images/flags/aq.png
create mode 100644 static/images/flags/ar.png
create mode 100644 static/images/flags/as.png
create mode 100644 static/images/flags/at.png
create mode 100644 static/images/flags/au.png
create mode 100644 static/images/flags/aw.png
create mode 100644 static/images/flags/ax.png
create mode 100644 static/images/flags/az.png
create mode 100644 static/images/flags/ba.png
create mode 100644 static/images/flags/bb.png
create mode 100644 static/images/flags/bd.png
create mode 100644 static/images/flags/be.png
create mode 100644 static/images/flags/bf.png
create mode 100644 static/images/flags/bg.png
create mode 100644 static/images/flags/bh.png
create mode 100644 static/images/flags/bi.png
create mode 100644 static/images/flags/bj.png
create mode 100644 static/images/flags/bl.png
create mode 100644 static/images/flags/bm.png
create mode 100644 static/images/flags/bn.png
create mode 100644 static/images/flags/bo.png
create mode 100644 static/images/flags/bq.png
create mode 100644 static/images/flags/br.png
create mode 100644 static/images/flags/bs.png
create mode 100644 static/images/flags/bt.png
create mode 100644 static/images/flags/bv.png
create mode 100644 static/images/flags/bw.png
create mode 100644 static/images/flags/by.png
create mode 100644 static/images/flags/bz.png
create mode 100644 static/images/flags/ca.png
create mode 100644 static/images/flags/cc.png
create mode 100644 static/images/flags/cd.png
create mode 100644 static/images/flags/cf.png
create mode 100644 static/images/flags/cg.png
create mode 100644 static/images/flags/ch.png
create mode 100644 static/images/flags/ci.png
create mode 100644 static/images/flags/ck.png
create mode 100644 static/images/flags/cl.png
create mode 100644 static/images/flags/cm.png
create mode 100644 static/images/flags/cn.png
create mode 100644 static/images/flags/co.png
create mode 100644 static/images/flags/cr.png
create mode 100644 static/images/flags/cs.png
create mode 100644 static/images/flags/cu.png
create mode 100644 static/images/flags/cv.png
create mode 100644 static/images/flags/cw.png
create mode 100644 static/images/flags/cx.png
create mode 100644 static/images/flags/cy.png
create mode 100644 static/images/flags/cz.png
create mode 100644 static/images/flags/de.png
create mode 100644 static/images/flags/dj.png
create mode 100644 static/images/flags/dk.png
create mode 100644 static/images/flags/dm.png
create mode 100644 static/images/flags/do.png
create mode 100644 static/images/flags/dz.png
create mode 100644 static/images/flags/ec.png
create mode 100644 static/images/flags/ee.png
create mode 100644 static/images/flags/eg.png
create mode 100644 static/images/flags/eh.png
create mode 100644 static/images/flags/er.png
create mode 100644 static/images/flags/es.png
create mode 100644 static/images/flags/et.png
create mode 100644 static/images/flags/fi.png
create mode 100644 static/images/flags/fj.png
create mode 100644 static/images/flags/fk.png
create mode 100644 static/images/flags/fm.png
create mode 100644 static/images/flags/fo.png
create mode 100644 static/images/flags/fr.png
create mode 100644 static/images/flags/ga.png
create mode 100644 static/images/flags/gb.png
create mode 100644 static/images/flags/gd.png
create mode 100644 static/images/flags/ge.png
create mode 100644 static/images/flags/gf.png
create mode 100644 static/images/flags/gg.png
create mode 100644 static/images/flags/gh.png
create mode 100644 static/images/flags/gi.png
create mode 100644 static/images/flags/gl.png
create mode 100644 static/images/flags/gm.png
create mode 100644 static/images/flags/gn.png
create mode 100644 static/images/flags/gp.png
create mode 100644 static/images/flags/gq.png
create mode 100644 static/images/flags/gr.png
create mode 100644 static/images/flags/gs.png
create mode 100644 static/images/flags/gt.png
create mode 100644 static/images/flags/gu.png
create mode 100644 static/images/flags/gw.png
create mode 100644 static/images/flags/gy.png
create mode 100644 static/images/flags/hk.png
create mode 100644 static/images/flags/hm.png
create mode 100644 static/images/flags/hn.png
create mode 100644 static/images/flags/hr.png
create mode 100644 static/images/flags/ht.png
create mode 100644 static/images/flags/hu.png
create mode 100644 static/images/flags/id.png
create mode 100644 static/images/flags/ie.png
create mode 100644 static/images/flags/il.png
create mode 100644 static/images/flags/im.png
create mode 100644 static/images/flags/in.png
create mode 100644 static/images/flags/io.png
create mode 100644 static/images/flags/iq.png
create mode 100644 static/images/flags/ir.png
create mode 100644 static/images/flags/is.png
create mode 100644 static/images/flags/it.png
create mode 100644 static/images/flags/je.png
create mode 100644 static/images/flags/jm.png
create mode 100644 static/images/flags/jo.png
create mode 100644 static/images/flags/jp.png
create mode 100644 static/images/flags/ke.png
create mode 100644 static/images/flags/kg.png
create mode 100644 static/images/flags/kh.png
create mode 100644 static/images/flags/ki.png
create mode 100644 static/images/flags/km.png
create mode 100644 static/images/flags/kn.png
create mode 100644 static/images/flags/kp.png
create mode 100644 static/images/flags/kr.png
create mode 100644 static/images/flags/kw.png
create mode 100644 static/images/flags/ky.png
create mode 100644 static/images/flags/kz.png
create mode 100644 static/images/flags/la.png
create mode 100644 static/images/flags/lb.png
create mode 100644 static/images/flags/lc.png
create mode 100644 static/images/flags/li.png
create mode 100644 static/images/flags/lk.png
create mode 100644 static/images/flags/lr.png
create mode 100644 static/images/flags/ls.png
create mode 100644 static/images/flags/lt.png
create mode 100644 static/images/flags/lu.png
create mode 100644 static/images/flags/lv.png
create mode 100644 static/images/flags/ly.png
create mode 100644 static/images/flags/ma.png
create mode 100644 static/images/flags/mc.png
create mode 100644 static/images/flags/md.png
create mode 100644 static/images/flags/me.png
create mode 100644 static/images/flags/mf.png
create mode 100644 static/images/flags/mg.png
create mode 100644 static/images/flags/mh.png
create mode 100644 static/images/flags/mk.png
create mode 100644 static/images/flags/ml.png
create mode 100644 static/images/flags/mm.png
create mode 100644 static/images/flags/mn.png
create mode 100644 static/images/flags/mo.png
create mode 100644 static/images/flags/mp.png
create mode 100644 static/images/flags/mq.png
create mode 100644 static/images/flags/mr.png
create mode 100644 static/images/flags/ms.png
create mode 100644 static/images/flags/mt.png
create mode 100644 static/images/flags/mu.png
create mode 100644 static/images/flags/mv.png
create mode 100644 static/images/flags/mw.png
create mode 100644 static/images/flags/mx.png
create mode 100644 static/images/flags/my.png
create mode 100644 static/images/flags/mz.png
create mode 100644 static/images/flags/na.png
create mode 100644 static/images/flags/nc.png
create mode 100644 static/images/flags/ne.png
create mode 100644 static/images/flags/nf.png
create mode 100644 static/images/flags/ng.png
create mode 100644 static/images/flags/ni.png
create mode 100644 static/images/flags/nl.png
create mode 100644 static/images/flags/no.png
create mode 100644 static/images/flags/np.png
create mode 100644 static/images/flags/nr.png
create mode 100644 static/images/flags/nu.png
create mode 100644 static/images/flags/nz.png
create mode 100644 static/images/flags/om.png
create mode 100644 static/images/flags/pa.png
create mode 100644 static/images/flags/pe.png
create mode 100644 static/images/flags/pf.png
create mode 100644 static/images/flags/pg.png
create mode 100644 static/images/flags/ph.png
create mode 100644 static/images/flags/pk.png
create mode 100644 static/images/flags/pl.png
create mode 100644 static/images/flags/pm.png
create mode 100644 static/images/flags/pn.png
create mode 100644 static/images/flags/pr.png
create mode 100644 static/images/flags/ps.png
create mode 100644 static/images/flags/pt.png
create mode 100644 static/images/flags/pw.png
create mode 100644 static/images/flags/py.png
create mode 100644 static/images/flags/qa.png
create mode 100644 static/images/flags/re.png
create mode 100644 static/images/flags/ro.png
create mode 100644 static/images/flags/rs.png
create mode 100644 static/images/flags/ru.png
create mode 100644 static/images/flags/rw.png
create mode 100644 static/images/flags/sa.png
create mode 100644 static/images/flags/sb.png
create mode 100644 static/images/flags/sc.png
create mode 100644 static/images/flags/sd.png
create mode 100644 static/images/flags/se.png
create mode 100644 static/images/flags/sg.png
create mode 100644 static/images/flags/sh.png
create mode 100644 static/images/flags/si.png
create mode 100644 static/images/flags/sj.png
create mode 100644 static/images/flags/sk.png
create mode 100644 static/images/flags/sl.png
create mode 100644 static/images/flags/sm.png
create mode 100644 static/images/flags/sn.png
create mode 100644 static/images/flags/so.png
create mode 100644 static/images/flags/sr.png
create mode 100644 static/images/flags/ss.png
create mode 100644 static/images/flags/st.png
create mode 100644 static/images/flags/sv.png
create mode 100644 static/images/flags/sx.png
create mode 100644 static/images/flags/sy.png
create mode 100644 static/images/flags/sz.png
create mode 100644 static/images/flags/tc.png
create mode 100644 static/images/flags/td.png
create mode 100644 static/images/flags/tf.png
create mode 100644 static/images/flags/tg.png
create mode 100644 static/images/flags/th.png
create mode 100644 static/images/flags/tj.png
create mode 100644 static/images/flags/tk.png
create mode 100644 static/images/flags/tl.png
create mode 100644 static/images/flags/tm.png
create mode 100644 static/images/flags/tn.png
create mode 100644 static/images/flags/to.png
create mode 100644 static/images/flags/tr.png
create mode 100644 static/images/flags/tt.png
create mode 100644 static/images/flags/tv.png
create mode 100644 static/images/flags/tw.png
create mode 100644 static/images/flags/tz.png
create mode 100644 static/images/flags/ua.png
create mode 100644 static/images/flags/ug.png
create mode 100644 static/images/flags/um.png
create mode 100644 static/images/flags/us.png
create mode 100644 static/images/flags/uy.png
create mode 100644 static/images/flags/uz.png
create mode 100644 static/images/flags/va.png
create mode 100644 static/images/flags/vc.png
create mode 100644 static/images/flags/ve.png
create mode 100644 static/images/flags/vg.png
create mode 100644 static/images/flags/vi.png
create mode 100644 static/images/flags/vn.png
create mode 100644 static/images/flags/vu.png
create mode 100644 static/images/flags/wf.png
create mode 100644 static/images/flags/ws.png
create mode 100644 static/images/flags/xk.png
create mode 100644 static/images/flags/ye.png
create mode 100644 static/images/flags/yt.png
create mode 100644 static/images/flags/za.png
create mode 100644 static/images/flags/zm.png
create mode 100644 static/images/flags/zw.png
create mode 100644 static/images/glyphicons-halflings-white.png
create mode 100644 static/images/glyphicons-halflings.png
create mode 100644 static/images/gplus-32.png
create mode 100644 static/images/twitter.png
create mode 100644 static/js/analytics.min.js
create mode 100644 static/js/bootstrap.min.js
create mode 100644 static/js/calendar.js
create mode 100644 static/js/dd_belatedpng.js
create mode 100644 static/js/jquery.js
create mode 100644 static/js/modernizr.custom.js
create mode 100644 static/js/share.js
create mode 100644 static/js/web2py.js
create mode 100644 static/js/web2py_bootstrap.js
create mode 100644 static/robots.txt
create mode 100644 static/sitemap.xml
create mode 100644 views/__init__.py
create mode 100644 views/ajax/search.json
create mode 100644 views/appadmin.html
create mode 100644 views/default/continent.html
create mode 100644 views/default/dynamic.html
create mode 100644 views/default/edit.html
create mode 100644 views/default/index.html
create mode 100644 views/default/search.html
create mode 100644 views/default/sitemap.xml
create mode 100644 views/default/user.html
create mode 100644 views/default/view.html
create mode 100644 views/generic.html
create mode 100644 views/generic.ics
create mode 100644 views/generic.json
create mode 100644 views/generic.jsonp
create mode 100644 views/generic.load
create mode 100644 views/generic.map
create mode 100644 views/generic.pdf
create mode 100644 views/generic.rss
create mode 100644 views/generic.xml
create mode 100644 views/layout.html
create mode 100644 views/web2py_ajax.html
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1 @@
+
diff --git a/controllers/ajax.py b/controllers/ajax.py
new file mode 100644
index 0000000..09f52e9
--- /dev/null
+++ b/controllers/ajax.py
@@ -0,0 +1,29 @@
+import re
+import math
+
+
+def search():
+ """Results of AJAX search
+ """
+ records = []
+ num_pages = 0
+ error = ''
+ search_term = request.vars.search_term
+ if search_term:
+ try:
+ page = int(request.vars.page or 0)
+ page_size = int(request.vars.page_size) or 10
+ except (ValueError, TypeError):
+ # passed arguments are invalid
+ error = 'Invalid parameters'
+ else:
+ try:
+ # 'like' not supported on GAE so manually compare strings
+ records = [dict(country=record.country, id=record.id, pretty_link=record.pretty_link) for record in places.search() if re.compile(search_term, flags=re.IGNORECASE).search(record.country)]
+ except re.error:
+ error = 'Invalid search term'
+ else:
+ num_pages = int(math.ceil(len(records) / float(page_size)))
+ records = records[page * page_size:(page + 1) * page_size]
+ return dict(records=records, num_pages=num_pages, error=error)
+
diff --git a/controllers/appadmin.py b/controllers/appadmin.py
new file mode 100644
index 0000000..a74f056
--- /dev/null
+++ b/controllers/appadmin.py
@@ -0,0 +1,672 @@
+# -*- coding: utf-8 -*-
+
+# ##########################################################
+# ## make sure administrator is on localhost
+# ###########################################################
+
+import os
+import socket
+import datetime
+import copy
+import gluon.contenttype
+import gluon.fileutils
+
+try:
+ import pygraphviz as pgv
+except ImportError:
+ pgv = None
+
+is_gae = request.env.web2py_runtime_gae or False
+
+# ## critical --- make a copy of the environment
+
+global_env = copy.copy(globals())
+global_env['datetime'] = datetime
+
+http_host = request.env.http_host.split(':')[0]
+remote_addr = request.env.remote_addr
+try:
+ hosts = (http_host, socket.gethostname(),
+ socket.gethostbyname(http_host),
+ '::1', '127.0.0.1', '::ffff:127.0.0.1')
+except:
+ hosts = (http_host, )
+
+if request.env.http_x_forwarded_for or request.is_https:
+ session.secure()
+elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1") and \
+ (request.function != 'manage'):
+ raise HTTP(200, T('appadmin is disabled because insecure channel'))
+
+if request.function == 'manage':
+ if not 'auth' in globals() or not request.args:
+ redirect(URL(request.controller, 'index'))
+ manager_action = auth.settings.manager_actions.get(request.args(0), None)
+ if manager_action is None and request.args(0) == 'auth':
+ manager_action = dict(role=auth.settings.auth_manager_role,
+ heading=T('Manage Access Control'),
+ tables=[auth.table_user(),
+ auth.table_group(),
+ auth.table_permission()])
+ manager_role = manager_action.get('role', None) if manager_action else None
+ auth.requires_membership(manager_role)(lambda: None)()
+ menu = False
+elif (request.application == 'admin' and not session.authorized) or \
+ (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
+ redirect(URL('admin', 'default', 'index',
+ vars=dict(send=URL(args=request.args, vars=request.vars))))
+else:
+ response.subtitle = T('Database Administration (appadmin)')
+ menu = True
+
+ignore_rw = True
+response.view = 'appadmin.html'
+if menu:
+ response.menu = [[T('design'), False, URL('admin', 'default', 'design',
+ args=[request.application])], [T('db'), False,
+ URL('index')], [T('state'), False,
+ URL('state')], [T('cache'), False,
+ URL('ccache')]]
+
+# ##########################################################
+# ## auxiliary functions
+# ###########################################################
+
+if False and request.tickets_db:
+ from gluon.restricted import TicketStorage
+ ts = TicketStorage()
+ ts._get_table(request.tickets_db, ts.tablename, request.application)
+
+def get_databases(request):
+ dbs = {}
+ for (key, value) in global_env.items():
+ cond = False
+ try:
+ cond = isinstance(value, GQLDB)
+ except:
+ cond = isinstance(value, SQLDB)
+ if cond:
+ dbs[key] = value
+ return dbs
+
+
+databases = get_databases(None)
+
+
+def eval_in_global_env(text):
+ exec ('_ret=%s' % text, {}, global_env)
+ return global_env['_ret']
+
+
+def get_database(request):
+ if request.args and request.args[0] in databases:
+ return eval_in_global_env(request.args[0])
+ else:
+ session.flash = T('invalid request')
+ redirect(URL('index'))
+
+
+def get_table(request):
+ db = get_database(request)
+ if len(request.args) > 1 and request.args[1] in db.tables:
+ return (db, request.args[1])
+ else:
+ session.flash = T('invalid request')
+ redirect(URL('index'))
+
+
+def get_query(request):
+ try:
+ return eval_in_global_env(request.vars.query)
+ except Exception:
+ return None
+
+
+def query_by_table_type(tablename, db, request=request):
+ keyed = hasattr(db[tablename], '_primarykey')
+ if keyed:
+ firstkey = db[tablename][db[tablename]._primarykey[0]]
+ cond = '>0'
+ if firstkey.type in ['string', 'text']:
+ cond = '!=""'
+ qry = '%s.%s.%s%s' % (
+ request.args[0], request.args[1], firstkey.name, cond)
+ else:
+ qry = '%s.%s.id>0' % tuple(request.args[:2])
+ return qry
+
+
+# ##########################################################
+# ## list all databases and tables
+# ###########################################################
+def index():
+ return dict(databases=databases)
+
+
+# ##########################################################
+# ## insert a new record
+# ###########################################################
+
+
+def insert():
+ (db, table) = get_table(request)
+ form = SQLFORM(db[table], ignore_rw=ignore_rw)
+ if form.accepts(request.vars, session):
+ response.flash = T('new record inserted')
+ return dict(form=form, table=db[table])
+
+
+# ##########################################################
+# ## list all records in table and insert new record
+# ###########################################################
+
+
+def download():
+ import os
+ db = get_database(request)
+ return response.download(request, db)
+
+
+def csv():
+ import gluon.contenttype
+ response.headers['Content-Type'] = \
+ gluon.contenttype.contenttype('.csv')
+ db = get_database(request)
+ query = get_query(request)
+ if not query:
+ return None
+ response.headers['Content-disposition'] = 'attachment; filename=%s_%s.csv'\
+ % tuple(request.vars.query.split('.')[:2])
+ return str(db(query, ignore_common_filters=True).select())
+
+
+def import_csv(table, file):
+ table.import_from_csv_file(file)
+
+
+def select():
+ import re
+ db = get_database(request)
+ dbname = request.args[0]
+ try:
+ is_imap = db._uri.startswith("imap://")
+ except (KeyError, AttributeError, TypeError):
+ is_imap = False
+ regex = re.compile('(?P
\w+)\.(?P\w+)=(?P\d+)')
+ if len(request.args) > 1 and hasattr(db[request.args[1]], '_primarykey'):
+ regex = re.compile('(?P\w+)\.(?P\w+)=(?P.+)')
+ if request.vars.query:
+ match = regex.match(request.vars.query)
+ if match:
+ request.vars.query = '%s.%s.%s==%s' % (request.args[0],
+ match.group('table'), match.group('field'),
+ match.group('value'))
+ else:
+ request.vars.query = session.last_query
+ query = get_query(request)
+ if request.vars.start:
+ start = int(request.vars.start)
+ else:
+ start = 0
+ nrows = 0
+
+ step = 100
+ fields = []
+
+ if is_imap:
+ step = 3
+
+ stop = start + step
+
+ table = None
+ rows = []
+ orderby = request.vars.orderby
+ if orderby:
+ orderby = dbname + '.' + orderby
+ if orderby == session.last_orderby:
+ if orderby[0] == '~':
+ orderby = orderby[1:]
+ else:
+ orderby = '~' + orderby
+ session.last_orderby = orderby
+ session.last_query = request.vars.query
+ form = FORM(TABLE(TR(T('Query:'), '', INPUT(_style='width:400px',
+ _name='query', _value=request.vars.query or '',
+ requires=IS_NOT_EMPTY(
+ error_message=T("Cannot be empty")))), TR(T('Update:'),
+ INPUT(_name='update_check', _type='checkbox',
+ value=False), INPUT(_style='width:400px',
+ _name='update_fields', _value=request.vars.update_fields
+ or '')), TR(T('Delete:'), INPUT(_name='delete_check',
+ _class='delete', _type='checkbox', value=False), ''),
+ TR('', '', INPUT(_type='submit', _value=T('submit')))),
+ _action=URL(r=request, args=request.args))
+
+ tb = None
+ if form.accepts(request.vars, formname=None):
+ regex = re.compile(request.args[0] + '\.(?P\w+)\..+')
+ match = regex.match(form.vars.query.strip())
+ if match:
+ table = match.group('table')
+ try:
+ nrows = db(query, ignore_common_filters=True).count()
+ if form.vars.update_check and form.vars.update_fields:
+ db(query, ignore_common_filters=True).update(
+ **eval_in_global_env('dict(%s)' % form.vars.update_fields))
+ response.flash = T('%s %%{row} updated', nrows)
+ elif form.vars.delete_check:
+ db(query, ignore_common_filters=True).delete()
+ response.flash = T('%s %%{row} deleted', nrows)
+ nrows = db(query, ignore_common_filters=True).count()
+
+ if is_imap:
+ fields = [db[table][name] for name in
+ ("id", "uid", "created", "to",
+ "sender", "subject")]
+ if orderby:
+ rows = db(query, ignore_common_filters=True).select(
+ *fields, limitby=(start, stop),
+ orderby=eval_in_global_env(orderby))
+ else:
+ rows = db(query, ignore_common_filters=True).select(
+ *fields, limitby=(start, stop))
+ except Exception, e:
+ import traceback
+ tb = traceback.format_exc()
+ (rows, nrows) = ([], 0)
+ response.flash = DIV(T('Invalid Query'), PRE(str(e)))
+ # begin handle upload csv
+ csv_table = table or request.vars.table
+ if csv_table:
+ formcsv = FORM(str(T('or import from csv file')) + " ",
+ INPUT(_type='file', _name='csvfile'),
+ INPUT(_type='hidden', _value=csv_table, _name='table'),
+ INPUT(_type='submit', _value=T('import')))
+ else:
+ formcsv = None
+ if formcsv and formcsv.process().accepted:
+ try:
+ import_csv(db[request.vars.table],
+ request.vars.csvfile.file)
+ response.flash = T('data uploaded')
+ except Exception, e:
+ response.flash = DIV(T('unable to parse csv file'), PRE(str(e)))
+ # end handle upload csv
+
+ return dict(
+ form=form,
+ table=table,
+ start=start,
+ stop=stop,
+ step=step,
+ nrows=nrows,
+ rows=rows,
+ query=request.vars.query,
+ formcsv=formcsv,
+ tb=tb
+ )
+
+
+# ##########################################################
+# ## edit delete one record
+# ###########################################################
+
+
+def update():
+ (db, table) = get_table(request)
+ keyed = hasattr(db[table], '_primarykey')
+ record = None
+ db[table]._common_filter = None
+ if keyed:
+ key = [f for f in request.vars if f in db[table]._primarykey]
+ if key:
+ record = db(db[table][key[0]] == request.vars[key[
+ 0]]).select().first()
+ else:
+ record = db(db[table].id == request.args(
+ 2)).select().first()
+
+ if not record:
+ qry = query_by_table_type(table, db)
+ session.flash = T('record does not exist')
+ redirect(URL('select', args=request.args[:1],
+ vars=dict(query=qry)))
+
+ if keyed:
+ for k in db[table]._primarykey:
+ db[table][k].writable = False
+
+ form = SQLFORM(
+ db[table], record, deletable=True, delete_label=T('Check to delete'),
+ ignore_rw=ignore_rw and not keyed,
+ linkto=URL('select',
+ args=request.args[:1]), upload=URL(r=request,
+ f='download', args=request.args[:1]))
+
+ if form.accepts(request.vars, session):
+ session.flash = T('done!')
+ qry = query_by_table_type(table, db)
+ redirect(URL('select', args=request.args[:1],
+ vars=dict(query=qry)))
+ return dict(form=form, table=db[table])
+
+
+# ##########################################################
+# ## get global variables
+# ###########################################################
+
+
+def state():
+ return dict()
+
+
+def ccache():
+ if is_gae:
+ form = FORM(
+ P(TAG.BUTTON(T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")))
+ else:
+ cache.ram.initialize()
+ cache.disk.initialize()
+
+ form = FORM(
+ P(TAG.BUTTON(
+ T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")),
+ P(TAG.BUTTON(
+ T("Clear RAM"), _type="submit", _name="ram", _value="ram")),
+ P(TAG.BUTTON(
+ T("Clear DISK"), _type="submit", _name="disk", _value="disk")),
+ )
+
+ if form.accepts(request.vars, session):
+ session.flash = ""
+ if is_gae:
+ if request.vars.yes:
+ cache.ram.clear()
+ session.flash += T("Cache Cleared")
+ else:
+ clear_ram = False
+ clear_disk = False
+ if request.vars.yes:
+ clear_ram = clear_disk = True
+ if request.vars.ram:
+ clear_ram = True
+ if request.vars.disk:
+ clear_disk = True
+ if clear_ram:
+ cache.ram.clear()
+ session.flash += T("Ram Cleared")
+ if clear_disk:
+ cache.disk.clear()
+ session.flash += T("Disk Cleared")
+ redirect(URL(r=request))
+
+ try:
+ from guppy import hpy
+ hp = hpy()
+ except ImportError:
+ hp = False
+
+ import shelve
+ import os
+ import copy
+ import time
+ import math
+ from gluon import portalocker
+
+ ram = {
+ 'entries': 0,
+ 'bytes': 0,
+ 'objects': 0,
+ 'hits': 0,
+ 'misses': 0,
+ 'ratio': 0,
+ 'oldest': time.time(),
+ 'keys': []
+ }
+
+ disk = copy.copy(ram)
+ total = copy.copy(ram)
+ disk['keys'] = []
+ total['keys'] = []
+
+ def GetInHMS(seconds):
+ hours = math.floor(seconds / 3600)
+ seconds -= hours * 3600
+ minutes = math.floor(seconds / 60)
+ seconds -= minutes * 60
+ seconds = math.floor(seconds)
+
+ return (hours, minutes, seconds)
+
+ if is_gae:
+ gae_stats = cache.ram.client.get_stats()
+ try:
+ gae_stats['ratio'] = ((gae_stats['hits'] * 100) /
+ (gae_stats['hits'] + gae_stats['misses']))
+ except ZeroDivisionError:
+ gae_stats['ratio'] = T("?")
+ gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age'])
+ total.update(gae_stats)
+ else:
+ for key, value in cache.ram.storage.iteritems():
+ if isinstance(value, dict):
+ ram['hits'] = value['hit_total'] - value['misses']
+ ram['misses'] = value['misses']
+ try:
+ ram['ratio'] = ram['hits'] * 100 / value['hit_total']
+ except (KeyError, ZeroDivisionError):
+ ram['ratio'] = 0
+ else:
+ if hp:
+ ram['bytes'] += hp.iso(value[1]).size
+ ram['objects'] += hp.iso(value[1]).count
+ ram['entries'] += 1
+ if value[0] < ram['oldest']:
+ ram['oldest'] = value[0]
+ ram['keys'].append((key, GetInHMS(time.time() - value[0])))
+ folder = os.path.join(request.folder,'cache')
+ if not os.path.exists(folder):
+ os.mkdir(folder)
+ locker = open(os.path.join(folder, 'cache.lock'), 'a')
+ portalocker.lock(locker, portalocker.LOCK_EX)
+ disk_storage = shelve.open(
+ os.path.join(folder, 'cache.shelve'))
+ try:
+ for key, value in disk_storage.items():
+ if isinstance(value, dict):
+ disk['hits'] = value['hit_total'] - value['misses']
+ disk['misses'] = value['misses']
+ try:
+ disk['ratio'] = disk['hits'] * 100 / value['hit_total']
+ except (KeyError, ZeroDivisionError):
+ disk['ratio'] = 0
+ else:
+ if hp:
+ disk['bytes'] += hp.iso(value[1]).size
+ disk['objects'] += hp.iso(value[1]).count
+ disk['entries'] += 1
+ if value[0] < disk['oldest']:
+ disk['oldest'] = value[0]
+ disk['keys'].append((key, GetInHMS(time.time() - value[0])))
+ finally:
+ portalocker.unlock(locker)
+ locker.close()
+ disk_storage.close()
+
+ total['entries'] = ram['entries'] + disk['entries']
+ total['bytes'] = ram['bytes'] + disk['bytes']
+ total['objects'] = ram['objects'] + disk['objects']
+ total['hits'] = ram['hits'] + disk['hits']
+ total['misses'] = ram['misses'] + disk['misses']
+ total['keys'] = ram['keys'] + disk['keys']
+ try:
+ total['ratio'] = total['hits'] * 100 / (total['hits'] +
+ total['misses'])
+ except (KeyError, ZeroDivisionError):
+ total['ratio'] = 0
+
+ if disk['oldest'] < ram['oldest']:
+ total['oldest'] = disk['oldest']
+ else:
+ total['oldest'] = ram['oldest']
+
+ ram['oldest'] = GetInHMS(time.time() - ram['oldest'])
+ disk['oldest'] = GetInHMS(time.time() - disk['oldest'])
+ total['oldest'] = GetInHMS(time.time() - total['oldest'])
+
+ def key_table(keys):
+ return TABLE(
+ TR(TD(B(T('Key'))), TD(B(T('Time in Cache (h:m:s)')))),
+ *[TR(TD(k[0]), TD('%02d:%02d:%02d' % k[1])) for k in keys],
+ **dict(_class='cache-keys',
+ _style="border-collapse: separate; border-spacing: .5em;"))
+
+ if not is_gae:
+ ram['keys'] = key_table(ram['keys'])
+ disk['keys'] = key_table(disk['keys'])
+ total['keys'] = key_table(total['keys'])
+
+ return dict(form=form, total=total,
+ ram=ram, disk=disk, object_stats=hp != False)
+
+
+def table_template(table):
+ from gluon.html import TR, TD, TABLE, TAG
+
+ def FONT(*args, **kwargs):
+ return TAG.font(*args, **kwargs)
+
+ def types(field):
+ f_type = field.type
+ if not isinstance(f_type,str):
+ return ' '
+ elif f_type == 'string':
+ return field.length
+ elif f_type == 'id':
+ return B('pk')
+ elif f_type.startswith('reference') or \
+ f_type.startswith('list:reference'):
+ return B('fk')
+ else:
+ return ' '
+
+ # This is horribe HTML but the only one graphiz understands
+ rows = []
+ cellpadding = 4
+ color = "#000000"
+ bgcolor = "#FFFFFF"
+ face = "Helvetica"
+ face_bold = "Helvetica Bold"
+ border = 0
+
+ rows.append(TR(TD(FONT(table, _face=face_bold, _color=bgcolor),
+ _colspan=3, _cellpadding=cellpadding,
+ _align="center", _bgcolor=color)))
+ for row in db[table]:
+ rows.append(TR(TD(FONT(row.name, _color=color, _face=face_bold),
+ _align="left", _cellpadding=cellpadding,
+ _border=border),
+ TD(FONT(row.type, _color=color, _face=face),
+ _align="left", _cellpadding=cellpadding,
+ _border=border),
+ TD(FONT(types(row), _color=color, _face=face),
+ _align="center", _cellpadding=cellpadding,
+ _border=border)))
+ return "< %s >" % TABLE(*rows, **dict(_bgcolor=bgcolor, _border=1,
+ _cellborder=0, _cellspacing=0)
+ ).xml()
+
+
+def bg_graph_model():
+ graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
+
+ subgraphs = dict()
+ for tablename in db.tables:
+ if hasattr(db[tablename],'_meta_graphmodel'):
+ meta_graphmodel = db[tablename]._meta_graphmodel
+ else:
+ meta_graphmodel = dict(group='Undefined', color='#ECECEC')
+
+ group = meta_graphmodel['group'].replace(' ', '')
+ if not subgraphs.has_key(group):
+ subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
+ subgraphs[group]['tables'].append(tablename)
+ else:
+ subgraphs[group]['tables'].append(tablename)
+
+ graph.add_node(tablename, name=tablename, shape='plaintext',
+ label=table_template(tablename))
+
+ for n, key in enumerate(subgraphs.iterkeys()):
+ graph.subgraph(nbunch=subgraphs[key]['tables'],
+ name='cluster%d' % n,
+ style='filled',
+ color=subgraphs[key]['meta']['color'],
+ label=subgraphs[key]['meta']['group'])
+
+ for tablename in db.tables:
+ for field in db[tablename]:
+ f_type = field.type
+ if isinstance(f_type,str) and (
+ f_type.startswith('reference') or
+ f_type.startswith('list:reference')):
+ referenced_table = f_type.split()[1].split('.')[0]
+ n1 = graph.get_node(tablename)
+ n2 = graph.get_node(referenced_table)
+ graph.add_edge(n1, n2, color="#4C4C4C", label='')
+
+ graph.layout()
+ if not request.args:
+ response.headers['Content-Type'] = 'image/png'
+ return graph.draw(format='png', prog='dot')
+ else:
+ response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
+ if request.args(0) == 'dot':
+ return graph.string()
+ else:
+ return graph.draw(format=request.args(0), prog='dot')
+
+def graph_model():
+ return dict(databases=databases, pgv=pgv)
+
+def manage():
+ tables = manager_action['tables']
+ if isinstance(tables[0], str):
+ db = manager_action.get('db', auth.db)
+ db = globals()[db] if isinstance(db, str) else db
+ tables = [db[table] for table in tables]
+ if request.args(0) == 'auth':
+ auth.table_user()._plural = T('Users')
+ auth.table_group()._plural = T('Roles')
+ auth.table_membership()._plural = T('Memberships')
+ auth.table_permission()._plural = T('Permissions')
+ if request.extension != 'load':
+ return dict(heading=manager_action.get('heading',
+ T('Manage %(action)s') % dict(action=request.args(0).replace('_', ' ').title())),
+ tablenames=[table._tablename for table in tables],
+ labels=[table._plural.title() for table in tables])
+
+ table = tables[request.args(1, cast=int)]
+ formname = '%s_grid' % table._tablename
+ linked_tables = orderby = None
+ if request.args(0) == 'auth':
+ auth.table_group()._id.readable = \
+ auth.table_membership()._id.readable = \
+ auth.table_permission()._id.readable = False
+ auth.table_membership().user_id.label = T('User')
+ auth.table_membership().group_id.label = T('Role')
+ auth.table_permission().group_id.label = T('Role')
+ auth.table_permission().name.label = T('Permission')
+ if table == auth.table_user():
+ linked_tables=[auth.settings.table_membership_name]
+ elif table == auth.table_group():
+ orderby = 'role' if not request.args(3) or '.group_id' not in request.args(3) else None
+ elif table == auth.table_permission():
+ orderby = 'group_id'
+ kwargs = dict(user_signature=True, maxtextlength=1000,
+ orderby=orderby, linked_tables=linked_tables)
+ smartgrid_args = manager_action.get('smartgrid_args', {})
+ kwargs.update(**smartgrid_args.get('DEFAULT', {}))
+ kwargs.update(**smartgrid_args.get(table._tablename, {}))
+ grid = SQLFORM.smartgrid(table, args=request.args[:2], formname=formname, **kwargs)
+ return grid
diff --git a/controllers/default.py b/controllers/default.py
new file mode 100644
index 0000000..dea649a
--- /dev/null
+++ b/controllers/default.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+
+
+def index():
+ """Index of locations
+ """
+ page = common.get_id(request.args)
+ page_size = 10
+ limitby = (page * page_size, (page + 1) * page_size)
+ records = places.search(limitby=limitby)
+ result = common.format_records(records)
+ return dict(result=result, num_records=len(records), page=page, page_size=page_size)
+
+
+def view():
+ """View a location
+ """
+ place_id = common.get_id(request.args)
+ record = places.get(place_id)
+ form = SQLFORM(db.places, record, readonly=True, showid=False)
+ return dict(form=form)
+
+
+def iso():
+ """Redirect from ISO to country view
+ """
+ if request.args:
+ record = places.get(iso=request.args(0))
+ if record:
+ redirect(record.pretty_url)
+ redirect(URL(f='index'))
+
+
+def continent():
+ """Show countries in this continent
+ """
+ if request.args:
+ logic = db.places.continent == request.args(0)
+ records = places.search(logic=logic)
+ result = common.format_records(records)
+ continent_name = {'AS': 'Asia', 'EU': 'Europe', 'NA': 'North America', 'SA': 'South America', 'AF': 'Africa', 'AN': 'Antractica', 'OC': 'Oceania'}.get(request.args(0), request.args(0))
+ return dict(continent_name=continent_name, result=result)
+ redirect(URL(f='index'))
+
+
+@auth.requires_login()
+def edit():
+ """Edit a location
+ """
+ place_id = common.get_id(request.args)
+ record = places.get(place_id)
+ form = SQLFORM(db.places, record, showid=False, submit_button='Update')
+ if form.process().accepted:
+ session.flash = 'Place updated'
+ redirect(URL(f='view', args=request.args))
+ elif form.errors:
+ response.flash = 'Form has errors'
+ return dict(form=form)
+
+
+def search():
+ """Search interface
+ """
+ return dict()
+
+
+
+def sitemap():
+ return dict(records=places.search())
+
+
+# XXX need to password protect
+def flush():
+ places.load()
+ return 'Places loaded'
+
+
+def user():
+ """
+ exposes:
+ http://..../[app]/default/user/login
+ http://..../[app]/default/user/logout
+ http://..../[app]/default/user/register
+ http://..../[app]/default/user/profile
+ http://..../[app]/default/user/manage_users (requires membership in
+ use @auth.requires_login()
+ @auth.requires_membership('group name')
+ @auth.requires_permission('read','table name',record_id)
+ to decorate functions that need access control
+ """
+
+ if request.args(0) == 'register' and not request.args(1):
+ captcha = local_import('captcha', reload=REFRESH)
+ auth.settings.register_captcha = captcha.Captcha(request, session)
+ return dict(form=auth())
+
+
+def trap():
+ ban_client()
+ return ''
+
+
+def dynamic():
+ return dict()
+
+
+def test():
+ return dict(request=request)
diff --git a/models/1_db.py b/models/1_db.py
new file mode 100644
index 0000000..8482f26
--- /dev/null
+++ b/models/1_db.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+
+REFRESH = request.is_local or 'refresh' in request.vars
+common = local_import('common', reload=REFRESH)
+
+session.connect(request, response, cookie_key='XXX', compression_level=None)
+
+
+## if SSL/HTTPS is properly configured and you want all HTTP requests to
+## be redirected to HTTPS, uncomment the line below:
+# request.requires_https()
+
+if not request.env.web2py_runtime_gae:
+ ## if NOT running on Google App Engine use SQLite or other DB
+ db = DAL('sqlite://storage.sqlite',pool_size=1,check_reserved=['all'])
+else:
+ ## connect to Google BigTable (optional 'google:datastore://namespace')
+ db = DAL('google:datastore')
+ ## store sessions and tickets there
+ session.connect(request, response, db=db)
+ ## or store session in Memcache, Redis, etc.
+ ## from gluon.contrib.memdb import MEMDB
+ ## from google.appengine.api.memcache import Client
+ ## session.connect(request, response, db = MEMDB(Client()))
+
+## by default give a view/generic.extension to all actions from localhost
+## none otherwise. a pattern can be 'controller/function.extension'
+response.generic_patterns = ['*'] if request.is_local else []
+## (optional) optimize handling of static files
+# response.optimize_css = 'concat,minify,inline'
+# response.optimize_js = 'concat,minify,inline'
+## (optional) static assets folder versioning
+# response.static_version = '0.0.0'
+#########################################################################
+## Here is sample code if you need for
+## - email capabilities
+## - authentication (registration, login, logout, ... )
+## - authorization (role based authorization)
+## - services (xml, csv, json, xmlrpc, jsonrpc, amf, rss)
+## - old style crud actions
+## (more options discussed in gluon/tools.py)
+#########################################################################
+
+from gluon.tools import Auth, Crud, Service, PluginManager, Recaptcha, prettydate
+auth = Auth(db)
+
+crud, service, plugins = Crud(db), Service(), PluginManager()
+
+## create all tables needed by auth if not custom tables
+auth.define_tables(username=False, signature=False)
+
+## configure auth policy
+auth.settings.registration_requires_verification = False
+auth.settings.registration_requires_approval = False
+auth.settings.reset_password_requires_verification = False
+auth.settings.actions_disabled = ['request_reset_password', 'change_password', 'retrieve_password']
+
+
+places = common.Places(db)
diff --git a/models/2_menu.py b/models/2_menu.py
new file mode 100644
index 0000000..0ae0e3c
--- /dev/null
+++ b/models/2_menu.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+#########################################################################
+## Customize your APP title, subtitle and menus here
+#########################################################################
+
+#response.logo = A(B('web scraping example'), _class="brand",_href="/")
+response.title = 'Example web scraping website'
+#response.subtitle = ''
+
+## read more at http://dev.w3.org/html5/markup/meta.name.html
+response.meta.author = 'Richard Penman'
+response.meta.keywords = 'web2py, python, web scraping'
+response.meta.generator = 'Web2py Web Framework'
+
+#########################################################################
+## this is the main application menu add/remove items as required
+#########################################################################
+
+response.menu = [
+ (T('Home'), False, URL('default', 'index'), []),
+ (T('Search'), False, URL('default', 'search'), [])
+]
+
diff --git a/models/3_cache.py b/models/3_cache.py
new file mode 100644
index 0000000..18fe0de
--- /dev/null
+++ b/models/3_cache.py
@@ -0,0 +1,37 @@
+import time
+
+# XXX global state going to mess threading?
+# how often to reset country data
+RESET_DB_SECS = 60 * 60
+# block malicious crawlers that access banned path
+BAN_IP_SECS = 60
+# block users who ignore crawl-delay and download too fast
+REQUEST_WINDOW = 1
+MAX_REQUESTS = 10
+
+
+def ban_client():
+ """Temporarily block this client
+ """
+ ban_key = request.client + 'ban'
+ cache.ram(ban_key, None)
+ cache.ram(ban_key, lambda: True, BAN_IP_SECS)
+
+
+if cache.ram(request.client + 'ban', lambda: False, BAN_IP_SECS):
+ # client is blocked
+ raise HTTP(429, 'IP temporarily blocked')
+
+cache.ram(request.client + 'requests', lambda: 0, REQUEST_WINDOW)
+if cache.ram.increment(request.client + 'requests') > MAX_REQUESTS:
+ ban_client()
+
+
+current_time = time.time()
+cache_time = cache.ram('resetdb', lambda: current_time)
+if current_time > cache_time + RESET_DB_SECS:
+ response.flash = response.session = 'test'
+ cache.ram.clear('resetdb')
+ common = local_import('common', reload=False)
+ places = common.Places(db)
+ places.load()
diff --git a/modules/Captcha/Base.py b/modules/Captcha/Base.py
new file mode 100644
index 0000000..241e83c
--- /dev/null
+++ b/modules/Captcha/Base.py
@@ -0,0 +1,127 @@
+""" Captcha.Base
+
+Base class for all types of CAPTCHA tests. All tests have one or
+more solution, determined when the test is generated. Solutions
+can be any python object,
+
+All tests can be solved by presenting at least some preset number
+of correct solutions. Some tests may only have one solution and require
+one solution, but other tests may require N correct solutions of M
+possible solutions.
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+import random, string, time, shelve
+
+__all__ = ["BaseCaptcha", "Factory", "PersistentFactory"]
+
+
+def randomIdentifier(alphabet = string.ascii_letters + string.digits,
+ length = 24):
+ return "".join([random.choice(alphabet) for i in xrange(length)])
+
+
+class BaseCaptcha(object):
+ """Base class for all CAPTCHA tests"""
+ # Subclasses can override these to set the solution criteria
+ minCorrectSolutions = 1
+ maxIncorrectSolutions = 0
+
+ def __init__(self):
+ self.solutions = []
+ self.valid = True
+
+ # Each test has a unique identifier, used to refer to that test
+ # later, and a creation time so it can expire later.
+ self.id = randomIdentifier()
+ self.creationTime = time.time()
+
+ def addSolution(self, solution):
+ self.solutions.append(solution)
+
+ def testSolutions(self, solutions):
+ """Test whether the given solutions are sufficient for this CAPTCHA.
+ A given CAPTCHA can only be tested once, after that it is invalid
+ and always returns False. This makes random guessing much less effective.
+ """
+ if not self.valid:
+ return False
+ self.valid = False
+
+ numCorrect = 0
+ numIncorrect = 0
+
+ for solution in solutions:
+ if solution in self.solutions:
+ numCorrect += 1
+ else:
+ numIncorrect += 1
+
+ return numCorrect >= self.minCorrectSolutions and \
+ numIncorrect <= self.maxIncorrectSolutions
+
+
+class Factory(object):
+ """Creates BaseCaptcha instances on demand, and tests solutions.
+ CAPTCHAs expire after a given amount of time, given in seconds.
+ The default is 15 minutes.
+ """
+ def __init__(self, lifetime=60*15):
+ self.lifetime = lifetime
+ self.storedInstances = {}
+
+ def new(self, cls, *args, **kwargs):
+ """Create a new instance of our assigned BaseCaptcha subclass, passing
+ it any extra arguments we're given. This stores the result for
+ later testing.
+ """
+ self.clean()
+ inst = cls(*args, **kwargs)
+ self.storedInstances[inst.id] = inst
+ return inst
+
+ def get(self, id):
+ """Retrieve the CAPTCHA with the given ID. If it's expired already,
+ this will return None. A typical web application will need to
+ new() a CAPTCHA when generating an html page, then get() it later
+ when its images or sounds must be rendered.
+ """
+ return self.storedInstances.get(id)
+
+ def clean(self):
+ """Removed expired tests"""
+ expiredIds = []
+ now = time.time()
+ for inst in self.storedInstances.itervalues():
+ if inst.creationTime + self.lifetime < now:
+ expiredIds.append(inst.id)
+ for id in expiredIds:
+ del self.storedInstances[id]
+
+ def test(self, id, solutions):
+ """Test the given list of solutions against the BaseCaptcha instance
+ created earlier with the given id. Returns True if the test passed,
+ False on failure. In either case, the test is invalidated. Returns
+ False in the case of an invalid id.
+ """
+ self.clean()
+ inst = self.storedInstances.get(id)
+ if not inst:
+ return False
+ result = inst.testSolutions(solutions)
+ return result
+
+
+class PersistentFactory(Factory):
+ """A simple persistent factory, for use in CGI or multi-process environments
+ where the state must remain across python interpreter sessions.
+ This implementation uses the 'shelve' module.
+ """
+ def __init__(self, filename, lifetime=60*15):
+ Factory.__init__(self, lifetime)
+ self.storedInstances = shelve.open(filename)
+
+### The End ###
diff --git a/modules/Captcha/File.py b/modules/Captcha/File.py
new file mode 100644
index 0000000..47e81c4
--- /dev/null
+++ b/modules/Captcha/File.py
@@ -0,0 +1,53 @@
+""" Captcha.File
+
+Utilities for finding and picking random files from our 'data' directory
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+import os, random
+
+# Determine the data directory. This can be overridden after import-time if needed.
+dataDir = os.path.join(os.path.split(os.path.abspath(__file__))[0], "data")
+
+
+class RandomFileFactory(object):
+ """Given a list of files and/or directories, this picks a random file.
+ Directories are searched for files matching any of a list of extensions.
+ Files are relative to our data directory plus a subclass-specified base path.
+ """
+ extensions = []
+ basePath = "."
+
+ def __init__(self, *fileList):
+ self.fileList = fileList
+ self._fullPaths = None
+
+ def _checkExtension(self, name):
+ """Check the file against our given list of extensions"""
+ for ext in self.extensions:
+ if name.endswith(ext):
+ return True
+ return False
+
+ def _findFullPaths(self):
+ """From our given file list, find a list of full paths to files"""
+ paths = []
+ for name in self.fileList:
+ path = os.path.join(dataDir, self.basePath, name)
+ if os.path.isdir(path):
+ for content in os.listdir(path):
+ if self._checkExtension(content):
+ paths.append(os.path.join(path, content))
+ else:
+ paths.append(path)
+ return paths
+
+ def pick(self):
+ if self._fullPaths is None:
+ self._fullPaths = self._findFullPaths()
+ return random.choice(self._fullPaths)
+
+### The End ###
diff --git a/modules/Captcha/Visual/Backgrounds.py b/modules/Captcha/Visual/Backgrounds.py
new file mode 100644
index 0000000..372212f
--- /dev/null
+++ b/modules/Captcha/Visual/Backgrounds.py
@@ -0,0 +1,95 @@
+""" Captcha.Visual.Backgrounds
+
+Background layers for visual CAPTCHAs
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+from Captcha.Visual import Layer, Pictures
+import random, os
+import ImageDraw, Image
+
+
+class SolidColor(Layer):
+ """A solid color background. Very weak on its own, but good
+ to combine with other backgrounds.
+ """
+ def __init__(self, color="white"):
+ self.color = color
+
+ def render(self, image):
+ image.paste(self.color)
+
+
+class Grid(Layer):
+ """A grid of lines, with a given foreground color.
+ The size is given in pixels. The background is transparent,
+ so another layer (like SolidColor) should be put behind it.
+ """
+ def __init__(self, size=16, foreground="black"):
+ self.size = size
+ self.foreground = foreground
+ self.offset = (random.uniform(0, self.size),
+ random.uniform(0, self.size))
+
+ def render(self, image):
+ draw = ImageDraw.Draw(image)
+
+ for i in xrange(image.size[0] / self.size + 1):
+ draw.line( (i*self.size+self.offset[0], 0,
+ i*self.size+self.offset[0], image.size[1]), fill=self.foreground)
+
+ for i in xrange(image.size[0] / self.size + 1):
+ draw.line( (0, i*self.size+self.offset[1],
+ image.size[0], i*self.size+self.offset[1]), fill=self.foreground)
+
+
+class TiledImage(Layer):
+ """Pick a random image and a random offset, and tile the rendered image with it"""
+ def __init__(self, imageFactory=Pictures.abstract):
+ self.tileName = imageFactory.pick()
+ self.offset = (random.uniform(0, 1),
+ random.uniform(0, 1))
+
+ def render(self, image):
+ tile = Image.open(self.tileName)
+ for j in xrange(-1, int(image.size[1] / tile.size[1]) + 1):
+ for i in xrange(-1, int(image.size[0] / tile.size[0]) + 1):
+ dest = (int((self.offset[0] + i) * tile.size[0]),
+ int((self.offset[1] + j) * tile.size[1]))
+ image.paste(tile, dest)
+
+
+class CroppedImage(Layer):
+ """Pick a random image, cropped randomly. Source images should be larger than the CAPTCHA."""
+ def __init__(self, imageFactory=Pictures.nature):
+ self.imageName = imageFactory.pick()
+ self.align = (random.uniform(0,1),
+ random.uniform(0,1))
+
+ def render(self, image):
+ i = Image.open(self.imageName)
+ image.paste(i, (int(self.align[0] * (image.size[0] - i.size[0])),
+ int(self.align[1] * (image.size[1] - i.size[1]))))
+
+
+class RandomDots(Layer):
+ """Draw random colored dots"""
+ def __init__(self, colors=("white", "black"), dotSize=4, numDots=400):
+ self.colors = colors
+ self.dotSize = dotSize
+ self.numDots = numDots
+ self.seed = random.random()
+
+ def render(self, image):
+ r = random.Random(self.seed)
+ for i in xrange(self.numDots):
+ bx = int(r.uniform(0, image.size[0]-self.dotSize))
+ by = int(r.uniform(0, image.size[1]-self.dotSize))
+ image.paste(r.choice(self.colors), (bx, by,
+ bx+self.dotSize-1,
+ by+self.dotSize-1))
+
+### The End ###
diff --git a/modules/Captcha/Visual/Base.py b/modules/Captcha/Visual/Base.py
new file mode 100644
index 0000000..6fafd4d
--- /dev/null
+++ b/modules/Captcha/Visual/Base.py
@@ -0,0 +1,69 @@
+""" Captcha.Visual.BAse
+
+Base classes for visual CAPTCHAs. We use the Python Imaging Library
+to manipulate these images.
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+import Captcha
+import Image
+
+__all__ = ['ImageCaptcha', 'Layer']
+
+
+class ImageCaptcha(Captcha.BaseCaptcha):
+ """Base class for image-based CAPTCHA tests.
+ The render() function generates the CAPTCHA image at the given size by
+ combining Layer instances from self.layers, which should be created by
+ the subclass-defined getLayers().
+ """
+ defaultSize = (256,96)
+
+ def __init__(self, *args, **kwargs):
+ Captcha.BaseCaptcha.__init__(self)
+ self._layers = self.getLayers(*args, **kwargs)
+
+ def getImage(self):
+ """Get a PIL image representing this CAPTCHA test, creating it if necessary"""
+ if not self._image:
+ self._image = self.render()
+ return self._image
+
+ def getLayers(self):
+ """Subclasses must override this to return a list of Layer instances to render.
+ Lists within the list of layers are recursively rendered.
+ """
+ return []
+
+ def render(self, size=None):
+ """Render this CAPTCHA, returning a PIL image"""
+ if size is None:
+ size = self.defaultSize
+ img = Image.new("RGB", size)
+ return self._renderList(self._layers, Image.new("RGB", size))
+
+ def _renderList(self, l, img):
+ for i in l:
+ if type(i) == tuple or type(i) == list:
+ img = self._renderList(i, img)
+ else:
+ img = i.render(img) or img
+ return img
+
+
+class Layer(object):
+ """A renderable object representing part of a CAPTCHA.
+ The render() function should return approximately the same result, regardless
+ of the image size. This means any randomization must occur in the constructor.
+
+ If the render() function returns something non-None, it is taken as an image to
+ replace the current image with. This can be used to implement transformations
+ that result in a separate image without having to copy the results back to the first.
+ """
+ def render(self, img):
+ pass
+
+### The End ###
diff --git a/modules/Captcha/Visual/Distortions.py b/modules/Captcha/Visual/Distortions.py
new file mode 100644
index 0000000..65ba284
--- /dev/null
+++ b/modules/Captcha/Visual/Distortions.py
@@ -0,0 +1,117 @@
+""" Captcha.Visual.Distortions
+
+Distortion layers for visual CAPTCHAs
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+from Captcha.Visual import Layer
+import ImageDraw, Image
+import random, math
+
+
+class WigglyBlocks(Layer):
+ """Randomly select and shift blocks of the image"""
+ def __init__(self, blockSize=16, sigma=0.01, iterations=300):
+ self.blockSize = blockSize
+ self.sigma = sigma
+ self.iterations = iterations
+ self.seed = random.random()
+
+ def render(self, image):
+ r = random.Random(self.seed)
+ for i in xrange(self.iterations):
+ # Select a block
+ bx = int(r.uniform(0, image.size[0]-self.blockSize))
+ by = int(r.uniform(0, image.size[1]-self.blockSize))
+ block = image.crop((bx, by, bx+self.blockSize-1, by+self.blockSize-1))
+
+ # Figure out how much to move it.
+ # The call to floor() is important so we always round toward
+ # 0 rather than to -inf. Just int() would bias the block motion.
+ mx = int(math.floor(r.normalvariate(0, self.sigma)))
+ my = int(math.floor(r.normalvariate(0, self.sigma)))
+
+ # Now actually move the block
+ image.paste(block, (bx+mx, by+my))
+
+
+class WarpBase(Layer):
+ """Abstract base class for image warping. Subclasses define a
+ function that maps points in the output image to points in the input image.
+ This warping engine runs a grid of points through this transform and uses
+ PIL's mesh transform to warp the image.
+ """
+ filtering = Image.BILINEAR
+ resolution = 10
+
+ def getTransform(self, image):
+ """Return a transformation function, subclasses should override this"""
+ return lambda x, y: (x, y)
+
+ def render(self, image):
+ r = self.resolution
+ xPoints = image.size[0] / r + 2
+ yPoints = image.size[1] / r + 2
+ f = self.getTransform(image)
+
+ # Create a list of arrays with transformed points
+ xRows = []
+ yRows = []
+ for j in xrange(yPoints):
+ xRow = []
+ yRow = []
+ for i in xrange(xPoints):
+ x, y = f(i*r, j*r)
+
+ # Clamp the edges so we don't get black undefined areas
+ x = max(0, min(image.size[0]-1, x))
+ y = max(0, min(image.size[1]-1, y))
+
+ xRow.append(x)
+ yRow.append(y)
+ xRows.append(xRow)
+ yRows.append(yRow)
+
+ # Create the mesh list, with a transformation for
+ # each square between points on the grid
+ mesh = []
+ for j in xrange(yPoints-1):
+ for i in xrange(xPoints-1):
+ mesh.append((
+ # Destination rectangle
+ (i*r, j*r,
+ (i+1)*r, (j+1)*r),
+ # Source quadrilateral
+ (xRows[j ][i ], yRows[j ][i ],
+ xRows[j+1][i ], yRows[j+1][i ],
+ xRows[j+1][i+1], yRows[j+1][i+1],
+ xRows[j ][i+1], yRows[j ][i+1]),
+ ))
+
+ return image.transform(image.size, Image.MESH, mesh, self.filtering)
+
+
+class SineWarp(WarpBase):
+ """Warp the image using a random composition of sine waves"""
+
+ def __init__(self,
+ amplitudeRange = (3, 6.5),
+ periodRange = (0.04, 0.1),
+ ):
+ self.amplitude = random.uniform(*amplitudeRange)
+ self.period = random.uniform(*periodRange)
+ self.offset = (random.uniform(0, math.pi * 2 / self.period),
+ random.uniform(0, math.pi * 2 / self.period))
+
+ def getTransform(self, image):
+ return (lambda x, y,
+ a = self.amplitude,
+ p = self.period,
+ o = self.offset:
+ (math.sin( (y+o[0])*p )*a + x,
+ math.sin( (x+o[1])*p )*a + y))
+
+### The End ###
diff --git a/modules/Captcha/Visual/Pictures.py b/modules/Captcha/Visual/Pictures.py
new file mode 100644
index 0000000..a20087b
--- /dev/null
+++ b/modules/Captcha/Visual/Pictures.py
@@ -0,0 +1,23 @@
+""" Captcha.Visual.Pictures
+
+Random collections of images
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+from Captcha import File
+import Image
+
+
+class ImageFactory(File.RandomFileFactory):
+ """A factory that generates random images from a list"""
+ extensions = [".png", ".jpeg"]
+ basePath = "pictures"
+
+
+abstract = ImageFactory("abstract")
+nature = ImageFactory("nature")
+
+### The End ###
diff --git a/modules/Captcha/Visual/Tests.py b/modules/Captcha/Visual/Tests.py
new file mode 100644
index 0000000..3ab4256
--- /dev/null
+++ b/modules/Captcha/Visual/Tests.py
@@ -0,0 +1,63 @@
+""" Captcha.Visual.Tests
+
+Visual CAPTCHA tests
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+from Captcha.Visual import Text, Backgrounds, Distortions, ImageCaptcha
+from Captcha import Words
+import random
+
+__all__ = ["PseudoGimpy", "AngryGimpy", "AntiSpam"]
+
+
+class PseudoGimpy(ImageCaptcha):
+ """A relatively easy CAPTCHA that's somewhat easy on the eyes"""
+ def getLayers(self):
+ word = Words.defaultWordList.pick()
+ self.addSolution(word)
+ return [
+ random.choice([
+ Backgrounds.CroppedImage(),
+ Backgrounds.TiledImage(),
+ ]),
+ Text.TextLayer(word, borderSize=1),
+ Distortions.SineWarp(),
+ ]
+
+
+class AngryGimpy(ImageCaptcha):
+ """A harder but less visually pleasing CAPTCHA"""
+ def getLayers(self):
+ word = Words.defaultWordList.pick()
+ self.addSolution(word)
+ return [
+ Backgrounds.TiledImage(),
+ Backgrounds.RandomDots(),
+ Text.TextLayer(word, borderSize=1),
+ Distortions.WigglyBlocks(),
+ ]
+
+
+class AntiSpam(ImageCaptcha):
+ """A fixed-solution CAPTCHA that can be used to hide email addresses or URLs from bots"""
+ fontFactory = Text.FontFactory(20, "vera/VeraBd.ttf")
+ defaultSize = (512,50)
+
+ def getLayers(self, solution="murray@example.com"):
+ self.addSolution(solution)
+
+ textLayer = Text.TextLayer(solution,
+ borderSize = 2,
+ fontFactory = self.fontFactory)
+
+ return [
+ Backgrounds.CroppedImage(),
+ textLayer,
+ Distortions.SineWarp(amplitudeRange = (2, 4)),
+ ]
+
+### The End ###
diff --git a/modules/Captcha/Visual/Text.py b/modules/Captcha/Visual/Text.py
new file mode 100644
index 0000000..afc381e
--- /dev/null
+++ b/modules/Captcha/Visual/Text.py
@@ -0,0 +1,100 @@
+""" Captcha.Visual.Text
+
+Text generation for visual CAPTCHAs.
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+import random, os
+from Captcha import Visual, File
+import ImageFont, ImageDraw
+
+
+class FontFactory(File.RandomFileFactory):
+ """Picks random fonts and/or sizes from a given list.
+ 'sizes' can be a single size or a (min,max) tuple.
+ If any of the given files are directories, all *.ttf found
+ in that directory will be added.
+ """
+ extensions = [".ttf"]
+ basePath = "fonts"
+
+ def __init__(self, sizes, *fileNames):
+ File.RandomFileFactory.__init__(self, *fileNames)
+
+ if type(sizes) is tuple:
+ self.minSize = sizes[0]
+ self.maxSize = sizes[1]
+ else:
+ self.minSize = sizes
+ self.maxSize = sizes
+
+ def pick(self):
+ """Returns a (fileName, size) tuple that can be passed to ImageFont.truetype()"""
+ fileName = File.RandomFileFactory.pick(self)
+ size = int(random.uniform(self.minSize, self.maxSize) + 0.5)
+ return (fileName, size)
+
+# Predefined font factories
+defaultFontFactory = FontFactory((30, 40), "vera")
+
+
+class TextLayer(Visual.Layer):
+ """Represents a piece of text rendered within the image.
+ Alignment is given such that (0,0) places the text in the
+ top-left corner and (1,1) places it in the bottom-left.
+
+ The font and alignment are optional, if not specified one is
+ chosen randomly. If no font factory is specified, the default is used.
+ """
+ def __init__(self, text,
+ alignment = None,
+ font = None,
+ fontFactory = None,
+ textColor = "black",
+ borderSize = 0,
+ borderColor = "white",
+ ):
+ if fontFactory is None:
+ global defaultFontFactory
+ fontFactory = defaultFontFactory
+
+ if font is None:
+ font = fontFactory.pick()
+
+ if alignment is None:
+ alignment = (random.uniform(0,1),
+ random.uniform(0,1))
+
+ self.text = text
+ self.alignment = alignment
+ self.font = font
+ self.textColor = textColor
+ self.borderSize = borderSize
+ self.borderColor = borderColor
+
+ def render(self, img):
+ font = ImageFont.truetype(*self.font)
+ textSize = font.getsize(self.text)
+ draw = ImageDraw.Draw(img)
+
+ # Find the text's origin given our alignment and current image size
+ x = int((img.size[0] - textSize[0] - self.borderSize*2) * self.alignment[0] + 0.5)
+ y = int((img.size[1] - textSize[1] - self.borderSize*2) * self.alignment[1] + 0.5)
+
+ # Draw the border if we need one. This is slow and ugly, but there doesn't
+ # seem to be a better way with PIL.
+ if self.borderSize > 0:
+ for bx in (-1,0,1):
+ for by in (-1,0,1):
+ if bx and by:
+ draw.text((x + bx * self.borderSize,
+ y + by * self.borderSize),
+ self.text, font=font, fill=self.borderColor)
+
+ # And the text itself...
+ draw.text((x,y), self.text, font=font, fill=self.textColor)
+
+### The End ###
diff --git a/modules/Captcha/Visual/__init__.py b/modules/Captcha/Visual/__init__.py
new file mode 100644
index 0000000..96a1f6e
--- /dev/null
+++ b/modules/Captcha/Visual/__init__.py
@@ -0,0 +1,14 @@
+""" Captcha.Visual
+
+This package contains functionality specific to visual CAPTCHA tests.
+
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+# Convenience imports
+from Base import *
+
+### The End ###
diff --git a/modules/Captcha/Words.py b/modules/Captcha/Words.py
new file mode 100644
index 0000000..35ee94a
--- /dev/null
+++ b/modules/Captcha/Words.py
@@ -0,0 +1,57 @@
+""" Captcha.Words
+
+Utilities for managing word lists and finding random words
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+import random, os
+import File
+
+
+class WordList(object):
+ """A class representing a word list read from disk lazily.
+ Blank lines and comment lines starting with '#' are ignored.
+ Any number of words per line may be used. The list can
+ optionally ingore words not within a given length range.
+ """
+ def __init__(self, fileName, minLength=None, maxLength=None):
+ self.words = None
+ self.fileName = fileName
+ self.minLength = minLength
+ self.maxLength = maxLength
+
+ def read(self):
+ """Read words from disk"""
+ f = open(os.path.join(File.dataDir, "words", self.fileName))
+
+ self.words = []
+ for line in f.xreadlines():
+ line = line.strip()
+ if not line:
+ continue
+ if line[0] == '#':
+ continue
+ for word in line.split():
+ if self.minLength is not None and len(word) < self.minLength:
+ continue
+ if self.maxLength is not None and len(word) > self.maxLength:
+ continue
+ self.words.append(word)
+
+ def pick(self):
+ """Pick a random word from the list, reading it in if necessary"""
+ if self.words is None:
+ self.read()
+ return random.choice(self.words)
+
+
+# Define several shared word lists that are read from disk on demand
+basic_english = WordList("basic-english")
+basic_english_restricted = WordList("basic-english", minLength=5, maxLength=8)
+
+defaultWordList = basic_english_restricted
+
+### The End ###
diff --git a/modules/Captcha/__init__.py b/modules/Captcha/__init__.py
new file mode 100644
index 0000000..a0933a5
--- /dev/null
+++ b/modules/Captcha/__init__.py
@@ -0,0 +1,41 @@
+""" Captcha
+
+This is the PyCAPTCHA package, a collection of Python modules
+implementing CAPTCHAs: automated tests that humans should pass,
+but current computer programs can't. These tests are often
+used for security.
+
+See http://www.captcha.net for more information and examples.
+
+This project was started because the CIA project, written in
+Python, needed a CAPTCHA to automate its user creation process
+safely. All existing implementations the author could find were
+written in Java or for the .NET framework, so a simple Python
+alternative was needed.
+"""
+#
+# PyCAPTCHA Package
+# Copyright (C) 2004 Micah Dowty
+#
+
+__version__ = "0.3-pre"
+
+
+# Check the python version here before we proceed further
+requiredPythonVersion = (2,2,1)
+def checkVersion():
+ import sys, string
+ if sys.version_info < requiredPythonVersion:
+ raise Exception("%s requires at least Python %s, found %s instead." % (
+ name,
+ string.join(map(str, requiredPythonVersion), "."),
+ string.join(map(str, sys.version_info), ".")))
+checkVersion()
+
+
+# Convenience imports
+from Base import *
+import File
+import Words
+
+### The End ###
diff --git a/modules/Captcha/data/fonts/vera/COPYRIGHT.TXT b/modules/Captcha/data/fonts/vera/COPYRIGHT.TXT
new file mode 100644
index 0000000..e651be1
--- /dev/null
+++ b/modules/Captcha/data/fonts/vera/COPYRIGHT.TXT
@@ -0,0 +1,124 @@
+Bitstream Vera Fonts Copyright
+
+The fonts have a generous copyright, allowing derivative works (as
+long as "Bitstream" or "Vera" are not in the names), and full
+redistribution (so long as they are not *sold* by themselves). They
+can be be bundled, redistributed and sold with any software.
+
+The fonts are distributed under the following copyright:
+
+Copyright
+=========
+
+Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream
+Vera is a trademark of Bitstream, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the fonts accompanying this license ("Fonts") and associated
+documentation files (the "Font Software"), to reproduce and distribute
+the Font Software, including without limitation the rights to use,
+copy, merge, publish, distribute, and/or sell copies of the Font
+Software, and to permit persons to whom the Font Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright and trademark notices and this permission notice
+shall be included in all copies of one or more of the Font Software
+typefaces.
+
+The Font Software may be modified, altered, or added to, and in
+particular the designs of glyphs or characters in the Fonts may be
+modified and additional glyphs or characters may be added to the
+Fonts, only if the fonts are renamed to names not containing either
+the words "Bitstream" or the word "Vera".
+
+This License becomes null and void to the extent applicable to Fonts
+or Font Software that has been modified and is distributed under the
+"Bitstream Vera" names.
+
+The Font Software may be sold as part of a larger software package but
+no copy of one or more of the Font Software typefaces may be sold by
+itself.
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
+BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL,
+OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT
+SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
+
+Except as contained in this notice, the names of Gnome, the Gnome
+Foundation, and Bitstream Inc., shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this Font
+Software without prior written authorization from the Gnome Foundation
+or Bitstream Inc., respectively. For further information, contact:
+fonts at gnome dot org.
+
+Copyright FAQ
+=============
+
+ 1. I don't understand the resale restriction... What gives?
+
+ Bitstream is giving away these fonts, but wishes to ensure its
+ competitors can't just drop the fonts as is into a font sale system
+ and sell them as is. It seems fair that if Bitstream can't make money
+ from the Bitstream Vera fonts, their competitors should not be able to
+ do so either. You can sell the fonts as part of any software package,
+ however.
+
+ 2. I want to package these fonts separately for distribution and
+ sale as part of a larger software package or system. Can I do so?
+
+ Yes. A RPM or Debian package is a "larger software package" to begin
+ with, and you aren't selling them independently by themselves.
+ See 1. above.
+
+ 3. Are derivative works allowed?
+ Yes!
+
+ 4. Can I change or add to the font(s)?
+ Yes, but you must change the name(s) of the font(s).
+
+ 5. Under what terms are derivative works allowed?
+
+ You must change the name(s) of the fonts. This is to ensure the
+ quality of the fonts, both to protect Bitstream and Gnome. We want to
+ ensure that if an application has opened a font specifically of these
+ names, it gets what it expects (though of course, using fontconfig,
+ substitutions could still could have occurred during font
+ opening). You must include the Bitstream copyright. Additional
+ copyrights can be added, as per copyright law. Happy Font Hacking!
+
+ 6. If I have improvements for Bitstream Vera, is it possible they might get
+ adopted in future versions?
+
+ Yes. The contract between the Gnome Foundation and Bitstream has
+ provisions for working with Bitstream to ensure quality additions to
+ the Bitstream Vera font family. Please contact us if you have such
+ additions. Note, that in general, we will want such additions for the
+ entire family, not just a single font, and that you'll have to keep
+ both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add
+ glyphs to the font, they must be stylistically in keeping with Vera's
+ design. Vera cannot become a "ransom note" font. Jim Lyles will be
+ providing a document describing the design elements used in Vera, as a
+ guide and aid for people interested in contributing to Vera.
+
+ 7. I want to sell a software package that uses these fonts: Can I do so?
+
+ Sure. Bundle the fonts with your software and sell your software
+ with the fonts. That is the intent of the copyright.
+
+ 8. If applications have built the names "Bitstream Vera" into them,
+ can I override this somehow to use fonts of my choosing?
+
+ This depends on exact details of the software. Most open source
+ systems and software (e.g., Gnome, KDE, etc.) are now converting to
+ use fontconfig (see www.fontconfig.org) to handle font configuration,
+ selection and substitution; it has provisions for overriding font
+ names and subsituting alternatives. An example is provided by the
+ supplied local.conf file, which chooses the family Bitstream Vera for
+ "sans", "serif" and "monospace". Other software (e.g., the XFree86
+ core server) has other mechanisms for font substitution.
+
diff --git a/modules/Captcha/data/fonts/vera/README.TXT b/modules/Captcha/data/fonts/vera/README.TXT
new file mode 100644
index 0000000..0f71795
--- /dev/null
+++ b/modules/Captcha/data/fonts/vera/README.TXT
@@ -0,0 +1,11 @@
+Contained herin is the Bitstream Vera font family.
+
+The Copyright information is found in the COPYRIGHT.TXT file (along
+with being incoporated into the fonts themselves).
+
+The releases notes are found in the file "RELEASENOTES.TXT".
+
+We hope you enjoy Vera!
+
+ Bitstream, Inc.
+ The Gnome Project
diff --git a/modules/Captcha/data/fonts/vera/RELEASENOTES.TXT b/modules/Captcha/data/fonts/vera/RELEASENOTES.TXT
new file mode 100644
index 0000000..270bc0d
--- /dev/null
+++ b/modules/Captcha/data/fonts/vera/RELEASENOTES.TXT
@@ -0,0 +1,162 @@
+Bitstream Vera Fonts - April 16, 2003
+=====================================
+
+The version number of these fonts is 1.10 to distinguish them from the
+beta test fonts.
+
+Note that the Vera copyright is incorporated in the fonts themselves.
+The License field in the fonts contains the copyright license as it
+appears below. The TrueType copyright field is not large enough to
+contain the full license, so the license is incorporated (as you might
+think if you thought about it) into the license field, which
+unfortunately can be obscure to find. (In pfaedit, see: Element->Font
+Info->TTFNames->License).
+
+Our apologies for it taking longer to complete the fonts than planned.
+Beta testers requested a tighter line spacing (less leading) and Jim
+Lyles redesigned Vera's accents to bring its line spacing to more
+typical of other fonts. This took additional time and effort. Our
+thanks to Jim for this effort above and beyond the call of duty.
+
+There are four monospace and sans faces (normal, oblique, bold, bold
+oblique) and two serif faces (normal and bold). Fontconfig/Xft2 (see
+www.fontconfig.org) can artificially oblique the serif faces for you:
+this loses hinting and distorts the faces slightly, but is visibly
+different than normal and bold, and reasonably pleasing.
+
+On systems with fontconfig 2.0 or 2.1 installed, making your sans,
+serif and monospace fonts default to these fonts is very easy. Just
+drop the file local.conf into your /etc/fonts directory. This will
+make the Bitstream fonts your default fonts for all applications using
+fontconfig (if sans, serif, or monospace names are used, as they often
+are as default values in many desktops). The XML in local.conf may
+need modification to enable subpixel decimation, if appropriate,
+however, the commented out phrase does so for XFree86 4.3, in the case
+that the server does not have sufficient information to identify the
+use of a flat panel. Fontconfig 2.2 adds Vera to the list of font
+families and will, by default use it as the default sans, serif and
+monospace fonts.
+
+During the testing of the final Vera fonts, we learned that screen
+fonts in general are only typically hinted to work correctly at
+integer pixel sizes. Vera is coded internally for integer sizes only.
+We need to investigate further to see if there are commonly used fonts
+that are hinted to be rounded but are not rounded to integer sizes due
+to oversights in their coding.
+
+Most fonts work best at 8 pixels and below if anti-aliased only, as
+the amount of work required to hint well at smaller and smaller sizes
+becomes astronomical. GASP tables are typically used to control
+whether hinting is used or not, but Freetype/Xft does not currently
+support GASP tables (which are present in Vera).
+
+To mitigate this problem, both for Vera and other fonts, there will be
+(very shortly) a new fontconfig 2.2 release that will, by default not
+apply hints if the size is below 8 pixels. if you should have a font
+that in fact has been hinted more agressively, you can use fontconfig
+to note this exception. We believe this should improve many hinted
+fonts in addition to Vera, though implemeting GASP support is likely
+the right long term solution.
+
+Font rendering in Gnome or KDE is the combination of algorithms in
+Xft2 and Freetype, along with hinting in the fonts themselves. It is
+vital to have sufficient information to disentangle problems that you
+may observe.
+
+Note that having your font rendering system set up correctly is vital
+to proper judgement of problems of the fonts:
+
+ * Freetype may or may not be configured to in ways that may
+ implement execution of possibly patented (in some parts of the world)
+ TrueType hinting algorithms, particularly at small sizes. Best
+ results are obtained while using these algorithms.
+
+ * The freetype autohinter (used when the possibly patented
+ algorithms are not used) continues to improve with each release. If
+ you are using the autohinter, please ensure you are using an up to
+ date version of freetype before reporting problems.
+
+ * Please identify what version of freetype you are using in any
+ bug reports, and how your freetype is configured.
+
+ * Make sure you are not using the freetype version included in
+ XFree86 4.3, as it has bugs that significantly degrade most fonts,
+ including Vera. if you build XFree86 4.3 from source yourself, you may
+ have installed this broken version without intending it (as I
+ did). Vera was verified with the recently released Freetype 2.1.4. On
+ many systems, 'ldd" can be used to see which freetype shared library
+ is actually being used.
+
+ * Xft/X Render does not (yet) implement gamma correction. This
+ causes significant problems rendering white text on a black background
+ (causing partial pixels to be insufficiently shaded) if the gamma of
+ your monitor has not been compensated for, and minor problems with
+ black text on a while background. The program "xgamma" can be used to
+ set a gamma correction value in the X server's color pallette. Most
+ monitors have a gamma near 2.
+
+ * Note that the Vera family uses minimal delta hinting. Your
+ results on other systems when not used anti-aliased may not be
+ entirely satisfying. We are primarily interested in reports of
+ problems on open source systems implementing Xft2/fontconfig/freetype
+ (which implements antialiasing and hinting adjustements, and
+ sophisticated subpixel decimation on flatpanels). Also, the
+ algorithms used by Xft2 adjust the hints to integer widths and the
+ results are crisper on open source systems than on Windows or
+ MacIntosh.
+
+ * Your fontconfig may (probably does) predate the release of
+ fontconfig 2.2, and you may see artifacts not present when the font is
+ used at very small sizes with hinting enabled. "vc-list -V" can be
+ used to see what version you have installed.
+
+We believe and hope that these fonts will resolve the problems
+reported during beta test. The largest change is the reduction of
+leading (interline spacing), which had annoyed a number of people, and
+reduced Vera's utility for some applcations. The Vera monospace font
+should also now make '0' and 'O' and '1' and 'l' more clearly
+distinguishable.
+
+The version of these fonts is version 1.10. Fontconfig should be
+choosing the new version of the fonts if both the released fonts and
+beta test fonts are installed (though please discard them: they have
+names of form tt20[1-12]gn.ttf). Note that older versions of
+fontconfig sometimes did not rebuild their cache correctly when new
+fonts are installed: please upgrade to fontconfig 2.2. "fc-cache -f"
+can be used to force rebuilding fontconfig's cache files.
+
+If you note problems, please send them to fonts at gnome dot org, with
+exactly which face and size and unicode point you observe the problem
+at. The xfd utility from XFree86 CVS may be useful for this (e.g. "xfd
+-fa sans"). A possibly more useful program to examine fonts at a
+variety of sizes is the "waterfall" program found in Keith Packard's
+CVS.
+
+ $ cvs -d :pserver:anoncvs@keithp.com:/local/src/CVS login
+ Logging in to :pserver:anoncvs@keithp.com:2401/local/src/CVS
+ CVS password:
+ $ cvs -d :pserver:anoncvs@keithp.com:/local/src/CVS co waterfall
+ $ cd waterfall
+ $ xmkmf -a
+ $ make
+ # make install
+ # make install.man
+
+Again, please make sure you are running an up-to-date freetype, and
+that you are only examining integer sizes.
+
+Reporting Problems
+==================
+
+Please send problem reports to fonts at gnome org, with the following
+information:
+
+ 1. Version of Freetype, Xft2 and fontconfig
+ 2. Whether TT hinting is being used, or the autohinter
+ 3. Application being used
+ 4. Character/Unicode code point that has problems (if applicable)
+ 5. Version of which operating system
+ 6. Please include a screenshot, when possible.
+
+Please check the fonts list archives before reporting problems to cut
+down on duplication.
diff --git a/modules/Captcha/data/fonts/vera/Vera.ttf b/modules/Captcha/data/fonts/vera/Vera.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..58cd6b5e61eff273e920942e28041f8ddcf1e1b5
GIT binary patch
literal 65932
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