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)
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
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."""
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)
53 value
= Column(Unicode
)
54 pended_id
= Column(Integer
, ForeignKey('pended.id'), index
=True)
56 def __init__(self
, key
, value
):
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
__()
76 self
.expiration_date
= expiration_date
80 @implementer(IPendable
)
81 class UnpendedPendable(dict):
86 @implementer(IPendings
)
88 """Implementation of the IPending interface."""
91 def add(self
, store
, pendable
, lifetime
=None):
92 verifyObject(IPendable
, pendable
)
93 # Calculate the token and the lifetime.
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:
112 raise RuntimeError('Could not find a valid pendings token')
113 # Create the record, and then the individual key/value pairs.
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
)
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:
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
151 store
.delete(keyvalue
)
153 store
.delete(pending
)
157 def evict(self
, store
):
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)
166 store
.delete(keyvalue
)
167 store
.delete(pending
)
171 def count(self
, store
):
172 return store
.query(Pended
).count()