diff --git a/config b/config index 0925a54..dd7b385 100644 --- a/config +++ b/config @@ -116,6 +116,12 @@ #hook = +[web] + +# Web interface backend +#type = none + + [logging] # Logging configuration file diff --git a/radicale/__init__.py b/radicale/__init__.py index 4fff67e..ec835ed 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -50,7 +50,7 @@ from xml.etree import ElementTree as ET import vobject -from . import auth, rights, storage, xmlutils +from . import auth, rights, storage, web, xmlutils VERSION = "2.0.0" @@ -211,6 +211,7 @@ class Application: self.Auth = auth.load(configuration, logger) self.Collection = storage.load(configuration, logger) self.authorized = rights.load(configuration, logger) + self.web = web.load(configuration, logger) self.encoding = configuration.get("encoding", "request") def headers_log(self, environ): @@ -552,9 +553,18 @@ class Application: def do_GET(self, environ, base_prefix, path, user): """Manage GET request.""" - # Display a "Radicale works!" message if the root URL is requested + # Redirect to .web if the root URL is requested if not path.strip("/"): - return client.OK, {"Content-Type": "text/plain"}, "Radicale works!" + web_path = ".web" + if not path.endswith("/"): + web_path = posixpath.join(posixpath.basename(base_prefix), + web_path) + return (client.SEE_OTHER, + {"Location": web_path, "Content-Type": "text/plain"}, + "Redirected to %s" % web_path) + # Dispatch .web URL to web module + if path == "/.web" or path.startswith("/.web/"): + return self.web.get(environ, base_prefix, path, user) if not self._access(user, path, "r"): return NOT_ALLOWED with self.Collection.acquire_lock("r", user): diff --git a/radicale/config.py b/radicale/config.py index 12af82f..0010dea 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -147,6 +147,11 @@ INITIAL_CONFIG = OrderedDict([ "value": "", "help": "command that is run after changes to storage", "type": str})])), + ("web", OrderedDict([ + ("type", { + "value": "none", + "help": "web interface backend", + "type": str})])), ("logging", OrderedDict([ ("config", { "value": "", diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 3e9b8b6..b8ceb84 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -38,8 +38,8 @@ class BaseRequestsMixIn: def test_root(self): """GET request at "/".""" status, headers, answer = self.request("GET", "/") - assert status == 200 - assert "Radicale works!" in answer + assert status == 303 + assert answer == "Redirected to .web" # Test the creation of the collection self.request("MKCOL", "/calendar.ics/") self.request( @@ -48,6 +48,17 @@ class BaseRequestsMixIn: assert "BEGIN:VCALENDAR" in answer assert "END:VCALENDAR" in answer + def test_script_name(self): + """GET request at "/" with SCRIPT_NAME.""" + status, headers, answer = self.request( + "GET", "/", SCRIPT_NAME="/radicale") + assert status == 303 + assert answer == "Redirected to .web" + status, headers, answer = self.request( + "GET", "", SCRIPT_NAME="/radicale") + assert status == 303 + assert answer == "Redirected to radicale/.web" + def test_add_event(self): """Add an event.""" self.request("MKCOL", "/calendar.ics/") @@ -168,7 +179,7 @@ class BaseRequestsMixIn: def test_head(self): status, headers, answer = self.request("HEAD", "/") - assert status == 200 + assert status == 303 def test_options(self): status, headers, answer = self.request("OPTIONS", "/") @@ -815,7 +826,7 @@ class BaseRequestsMixIn: "storage", "hook", "mkdir %s" % os.path.join( "collection-root", "created_by_hook")) status, headers, answer = self.request("GET", "/") - assert status == 200 + assert status == 303 status, headers, answer = self.request("GET", "/created_by_hook/") assert status == 404 @@ -834,7 +845,7 @@ class BaseRequestsMixIn: "storage", "hook", "mkdir %s" % os.path.join( "collection-root", "created_by_hook")) status, headers, answer = self.request("GET", "/", REMOTE_USER="user") - assert status == 200 + assert status == 303 status, headers, answer = self.request("PROPFIND", "/created_by_hook/") assert status == 207 diff --git a/radicale/web.py b/radicale/web.py new file mode 100644 index 0000000..b2e8225 --- /dev/null +++ b/radicale/web.py @@ -0,0 +1,66 @@ +# This file is part of Radicale Server - Calendar Server +# Copyright (C) 2017 Unrud +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +from http import client +from importlib import import_module + +NOT_FOUND = ( + client.NOT_FOUND, (("Content-Type", "text/plain"),), + "The requested resource could not be found.") + +MIMETYPES = { + ".css": "text/css", + ".eot": "application/vnd.ms-fontobject", + ".gif": "image/gif", + ".html": "text/html", + ".js": "application/javascript", + ".manifest": "text/cache-manifest", + ".png": "image/png", + ".svg": "image/svg+xml", + ".ttf": "application/font-sfnt", + ".txt": "text/plain", + ".woff": "application/font-woff", + ".woff2": "font/woff2", + ".xml": "text/xml"} +FALLBACK_MIMETYPE = "application/octet-stream" + + +def load(configuration, logger): + """Load the web module chosen in configuration.""" + web_type = configuration.get("web", "type") + if web_type in ("None", "none"): # DEPRECATED: use "none" + web_class = NoneWeb + else: + try: + web_class = import_module(web_type).Web + except ImportError as e: + raise RuntimeError("Web module %r not found" % + web_type) from e + logger.info("Web type is %r", web_type) + return web_class(configuration, logger) + + +class BaseWeb: + def __init__(self, configuration, logger): + self.configuration = configuration + self.logger = logger + + +class NoneWeb(BaseWeb): + def get(self, environ, base_prefix, path, user): + if path != "/.web": + return NOT_FOUND + return client.OK, {"Content-Type": "text/plain"}, "Radicale works!"