* Mailing list subscription policy work flow has been completely rewritten.
[mailman.git] / src / mailman / model / pending.py
blobbbe95d5f06e0b5acc081a12009a123437b9c9d88
1 # Copyright (C) 2007-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Implementations of the IPendable and IPending interfaces."""
20 __all__ = [
21 'Pended',
22 'Pendings',
26 import json
27 import time
28 import random
29 import hashlib
31 from lazr.config import as_timedelta
32 from mailman.config import config
33 from mailman.database.model import Model
34 from mailman.database.transaction import dbconnection
35 from mailman.interfaces.pending import (
36 IPendable, IPended, IPendedKeyValue, IPendings)
37 from mailman.utilities.datetime import now
38 from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
39 from sqlalchemy.orm import relationship
40 from zope.interface import implementer
41 from zope.interface.verify import verifyObject
45 @implementer(IPendedKeyValue)
46 class PendedKeyValue(Model):
47 """A pended key/value pair, tied to a token."""
49 __tablename__ = 'pendedkeyvalue'
51 id = Column(Integer, primary_key=True)
52 key = Column(Unicode)
53 value = Column(Unicode)
54 pended_id = Column(Integer, ForeignKey('pended.id'), index=True)
56 def __init__(self, key, value):
57 self.key = key
58 self.value = value
62 @implementer(IPended)
63 class Pended(Model):
64 """A pended event, tied to a token."""
66 __tablename__ = 'pended'
68 id = Column(Integer, primary_key=True)
69 token = Column(Unicode)
70 expiration_date = Column(DateTime)
71 key_values = relationship('PendedKeyValue')
73 def __init__(self, token, expiration_date):
74 super(Pended, self).__init__()
75 self.token = token
76 self.expiration_date = expiration_date
80 @implementer(IPendable)
81 class UnpendedPendable(dict):
82 pass
86 @implementer(IPendings)
87 class Pendings:
88 """Implementation of the IPending interface."""
90 @dbconnection
91 def add(self, store, pendable, lifetime=None):
92 verifyObject(IPendable, pendable)
93 # Calculate the token and the lifetime.
94 if lifetime is None:
95 lifetime = as_timedelta(config.mailman.pending_request_life)
96 # Calculate a unique token. Algorithm vetted by the Timbot. time()
97 # has high resolution on Linux, clock() on Windows. random gives us
98 # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and
99 # clock values basically help obscure the random number generator, as
100 # does the hash calculation. The integral parts of the time values
101 # are discarded because they're the most predictable bits.
102 for attempts in range(3):
103 right_now = time.time()
104 x = random.random() + right_now % 1.0 + time.clock() % 1.0
105 # Use sha1 because it produces shorter strings.
106 token = hashlib.sha1(repr(x).encode('utf-8')).hexdigest()
107 # In practice, we'll never get a duplicate, but we'll be anal
108 # about checking anyway.
109 if store.query(Pended).filter_by(token=token).count() == 0:
110 break
111 else:
112 raise RuntimeError('Could not find a valid pendings token')
113 # Create the record, and then the individual key/value pairs.
114 pending = Pended(
115 token=token,
116 expiration_date=now() + lifetime)
117 for key, value in pendable.items():
118 # Both keys and values must be strings.
119 if isinstance(key, bytes):
120 key = key.decode('utf-8')
121 if isinstance(value, bytes):
122 # Make sure we can turn this back into a bytes.
123 value = dict(__encoding__='utf-8',
124 value=value.decode('utf-8'))
125 keyval = PendedKeyValue(key=key, value=json.dumps(value))
126 pending.key_values.append(keyval)
127 store.add(pending)
128 return token
130 @dbconnection
131 def confirm(self, store, token, *, expunge=True):
132 # Token can come in as a unicode, but it's stored in the database as
133 # bytes. They must be ascii.
134 pendings = store.query(Pended).filter_by(token=str(token))
135 if pendings.count() == 0:
136 return None
137 assert pendings.count() == 1, (
138 'Unexpected token count: {0}'.format(pendings.count()))
139 pending = pendings[0]
140 pendable = UnpendedPendable()
141 # Find all PendedKeyValue entries that are associated with the pending
142 # object's ID. Watch out for type conversions.
143 entries = store.query(PendedKeyValue).filter(
144 PendedKeyValue.pended_id == pending.id)
145 for keyvalue in entries:
146 value = json.loads(keyvalue.value)
147 if isinstance(value, dict) and '__encoding__' in value:
148 value = value['value'].encode(value['__encoding__'])
149 pendable[keyvalue.key] = value
150 if expunge:
151 store.delete(keyvalue)
152 if expunge:
153 store.delete(pending)
154 return pendable
156 @dbconnection
157 def evict(self, store):
158 right_now = now()
159 for pending in store.query(Pended).all():
160 if pending.expiration_date < right_now:
161 # Find all PendedKeyValue entries that are associated with the
162 # pending object's ID.
163 q = store.query(PendedKeyValue).filter(
164 PendedKeyValue.pended_id == pending.id)
165 for keyvalue in q:
166 store.delete(keyvalue)
167 store.delete(pending)
169 @property
170 @dbconnection
171 def count(self, store):
172 return store.query(Pended).count()