diff --git a/plugins/Config/test.py b/plugins/Config/test.py index f59646683..49130b08e 100644 --- a/plugins/Config/test.py +++ b/plugins/Config/test.py @@ -34,7 +34,7 @@ import supybot.conf as conf class ConfigTestCase(ChannelPluginTestCase): # We add utilities so there's something in supybot.plugins. - plugins = ('Config', 'Utilities', 'AutoMode') + plugins = ('Config', 'Utilities') def testGet(self): self.assertNotRegexp('config get supybot.reply', r'registry\.Group') self.assertResponse('config supybot.protocols.irc.throttleTime', '0.0') @@ -49,9 +49,6 @@ class ConfigTestCase(ChannelPluginTestCase): def testHelp(self): self.assertError('config help alsdkfj') self.assertError('config help supybot.alsdkfj') - self.assertNotRegexp('config list supybot', '.*alsdkfj.*') - self.assertError('config help supybot.plugins.AutoMode.ban.alsdkfj') - self.assertNotRegexp('config list supybot.plugins.AutoMode.ban', '.*alsdkfj.*') self.assertNotError('config help supybot') # We tell the user to list. self.assertNotError('config help supybot.plugins') self.assertNotError('config help supybot.replies.success') diff --git a/plugins/Later/locale/it.po b/plugins/Later/locale/it.po index 520b48e0d..fc610588e 100644 --- a/plugins/Later/locale/it.po +++ b/plugins/Later/locale/it.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Supybot-fr\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" -"PO-Revision-Date: 2011-06-26 18:57+0200\n" +"PO-Revision-Date: 2011-08-10 14:27+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" @@ -159,7 +159,28 @@ msgstr "" msgid "There were no notes for %r" msgstr "Non ci sono note per %r" -#: plugin.py:231 +#: plugin.py:212 +#, docstring +msgid "" +"\n" +"\n" +" Removes the latest note you sent to .\n" +" " +msgstr "" +"\n" +"\n" +" Rimuove l'ultima nota inviata a .\n" +" " + +#: plugin.py:217 +msgid "There are no note waiting for %s." +msgstr "Non ci sono note in attesa per %s." + +#: plugin.py:228 +msgid "There are no note from you waiting for %s." +msgstr "Non ci sono note in attesa per %s da te inviate." + +#: plugin.py:252 msgid "Sent %s: <%s> %s" msgstr "Inviata %s: <%s> %s" diff --git a/plugins/Misc/locale/it.po b/plugins/Misc/locale/it.po index 17dc655a6..b2fbb2b68 100644 --- a/plugins/Misc/locale/it.po +++ b/plugins/Misc/locale/it.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Supybot-fr\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" -"PO-Revision-Date: 2011-08-10 02:11+0200\n" +"PO-Revision-Date: 2011-08-10 14:39+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" @@ -161,12 +161,12 @@ msgstr "La versione di questo Supybot è %s. %s" msgid "" "takes no arguments\n" "\n" -" Returns a URL saying where to get Supybot.\n" +" Returns a URL saying where to get Limnoria.\n" " " msgstr "" "non necessita argomenti\n" "\n" -" Riporta un URL che dice dove ottenere Supybot.\n" +" Riporta un URL che dice dove ottenere Limnoria.\n" " " #: plugin.py:243 @@ -274,7 +274,7 @@ msgstr "Non ho mai visto %s, lascio a te l'invio del messaggio." msgid "%s wants me to tell you: %s" msgstr "%s vuole che ti dica: %s" -#: plugin.py:430 +#: plugin.py:431 #, docstring msgid "" "takes no arguments\n" @@ -287,7 +287,7 @@ msgstr "" " Controlla che il bot sia ancora vivo.\n" " " -#: plugin.py:434 +#: plugin.py:435 msgid "pong" msgstr "pong" diff --git a/plugins/Misc/plugin.py b/plugins/Misc/plugin.py index 08623811e..2f20de0a0 100644 --- a/plugins/Misc/plugin.py +++ b/plugins/Misc/plugin.py @@ -43,10 +43,15 @@ import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks +from supybot import commands + from supybot.utils.iter import ifilter from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Misc') +class RegexpTimeout(Exception): + pass + class Misc(callbacks.Plugin): def __init__(self, irc): self.__parent = super(Misc, self) @@ -332,10 +337,27 @@ class Misc(callbacks.Plugin): predicates.setdefault('without', []).append(f) elif option == 'regexp': def f(m, arg=arg): + def f1(s, arg): + """Since we can't enqueue match objects into the multiprocessing queue, + we'll just wrap the function to return bools.""" + if arg.search(s) is not None: + return True + else: + return False if ircmsgs.isAction(m): - return arg.search(ircmsgs.unAction(m)) + m1 = ircmsgs.unAction(m) + #return arg.search(ircmsgs.unAction(m)) else: - return arg.search(m.args[1]) + m1 = m.args[1] + #return arg.search(m.args[1]) + try: + # use a subprocess here, since specially crafted regexps can + # take exponential time and hang up the bot. + # timeout of 0.1 should be more than enough for any normal regexp. + v = commands.process(f1, m1, arg, timeout=0.1, pn=self.name(), cn='last') + return v + except commands.ProcessTimeoutError: + return False predicates.setdefault('regexp', []).append(f) elif option == 'nolimit': nolimit = True @@ -370,8 +392,12 @@ class Misc(callbacks.Plugin): showNick = True for m in iterable: for predicate in predicates: - if not predicate(m): - break + try: + if not predicate(m): + break + except RegexpTimeout: + irc.error(_('The regular expression timed out.')) + return else: if nolimit: resp.append(ircmsgs.prettyPrint(m, diff --git a/plugins/Note/plugin.py b/plugins/Note/plugin.py index b89de964d..f76921a62 100644 --- a/plugins/Note/plugin.py +++ b/plugins/Note/plugin.py @@ -42,6 +42,7 @@ import supybot.ircmsgs as ircmsgs import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks +from supybot import commands from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Note') @@ -294,7 +295,8 @@ class Note(callbacks.Plugin): own = to for (option, arg) in optlist: if option == 'regexp': - criteria.append(arg.search) + criteria.append(lambda x: commands.regexp_wrapper(x, reobj=arg, + timeout=0.1, plugin_name = self.name(), fcn_name='search')) elif option == 'sent': own = frm if glob: diff --git a/plugins/Seen/locale/it.po b/plugins/Seen/locale/it.po index 71ef8369f..a4e2b416a 100644 --- a/plugins/Seen/locale/it.po +++ b/plugins/Seen/locale/it.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Supybot-fr\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" -"PO-Revision-Date: 2011-08-10 02:27+0200\n" +"PO-Revision-Date: 2011-08-10 14:43+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" @@ -10,47 +10,48 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: plugin.py:203 plugin.py:284 +#: plugin.py:204 plugin.py:285 msgid "%s was last seen in %s %s ago: %s" msgstr "%s è stato visto per l'ultima volta in %s %s fa: %s" -#: plugin.py:210 +#: plugin.py:211 msgid "%s (%s ago)" msgstr "%s (%s fa)" -#: plugin.py:212 +#: plugin.py:213 msgid "%s could be %L" msgstr "%s potrebbe essere %L" -#: plugin.py:212 +#: plugin.py:213 msgid "or" msgstr "oppure" -#: plugin.py:214 +#: plugin.py:215 msgid "I haven't seen anyone matching %s." msgstr "Non ho visto nessuno che corrisponda a %s." -#: plugin.py:216 plugin.py:288 +#: plugin.py:217 plugin.py:289 msgid "I have not seen %s." msgstr "Non ho visto %s." -#: plugin.py:220 +#: plugin.py:221 #, docstring msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. is only necessary if the message isn't sent on the\n" -" channel itself.\n" +" channel itself. may contain * as a wildcard.\n" " " msgstr "" "[] \n" "\n" " Riporta l'ultima volta che è stato visto e cosa stava dicendo.\n" -" è necessario solo se il messaggio non viene inviato nel canale stesso.\n" +" è necessario solo se il messaggio non viene inviato nel canale\n" +" stesso. può contenere * come wildcard.\n" " " -#: plugin.py:231 +#: plugin.py:232 #, docstring msgid "" "[] [--user ] []\n" @@ -73,15 +74,15 @@ msgstr "" " solo se il messaggio non viene inviato nel canale stesso.\n" " " -#: plugin.py:261 +#: plugin.py:262 msgid "Someone was last seen in %s %s ago: %s" msgstr "Qualcuno è stato visto per l'ultima volta in %s %s fa: %s" -#: plugin.py:265 +#: plugin.py:266 msgid "I have never seen anyone." msgstr "Non ho mai visto nessuno." -#: plugin.py:269 +#: plugin.py:270 #, docstring msgid "" "[]\n" @@ -96,15 +97,15 @@ msgstr "" " solo se il messaggio non viene inviato nel canale stesso.\n" " " -#: plugin.py:292 +#: plugin.py:293 #, docstring msgid "" -"[] \n" +"[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. This looks up in the user seen database, which means\n" " that it could be any nick recognized as user that was seen.\n" -" is only necessary if the message isn't sent in the channel\n" +" is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" @@ -116,7 +117,7 @@ msgstr "" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " -#: plugin.py:305 +#: plugin.py:306 #, docstring msgid "" "[] \n" @@ -129,15 +130,15 @@ msgstr "" " Riporta i messaggi da quando ha lasciato il canale l'ultima volta.\n" " " -#: plugin.py:312 +#: plugin.py:313 msgid "You must be in %s to use this command." msgstr "Per usare questo comando bisogna essere in %s." -#: plugin.py:333 +#: plugin.py:334 msgid "I couldn't find in my history of %s messages where %r last left the %s" msgstr "Non trovo nella cronologia dei messaggi di %s dove %r ha lasciato %s l'ultima volta." -#: plugin.py:342 +#: plugin.py:343 msgid "Either %s didn't leave, or no messages were sent while %s was gone." msgstr "%s non è uscito o non ha inviato alcun messaggio quando se n'é andato." diff --git a/plugins/String/config.py b/plugins/String/config.py index d451b6a9e..6ebec26de 100644 --- a/plugins/String/config.py +++ b/plugins/String/config.py @@ -53,4 +53,11 @@ conf.registerGlobalValue(String.levenshtein, 'max', this variable, to limit the size of arguments passed to the levenshtein command."""))) +conf.registerGroup(String, 're') +conf.registerGlobalValue(String.re, 'timeout', + registry.PositiveFloat(0.1, """Determines the maximum time, in seconds, that + a regular expression is given to execute before being terminated. Since + there is a possibility that user input for the re command can cause it to + eat up large amounts of ram or cpu time, it's a good idea to keep this + low. Most normal regexps should not take very long at all.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/plugins/String/plugin.py b/plugins/String/plugin.py index ccc6496ec..2aa58c222 100644 --- a/plugins/String/plugin.py +++ b/plugins/String/plugin.py @@ -146,10 +146,14 @@ class String(callbacks.Plugin): s = _('You probably don\'t want to match the empty string.') irc.error(s) else: - irc.reply(f(text)) - re = wrap(re, [('checkCapability', 'trusted'), - first('regexpMatcher', 'regexpReplacer'), - 'text']) + t = self.registryValue('re.timeout') + try: + v = commands.process(f, text, timeout=t, pn=self.name(), cn='re') + irc.reply(v) + except commands.ProcessTimeoutError, e: + irc.error("ProcessTimeoutError: %s" % (e,)) + re = thread(wrap(re, [first('regexpMatcher', 'regexpReplacer'), + 'text'])) @internationalizeDocstring def xor(self, irc, msg, args, password, text): diff --git a/plugins/Time/plugin.py b/plugins/Time/plugin.py index c7cdc8b7b..b1258a912 100644 --- a/plugins/Time/plugin.py +++ b/plugins/Time/plugin.py @@ -171,8 +171,6 @@ class Time(callbacks.Plugin): except ImportError: irc.error(_('Python-tz is required by the command, but is not ' 'installed on this computer.')) - if len(timezone.split('/')) != 2: - irc.error(_('A timezone must be in the format region/city.')) try: timezone = pytz.timezone(timezone) except pytz.UnknownTimeZoneError: diff --git a/plugins/Todo/locale/it.po b/plugins/Todo/locale/it.po index ea6d8c935..326ffad0a 100644 --- a/plugins/Todo/locale/it.po +++ b/plugins/Todo/locale/it.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Supybot-fr\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" -"PO-Revision-Date: 2011-08-10 02:33+0200\n" +"PO-Revision-Date: 2011-08-10 14:53+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" @@ -10,6 +10,14 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" + +#: config.py:50 +msgid "" +"Determines whether users can read the\n" +" todo-list of another user." +msgstr "" +"Determina se un utente possa leggere la lista delle cose da fare di un altro utente." + #: plugin.py:135 #, docstring msgid "" @@ -27,43 +35,47 @@ msgstr "" " ha aggiunto alla sua lista delle cose da fare.\n" " " -#: plugin.py:150 +#: plugin.py:146 +msgid "You are not allowed to see other users todo-list." +msgstr "Non hai l'autorizzazione pe rleggere la lista delle cose da fare degli altri utenti." + +#: plugin.py:153 msgid "#%i: %s" msgstr "#%i: %s" -#: plugin.py:155 +#: plugin.py:158 msgid "%s for %s: %L" msgstr "%s per %s: %L" -#: plugin.py:159 +#: plugin.py:162 msgid "That user has no tasks in their todo list." msgstr "Questo utente non ha compiti nella sua lista delle cose da fare." -#: plugin.py:161 +#: plugin.py:164 msgid "You have no tasks in your todo list." msgstr "Non hai compiti nella tua lista delle cose da fare." -#: plugin.py:168 +#: plugin.py:171 msgid "Active" msgstr "Attivo" -#: plugin.py:170 +#: plugin.py:173 msgid "Inactive" msgstr "Inattivo" -#: plugin.py:172 +#: plugin.py:175 msgid ", priority: %i" msgstr ", priorità: %i" -#: plugin.py:175 +#: plugin.py:178 msgid "%s todo for %s: %s (Added at %s)" msgstr "%s compito per %s: %s (Aggiunto il %s)" -#: plugin.py:179 plugin.py:260 plugin.py:274 +#: plugin.py:182 plugin.py:263 plugin.py:277 msgid "task id" msgstr "id compito" -#: plugin.py:184 +#: plugin.py:187 #, docstring msgid "" "[--priority=] \n" @@ -80,11 +92,11 @@ msgstr "" " Ogni numero intero è valido.\n" " " -#: plugin.py:195 +#: plugin.py:198 msgid "(Todo #%i added)" msgstr "(Compito #%i aggiunto)" -#: plugin.py:201 +#: plugin.py:204 #, docstring msgid "" " [ ...]\n" @@ -97,15 +109,15 @@ msgstr "" " Rimuove dalla lista personale delle cose da fare.\n" " " -#: plugin.py:212 +#: plugin.py:215 msgid "Task %i could not be removed either because that id doesn't exist or it has been removed already." msgstr "Il compito %i non può essere rimosso in quanto l'id non esiste o è già stato rimosso." -#: plugin.py:216 +#: plugin.py:219 msgid "No tasks were removed because the following tasks could not be removed: %L." msgstr "Non è stato rimosso nessun compito perché i seguenti non possono essere rimossi: %L." -#: plugin.py:226 +#: plugin.py:229 #, docstring msgid "" "[--{regexp} ] [ ...]\n" @@ -121,11 +133,11 @@ msgstr "" " Se --regexp è fornita, il suo valore associato è usato come regexp e confrontato con i compiti.\n" " " -#: plugin.py:246 +#: plugin.py:249 msgid "No tasks matched that query." msgstr "Nessun compito corrisponde alla richiesta." -#: plugin.py:252 +#: plugin.py:255 #, docstring msgid "" " \n" @@ -138,7 +150,7 @@ msgstr "" " Imposta la priorità del compito con l'id fornito al valore specificato.\n" " " -#: plugin.py:266 +#: plugin.py:269 #, docstring msgid "" " \n" diff --git a/plugins/Todo/plugin.py b/plugins/Todo/plugin.py index 91a2f98e8..90065e83a 100644 --- a/plugins/Todo/plugin.py +++ b/plugins/Todo/plugin.py @@ -41,6 +41,7 @@ from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks +from supybot import commands from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Todo') @@ -237,6 +238,8 @@ class Todo(callbacks.Plugin): criteria = [] for (option, arg) in optlist: if option == 'regexp': + criteria.append(lambda x: commands.regexp_wrapper(x, reobj=arg, + timeout=0.1, plugin_name = self.name(), fcn_name='search')) criteria.append(arg.search) for glob in globs: glob = utils.python.glob2re(glob) diff --git a/plugins/__init__.py b/plugins/__init__.py index 2e9b2c14f..5c86f5162 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -50,6 +50,7 @@ import supybot.world as world from supybot.commands import * import supybot.ircutils as ircutils import supybot.callbacks as callbacks +from supybot import commands ## i think we don't need any of this with sqlite3 #try: @@ -431,7 +432,9 @@ class ChannelIdDatabasePlugin(callbacks.Plugin): if opt == 'by': predicates.append(lambda r, arg=arg: r.by == arg.id) elif opt == 'regexp': - predicates.append(lambda r, arg=arg: arg.search(r.text)) + predicates.append(lambda x: commands.regexp_wrapper(x.text, reobj=arg, + timeout=0.1, plugin_name = self.name(), fcn_name='search')) + #predicates.append(lambda r, arg=arg: arg.search(r.text)) if glob: def globP(r, glob=glob.lower()): return fnmatch.fnmatch(r.text.lower(), glob) diff --git a/src/commands.py b/src/commands.py index 45531d322..4c26e29a6 100644 --- a/src/commands.py +++ b/src/commands.py @@ -69,6 +69,61 @@ def thread(f): f(self, irc, msg, args, *L, **kwargs) return utils.python.changeFunctionName(newf, f.func_name, f.__doc__) +class ProcessTimeoutError(Exception): + """Gets raised when a process is killed due to timeout.""" + pass + +def process(f, *args, **kwargs): + """Runs a function in a subprocess. + + Several extra keyword arguments can be supplied. + , the pluginname, and , the command name, are strings used to + create the process name, for identification purposes. + , if supplied, limits the length of execution of target + function to seconds.""" + timeout = kwargs.pop('timeout', None) + + q = multiprocessing.Queue() + def newf(f, q, *args, **kwargs): + try: + r = f(*args, **kwargs) + q.put(r) + except Exception as e: + q.put(e) + targetArgs = (f, q,) + args + p = callbacks.CommandProcess(target=newf, + args=targetArgs, kwargs=kwargs) + p.start() + p.join(timeout) + if p.is_alive(): + p.terminate() + raise ProcessTimeoutError, "%s aborted due to timeout." % (p.name,) + try: + v = q.get(block=False) + except Queue.Empty: + v = "Nothing returned." + if isinstance(v, Exception): + v = "Error: " + str(v) + return v + +def regexp_wrapper(s, reobj, timeout, plugin_name, fcn_name): + '''A convenient wrapper to stuff regexp search queries through a subprocess. + + This is used because specially-crafted regexps can use exponential time + and hang the bot.''' + def re_bool(s, reobj): + """Since we can't enqueue match objects into the multiprocessing queue, + we'll just wrap the function to return bools.""" + if reobj.search(s) is not None: + return True + else: + return False + try: + v = process(re_bool, s, reobj, timeout=timeout, pn=plugin_name, cn=fcn_name) + return v + except ProcessTimeoutError: + return False + class UrlSnarfThread(world.SupyThread): def __init__(self, *args, **kwargs): assert 'url' in kwargs diff --git a/src/registry.py b/src/registry.py index e8a6c340b..9850ec9b0 100644 --- a/src/registry.py +++ b/src/registry.py @@ -206,7 +206,7 @@ class Group(object): def __getattr__(self, attr): if attr in self._children: return self._children[attr] - elif self._supplyDefault and attr.startswith('#'): + elif self._supplyDefault: return self.__makeChild(attr, str(self)) else: self.__nonExistentEntry(attr) diff --git a/src/version.py b/src/version.py index 9c662f635..8b46f4516 100644 --- a/src/version.py +++ b/src/version.py @@ -1,3 +1,3 @@ """stick the various versioning attributes in here, so we only have to change them once.""" -version = '0.83.4.1+limnoria (2011-08-10T12:00:42+0200)' +version = '0.83.4.1+limnoria (2011-08-13T01:59:18+0200)'