1# Copyright (C) 2007-2020 by the Free Software Foundation, Inc. 2# 3# This file is part of GNU Mailman. 4# 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. 9# 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. 14# 15# You should have received a copy of the GNU General Public License along with 16# GNU Mailman. If not, see <https://www.gnu.org/licenses/>. 17 18"""Implementations of the pending requests interfaces.""" 19 20from datetime import timedelta 21from mailman.database.model import Model 22from mailman.database.transaction import dbconnection 23from mailman.database.types import Enum, SAUnicode 24from mailman.interfaces.pending import IPendable, IPendings 25from mailman.interfaces.requests import IListRequests, RequestType 26from mailman.model.pending import Pended, PendedKeyValue 27from mailman.utilities.queries import QuerySequence 28from pickle import dumps, loads 29from public import public 30from sqlalchemy import Column, ForeignKey, Integer 31from sqlalchemy.orm import relationship 32from zope.component import getUtility 33from zope.interface import implementer 34 35 36@public 37@implementer(IPendable) 38class DataPendable(dict): 39 """See `IPendable`.""" 40 41 PEND_TYPE = 'data' 42 43 def update(self, mapping): 44 # Keys and values must be strings (unicodes, but bytes values are 45 # accepted for now). Any other types for keys are a programming 46 # error. If we find a non-SAUnicode value, pickle it and encode it in 47 # such a way that it will be properly reconstituted when unpended. 48 clean_mapping = {} 49 for key, value in mapping.items(): 50 assert isinstance(key, (bytes, str)) 51 if not isinstance(value, str): 52 key = '_pck_' + key 53 value = dumps(value).decode('raw-unicode-escape') 54 clean_mapping[key] = value 55 super().update(clean_mapping) 56 57 58@public 59@implementer(IListRequests) 60class ListRequests: 61 """See `IListRequests`.""" 62 63 def __init__(self, mailing_list): 64 self.mailing_list = mailing_list 65 66 @property 67 @dbconnection 68 def count(self, store): 69 return store.query(_Request).filter_by( 70 mailing_list=self.mailing_list).count() 71 72 @dbconnection 73 def count_of(self, store, request_type): 74 return store.query(_Request).filter_by( 75 mailing_list=self.mailing_list, request_type=request_type).count() 76 77 @property 78 @dbconnection 79 def held_requests(self, store): 80 results = store.query(_Request).filter_by( 81 mailing_list=self.mailing_list) 82 yield from results 83 84 @dbconnection 85 def of_type(self, store, request_type): 86 return QuerySequence( 87 store.query(_Request).filter_by( 88 mailing_list=self.mailing_list, request_type=request_type 89 ).order_by(_Request.id)) 90 91 @dbconnection 92 def hold_request(self, store, request_type, key, data=None): 93 # Check to make sure `request_type` is a valid Enum. 94 if not isinstance(request_type, RequestType): 95 try: 96 request_type = RequestType(request_type) 97 except ValueError: 98 raise TypeError(request_type) 99 100 if data is None: 101 data_hash = None 102 else: 103 pendable = DataPendable() 104 pendable.update(data) 105 token = getUtility(IPendings).add(pendable, timedelta(days=5000)) 106 data_hash = token 107 request = _Request(key, request_type, self.mailing_list, data_hash) 108 store.add(request) 109 # XXX The caller needs a valid id immediately, so flush the changes 110 # now to the SA transaction context. Otherwise .id would not be 111 # valid. Hopefully this has no unintended side-effects. 112 store.flush() 113 return request.id 114 115 @dbconnection 116 def get_request(self, store, request_id, request_type=None): 117 result = store.query(_Request).get(request_id) 118 if result is None or result.mailing_list != self.mailing_list: 119 return None 120 if request_type is not None and result.request_type != request_type: 121 return None 122 if result.data_hash is None: 123 return result.key, None 124 pendable = getUtility(IPendings).confirm( 125 result.data_hash, expunge=False) 126 if pendable is None: 127 return None 128 data = dict() 129 # Unpickle any non-SAUnicode values. 130 for key, value in pendable.items(): 131 if key.startswith('_pck_'): 132 data[key[5:]] = loads(value.encode('raw-unicode-escape')) 133 else: 134 data[key] = value 135 # Some APIs need the request type. 136 data['_request_type'] = result.request_type.name 137 return result.key, data 138 139 @dbconnection 140 def delete_request(self, store, request_id): 141 request = store.query(_Request).get(request_id) 142 if request is None: 143 raise KeyError(request_id) 144 # Throw away the pended data. 145 getUtility(IPendings).confirm(request.data_hash) 146 store.delete(request) 147 148 @dbconnection 149 def clear(self, store): 150 for token, pendable in getUtility(IPendings).find( 151 mlist=self.mailing_list, 152 confirm=False): 153 pended = store.query(Pended).filter_by(token=token).first() 154 store.query(PendedKeyValue).filter_by(pended_id=pended.id).delete() 155 store.delete(pended) 156 157 158class _Request(Model): 159 """Table for mailing list hold requests.""" 160 161 __tablename__ = '_request' 162 163 id = Column(Integer, primary_key=True) 164 key = Column(SAUnicode) 165 request_type = Column(Enum(RequestType)) 166 data_hash = Column(SAUnicode) 167 168 mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True) 169 mailing_list = relationship('MailingList') 170 171 def __init__(self, key, request_type, mailing_list, data_hash): 172 super().__init__() 173 self.key = key 174 self.request_type = request_type 175 self.mailing_list = mailing_list 176 self.data_hash = data_hash 177