1###
2# Copyright (c) 2002-2009, Jeremiah Fincher
3# Copyright (c) 2011, Valentin Lorentz
4# Copyright (c) 2009,2013, James McCoy
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9#
10#   * Redistributions of source code must retain the above copyright notice,
11#     this list of conditions, and the following disclaimer.
12#   * Redistributions in binary form must reproduce the above copyright notice,
13#     this list of conditions, and the following disclaimer in the
14#     documentation and/or other materials provided with the distribution.
15#   * Neither the name of the author of this software nor the name of
16#     contributors to this software may be used to endorse or promote products
17#     derived from this software without specific prior written consent.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29# POSSIBILITY OF SUCH DAMAGE.
30###
31
32import os
33import time
34import operator
35
36from . import conf, ircutils, log, registry, unpreserve, utils, world
37from .utils import minisix
38
39def isCapability(capability):
40    return len(capability.split(None, 1)) == 1
41
42def fromChannelCapability(capability):
43    """Returns a (channel, capability) tuple from a channel capability."""
44    assert isChannelCapability(capability), 'got %s' % capability
45    return capability.split(',', 1)
46
47def isChannelCapability(capability):
48    """Returns True if capability is a channel capability; False otherwise."""
49    if ',' in capability:
50        (channel, capability) = capability.split(',', 1)
51        return ircutils.isChannel(channel) and isCapability(capability)
52    else:
53        return False
54
55def makeChannelCapability(channel, capability):
56    """Makes a channel capability given a channel and a capability."""
57    assert isCapability(capability), 'got %s' % capability
58    assert ircutils.isChannel(channel), 'got %s' % channel
59    return '%s,%s' % (channel, capability)
60
61def isAntiCapability(capability):
62    """Returns True if capability is an anticapability; False otherwise."""
63    if isChannelCapability(capability):
64        (_, capability) = fromChannelCapability(capability)
65    return isCapability(capability) and capability[0] == '-'
66
67def makeAntiCapability(capability):
68    """Returns the anticapability of a given capability."""
69    assert isCapability(capability), 'got %s' % capability
70    assert not isAntiCapability(capability), \
71           'makeAntiCapability does not work on anticapabilities.  ' \
72           'You probably want invertCapability; got %s.' % capability
73    if isChannelCapability(capability):
74        (channel, capability) = fromChannelCapability(capability)
75        return makeChannelCapability(channel, '-' + capability)
76    else:
77        return '-' + capability
78
79def unAntiCapability(capability):
80    """Takes an anticapability and returns the non-anti form."""
81    assert isCapability(capability), 'got %s' % capability
82    if not isAntiCapability(capability):
83        raise ValueError('%s is not an anti capability' % capability)
84    if isChannelCapability(capability):
85        (channel, capability) = fromChannelCapability(capability)
86        return ','.join((channel, capability[1:]))
87    else:
88        return capability[1:]
89
90def invertCapability(capability):
91    """Make a capability into an anticapability and vice versa."""
92    assert isCapability(capability), 'got %s' % capability
93    if isAntiCapability(capability):
94        return unAntiCapability(capability)
95    else:
96        return makeAntiCapability(capability)
97
98def canonicalCapability(capability):
99    if callable(capability):
100        capability = capability()
101    assert isCapability(capability), 'got %s' % capability
102    return capability.lower()
103
104_unwildcard_remover = utils.str.MultipleRemover('!@*?')
105def unWildcardHostmask(hostmask):
106    return _unwildcard_remover(hostmask)
107
108_invert = invertCapability
109class CapabilitySet(set):
110    """A subclass of set handling basic capability stuff."""
111    __slots__ = ('__parent',)
112    def __init__(self, capabilities=()):
113        self.__parent = super(CapabilitySet, self)
114        self.__parent.__init__()
115        for capability in capabilities:
116            self.add(capability)
117
118    def add(self, capability):
119        """Adds a capability to the set."""
120        capability = ircutils.toLower(capability)
121        inverted = _invert(capability)
122        if self.__parent.__contains__(inverted):
123            self.__parent.remove(inverted)
124        self.__parent.add(capability)
125
126    def remove(self, capability):
127        """Removes a capability from the set."""
128        capability = ircutils.toLower(capability)
129        self.__parent.remove(capability)
130
131    def __contains__(self, capability):
132        capability = ircutils.toLower(capability)
133        if self.__parent.__contains__(capability):
134            return True
135        if self.__parent.__contains__(_invert(capability)):
136            return True
137        else:
138            return False
139
140    def check(self, capability, ignoreOwner=False):
141        """Returns the appropriate boolean for whether a given capability is
142        'allowed' given its (or its anticapability's) presence in the set.
143        """
144        capability = ircutils.toLower(capability)
145        if self.__parent.__contains__(capability):
146            return True
147        elif self.__parent.__contains__(_invert(capability)):
148            return False
149        else:
150            raise KeyError
151
152    def __repr__(self):
153        return '%s([%s])' % (self.__class__.__name__,
154                             ', '.join(map(repr, self)))
155
156antiOwner = makeAntiCapability('owner')
157class UserCapabilitySet(CapabilitySet):
158    """A subclass of CapabilitySet to handle the owner capability correctly."""
159    __slots__ = ('__parent',)
160    def __init__(self, *args, **kwargs):
161        self.__parent = super(UserCapabilitySet, self)
162        self.__parent.__init__(*args, **kwargs)
163
164    def __contains__(self, capability, ignoreOwner=False):
165        capability = ircutils.toLower(capability)
166        if not ignoreOwner and capability == 'owner' or capability == antiOwner:
167            return True
168        elif not ignoreOwner and self.__parent.__contains__('owner'):
169            return True
170        else:
171            return self.__parent.__contains__(capability)
172
173    def check(self, capability, ignoreOwner=False):
174        """Returns the appropriate boolean for whether a given capability is
175        'allowed' given its (or its anticapability's) presence in the set.
176        Differs from CapabilitySet in that it handles the 'owner' capability
177        appropriately.
178        """
179        capability = ircutils.toLower(capability)
180        if capability == 'owner' or capability == antiOwner:
181            if self.__parent.__contains__('owner'):
182                return not isAntiCapability(capability)
183            else:
184                return isAntiCapability(capability)
185        elif not ignoreOwner and self.__parent.__contains__('owner'):
186            if isAntiCapability(capability):
187                return False
188            else:
189                return True
190        else:
191            return self.__parent.check(capability)
192
193    def add(self, capability):
194        """Adds a capability to the set.  Just make sure it's not -owner."""
195        capability = ircutils.toLower(capability)
196        assert capability != '-owner', '"-owner" disallowed.'
197        self.__parent.add(capability)
198
199class IrcUser(object):
200    """This class holds the capabilities and authentications for a user."""
201    __slots__ = ('id', 'auth', 'name', 'ignore', 'secure', 'hashed',
202            'password', 'capabilities', 'hostmasks', 'nicks', 'gpgkeys')
203    def __init__(self, ignore=False, password='', name='',
204                 capabilities=(), hostmasks=None, nicks=None,
205                 secure=False, hashed=False):
206        self.id = None
207        self.auth = [] # The (time, hostmask) list of auth crap.
208        self.name = name # The name of the user.
209        self.ignore = ignore # A boolean deciding if the person is ignored.
210        self.secure = secure # A boolean describing if hostmasks *must* match.
211        self.hashed = hashed # True if the password is hashed on disk.
212        self.password = password # password (plaintext? hashed?)
213        self.capabilities = UserCapabilitySet()
214        for capability in capabilities:
215            self.capabilities.add(capability)
216        if hostmasks is None:
217            self.hostmasks = ircutils.IrcSet() # hostmasks used for recognition
218        else:
219            self.hostmasks = hostmasks
220        if nicks is None:
221            self.nicks = {} # {'network1': ['foo', 'bar'], 'network': ['baz']}
222        else:
223            self.nicks = nicks
224        self.gpgkeys = [] # GPG key ids
225
226    def __repr__(self):
227        return format('%s(id=%s, ignore=%s, password="", name=%q, hashed=%r, '
228                      'capabilities=%r, hostmasks=[], secure=%r)\n',
229                      self.__class__.__name__, self.id, self.ignore,
230                      self.name, self.hashed, self.capabilities, self.secure)
231
232    def __hash__(self):
233        return hash(self.id)
234
235    def addCapability(self, capability):
236        """Gives the user the given capability."""
237        self.capabilities.add(capability)
238
239    def removeCapability(self, capability):
240        """Takes from the user the given capability."""
241        self.capabilities.remove(capability)
242
243    def _checkCapability(self, capability, ignoreOwner=False):
244        """Checks the user for a given capability."""
245        if self.ignore:
246            if isAntiCapability(capability):
247                return True
248            else:
249                return False
250        else:
251            return self.capabilities.check(capability, ignoreOwner=ignoreOwner)
252
253    def setPassword(self, password, hashed=False):
254        """Sets the user's password."""
255        if hashed or self.hashed:
256            self.hashed = True
257            self.password = utils.saltHash(password)
258        else:
259            self.password = password
260
261    def checkPassword(self, password):
262        """Checks the user's password."""
263        if password is None:
264            return False
265        if self.hashed:
266            (salt, _) = self.password.split('|')
267            return (self.password == utils.saltHash(password, salt=salt))
268        else:
269            return (self.password == password)
270
271    def checkHostmask(self, hostmask, useAuth=True):
272        """Checks a given hostmask against the user's hostmasks or current
273        authentication.  If useAuth is False, only checks against the user's
274        hostmasks.
275        """
276        if useAuth:
277            timeout = conf.supybot.databases.users.timeoutIdentification()
278            removals = []
279            try:
280                for (when, authmask) in self.auth:
281                    if timeout and when+timeout < time.time():
282                        removals.append((when, authmask))
283                    elif hostmask == authmask:
284                        return True
285            finally:
286                while removals:
287                    self.auth.remove(removals.pop())
288        for pat in self.hostmasks:
289            if ircutils.hostmaskPatternEqual(pat, hostmask):
290                return pat
291        return False
292
293    def addHostmask(self, hostmask):
294        """Adds a hostmask to the user's hostmasks."""
295        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
296        if len(unWildcardHostmask(hostmask)) < 3:
297            raise ValueError('Hostmask must contain at least 3 non-wildcard characters.')
298        self.hostmasks.add(hostmask)
299
300    def removeHostmask(self, hostmask):
301        """Removes a hostmask from the user's hostmasks."""
302        self.hostmasks.remove(hostmask)
303
304    def checkNick(self, network, nick):
305        """Checks a given nick against the user's nicks."""
306        return nick in self.nicks[network]
307
308    def addNick(self, network, nick):
309        """Adds a nick to the user's registered nicks on the network."""
310        global users
311        assert isinstance(network, minisix.string_types)
312        assert ircutils.isNick(nick), 'got %s' % nick
313        if users.getUserFromNick(network, nick) is not None:
314            raise KeyError
315        if network not in self.nicks:
316            self.nicks[network] = []
317        if nick not in self.nicks[network]:
318            self.nicks[network].append(nick)
319
320    def removeNick(self, network, nick):
321        """Removes a nick from the user's registered nicks on the network."""
322        assert isinstance(network, minisix.string_types)
323        if nick not in self.nicks[network]:
324            raise KeyError
325        self.nicks[network].remove(nick)
326
327    def addAuth(self, hostmask):
328        """Sets a user's authenticated hostmask.  This times out according to
329        conf.supybot.timeoutIdentification.  If hostmask exactly matches an
330        existing, known hostmask, the previous entry is removed."""
331        if self.checkHostmask(hostmask, useAuth=False) or not self.secure:
332            self.auth.append((time.time(), hostmask))
333            knownHostmasks = set()
334            def uniqueHostmask(auth):
335                (_, mask) = auth
336                if mask not in knownHostmasks:
337                    knownHostmasks.add(mask)
338                    return True
339                return False
340            uniqued = list(filter(uniqueHostmask, reversed(self.auth)))
341            self.auth = list(reversed(uniqued))
342        else:
343            raise ValueError('secure flag set, unmatched hostmask')
344
345    def clearAuth(self):
346        """Unsets a user's authenticated hostmask."""
347        for (when, hostmask) in self.auth:
348            users.invalidateCache(hostmask=hostmask)
349        self.auth = []
350
351    def preserve(self, fd, indent=''):
352        def write(s):
353            fd.write(indent)
354            fd.write(s)
355            fd.write(os.linesep)
356        write('name %s' % self.name)
357        write('ignore %s' % self.ignore)
358        write('secure %s' % self.secure)
359        if self.password:
360            write('hashed %s' % self.hashed)
361            write('password %s' % self.password)
362        for capability in self.capabilities:
363            write('capability %s' % capability)
364        for hostmask in self.hostmasks:
365            write('hostmask %s' % hostmask)
366        for network, nicks in self.nicks.items():
367            write('nicks %s %s' % (network, ' '.join(nicks)))
368        for key in self.gpgkeys:
369            write('gpgkey %s' % key)
370        fd.write(os.linesep)
371
372
373class IrcChannel(object):
374    """This class holds the capabilities, bans, and ignores of a channel."""
375    __slots__ = ('defaultAllow', 'expiredBans', 'bans', 'ignores', 'silences',
376            'exceptions', 'capabilities', 'lobotomized')
377    defaultOff = ('op', 'halfop', 'voice', 'protected')
378    def __init__(self, bans=None, silences=None, exceptions=None, ignores=None,
379                 capabilities=None, lobotomized=False, defaultAllow=True):
380        self.defaultAllow = defaultAllow
381        self.expiredBans = []
382        self.bans = bans or {}
383        self.ignores = ignores or {}
384        self.silences = silences or []
385        self.exceptions = exceptions or []
386        self.capabilities = capabilities or CapabilitySet()
387        for capability in self.defaultOff:
388            if capability not in self.capabilities:
389                self.capabilities.add(makeAntiCapability(capability))
390        self.lobotomized = lobotomized
391
392    def __repr__(self):
393        return '%s(bans=%r, ignores=%r, capabilities=%r, ' \
394               'lobotomized=%r, defaultAllow=%s, ' \
395               'silences=%r, exceptions=%r)\n' % \
396               (self.__class__.__name__, self.bans, self.ignores,
397                self.capabilities, self.lobotomized,
398                self.defaultAllow, self.silences, self.exceptions)
399
400    def addBan(self, hostmask, expiration=0):
401        """Adds a ban to the channel banlist."""
402        assert not conf.supybot.protocols.irc.strictRfc() or \
403                ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
404        self.bans[hostmask] = int(expiration)
405
406    def removeBan(self, hostmask):
407        """Removes a ban from the channel banlist."""
408        assert not conf.supybot.protocols.irc.strictRfc() or \
409                ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
410        return self.bans.pop(hostmask)
411
412    def checkBan(self, hostmask):
413        """Checks whether a given hostmask is banned by the channel banlist."""
414        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
415        now = time.time()
416        for (pattern, expiration) in list(self.bans.items()):
417            if now < expiration or not expiration:
418                if ircutils.hostmaskPatternEqual(pattern, hostmask):
419                    return True
420            else:
421                self.expiredBans.append((pattern, expiration))
422                del self.bans[pattern]
423        return False
424
425    def addIgnore(self, hostmask, expiration=0):
426        """Adds an ignore to the channel ignore list."""
427        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
428        self.ignores[hostmask] = int(expiration)
429
430    def removeIgnore(self, hostmask):
431        """Removes an ignore from the channel ignore list."""
432        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
433        return self.ignores.pop(hostmask)
434
435    def addCapability(self, capability):
436        """Adds a capability to the channel's default capabilities."""
437        assert isCapability(capability), 'got %s' % capability
438        self.capabilities.add(capability)
439
440    def removeCapability(self, capability):
441        """Removes a capability from the channel's default capabilities."""
442        assert isCapability(capability), 'got %s' % capability
443        self.capabilities.remove(capability)
444
445    def setDefaultCapability(self, b):
446        """Sets the default capability in the channel."""
447        self.defaultAllow = b
448
449    def _checkCapability(self, capability, ignoreOwner=False):
450        """Checks whether a certain capability is allowed by the channel."""
451        assert isCapability(capability), 'got %s' % capability
452        if capability in self.capabilities:
453            return self.capabilities.check(capability, ignoreOwner=ignoreOwner)
454        else:
455            if isAntiCapability(capability):
456                return not self.defaultAllow
457            else:
458                return self.defaultAllow
459
460    def checkIgnored(self, hostmask):
461        """Checks whether a given hostmask is to be ignored by the channel."""
462        if self.lobotomized:
463            return True
464        if world.testing:
465            return False
466        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
467        if self.checkBan(hostmask):
468            return True
469        now = time.time()
470        for (pattern, expiration) in list(self.ignores.items()):
471            if now < expiration or not expiration:
472                if ircutils.hostmaskPatternEqual(pattern, hostmask):
473                    return True
474            else:
475                del self.ignores[pattern]
476                # Later we may wish to keep expiredIgnores, but not now.
477        return False
478
479    def preserve(self, fd, indent=''):
480        def write(s):
481            fd.write(indent)
482            fd.write(s)
483            fd.write(os.linesep)
484        write('lobotomized %s' % self.lobotomized)
485        write('defaultAllow %s' % self.defaultAllow)
486        for capability in self.capabilities:
487            write('capability ' + capability)
488        bans = list(self.bans.items())
489        utils.sortBy(operator.itemgetter(1), bans)
490        for (ban, expiration) in bans:
491            write('ban %s %d' % (ban, expiration))
492        ignores = list(self.ignores.items())
493        utils.sortBy(operator.itemgetter(1), ignores)
494        for (ignore, expiration) in ignores:
495            write('ignore %s %d' % (ignore, expiration))
496        fd.write(os.linesep)
497
498
499class Creator(object):
500    __slots__ = ()
501    def badCommand(self, command, rest, lineno):
502        raise ValueError('Invalid command on line %s: %s' % (lineno, command))
503
504class IrcUserCreator(Creator):
505    __slots__ = ('users')
506    u = None
507    def __init__(self, users):
508        if self.u is None:
509            IrcUserCreator.u = IrcUser()
510        self.users = users
511
512    def user(self, rest, lineno):
513        if self.u.id is not None:
514            raise ValueError('Unexpected user command on line %s.' % lineno)
515        self.u.id = int(rest)
516
517    def _checkId(self):
518        if self.u.id is None:
519            raise ValueError('Unexpected user description without user.')
520
521    def name(self, rest, lineno):
522        self._checkId()
523        self.u.name = rest
524
525    def ignore(self, rest, lineno):
526        self._checkId()
527        self.u.ignore = bool(utils.gen.safeEval(rest))
528
529    def secure(self, rest, lineno):
530        self._checkId()
531        self.u.secure = bool(utils.gen.safeEval(rest))
532
533    def hashed(self, rest, lineno):
534        self._checkId()
535        self.u.hashed = bool(utils.gen.safeEval(rest))
536
537    def password(self, rest, lineno):
538        self._checkId()
539        self.u.password = rest
540
541    def hostmask(self, rest, lineno):
542        self._checkId()
543        self.u.hostmasks.add(rest)
544
545    def nicks(self, rest, lineno):
546        self._checkId()
547        network, nicks = rest.split(' ', 1)
548        self.u.nicks[network] = nicks.split(' ')
549
550    def capability(self, rest, lineno):
551        self._checkId()
552        self.u.capabilities.add(rest)
553
554    def gpgkey(self, rest, lineno):
555        self._checkId()
556        self.u.gpgkeys.append(rest)
557
558    def finish(self):
559        if self.u.name:
560            try:
561                self.users.setUser(self.u)
562            except DuplicateHostmask:
563                log.error('Hostmasks for %s collided with another user\'s.  '
564                          'Resetting hostmasks for %s.',
565                          self.u.name, self.u.name)
566                # Some might argue that this is arbitrary, and perhaps it is.
567                # But we've got to do *something*, so we'll show some deference
568                # to our lower-numbered users.
569                self.u.hostmasks.clear()
570                self.users.setUser(self.u)
571            IrcUserCreator.u = None
572
573class IrcChannelCreator(Creator):
574    __slots__ = ('c', 'channels', 'hadChannel')
575    name = None
576    def __init__(self, channels):
577        self.c = IrcChannel()
578        self.channels = channels
579        self.hadChannel = bool(self.name)
580
581    def channel(self, rest, lineno):
582        if self.name is not None:
583            raise ValueError('Unexpected channel command on line %s' % lineno)
584        IrcChannelCreator.name = rest
585
586    def _checkId(self):
587        if self.name is None:
588            raise ValueError('Unexpected channel description without channel.')
589
590    def lobotomized(self, rest, lineno):
591        self._checkId()
592        self.c.lobotomized = bool(utils.gen.safeEval(rest))
593
594    def defaultallow(self, rest, lineno):
595        self._checkId()
596        self.c.defaultAllow = bool(utils.gen.safeEval(rest))
597
598    def capability(self, rest, lineno):
599        self._checkId()
600        self.c.capabilities.add(rest)
601
602    def ban(self, rest, lineno):
603        self._checkId()
604        (pattern, expiration) = rest.split()
605        self.c.bans[pattern] = int(float(expiration))
606
607    def ignore(self, rest, lineno):
608        self._checkId()
609        (pattern, expiration) = rest.split()
610        self.c.ignores[pattern] = int(float(expiration))
611
612    def finish(self):
613        if self.hadChannel:
614            self.channels.setChannel(self.name, self.c)
615            IrcChannelCreator.name = None
616
617
618class DuplicateHostmask(ValueError):
619    pass
620
621class UsersDictionary(utils.IterableMap):
622    """A simple serialized-to-file User Database."""
623    __slots__ = ('noFlush', 'filename', 'users', '_nameCache',
624            '_hostmaskCache')
625    def __init__(self):
626        self.noFlush = False
627        self.filename = None
628        self.users = {}
629        self.nextId = 0
630        self._nameCache = utils.structures.CacheDict(1000)
631        self._hostmaskCache = utils.structures.CacheDict(1000)
632
633    # This is separate because the Creator has to access our instance.
634    def open(self, filename):
635        self.filename = filename
636        reader = unpreserve.Reader(IrcUserCreator, self)
637        try:
638            self.noFlush = True
639            try:
640                reader.readFile(filename)
641                self.noFlush = False
642                self.flush()
643            except EnvironmentError as e:
644                log.error('Invalid user dictionary file, resetting to empty.')
645                log.error('Exact error: %s', utils.exnToString(e))
646            except Exception as e:
647                log.exception('Exact error:')
648        finally:
649            self.noFlush = False
650
651    def reload(self):
652        """Reloads the database from its file."""
653        self.nextId = 0
654        self.users.clear()
655        self._nameCache.clear()
656        self._hostmaskCache.clear()
657        if self.filename is not None:
658            try:
659                self.open(self.filename)
660            except EnvironmentError as e:
661                log.warning('UsersDictionary.reload failed: %s', e)
662        else:
663            log.error('UsersDictionary.reload called with no filename.')
664
665    def flush(self):
666        """Flushes the database to its file."""
667        if not self.noFlush:
668            if self.filename is not None:
669                L = list(self.users.items())
670                L.sort()
671                fd = utils.file.AtomicFile(self.filename)
672                for (id, u) in L:
673                    fd.write('user %s' % id)
674                    fd.write(os.linesep)
675                    u.preserve(fd, indent='  ')
676                fd.close()
677            else:
678                log.error('UsersDictionary.flush called with no filename.')
679        else:
680            log.debug('Not flushing UsersDictionary because of noFlush.')
681
682    def close(self):
683        self.flush()
684        if self.flush in world.flushers:
685            world.flushers.remove(self.flush)
686        self.users.clear()
687
688    def items(self):
689        return self.users.items()
690
691    def getUserId(self, s):
692        """Returns the user ID of a given name or hostmask."""
693        if ircutils.isUserHostmask(s):
694            try:
695                return self._hostmaskCache[s]
696            except KeyError:
697                ids = {}
698                for (id, user) in self.users.items():
699                    x = user.checkHostmask(s)
700                    if x:
701                        ids[id] = x
702                if len(ids) == 1:
703                    id = list(ids.keys())[0]
704                    self._hostmaskCache[s] = id
705                    try:
706                        self._hostmaskCache[id].add(s)
707                    except KeyError:
708                        self._hostmaskCache[id] = set([s])
709                    return id
710                elif len(ids) == 0:
711                    raise KeyError(s)
712                else:
713                    log.error('Multiple matches found in user database.  '
714                              'Removing the offending hostmasks.')
715                    for (id, hostmask) in ids.items():
716                        log.error('Removing %q from user %s.', hostmask, id)
717                        self.users[id].removeHostmask(hostmask)
718                    raise DuplicateHostmask('Ids %r matched.' % ids)
719        else: # Not a hostmask, must be a name.
720            s = s.lower()
721            try:
722                return self._nameCache[s]
723            except KeyError:
724                for (id, user) in self.users.items():
725                    if s == user.name.lower():
726                        self._nameCache[s] = id
727                        self._nameCache[id] = s
728                        return id
729                else:
730                    raise KeyError(s)
731
732    def getUser(self, id):
733        """Returns a user given its id, name, or hostmask."""
734        if not isinstance(id, int):
735            # Must be a string.  Get the UserId first.
736            id = self.getUserId(id)
737        u = self.users[id]
738        while isinstance(u, int):
739            id = u
740            u = self.users[id]
741        u.id = id
742        return u
743
744    def getUserFromNick(self, network, nick):
745        """Return a user given its nick."""
746        for user in self.users.values():
747            try:
748                if nick in user.nicks[network]:
749                    return user
750            except KeyError:
751                pass
752        return None
753
754    def hasUser(self, id):
755        """Returns the database has a user given its id, name, or hostmask."""
756        try:
757            self.getUser(id)
758            return True
759        except KeyError:
760            return False
761
762    def numUsers(self):
763        return len(self.users)
764
765    def invalidateCache(self, id=None, hostmask=None, name=None):
766        if hostmask is not None:
767            if hostmask in self._hostmaskCache:
768                id = self._hostmaskCache.pop(hostmask)
769                self._hostmaskCache[id].remove(hostmask)
770                if not self._hostmaskCache[id]:
771                    del self._hostmaskCache[id]
772        if name is not None:
773            del self._nameCache[self._nameCache[id]]
774            del self._nameCache[id]
775        if id is not None:
776            if id in self._nameCache:
777                del self._nameCache[self._nameCache[id]]
778                del self._nameCache[id]
779            if id in self._hostmaskCache:
780                for hostmask in self._hostmaskCache[id]:
781                    del self._hostmaskCache[hostmask]
782                del self._hostmaskCache[id]
783
784    def setUser(self, user, flush=True):
785        """Sets a user (given its id) to the IrcUser given it."""
786        self.nextId = max(self.nextId, user.id)
787        try:
788            if self.getUserId(user.name) != user.id:
789                raise DuplicateHostmask(user.name, user.name)
790        except KeyError:
791            pass
792        for hostmask in user.hostmasks:
793            for (i, u) in self.items():
794                if i == user.id:
795                    continue
796                elif u.checkHostmask(hostmask):
797                    # We used to remove the hostmask here, but it's not
798                    # appropriate for us both to remove the hostmask and to
799                    # raise an exception.  So instead, we'll raise an
800                    # exception, but be nice and give the offending hostmask
801                    # back at the same time.
802                    raise DuplicateHostmask(u.name, hostmask)
803                for otherHostmask in u.hostmasks:
804                    if ircutils.hostmaskPatternEqual(hostmask, otherHostmask):
805                        raise DuplicateHostmask(u.name, hostmask)
806        self.invalidateCache(user.id)
807        self.users[user.id] = user
808        if flush:
809            self.flush()
810
811    def delUser(self, id):
812        """Removes a user from the database."""
813        del self.users[id]
814        if id in self._nameCache:
815            del self._nameCache[self._nameCache[id]]
816            del self._nameCache[id]
817        if id in self._hostmaskCache:
818            for hostmask in list(self._hostmaskCache[id]):
819                del self._hostmaskCache[hostmask]
820            del self._hostmaskCache[id]
821        self.flush()
822
823    def newUser(self):
824        """Allocates a new user in the database and returns it and its id."""
825        user = IrcUser(hashed=True)
826        self.nextId += 1
827        id = self.nextId
828        self.users[id] = user
829        self.flush()
830        user.id = id
831        return user
832
833
834class ChannelsDictionary(utils.IterableMap):
835    __slots__ = ('noFlush', 'filename', 'channels')
836    def __init__(self):
837        self.noFlush = False
838        self.filename = None
839        self.channels = ircutils.IrcDict()
840
841    def open(self, filename):
842        self.noFlush = True
843        try:
844            self.filename = filename
845            reader = unpreserve.Reader(IrcChannelCreator, self)
846            try:
847                reader.readFile(filename)
848                self.noFlush = False
849                self.flush()
850            except EnvironmentError as e:
851                log.error('Invalid channel database, resetting to empty.')
852                log.error('Exact error: %s', utils.exnToString(e))
853            except Exception as e:
854                log.error('Invalid channel database, resetting to empty.')
855                log.exception('Exact error:')
856        finally:
857            self.noFlush = False
858
859    def flush(self):
860        """Flushes the channel database to its file."""
861        if not self.noFlush:
862            if self.filename is not None:
863                fd = utils.file.AtomicFile(self.filename)
864                for (channel, c) in self.channels.items():
865                    fd.write('channel %s' % channel)
866                    fd.write(os.linesep)
867                    c.preserve(fd, indent='  ')
868                fd.close()
869            else:
870                log.warning('ChannelsDictionary.flush without self.filename.')
871        else:
872            log.debug('Not flushing ChannelsDictionary because of noFlush.')
873
874    def close(self):
875        self.flush()
876        if self.flush in world.flushers:
877            world.flushers.remove(self.flush)
878        self.channels.clear()
879
880    def reload(self):
881        """Reloads the channel database from its file."""
882        if self.filename is not None:
883            self.channels.clear()
884            try:
885                self.open(self.filename)
886            except EnvironmentError as e:
887                log.warning('ChannelsDictionary.reload failed: %s', e)
888        else:
889            log.warning('ChannelsDictionary.reload without self.filename.')
890
891    def getChannel(self, channel):
892        """Returns an IrcChannel object for the given channel."""
893        channel = channel.lower()
894        if channel in self.channels:
895            return self.channels[channel]
896        else:
897            c = IrcChannel()
898            self.channels[channel] = c
899            return c
900
901    def setChannel(self, channel, ircChannel):
902        """Sets a given channel to the IrcChannel object given."""
903        channel = channel.lower()
904        self.channels[channel] = ircChannel
905        self.flush()
906
907    def items(self):
908        return self.channels.items()
909
910
911class IgnoresDB(object):
912    __slots__ = ('filename', 'hostmasks')
913    def __init__(self):
914        self.filename = None
915        self.hostmasks = {}
916
917    def open(self, filename):
918        self.filename = filename
919        fd = open(self.filename)
920        for line in utils.file.nonCommentNonEmptyLines(fd):
921            try:
922                line = line.rstrip('\r\n')
923                L = line.split()
924                hostmask = L.pop(0)
925                if L:
926                    expiration = int(float(L.pop(0)))
927                else:
928                    expiration = 0
929                self.add(hostmask, expiration)
930            except Exception:
931                log.error('Invalid line in ignores database: %q', line)
932        fd.close()
933
934    def flush(self):
935        if self.filename is not None:
936            fd = utils.file.AtomicFile(self.filename)
937            now = time.time()
938            for (hostmask, expiration) in self.hostmasks.items():
939                if now < expiration or not expiration:
940                    fd.write('%s %s' % (hostmask, expiration))
941                    fd.write(os.linesep)
942            fd.close()
943        else:
944            log.warning('IgnoresDB.flush called without self.filename.')
945
946    def close(self):
947        if self.flush in world.flushers:
948            world.flushers.remove(self.flush)
949        self.flush()
950        self.hostmasks.clear()
951
952    def reload(self):
953        if self.filename is not None:
954            oldhostmasks = self.hostmasks.copy()
955            self.hostmasks.clear()
956            try:
957                self.open(self.filename)
958            except EnvironmentError as e:
959                log.warning('IgnoresDB.reload failed: %s', e)
960                # Let's be somewhat transactional.
961                self.hostmasks.update(oldhostmasks)
962        else:
963            log.warning('IgnoresDB.reload called without self.filename.')
964
965    def checkIgnored(self, prefix):
966        now = time.time()
967        for (hostmask, expiration) in list(self.hostmasks.items()):
968            if expiration and now > expiration:
969                del self.hostmasks[hostmask]
970            else:
971                if ircutils.hostmaskPatternEqual(hostmask, prefix):
972                    return True
973        return False
974
975    def add(self, hostmask, expiration=0):
976        assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask
977        self.hostmasks[hostmask] = expiration
978
979    def remove(self, hostmask):
980        del self.hostmasks[hostmask]
981
982
983confDir = conf.supybot.directories.conf()
984try:
985    userFile = os.path.join(confDir, conf.supybot.databases.users.filename())
986    users = UsersDictionary()
987    users.open(userFile)
988except EnvironmentError as e:
989    log.warning('Couldn\'t open user database: %s', e)
990
991try:
992    channelFile = os.path.join(confDir,
993                               conf.supybot.databases.channels.filename())
994    channels = ChannelsDictionary()
995    channels.open(channelFile)
996except EnvironmentError as e:
997    log.warning('Couldn\'t open channel database: %s', e)
998
999try:
1000    ignoreFile = os.path.join(confDir,
1001                              conf.supybot.databases.ignores.filename())
1002    ignores = IgnoresDB()
1003    ignores.open(ignoreFile)
1004except EnvironmentError as e:
1005    log.warning('Couldn\'t open ignore database: %s', e)
1006
1007
1008world.flushers.append(users.flush)
1009world.flushers.append(ignores.flush)
1010world.flushers.append(channels.flush)
1011
1012
1013###
1014# Useful functions for checking credentials.
1015###
1016def checkIgnored(hostmask, recipient='', users=users, channels=channels):
1017    """checkIgnored(hostmask, recipient='') -> True/False
1018
1019    Checks if the user is ignored by the recipient of the message.
1020    """
1021    try:
1022        id = users.getUserId(hostmask)
1023        user = users.getUser(id)
1024        if user._checkCapability('owner'):
1025            # Owners shouldn't ever be ignored.
1026            return False
1027        elif user.ignore:
1028            log.debug('Ignoring %s due to their IrcUser ignore flag.', hostmask)
1029            return True
1030    except KeyError:
1031        # If there's no user...
1032        if conf.supybot.defaultIgnore():
1033            log.debug('Ignoring %s due to conf.supybot.defaultIgnore',
1034                     hostmask)
1035            return True
1036    if ignores.checkIgnored(hostmask):
1037        log.debug('Ignoring %s due to ignore database.', hostmask)
1038        return True
1039    if ircutils.isChannel(recipient):
1040        channel = channels.getChannel(recipient)
1041        if channel.checkIgnored(hostmask):
1042            log.debug('Ignoring %s due to the channel ignores.', hostmask)
1043            return True
1044    return False
1045
1046def _x(capability, ret):
1047    if isAntiCapability(capability):
1048        return not ret
1049    else:
1050        return ret
1051
1052def _checkCapabilityForUnknownUser(capability, users=users, channels=channels,
1053        ignoreDefaultAllow=False):
1054    if isChannelCapability(capability):
1055        (channel, capability) = fromChannelCapability(capability)
1056        try:
1057            c = channels.getChannel(channel)
1058            if capability in c.capabilities:
1059                return c._checkCapability(capability)
1060            else:
1061                return _x(capability, (not ignoreDefaultAllow) and c.defaultAllow)
1062        except KeyError:
1063            pass
1064    defaultCapabilities = conf.supybot.capabilities()
1065    if capability in defaultCapabilities:
1066        return defaultCapabilities.check(capability)
1067    elif ignoreDefaultAllow:
1068        return _x(capability, False)
1069    else:
1070        return _x(capability, conf.supybot.capabilities.default())
1071
1072def checkCapability(hostmask, capability, users=users, channels=channels,
1073                    ignoreOwner=False, ignoreChannelOp=False,
1074                    ignoreDefaultAllow=False):
1075    """Checks that the user specified by name/hostmask has the capability given.
1076
1077    ``users`` and ``channels`` default to ``ircdb.users`` and
1078    ``ircdb.channels``.
1079
1080    ``ignoreOwner``, ``ignoreChannelOp``, and ``ignoreDefaultAllow`` are
1081    used to override default behavior of the capability system in special
1082    cases (actually, in the AutoMode plugin):
1083
1084    * ``ignoreOwner`` disables the behavior "owners have all capabilites"
1085    * ``ignoreChannelOp`` disables the behavior "channel ops have all
1086      channel capabilities"
1087    * ``ignoreDefaultAllow`` disables the behavior "if a user does not have
1088      a capability or the associated anticapability, then they have the
1089      capability"
1090    """
1091    if world.testing and (not isinstance(hostmask, str) or
1092            '@' not in hostmask or
1093            '__no_testcap__' not in hostmask.split('@')[1]):
1094        return _x(capability, True)
1095    try:
1096        u = users.getUser(hostmask)
1097        if u.secure and not u.checkHostmask(hostmask, useAuth=False):
1098            raise KeyError
1099    except KeyError:
1100        # Raised when no hostmasks match.
1101        return _checkCapabilityForUnknownUser(capability, users=users,
1102                channels=channels, ignoreDefaultAllow=ignoreDefaultAllow)
1103    except ValueError as e:
1104        # Raised when multiple hostmasks match.
1105        log.warning('%s: %s', hostmask, e)
1106        return _checkCapabilityForUnknownUser(capability, users=users,
1107              channels=channels, ignoreDefaultAllow=ignoreDefaultAllow)
1108    if capability in u.capabilities:
1109        try:
1110            return u._checkCapability(capability, ignoreOwner)
1111        except KeyError:
1112            pass
1113    if isChannelCapability(capability):
1114        (channel, capability) = fromChannelCapability(capability)
1115        if not ignoreChannelOp:
1116            try:
1117                chanop = makeChannelCapability(channel, 'op')
1118                if u._checkCapability(chanop):
1119                    return _x(capability, True)
1120            except KeyError:
1121                pass
1122        c = channels.getChannel(channel)
1123        if capability in c.capabilities:
1124            return c._checkCapability(capability)
1125        elif not ignoreDefaultAllow:
1126            return _x(capability, c.defaultAllow)
1127        else:
1128            return False
1129    defaultCapabilities = conf.supybot.capabilities()
1130    defaultCapabilitiesRegistered = conf.supybot.capabilities.registeredUsers()
1131    if capability in defaultCapabilities:
1132        return defaultCapabilities.check(capability)
1133    elif capability in defaultCapabilitiesRegistered:
1134        return defaultCapabilitiesRegistered.check(capability)
1135    elif ignoreDefaultAllow:
1136        return _x(capability, False)
1137    else:
1138        return _x(capability, conf.supybot.capabilities.default())
1139
1140
1141def checkCapabilities(hostmask, capabilities, requireAll=False):
1142    """Checks that a user has capabilities in a list.
1143
1144    requireAll is True if *all* capabilities in the list must be had, False if
1145    *any* of the capabilities in the list must be had.
1146    """
1147    for capability in capabilities:
1148        if requireAll:
1149            if not checkCapability(hostmask, capability):
1150                return False
1151        else:
1152            if checkCapability(hostmask, capability):
1153                return True
1154    return requireAll
1155
1156###
1157# supybot.capabilities
1158###
1159
1160class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedListOfStrings):
1161    __slots__ = ()
1162    List = CapabilitySet
1163
1164class DefaultCapabilities(SpaceSeparatedListOfCapabilities):
1165    __slots__ = ()
1166    # We use a keyword argument trick here to prevent eval'ing of code that
1167    # changes allowDefaultOwner from affecting this.  It's not perfect, but
1168    # it's still an improvement, raising the bar for potential crackers.
1169    def setValue(self, v, allowDefaultOwner=conf.allowDefaultOwner):
1170        registry.SpaceSeparatedListOfStrings.setValue(self, v)
1171        if '-owner' not in self.value and not allowDefaultOwner:
1172            print('*** You must run supybot with the --allow-default-owner')
1173            print('*** option in order to allow a default capability of owner.')
1174            print('*** Don\'t do that, it\'s dumb.')
1175            self.value.add('-owner')
1176
1177conf.registerGlobalValue(conf.supybot, 'capabilities',
1178    DefaultCapabilities([
1179        '-owner', '-admin', '-trusted',
1180        '-aka.add', '-aka.set', '-aka.remove',
1181        '-alias.add', '-alias.remove',
1182        '-scheduler.add', '-scheduler.remove',
1183    ],
1184    """These are the
1185    capabilities that are given to everyone by default.  If they are normal
1186    capabilities, then the user will have to have the appropriate
1187    anti-capability if you want to override these capabilities; if they are
1188    anti-capabilities, then the user will have to have the actual capability
1189    to override these capabilities.  See docs/CAPABILITIES if you don't
1190    understand why these default to what they do."""))
1191
1192conf.registerGlobalValue(conf.supybot.capabilities, 'registeredUsers',
1193    SpaceSeparatedListOfCapabilities([], """These are the
1194    capabilities that are given to every authenticated user by default.
1195    You probably want to use supybot.capabilities instead, to give these
1196    capabilities both to registered and non-registered users."""))
1197conf.registerGlobalValue(conf.supybot.capabilities, 'default',
1198    registry.Boolean(True, """Determines whether the bot by default will allow
1199    users to have a capability.  If this is disabled, a user must explicitly
1200    have the capability for whatever command they wish to run."""))
1201conf.registerGlobalValue(conf.supybot.capabilities, 'private',
1202    registry.SpaceSeparatedListOfStrings([], """Determines what capabilities
1203    the bot will never tell to a non-admin whether or not a user has them."""))
1204
1205
1206# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
1207