Mercurial > moin > 1.9
changeset 2983:7b0aadb97d01
new antispam stuff: textchas (text CAPTCHAs), cleanup AttachFile handler (port from 1.6)
author | Thomas Waldmann <tw AT waldmann-edv DOT de> |
---|---|
date | Sun, 06 Jan 2008 02:49:32 +0100 |
parents | 24854dcf16ba |
children | 52f9d511f155 |
files | MoinMoin/PageEditor.py MoinMoin/PageGraphicalEditor.py MoinMoin/action/AttachFile.py MoinMoin/action/edit.py MoinMoin/action/newaccount.py MoinMoin/config/multiconfig.py MoinMoin/security/textcha.py wiki/htdocs/classic/css/screen.css wiki/htdocs/modern/css/screen.css wiki/htdocs/rightsidebar/css/screen.css |
diffstat | 10 files changed, 293 insertions(+), 38 deletions(-) [+] |
line wrap: on
line diff
--- a/MoinMoin/PageEditor.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/PageEditor.py Sun Jan 06 02:49:32 2008 +0100 @@ -399,6 +399,9 @@ <input type="hidden" name="editor" value="text"> ''' % (button_spellcheck, cancel_button_text, )) + from MoinMoin.security.textcha import TextCha + request.write(TextCha(request).render()) + # Add textarea with page text self.sendconfirmleaving()
--- a/MoinMoin/PageGraphicalEditor.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/PageGraphicalEditor.py Sun Jan 06 02:49:32 2008 +0100 @@ -288,6 +288,9 @@ <input type="hidden" name="editor" value="gui"> ''' % (button_spellcheck, cancel_button_text, )) + from MoinMoin.security.textcha import TextCha + request.write(TextCha(request).render()) + self.sendconfirmleaving() # TODO update state of flgChange to make this work, see PageEditor # Add textarea with page text
--- a/MoinMoin/action/AttachFile.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/action/AttachFile.py Sun Jan 06 02:49:32 2008 +0100 @@ -31,6 +31,7 @@ from MoinMoin import config, wikiutil, packages from MoinMoin.Page import Page from MoinMoin.util import filesys, timefuncs +from MoinMoin.security.textcha import TextCha from MoinMoin.events import FileAttachedEvent, send_event import MoinMoin.events.notification as notification @@ -514,6 +515,7 @@ <dt>%(upload_label_overwrite)s</dt> <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd> </dl> +%(textcha)s <p> <input type="hidden" name="action" value="%(action_name)s"> <input type="hidden" name="do" value="upload"> @@ -530,6 +532,7 @@ 'upload_label_overwrite': _('Overwrite existing attachment of same name'), 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'], 'upload_button': _('Upload'), + 'textcha': TextCha(request).render(), }) #<dt>%(upload_label_mime)s</dt> @@ -555,14 +558,59 @@ """ _ = request.getText - msg = None - do = request.form.get('do') - if do is not None: - do = do[0] if action_name in request.cfg.actions_excluded: msg = _('File attachments are not allowed in this wiki!') - elif 'do' not in request.form: + error_msg(pagename, request, msg) + return + + do = request.form.get('do') + if do is None: upload_form(pagename, request) + return + + msg = None + do = do[0] + + # First handle read-only access to attachments: + if do == 'get': + if request.user.may.read(pagename): + get_file(pagename, request) + else: + msg = _('You are not allowed to get attachments from this page.') + elif do == 'view': + if request.user.may.read(pagename): + view_file(pagename, request) + else: + msg = _('You are not allowed to view attachments of this page.') + elif do == 'move': + if request.user.may.delete(pagename): + send_moveform(pagename, request) + else: + msg = _('You are not allowed to move attachments from this page.') + + # Second handle write access: + elif do == 'upload': + # Currently we only check TextCha for upload (this is what spammers ususally do), + # but it could be extended to more/all attachment write access + if not TextCha(request).check_answer_from_form(): + msg = _('TextCha: Wrong answer! Go back and try again...', formatted=False) + else: + overwrite = 0 + if 'overwrite' in request.form: + try: + overwrite = int(request.form['overwrite'][0]) + except: + pass + if (not overwrite and request.user.may.write(pagename)) or \ + (overwrite and request.user.may.write(pagename) and request.user.may.delete(pagename)): + if 'file' in request.form: + do_upload(pagename, request, overwrite) + else: + # This might happen when trying to upload file names + # with non-ascii characters on Safari. + msg = _("No file content. Delete non ASCII characters from the file name and try again.") + else: + msg = _('You are not allowed to attach a file to this page.') elif do == 'savedrawing': if request.user.may.write(pagename): save_drawing(pagename, request) @@ -570,33 +618,11 @@ request.write("OK") else: msg = _('You are not allowed to save a drawing on this page.') - elif do == 'upload': - overwrite = 0 - if 'overwrite' in request.form: - try: - overwrite = int(request.form['overwrite'][0]) - except: - pass - if (not overwrite and request.user.may.write(pagename)) or \ - (overwrite and request.user.may.write(pagename) and request.user.may.delete(pagename)): - if 'file' in request.form: - do_upload(pagename, request, overwrite) - else: - # This might happen when trying to upload file names - # with non-ascii characters on Safari. - msg = _("No file content. Delete non ASCII characters from the file name and try again.") - else: - msg = _('You are not allowed to attach a file to this page.') elif do == 'del': if request.user.may.delete(pagename): del_file(pagename, request) else: msg = _('You are not allowed to delete attachments on this page.') - elif do == 'move': - if request.user.may.delete(pagename): - send_moveform(pagename, request) - else: - msg = _('You are not allowed to move attachments from this page.') elif do == 'attachment_move': if 'cancel' in request.form: msg = _('Move aborted!') @@ -610,11 +636,6 @@ attachment_move(pagename, request) else: msg = _('You are not allowed to move attachments from this page.') - elif do == 'get': - if request.user.may.read(pagename): - get_file(pagename, request) - else: - msg = _('You are not allowed to get attachments from this page.') elif do == 'unzip': if request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename): unzip_file(pagename, request) @@ -625,13 +646,8 @@ install_package(pagename, request) else: msg = _('You are not allowed to install files.') - elif do == 'view': - if request.user.may.read(pagename): - view_file(pagename, request) - else: - msg = _('You are not allowed to view attachments of this page.') else: - msg = _('Unsupported upload action: %s') % (wikiutil.escape(do), ) + msg = _('Unsupported AttachFile sub-action: %s') % (wikiutil.escape(do), ) if msg: error_msg(pagename, request, msg)
--- a/MoinMoin/action/edit.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/action/edit.py Sun Jan 06 02:49:32 2008 +0100 @@ -158,6 +158,9 @@ # Save new text else: try: + from MoinMoin.security.textcha import TextCha + if not TextCha(request).check_answer_from_form(): + raise pg.SaveError(_('TextCha: Wrong answer! Go back and try again...', formatted=False)) savemsg = pg.saveText(savetext, rev, trivial=trivial, comment=comment) except pg.EditConflict, e: msg = e.message
--- a/MoinMoin/action/newaccount.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/action/newaccount.py Sun Jan 06 02:49:32 2008 +0100 @@ -10,6 +10,7 @@ from MoinMoin.Page import Page from MoinMoin.widget import html import MoinMoin.events as events +from MoinMoin.security.textcha import TextCha _debug = False @@ -20,6 +21,10 @@ if request.request_method != 'POST': return _("Use UserPreferences to change your settings or create an account.") + + if not TextCha(request).check_answer_from_form(): + return _('TextCha: Wrong answer! Go back and try again...', formatted=False) + # Create user profile theuser = user.User(request, auth_method="new-user") @@ -127,6 +132,16 @@ row = html.TR() tbl.append(row) + row.append(html.TD().append(html.STRONG().append( + html.Text(_('TextCha (required)', formatted=False))))) + td = html.TD() + textcha = TextCha(request).render() + if textcha: + td.append(textcha) + row.append(td) + + row = html.TR() + tbl.append(row) row.append(html.TD()) td = html.TD() row.append(td)
--- a/MoinMoin/config/multiconfig.py Sat Jan 05 23:41:20 2008 +0100 +++ b/MoinMoin/config/multiconfig.py Sun Jan 06 02:49:32 2008 +0100 @@ -569,6 +569,9 @@ } surge_lockout_time = 3600 # secs you get locked out when you ignore warnings + textchas = None + textchas_disabled_group = None # e.g. u'NoTextChasGroup' if you are a member of this group, you don't get textchas + theme_default = 'modern' theme_force = False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/security/textcha.py Sun Jan 06 02:49:32 2008 +0100 @@ -0,0 +1,170 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Text CAPTCHAs + + This is just asking some (admin configured) questions and + checking if the answer is as expected. It is up to the wiki + admin to setup questions that a bot can not easily answer, but + humans can. It is recommended to setup SITE SPECIFIC questions + and not to share the questions with other sites (if everyone + asks the same questions / expects the same answers, spammers + could adapt to that). + + TODO: + * roundtrip the question in some other way: + * use safe encoding / encryption for the q + * make sure a q/a pair in the POST is for the q in the GET before + * make some nice CSS + * make similar changes to GUI editor + + @copyright: 2007 by MoinMoin:ThomasWaldmann + @license: GNU GPL, see COPYING for details. +""" + +import re +import random +import logging + +from MoinMoin import wikiutil + +class TextCha(object): + """ Text CAPTCHA support """ + + def __init__(self, request, question=None): + """ Initialize the TextCha. + + @param request: the request object + @param question: see _init_qa() + """ + self.request = request + self.user_info = request.user.valid and request.user.name or request.remote_addr + self.textchas = self._get_textchas() + self._init_qa(question) + + def _get_textchas(self): + """ get textchas from the wiki config for the user's language (or default_language or en) """ + request = self.request + cfg = request.cfg + user = request.user + disabled_group = cfg.textchas_disabled_group + if disabled_group and user.name and request.dicts.has_member(disabled_group, user.name): + return None + textchas = cfg.textchas + if textchas: + lang = user.language or request.lang + #logging.debug(u"TextCha: user.language == '%s'." % lang) + if lang not in textchas: + lang = cfg.language_default + #logging.debug(u"TextCha: fallback to language_default == '%s'." % lang) + if lang not in textchas: + logging.error(u"TextCha: The textchas do not have content for language_default == '%s'! Falling back to English." % lang) + lang = 'en' + if lang not in textchas: + logging.error(u"TextCha: The textchas do not have content for 'en', auto-disabling textchas!") + cfg.textchas = None + lang = None + else: + lang = None + if lang is None: + return None + else: + #logging.debug(u"TextCha: using lang = '%s'" % lang) + return textchas[lang] + + def _init_qa(self, question=None): + """ Initialize the question / answer. + + @param question: If given, the given question will be used. + If None, a new question will be generated. + """ + if self.is_enabled(): + if question is None: + self.question = random.choice(self.textchas.keys()) + else: + self.question = question + try: + self.answer_regex = self.textchas[self.question] + self.answer_re = re.compile(self.answer_regex, re.U|re.I) + except KeyError: + # this question does not exist, thus there is no answer + self.answer_regex = ur"[^.]*" # this shall never match! + self.answer_re = re.compile(self.answer_regex, re.U|re.I) + logging.warning(u"TextCha: Non-existing question '%s'. User '%s' trying to cheat?" % ( + self.question, self.user_info)) + except re.error: + logging.error(u"TextCha: Invalid regex in answer for question '%s'" % self.question) + self._init_qa() + + def is_enabled(self): + """ check if textchas are enabled. + + They can be disabled for all languages if you use textchas = None or = {}, + also they can be disabled for some specific language, like: + textchas = { + 'en': { + 'some question': 'some answer', + # ... + }, + 'de': {}, # having no questions for 'de' means disabling textchas for 'de' + # ... + } + """ + return not not self.textchas # we don't want to return the dict + + def check_answer(self, given_answer): + """ check if the given answer to the question is correct """ + if self.is_enabled(): + success = self.answer_re.match(given_answer.strip()) is not None + success_status = success and u"success" or u"failure" + logging.info(u"TextCha: %s (u='%s', a='%s', re='%s', q='%s')" % ( + success_status, + self.user_info, + given_answer, + self.answer_regex, + self.question, + )) + return success + else: + return True + + def _make_form_values(self, question, given_answer): + question_form = wikiutil.escape(question, True) + given_answer_form = wikiutil.escape(given_answer, True) + return question_form, given_answer_form + + def _extract_form_values(self, form=None): + if form is None: + form = self.request.form + question = form.get('textcha-question', [None])[0] + given_answer = form.get('textcha-answer', [u''])[0] + return question, given_answer + + def render(self, form=None): + """ Checks if textchas are enabled and returns HTML for one, + or an empty string if they are not enabled. + + @return: unicode result html + """ + if self.is_enabled(): + question, given_answer = self._extract_form_values(form) + if question is None: + question = self.question + question_form, given_answer_form = self._make_form_values(question, given_answer) + result = u""" +<div id="textcha"> +<span id="textcha-question">%s</span> +<input type="hidden" name="textcha-question" value="%s"> +<input id="textcha-answer" type="text" name="textcha-answer" value="%s" size="20" maxlength="80"> +</div> +""" % (wikiutil.escape(question), question_form, given_answer_form) + else: + result = u'' + return result + + def check_answer_from_form(self, form=None): + if self.is_enabled(): + question, given_answer = self._extract_form_values(form) + self._init_qa(question) + return self.check_answer(given_answer) + else: + return True
--- a/wiki/htdocs/classic/css/screen.css Sat Jan 05 23:41:20 2008 +0100 +++ b/wiki/htdocs/classic/css/screen.css Sun Jan 06 02:49:32 2008 +0100 @@ -361,6 +361,20 @@ background: url(../img/draft.png); } +#textcha { + font-size: 100%; + margin-top: 0.5em; + border: 2px solid #FF8888; + color: black; + vertical-align: middle; + padding: 3px 2px; +} + +#textcha-answer { + border: 2px solid #000000; + padding: 3px 2px; +} + .diff { width:99%; }
--- a/wiki/htdocs/modern/css/screen.css Sat Jan 05 23:41:20 2008 +0100 +++ b/wiki/htdocs/modern/css/screen.css Sun Jan 06 02:49:32 2008 +0100 @@ -383,6 +383,20 @@ margin-top: 0.5em; } +#textcha { + font-size: 100%; + margin-top: 0.5em; + border: 2px solid #FF8888; + color: black; + vertical-align: middle; + padding: 3px 2px; +} + +#textcha-answer { + border: 2px solid #000000; + padding: 3px 2px; +} + input.button { /* border: 1px solid #8cacbb;
--- a/wiki/htdocs/rightsidebar/css/screen.css Sat Jan 05 23:41:20 2008 +0100 +++ b/wiki/htdocs/rightsidebar/css/screen.css Sun Jan 06 02:49:32 2008 +0100 @@ -289,6 +289,20 @@ background: url(../img/draft.png); } +#textcha { + font-size: 100%; + margin-top: 0.5em; + border: 2px solid #FF8888; + color: black; + vertical-align: middle; + padding: 3px 2px; +} + +#textcha-answer { + border: 2px solid #000000; + padding: 3px 2px; +} + #pagebottom { clear: both; }