1# Copyright (C) 2010-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"""The root of the REST API.""" 19 20from mailman.config import config 21from mailman.core.api import API30, API31 22from mailman.core.constants import system_preferences 23from mailman.core.system import system 24from mailman.interfaces.listmanager import IListManager 25from mailman.model.uid import UID 26from mailman.rest.addresses import AllAddresses, AnAddress 27from mailman.rest.bans import BannedEmail, BannedEmails 28from mailman.rest.domains import ADomain, AllDomains 29from mailman.rest.helpers import ( 30 BadRequest, NotFound, child, etag, no_content, not_found, okay) 31from mailman.rest.lists import AList, AllLists, FindLists, Styles 32from mailman.rest.members import AMember, AllMembers, FindMembers 33from mailman.rest.plugins import APlugin, AllPlugins 34from mailman.rest.preferences import ReadOnlyPreferences 35from mailman.rest.queues import AQueue, AQueueFile, AllQueues 36from mailman.rest.templates import TemplateFinder 37from mailman.rest.uris import ASiteURI, AllSiteURIs 38from mailman.rest.users import AUser, AllUsers, ServerOwners 39from public import public 40from zope.component import getUtility 41 42 43SLASH = '/' 44 45 46@public 47class Root: 48 """The RESTful root resource. 49 50 At the root of the tree are the API version numbers. Everything else 51 lives underneath those. Currently there is only one API version number, 52 and we start at 3.0 to match the Mailman version number. That may not 53 always be the case though. 54 """ 55 56 @child('3.0') 57 def api_version_30(self, context, segments): 58 # API version 3.0 was introduced in Mailman 3.0. 59 context['api'] = API30 60 return TopLevel() 61 62 @child('3.1') 63 def api_version_31(self, context, segments): 64 # API version 3.1 was introduced in Mailman 3.1. Primary backward 65 # incompatible difference is that uuids are represented as hex strings 66 # instead of 128 bit integers. The latter is not compatible with all 67 # versions of JavaScript. 68 context['api'] = API31 69 return TopLevel() 70 71 72@public 73class Versions: 74 def on_get(self, request, response): 75 """/<api>/system/versions""" 76 resource = dict( 77 mailman_version=system.mailman_version, 78 python_version=system.python_version, 79 api_version=self.api.version, 80 self_link=self.api.path_to('system/versions'), 81 ) 82 okay(response, etag(resource)) 83 84 85@public 86class SystemConfiguration: 87 def __init__(self, section=None): 88 self._section = section 89 90 def on_get(self, request, response): 91 if self._section is None: 92 resource = dict( 93 sections=sorted(section.name for section in config), 94 self_link=self.api.path_to('system/configuration'), 95 ) 96 okay(response, etag(resource)) 97 return 98 missing = object() 99 section = getattr(config, self._section, missing) 100 if section is missing: 101 not_found(response) 102 return 103 # Sections don't have .keys(), .values(), or .items() but we can 104 # iterate over them. 105 resource = {key: section[key] for key in section} 106 # Add a `self_link` attribute to the resource. This is a little ugly 107 # because technically speaking we're mixing namespaces. We can't have 108 # a variable named `self_link` in any section, but also we can't have 109 # `http_etag` either, so unless we want to shove all these values into 110 # a sub dictionary (which we don't), we have to live with it. 111 self_link = self.api.path_to( 112 'system/configuration/{}'.format(section.name)) 113 resource['self_link'] = self_link 114 okay(response, etag(resource)) 115 116 117@public 118class Pipelines: 119 def on_get(self, request, response): 120 resource = dict(pipelines=sorted(config.pipelines)) 121 okay(response, etag(resource)) 122 123 124@public 125class Chains: 126 def on_get(self, request, response): 127 resource = dict(chains=sorted(config.chains)) 128 okay(response, etag(resource)) 129 130 131@public 132class Reserved: 133 """Top level API for reserved operations. 134 135 Nothing under this resource should be considered part of the stable API. 136 The resources that appear here are purely for the support of external 137 non-production systems, such as testing infrastructures for cooperating 138 components. Use at your own risk. 139 """ 140 def __init__(self, segments): 141 self._resource_path = SLASH.join(segments) 142 143 def on_delete(self, request, response): 144 if self._resource_path != 'uids/orphans': 145 not_found(response) 146 return 147 UID.cull_orphans() 148 no_content(response) 149 150 151@public 152class TopLevel: 153 """Top level collections and entries.""" 154 155 @child() 156 def system(self, context, segments): 157 """/<api>/system""" 158 if len(segments) == 0: 159 # This provides backward compatibility; see /system/versions. 160 return Versions() 161 elif segments[0] == 'preferences': 162 if len(segments) > 1: 163 return BadRequest(), [] 164 return ReadOnlyPreferences(system_preferences, 'system'), [] 165 elif segments[0] == 'versions': 166 if len(segments) > 1: 167 return BadRequest(), [] 168 return Versions(), [] 169 elif segments[0] == 'configuration': 170 if len(segments) <= 2: 171 return SystemConfiguration(*segments[1:]), [] 172 return BadRequest(), [] 173 elif segments[0] == 'pipelines': 174 if len(segments) > 1: 175 return BadRequest(), [] 176 return Pipelines(), [] 177 elif segments[0] == 'chains': 178 if len(segments) > 1: 179 return BadRequest(), [] 180 return Chains(), [] 181 else: 182 return NotFound(), [] 183 184 @child() 185 def addresses(self, context, segments): 186 """/<api>/addresses 187 /<api>/addresses/<email> 188 """ 189 if len(segments) == 0: 190 return AllAddresses() 191 else: 192 email = segments.pop(0) 193 return AnAddress(email), segments 194 195 @child() 196 def domains(self, context, segments): 197 """/<api>/domains 198 /<api>/domains/<domain> 199 """ 200 if len(segments) == 0: 201 return AllDomains() 202 else: 203 domain = segments.pop(0) 204 return ADomain(domain), segments 205 206 @child() 207 def lists(self, context, segments): 208 """/<api>/lists 209 /<api>/lists/styles 210 /<api>/lists/<list> 211 /<api>/lists/<list>/... 212 """ 213 if len(segments) == 0: 214 return AllLists() 215 # This does not prevent a mailing list being created with a short name 216 # 'styles', since list identifiers (see below) must either be a 217 # List-Id like styles.example.com, or an fqdn_listname like 218 # styles@example.com. 219 elif len(segments) == 1 and segments[0] == 'styles': 220 return Styles(), [] 221 elif len(segments) == 1 and segments[0] == 'find': 222 return FindLists(), [] 223 else: 224 # list-id is preferred, but for backward compatibility, 225 # fqdn_listname is also accepted. 226 list_identifier = segments.pop(0) 227 return AList(list_identifier), segments 228 229 @child() 230 def members(self, context, segments): 231 """/<api>/members""" 232 if len(segments) == 0: 233 return AllMembers() 234 # Either the next segment is the string "find" or a member id. They 235 # cannot collide. 236 segment = segments.pop(0) 237 if segment == 'find': 238 resource = FindMembers() 239 else: 240 try: 241 member_id = self.api.to_uuid(segment) 242 except ValueError: 243 member_id = None 244 resource = AMember(member_id) 245 return resource, segments 246 247 @child() 248 def users(self, context, segments): 249 """/<api>/users""" 250 if len(segments) == 0: 251 return AllUsers() 252 else: 253 user_identifier = segments.pop(0) 254 return AUser(user_identifier), segments 255 256 @child() 257 def owners(self, context, segments): 258 """/<api>/owners""" 259 if len(segments) != 0: 260 return BadRequest(), [] 261 else: 262 return ServerOwners(), segments 263 264 @child() 265 def templates(self, context, segments): 266 """/<api>/templates/<fqdn_listname>/<template>/[<language>] 267 268 Use content negotiation to context language and suffix (content-type). 269 """ 270 # This resource is removed in API 3.1; use the /uris resource instead. 271 if self.api.version_info > (3, 0): 272 return NotFound(), [] 273 if len(segments) == 3: 274 fqdn_listname, template, language = segments 275 elif len(segments) == 2: 276 fqdn_listname, template = segments 277 language = 'en' 278 else: 279 return BadRequest(), [] 280 mlist = getUtility(IListManager).get(fqdn_listname) 281 if mlist is None: 282 return NotFound(), [] 283 # XXX dig out content-type from context. 284 content_type = None 285 return TemplateFinder( 286 fqdn_listname, template, language, content_type) 287 288 @child() 289 def uris(self, content, segments): 290 if self.api.version_info < (3, 1): 291 return NotFound(), [] 292 if len(segments) == 0: 293 return AllSiteURIs() 294 if len(segments) > 1: 295 return BadRequest(), [] 296 template = segments[0] 297 if template not in AllSiteURIs.URIs: 298 return NotFound(), [] 299 return ASiteURI(template), [] 300 301 @child() 302 def queues(self, context, segments): 303 """/<api>/queues[/<name>[/file]]""" 304 if len(segments) == 0: 305 return AllQueues() 306 elif len(segments) == 1: 307 return AQueue(segments[0]), [] 308 elif len(segments) == 2: 309 return AQueueFile(segments[0], segments[1]), [] 310 else: 311 return BadRequest(), [] 312 313 @child() 314 def plugins(self, context, segments): 315 """/<api>/plugins 316 /<api>/plugins/<plugin_name> 317 /<api>/plugins/<plugin_name>/... 318 """ 319 if self.api.version_info < (3, 1): 320 return NotFound(), [] 321 if len(segments) == 0: 322 return AllPlugins(), [] 323 else: 324 plugin_name = segments.pop(0) 325 return APlugin(plugin_name), segments 326 327 @child() 328 def bans(self, context, segments): 329 """/<api>/bans 330 /<api>/bans/<email> 331 """ 332 if len(segments) == 0: 333 return BannedEmails(None) 334 else: 335 email = segments.pop(0) 336 return BannedEmail(None, email), segments 337 338 @child() 339 def reserved(self, context, segments): 340 """/<api>/reserved/[...]""" 341 return Reserved(segments), [] 342