Index: templates/settings.html
===================================================================
--- templates/settings.html	(revision 4062)
+++ templates/settings.html	(working copy)
@@ -15,6 +15,10 @@
     <div id="content" class="settings">
 
       <h1>Settings and Session Management</h1>
+      
+      <p class="error" py:if="settings.session.capmode == 'error'">
+	      Invalid answer to the test question, sorry. Please, try again.
+      </p>
 
       <h2>User Settings</h2>
       <p>This page lets you customize your personal settings for this site.
@@ -94,10 +98,27 @@
           </div>
         </fieldset>
 
-        <div class="buttons">
+		<fieldset py:if="settings.session.capmode">
+			<legend>Accept changes</legend>
+			<p class="hint">By answering this question you confirm that you are a human
+			being as this page is not serving bots. This is one time operation; 
+			sorry for inconvenience.</p>
+			<div class="field">
+			<label>How many is ${question}?
+				<input type="text" name="answer" size="16" />
+			</label>
+			</div>
+	        <div class="buttons">
+	          <input type="hidden" name="action" value="save" />
+	          <input type="submit" value="Submit changes" />
+	        </div>
+        </fieldset>
+        
+        <div class="buttons" py:if="not settings.session.capmode">
           <input type="hidden" name="action" value="save" />
           <input type="submit" value="Submit changes" />
-        </div >
+        </div>
+      
       </form>
 
       <py:if test="settings.session_id">
Index: trac/Settings.py
===================================================================
--- trac/Settings.py	(revision 4062)
+++ trac/Settings.py	(working copy)
@@ -21,8 +21,8 @@
 from trac.util.datefmt import all_timezones, utc
 from trac.web import IRequestHandler
 from trac.web.chrome import INavigationContributor
+from trac.util.numcaptcha import numCaptchaQuestion
 
-
 class SettingsModule(Component):
 
     implements(INavigationContributor, IRequestHandler)
@@ -45,35 +45,52 @@
 
     def process_request(self, req):
         action = req.args.get('action')
-
+        
+        question = None
         if req.method == 'POST':
             if action == 'save':
                 self._do_save(req)
             elif action == 'load':
                 self._do_load(req)
+        else:
+            # Get: consider whether to use captcha
+            if (req.session.has_key('name') and req.session['name']) or \
+               (req.session.has_key('email') and req.session['email']):
+                capmode = req.session['capmode'] = None
+            else:
+                # Ask captcha question
+                question, req.session['capanswer'] = numCaptchaQuestion()
+                if not 'capmode' in req.session:
+                    if req.session['capmode'] != 'error':
+                        req.session['capmode'] = 'check'
 
         data = {'session': req.session}
         if req.authname == 'anonymous':
             data['session_id'] = req.session.sid
-
-        return 'settings.html', {'settings': data,
+            
+        return 'settings.html', {'settings': data, 'question':question,
                                  'timezones': all_timezones}, None
 
     # Internal methods
 
     def _do_save(self, req):
-        for field in self._form_fields:
-            val = req.args.get(field)
-            if val:
-                if field == 'tz' and 'tz' in req.session and \
-                        val not in all_timezones:
-                    del req.session['tz']
-                elif field == 'newsid' and val:
-                    req.session.change_sid(val)
-                else:
-                    req.session[field] = val
-            elif field in req.session:
-                del req.session[field]
+        self.error = req.session['capmode'] > 0 and req.session['capanswer'] != req.args.get('answer')
+        if self.error:
+            req.session['capmode'] = 'error'
+        else:
+            req.session['capmode'] = 'check'
+            for field in self._form_fields:
+                val = req.args.get(field)
+                if val:
+                    if field == 'tz' and 'tz' in req.session and \
+                            val not in all_timezones:
+                        del req.session['tz']
+                    elif field == 'newsid' and val:
+                        req.session.change_sid(val)
+                    else:
+                        req.session[field] = val
+                elif field in req.session:
+                    del req.session[field]
         req.redirect(req.href.settings())
 
     def _do_load(self, req):
Index: trac/util/numcaptcha.py
===================================================================
--- trac/util/numcaptcha.py	(revision 0)
+++ trac/util/numcaptcha.py	(revision 0)
@@ -0,0 +1,86 @@
+#
+# Arithmetic captcha, English only
+#
+# The idea is to generate some expression
+# like 5 * 7 or 3 * 11 + 2 and translate it
+# to the text form
+
+import re
+
+_numerals = ( 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
+              'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 
+              'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen' )
+
+_tens = ( 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety' )
+
+# small positive numeral 0-99
+def numeral(number):
+    number = int(number)
+    if number < 20:
+        return _numerals[number]
+    import math
+    t, r = (number / 10) - 2, number % 10
+    if r:
+        return _tens[ t ] + '-' + _numerals[ r ]
+    return _tens[t]
+
+# basic operations
+def operation(opchar):
+    if opchar == '+':
+        return 'plus'
+    elif opchar == '-':
+        return 'minus'
+    elif opchar == '*':
+        return 'multiplied by'
+    elif opchar == '/':
+        return 'divided by'
+    raise NotImplementedError( 'Operation '+opchar+' is not supported' )
+
+# numeral or operation
+def term( op_or_number ):
+    try:
+        return operation(op_or_number)
+    except NotImplementedError:
+        return numeral(op_or_number)
+
+# formulae parsing re
+_re_formulae = re.compile( '([0-9]+|[+\-*/])' )
+
+# translate simple formulae to English
+def say( formulae ):
+    parts = re.findall( _re_formulae, formulae )
+    return ' '.join( [ term(x) for x in parts ] )
+    
+from random import randint
+
+# Get numeric captcha question as English phrase and result as str
+def numCaptchaQuestion():
+    expr, res = numCaptchaExpr()
+    return say(expr), str(res)
+
+# Build numeric captcha expression as formulae return it and result as int
+def numCaptchaExpr():
+    
+    total = randint(7,49)
+    m1 = randint( 2, total/2 )
+    m2 = total / m1
+    rem = total - m1*m2
+    if rem:
+        return '%d * %d + %d' % (m1, m2, rem), total
+    return '%d * %d' % (m1, m2), total
+
+if __name__ == '__main__':
+
+    # parts of tests
+    
+    def _test_buildNumExpr():
+        print 'Testing buildNumExpr'
+        for cnt in xrange(100000):
+            ex, res = numCaptchaExpr()
+            if eval(ex) != res:
+                print 'Error: %s is %d not %d!' % (ex, eval(ex), res)
+                return False
+        print 'BuildNumExpr test passed'
+        return True
+            
+    _test_buildNumExpr()

