2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2017-05-27 17:28:07 +02:00
# Copyright © 2008-2017 Guillaume Ayoub
2009-07-27 15:04:54 +00:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
2024-06-07 08:36:05 +02:00
# Copyright © 2017-2020 Unrud <unrud@outlook.com>
2025-01-14 08:57:15 +01:00
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
2008-12-30 16:25:42 +00:00
#
# 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 <http://www.gnu.org/licenses/>.
2009-07-27 15:04:54 +00:00
"""
2020-01-12 23:32:28 +01:00
Configuration module
2009-07-27 15:04:54 +00:00
2020-01-12 23:32:28 +01:00
Use ` ` load ( ) ` ` to obtain an instance of ` ` Configuration ` ` for use with
` ` radicale . app . Application ` ` .
2010-02-10 18:57:21 +01:00
2009-07-27 15:04:54 +00:00
"""
2020-02-19 09:50:25 +01:00
import contextlib
2024-04-22 12:23:24 +03:00
import json
2017-06-02 12:42:19 +02:00
import math
2010-01-18 10:48:06 +01:00
import os
2020-05-19 07:03:58 +02:00
import string
2021-07-26 20:56:46 +02:00
import sys
2016-10-11 18:17:01 +02:00
from collections import OrderedDict
2019-06-17 04:13:25 +02:00
from configparser import RawConfigParser
2021-07-26 20:56:46 +02:00
from typing import ( Any , Callable , ClassVar , Iterable , List , Optional ,
Sequence , Tuple , TypeVar , Union )
2008-12-30 16:25:42 +00:00
2023-02-10 23:32:46 +01:00
from radicale import auth , hook , rights , storage , types , web
2024-05-03 23:07:04 +03:00
from radicale . item import check_and_sanitize_props
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
DEFAULT_CONFIG_PATH : str = os . pathsep . join ( [
2019-06-17 04:13:25 +02:00
" ?/etc/radicale/config " ,
" ?~/.config/radicale/config " ] )
2017-06-21 09:48:57 +02:00
2017-06-02 12:42:19 +02:00
2021-07-26 20:56:46 +02:00
def positive_int ( value : Any ) - > int :
2017-06-02 12:42:19 +02:00
value = int ( value )
if value < 0 :
raise ValueError ( " value is negative: %d " % value )
return value
2021-07-26 20:56:46 +02:00
def positive_float ( value : Any ) - > float :
2017-06-02 12:42:19 +02:00
value = float ( value )
if not math . isfinite ( value ) :
raise ValueError ( " value is infinite " )
if math . isnan ( value ) :
raise ValueError ( " value is not a number " )
if value < 0 :
raise ValueError ( " value is negative: %f " % value )
return value
2021-07-26 20:56:46 +02:00
def logging_level ( value : Any ) - > str :
2018-08-16 08:00:02 +02:00
if value not in ( " debug " , " info " , " warning " , " error " , " critical " ) :
2019-06-17 04:13:25 +02:00
raise ValueError ( " unsupported level: %r " % value )
2018-08-16 08:00:02 +02:00
return value
2021-07-26 20:56:46 +02:00
def filepath ( value : Any ) - > str :
2019-06-17 04:13:25 +02:00
if not value :
return " "
value = os . path . expanduser ( value )
2021-07-26 20:56:46 +02:00
if sys . platform == " win32 " :
2019-06-17 04:13:25 +02:00
value = os . path . expandvars ( value )
return os . path . abspath ( value )
2021-07-26 20:56:46 +02:00
def list_of_ip_address ( value : Any ) - > List [ Tuple [ str , int ] ] :
2019-06-17 04:13:25 +02:00
def ip_address ( value ) :
try :
2020-05-19 06:46:07 +02:00
address , port = value . rsplit ( " : " , 1 )
2020-05-19 07:03:58 +02:00
return address . strip ( string . whitespace + " [] " ) , int ( port )
2019-06-17 04:13:25 +02:00
except ValueError :
raise ValueError ( " malformed IP address: %r " % value )
2020-05-19 06:46:07 +02:00
return [ ip_address ( s ) for s in value . split ( " , " ) ]
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def str_or_callable ( value : Any ) - > Union [ str , Callable ] :
2020-01-15 03:20:48 +01:00
if callable ( value ) :
return value
return str ( value )
2021-07-26 20:56:46 +02:00
def unspecified_type ( value : Any ) - > Any :
2020-02-19 09:50:30 +01:00
return value
2021-07-26 20:56:46 +02:00
def _convert_to_bool ( value : Any ) - > bool :
2019-06-17 04:13:25 +02:00
if value . lower ( ) not in RawConfigParser . BOOLEAN_STATES :
2020-09-26 22:08:23 +02:00
raise ValueError ( " not a boolean: %r " % value )
2019-06-17 04:13:25 +02:00
return RawConfigParser . BOOLEAN_STATES [ value . lower ( ) ]
2025-01-16 06:02:06 +01:00
def imap_address ( value ) :
if " ] " in value :
pre_address , pre_address_port = value . rsplit ( " ] " , 1 )
else :
pre_address , pre_address_port = " " , value
if " : " in pre_address_port :
pre_address2 , port = pre_address_port . rsplit ( " : " , 1 )
address = pre_address + pre_address2
else :
address , port = pre_address + pre_address_port , None
try :
return ( address . strip ( string . whitespace + " [] " ) ,
None if port is None else int ( port ) )
except ValueError :
raise ValueError ( " malformed IMAP address: %r " % value )
def imap_security ( value ) :
if value not in ( " tls " , " starttls " , " none " ) :
raise ValueError ( " unsupported IMAP security: %r " % value )
return value
2024-04-22 12:23:24 +03:00
def json_str ( value : Any ) - > dict :
if not value :
return { }
2024-05-03 23:07:04 +03:00
ret = json . loads ( value )
for ( name_coll , props ) in ret . items ( ) :
checked_props = check_and_sanitize_props ( props )
ret [ name_coll ] = checked_props
return ret
2024-04-22 12:23:24 +03:00
2021-07-26 20:56:46 +02:00
INTERNAL_OPTIONS : Sequence [ str ] = ( " _allow_extra " , )
2010-02-10 18:57:21 +01:00
# Default configuration
2021-07-26 20:56:46 +02:00
DEFAULT_CONFIG_SCHEMA : types . CONFIG_SCHEMA = OrderedDict ( [
2016-10-11 18:17:01 +02:00
( " server " , OrderedDict ( [
2016-10-12 14:30:18 +02:00
( " hosts " , {
2020-02-19 09:49:56 +01:00
" value " : " localhost:5232 " ,
2016-10-12 14:30:18 +02:00
" help " : " set server hostnames including ports " ,
2020-10-04 05:38:58 +02:00
" aliases " : ( " -H " , " --hosts " , ) ,
2019-06-17 04:13:25 +02:00
" type " : list_of_ip_address } ) ,
2016-10-12 14:30:18 +02:00
( " max_connections " , {
2018-08-28 16:19:48 +02:00
" value " : " 8 " ,
2017-05-31 11:08:32 +02:00
" help " : " maximum number of parallel connections " ,
2017-06-02 12:42:19 +02:00
" type " : positive_int } ) ,
2016-10-12 14:30:18 +02:00
( " max_content_length " , {
2018-04-29 21:20:23 +02:00
" value " : " 100000000 " ,
2017-05-31 11:08:32 +02:00
" help " : " maximum size of request body in bytes " ,
2017-06-02 12:42:19 +02:00
" type " : positive_int } ) ,
2016-10-12 14:30:18 +02:00
( " timeout " , {
2018-04-29 21:20:23 +02:00
" value " : " 30 " ,
2017-05-31 11:08:32 +02:00
" help " : " socket timeout " ,
2020-10-25 20:27:26 +01:00
" type " : positive_float } ) ,
2016-10-12 14:30:18 +02:00
( " ssl " , {
" value " : " False " ,
" help " : " use SSL connection " ,
2020-10-04 05:38:58 +02:00
" aliases " : ( " -s " , " --ssl " , ) ,
2021-11-10 22:16:30 +01:00
" opposite_aliases " : ( " -S " , " --no-ssl " , ) ,
2017-05-31 11:08:32 +02:00
" type " : bool } ) ,
2024-11-13 22:19:44 +01:00
( " protocol " , {
" value " : " " ,
" help " : " SSL/TLS protocol (Apache SSLProtocol format) " ,
" type " : str } ) ,
( " ciphersuite " , {
" value " : " " ,
" help " : " SSL/TLS Cipher Suite (OpenSSL cipher list format) " ,
" type " : str } ) ,
2016-10-12 14:30:18 +02:00
( " certificate " , {
2017-03-04 14:06:09 +01:00
" value " : " /etc/ssl/radicale.cert.pem " ,
2016-10-12 14:30:18 +02:00
" help " : " set certificate file " ,
2020-10-04 05:38:58 +02:00
" aliases " : ( " -c " , " --certificate " , ) ,
2019-06-17 04:13:25 +02:00
" type " : filepath } ) ,
2016-10-12 14:30:18 +02:00
( " key " , {
2017-03-04 14:06:09 +01:00
" value " : " /etc/ssl/radicale.key.pem " ,
2016-10-12 14:30:18 +02:00
" help " : " set private key file " ,
2020-10-04 05:38:58 +02:00
" aliases " : ( " -k " , " --key " , ) ,
2019-06-17 04:13:25 +02:00
" type " : filepath } ) ,
2017-06-02 12:41:03 +02:00
( " certificate_authority " , {
" value " : " " ,
" help " : " set CA certificate for validating clients " ,
2020-10-04 05:38:58 +02:00
" aliases " : ( " --certificate-authority " , ) ,
2020-02-19 09:50:36 +01:00
" type " : filepath } ) ,
2025-03-02 09:02:10 +01:00
( " script_name " , {
" value " : " " ,
" help " : " script name to strip from URI if called by reverse proxy (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME) " ,
" type " : str } ) ,
2020-02-19 09:50:36 +01:00
( " _internal_server " , {
" value " : " False " ,
" help " : " the internal server is used " ,
" type " : bool } ) ] ) ) ,
2016-10-11 18:17:01 +02:00
( " encoding " , OrderedDict ( [
2016-10-12 14:30:18 +02:00
( " request " , {
" value " : " utf-8 " ,
2017-05-31 11:08:32 +02:00
" help " : " encoding for responding requests " ,
" type " : str } ) ,
2016-10-12 14:30:18 +02:00
( " stock " , {
" value " : " utf-8 " ,
2017-05-31 11:08:32 +02:00
" help " : " encoding for storing local collections " ,
" type " : str } ) ] ) ) ,
2016-10-11 18:17:01 +02:00
( " auth " , OrderedDict ( [
2016-10-12 14:30:18 +02:00
( " type " , {
2025-03-15 14:34:51 +01:00
" value " : " denyall " ,
2025-03-01 13:16:57 +01:00
" help " : " authentication method ( " + " | " . join ( auth . INTERNAL_TYPES ) + " ) " ,
2020-01-15 03:20:48 +01:00
" type " : str_or_callable ,
2017-06-21 09:48:57 +02:00
" internal " : auth . INTERNAL_TYPES } ) ,
2024-12-30 08:16:45 +01:00
( " cache_logins " , {
" value " : " false " ,
2024-12-31 07:57:54 +01:00
" help " : " cache successful/failed logins for until expiration time " ,
2025-03-24 20:10:10 +01:00
" type " : bool } ) ,
2024-12-31 07:57:54 +01:00
( " cache_successful_logins_expiry " , {
2024-12-31 16:13:05 +01:00
" value " : " 15 " ,
2024-12-30 08:16:45 +01:00
" help " : " expiration time for caching successful logins in seconds " ,
" type " : int } ) ,
2024-12-31 07:57:54 +01:00
( " cache_failed_logins_expiry " , {
2024-12-31 16:13:05 +01:00
" value " : " 90 " ,
2024-12-31 07:57:54 +01:00
" help " : " expiration time for caching failed logins in seconds " ,
" type " : int } ) ,
2016-10-12 14:30:18 +02:00
( " htpasswd_filename " , {
" value " : " /etc/radicale/users " ,
2017-05-31 11:08:32 +02:00
" help " : " htpasswd filename " ,
2019-06-17 04:13:25 +02:00
" type " : filepath } ) ,
2016-10-12 14:30:18 +02:00
( " htpasswd_encryption " , {
2024-09-01 17:19:53 +02:00
" value " : " autodetect " ,
2017-05-31 11:08:32 +02:00
" help " : " htpasswd encryption method " ,
" type " : str } ) ,
2025-01-01 16:31:31 +01:00
( " htpasswd_cache " , {
" value " : " False " ,
" help " : " enable caching of htpasswd file " ,
" type " : bool } ) ,
2025-01-13 23:10:18 -08:00
( " dovecot_connection_type " , {
" value " : " AF_UNIX " ,
" help " : " Connection type for dovecot authentication " ,
" type " : str_or_callable ,
" internal " : auth . AUTH_SOCKET_FAMILY } ) ,
2024-10-30 10:33:10 -10:00
( " dovecot_socket " , {
" value " : " /var/run/dovecot/auth-client " ,
2025-01-13 23:10:18 -08:00
" help " : " dovecot auth AF_UNIX socket " ,
" type " : str } ) ,
( " dovecot_host " , {
2025-01-14 08:57:15 +01:00
" value " : " localhost " ,
2025-01-13 23:10:18 -08:00
" help " : " dovecot auth AF_INET or AF_INET6 host " ,
2024-10-30 10:33:10 -10:00
" type " : str } ) ,
2025-01-13 23:10:18 -08:00
( " dovecot_port " , {
" value " : " 12345 " ,
" help " : " dovecot auth port " ,
" type " : int } ) ,
2018-08-16 08:00:01 +02:00
( " realm " , {
" value " : " Radicale - Password Required " ,
" help " : " message displayed when a password is needed " ,
" type " : str } ) ,
2017-05-23 03:11:41 +02:00
( " delay " , {
" value " : " 1 " ,
2017-05-31 11:08:32 +02:00
" help " : " incorrect authentication delay " ,
2022-02-19 11:57:58 +01:00
" type " : positive_float } ) ,
2025-03-24 20:09:35 +01:00
( " ldap_ignore_attribute_create_modify_timestamp " , {
2025-03-23 18:08:00 +01:00
" value " : " false " ,
2025-03-24 20:09:35 +01:00
" help " : " Ignore modifyTimestamp and createTimestamp attributes. Need if Authentik LDAP server is used. " ,
2025-03-23 18:08:00 +01:00
" type " : bool } ) ,
2022-02-19 11:57:58 +01:00
( " ldap_uri " , {
" value " : " ldap://localhost " ,
" help " : " URI to the ldap server " ,
" type " : str } ) ,
( " ldap_base " , {
2024-07-30 00:22:07 -07:00
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " The base DN of the ldap server where the user can be find. " ,
2022-02-19 11:57:58 +01:00
" type " : str } ) ,
( " ldap_reader_dn " , {
2024-07-30 00:22:07 -07:00
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " The DN of a ldap user with read access to get the user accounts " ,
2022-02-19 11:57:58 +01:00
" type " : str } ) ,
( " ldap_secret " , {
2024-07-30 00:22:07 -07:00
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " The password of the ldap_reader_dn " ,
2022-02-19 11:57:58 +01:00
" type " : str } ) ,
2024-07-30 00:22:07 -07:00
( " ldap_secret_file " , {
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " Path of the file containing the password of the ldap_reader_dn " ,
2024-07-30 00:22:07 -07:00
" type " : str } ) ,
2022-02-19 11:57:58 +01:00
( " ldap_filter " , {
" value " : " (cn= {0} ) " ,
2025-05-30 17:04:52 +02:00
" help " : " The search filter to find the user DN to authenticate by the username " ,
2022-02-19 11:57:58 +01:00
" type " : str } ) ,
2024-12-29 08:05:42 +01:00
( " ldap_user_attribute " , {
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " The attribute to be used as username after authentication " ,
2024-12-29 08:05:42 +01:00
" type " : str } ) ,
2025-01-01 20:41:55 +01:00
( " ldap_groups_attribute " , {
2025-01-01 20:52:55 +01:00
" value " : " " ,
2025-05-30 17:04:52 +02:00
" help " : " Attribute to read the group memberships from. Valid values are memberOf, member or uniqueMember. If no value is given group memebership will be ignored. " ,
" type " : str } ) ,
( " ldap_groups_base_dn " , {
" value " : " " ,
" help " : " The base dn to find the groups. Necessary only if ldap_groups attribute is member or uniqueMember. If not given ldap_base will be used. " ,
2025-01-01 20:41:55 +01:00
" type " : str } ) ,
2024-09-23 10:19:50 +02:00
( " ldap_use_ssl " , {
" value " : " False " ,
2025-04-21 21:26:58 +02:00
" help " : " Use ssl on the ldap connection. Soon to be deprecated, use ldap_security instead " ,
2024-09-23 10:19:50 +02:00
" type " : bool } ) ,
2025-04-21 21:26:58 +02:00
( " ldap_security " , {
2025-04-19 17:02:45 +02:00
" value " : " none " ,
2025-04-21 21:26:58 +02:00
" help " : " the encryption mode to be used: *none*|tls|starttls " ,
2025-04-19 17:02:45 +02:00
" type " : str } ) ,
2024-09-23 10:19:50 +02:00
( " ldap_ssl_verify_mode " , {
" value " : " REQUIRED " ,
2025-04-21 21:26:58 +02:00
" help " : " The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED " ,
2024-09-23 10:19:50 +02:00
" type " : str } ) ,
( " ldap_ssl_ca_file " , {
" value " : " " ,
" help " : " The path to the CA file in pem format which is used to certificate the server certificate " ,
" type " : str } ) ,
2025-01-16 06:02:06 +01:00
( " imap_host " , {
" value " : " localhost " ,
" help " : " IMAP server hostname: address|address:port|[address]:port|*localhost* " ,
" type " : imap_address } ) ,
( " imap_security " , {
" value " : " tls " ,
" help " : " Secure the IMAP connection: *tls*|starttls|none " ,
" type " : imap_security } ) ,
2025-02-02 09:03:42 +01:00
( " oauth2_token_endpoint " , {
" value " : " " ,
" help " : " OAuth2 token endpoint URL " ,
" type " : str } ) ,
2025-02-22 17:50:24 +01:00
( " pam_group_membership " , {
" value " : " " ,
" help " : " PAM group user should be member of " ,
" type " : str } ) ,
( " pam_service " , {
" value " : " radicale " ,
" help " : " PAM service " ,
" type " : str } ) ,
2024-07-18 06:50:29 +02:00
( " strip_domain " , {
" value " : " False " ,
" help " : " strip domain from username " ,
" type " : bool } ) ,
2024-12-14 09:25:36 +01:00
( " uc_username " , {
" value " : " False " ,
" help " : " convert username to uppercase, must be true for case-insensitive auth providers " ,
" type " : bool } ) ,
2024-04-17 18:31:51 +03:00
( " lc_username " , {
" value " : " False " ,
" help " : " convert username to lowercase, must be true for case-insensitive auth providers " ,
2025-05-02 18:02:43 -04:00
" type " : bool } ) ,
( " urldecode_username " , {
" value " : " False " ,
" help " : " url-decode the username, set to True when clients send url-encoded email address as username " ,
2024-04-17 18:31:51 +03:00
" type " : bool } ) ] ) ) ,
2016-10-11 18:17:01 +02:00
( " rights " , OrderedDict ( [
2016-10-12 14:30:18 +02:00
( " type " , {
2017-03-04 14:06:09 +01:00
" value " : " owner_only " ,
2017-05-31 11:08:32 +02:00
" help " : " rights backend " ,
2020-01-15 03:20:48 +01:00
" type " : str_or_callable ,
2017-06-21 09:48:57 +02:00
" internal " : rights . INTERNAL_TYPES } ) ,
2024-03-09 06:43:39 +01:00
( " permit_delete_collection " , {
" value " : " True " ,
" help " : " permit delete of a collection " ,
" type " : bool } ) ,
2024-09-29 18:15:42 +02:00
( " permit_overwrite_collection " , {
2024-09-30 21:43:50 +02:00
" value " : " True " ,
2024-09-29 18:15:42 +02:00
" help " : " permit overwrite of a collection " ,
" type " : bool } ) ,
2016-10-12 14:30:18 +02:00
( " file " , {
2017-03-04 14:06:09 +01:00
" value " : " /etc/radicale/rights " ,
2017-05-31 11:08:32 +02:00
" help " : " file for rights management from_file " ,
2019-06-17 04:13:25 +02:00
" type " : filepath } ) ] ) ) ,
2016-10-11 18:17:01 +02:00
( " storage " , OrderedDict ( [
2016-10-12 14:30:18 +02:00
( " type " , {
" value " : " multifilesystem " ,
2017-05-31 11:08:32 +02:00
" help " : " storage backend " ,
2020-01-15 03:20:48 +01:00
" type " : str_or_callable ,
2017-06-21 09:48:57 +02:00
" internal " : storage . INTERNAL_TYPES } ) ,
2016-10-12 14:30:18 +02:00
( " filesystem_folder " , {
2019-06-17 04:13:25 +02:00
" value " : " /var/lib/radicale/collections " ,
2017-05-31 11:08:32 +02:00
" help " : " path where collections are stored " ,
2019-06-17 04:13:25 +02:00
" type " : filepath } ) ,
2024-12-10 08:23:32 +01:00
( " filesystem_cache_folder " , {
2024-12-10 08:52:51 +01:00
" value " : " " ,
2024-12-10 08:23:32 +01:00
" help " : " path where cache of collections is stored in case of use_cache_subfolder_* options are active " ,
" type " : filepath } ) ,
2024-12-03 21:31:57 +01:00
( " use_cache_subfolder_for_item " , {
" value " : " False " ,
2024-12-10 08:23:32 +01:00
" help " : " use subfolder ' collection-cache ' for ' item ' cache file structure instead of inside collection folder " ,
" type " : bool } ) ,
( " use_cache_subfolder_for_history " , {
" value " : " False " ,
" help " : " use subfolder ' collection-cache ' for ' history ' cache file structure instead of inside collection folder " ,
2024-12-03 21:31:57 +01:00
" type " : bool } ) ,
2024-12-10 08:23:32 +01:00
( " use_cache_subfolder_for_synctoken " , {
" value " : " False " ,
" help " : " use subfolder ' collection-cache ' for ' sync-token ' cache file structure instead of inside collection folder " ,
" type " : bool } ) ,
2024-12-15 11:40:02 +01:00
( " use_mtime_and_size_for_item_cache " , {
" value " : " False " ,
" help " : " use mtime and file size instead of SHA256 for ' item ' cache (improves speed) " ,
" type " : bool } ) ,
2024-12-10 08:23:32 +01:00
( " folder_umask " , {
" value " : " " ,
" help " : " umask for folder creation (empty: system default) " ,
" type " : str } ) ,
2017-06-02 12:44:39 +02:00
( " max_sync_token_age " , {
2018-09-09 14:58:43 +02:00
" value " : " 2592000 " , # 30 days
2017-06-02 12:44:39 +02:00
" help " : " delete sync token that are older " ,
2019-06-17 04:13:25 +02:00
" type " : positive_int } ) ,
2024-06-09 13:57:32 +02:00
( " skip_broken_item " , {
2024-06-18 17:42:49 +02:00
" value " : " True " ,
2024-06-09 13:57:32 +02:00
" help " : " skip broken item instead of triggering exception " ,
" type " : bool } ) ,
2016-10-12 14:30:18 +02:00
( " hook " , {
" value " : " " ,
2017-05-31 11:08:32 +02:00
" help " : " command that is run after changes to storage " ,
2020-02-19 09:50:36 +01:00
" type " : str } ) ,
( " _filesystem_fsync " , {
" value " : " True " ,
" help " : " sync all changes to filesystem during requests " ,
2024-04-22 12:23:24 +03:00
" type " : bool } ) ,
( " predefined_collections " , {
" value " : " " ,
" help " : " predefined user collections " ,
" type " : json_str } ) ] ) ) ,
2020-08-17 02:05:02 +02:00
( " hook " , OrderedDict ( [
( " type " , {
" value " : " none " ,
" help " : " hook backend " ,
" type " : str ,
" internal " : hook . INTERNAL_TYPES } ) ,
( " rabbitmq_endpoint " , {
" value " : " " ,
" help " : " endpoint where rabbitmq server is running " ,
" type " : str } ) ,
( " rabbitmq_topic " , {
" value " : " " ,
" help " : " topic to declare queue " ,
2024-03-02 13:38:42 +01:00
" type " : str } ) ,
( " rabbitmq_queue_type " , {
" value " : " " ,
" help " : " queue type for topic declaration " ,
2020-08-17 02:05:02 +02:00
" type " : str } ) ] ) ) ,
2017-05-31 13:18:40 +02:00
( " web " , OrderedDict ( [
( " type " , {
2017-05-31 13:18:42 +02:00
" value " : " internal " ,
2017-05-31 13:18:40 +02:00
" help " : " web interface backend " ,
2020-01-15 03:20:48 +01:00
" type " : str_or_callable ,
2017-06-21 09:48:57 +02:00
" internal " : web . INTERNAL_TYPES } ) ] ) ) ,
2016-10-11 18:17:01 +02:00
( " logging " , OrderedDict ( [
2018-08-16 08:00:02 +02:00
( " level " , {
2024-03-22 07:15:20 +01:00
" value " : " info " ,
2018-08-16 08:00:02 +02:00
" help " : " threshold for the logger " ,
" type " : logging_level } ) ,
2024-05-29 06:07:36 +02:00
( " bad_put_request_content " , {
" value " : " False " ,
" help " : " log bad PUT request content " ,
" type " : bool } ) ,
2024-06-09 13:42:08 +02:00
( " backtrace_on_debug " , {
2024-06-18 17:43:35 +02:00
" value " : " False " ,
2024-06-09 13:42:08 +02:00
" help " : " log backtrace on level=debug " ,
" type " : bool } ) ,
2024-06-11 13:23:03 +02:00
( " request_header_on_debug " , {
2024-06-18 17:43:35 +02:00
" value " : " False " ,
2024-06-11 13:23:03 +02:00
" help " : " log request header on level=debug " ,
" type " : bool } ) ,
( " request_content_on_debug " , {
2024-06-18 17:43:35 +02:00
" value " : " False " ,
2024-06-11 13:23:03 +02:00
" help " : " log request content on level=debug " ,
" type " : bool } ) ,
( " response_content_on_debug " , {
2024-06-18 17:43:35 +02:00
" value " : " False " ,
2024-06-11 13:23:03 +02:00
" help " : " log response content on level=debug " ,
" type " : bool } ) ,
2024-08-28 08:59:32 +02:00
( " rights_rule_doesnt_match_on_debug " , {
2024-08-28 08:03:16 +02:00
" value " : " False " ,
2024-08-28 08:59:32 +02:00
" help " : " log rights rules which doesn ' t match on level=debug " ,
2024-08-28 08:03:16 +02:00
" type " : bool } ) ,
2024-12-15 12:21:39 +01:00
( " storage_cache_actions_on_debug " , {
2024-12-14 16:49:54 +01:00
" value " : " False " ,
" help " : " log storage cache action on level=debug " ,
" type " : bool } ) ,
2016-10-12 14:30:18 +02:00
( " mask_passwords " , {
" value " : " True " ,
2017-05-31 11:08:32 +02:00
" help " : " mask passwords in logs " ,
2019-06-17 04:13:25 +02:00
" type " : bool } ) ] ) ) ,
( " headers " , OrderedDict ( [
2023-10-11 12:09:11 -06:00
( " _allow_extra " , str ) ] ) ) ,
( " reporting " , OrderedDict ( [
( " max_freebusy_occurrence " , {
" value " : " 10000 " ,
" help " : " number of occurrences per event when reporting " ,
" type " : positive_int } ) ] ) )
] )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def parse_compound_paths ( * compound_paths : Optional [ str ]
) - > List [ Tuple [ str , bool ] ] :
2019-06-17 04:13:25 +02:00
""" Parse a compound path and return the individual paths.
Paths in a compound path are joined by ` ` os . pathsep ` ` . If a path starts
with ` ` ? ` ` the return value ` ` IGNORE_IF_MISSING ` ` is set .
When multiple ` ` compound_paths ` ` are passed , the last argument that is
not ` ` None ` ` is used .
Returns a dict of the format ` ` [ ( PATH , IGNORE_IF_MISSING ) , . . . ] ` `
"""
compound_path = " "
for p in compound_paths :
if p is not None :
compound_path = p
paths = [ ]
for path in compound_path . split ( os . pathsep ) :
ignore_if_missing = path . startswith ( " ? " )
if ignore_if_missing :
path = path [ 1 : ]
path = filepath ( path )
if path :
paths . append ( ( path , ignore_if_missing ) )
return paths
2021-07-26 20:56:46 +02:00
def load ( paths : Optional [ Iterable [ Tuple [ str , bool ] ] ] = None
) - > " Configuration " :
2020-01-12 23:32:28 +01:00
"""
Create instance of ` ` Configuration ` ` for use with
` ` radicale . app . Application ` ` .
` ` paths ` ` a list of configuration files with the format
` ` [ ( PATH , IGNORE_IF_MISSING ) , . . . ] ` ` .
If a configuration file is missing and IGNORE_IF_MISSING is set , the
config is set to ` ` Configuration . SOURCE_MISSING ` ` .
2019-06-17 04:13:25 +02:00
2020-01-12 23:32:28 +01:00
The configuration can later be changed with ` ` Configuration . update ( ) ` ` .
2019-06-17 04:13:25 +02:00
"""
2021-07-26 20:56:46 +02:00
if paths is None :
paths = [ ]
2019-06-17 04:13:25 +02:00
configuration = Configuration ( DEFAULT_CONFIG_SCHEMA )
for path , ignore_if_missing in paths :
parser = RawConfigParser ( )
config_source = " config file %r " % path
2022-01-11 20:22:19 +02:00
config : types . CONFIG
2019-06-17 04:13:25 +02:00
try :
2024-08-06 13:23:44 +02:00
with open ( path ) as f :
2022-01-11 20:22:19 +02:00
parser . read_file ( f )
2019-06-17 04:13:25 +02:00
config = { s : { o : parser [ s ] [ o ] for o in parser . options ( s ) }
for s in parser . sections ( ) }
except Exception as e :
2023-02-10 23:32:46 +01:00
if not ( ignore_if_missing and isinstance ( e , (
FileNotFoundError , NotADirectoryError , PermissionError ) ) ) :
2022-01-11 20:22:19 +02:00
raise RuntimeError ( " Failed to load %s : %s " % ( config_source , e )
) from e
2022-01-15 22:32:36 +01:00
config = Configuration . SOURCE_MISSING
2020-01-12 23:32:26 +01:00
configuration . update ( config , config_source )
2019-06-17 04:13:25 +02:00
return configuration
2021-07-26 20:56:46 +02:00
_Self = TypeVar ( " _Self " , bound = " Configuration " )
2019-06-17 04:13:25 +02:00
class Configuration :
2021-07-26 20:56:46 +02:00
SOURCE_MISSING : ClassVar [ types . CONFIG ] = { }
_schema : types . CONFIG_SCHEMA
_values : types . MUTABLE_CONFIG
_configs : List [ Tuple [ types . CONFIG , str , bool ] ]
def __init__ ( self , schema : types . CONFIG_SCHEMA ) - > None :
2019-06-17 04:13:25 +02:00
""" Initialize configuration.
` ` schema ` ` a dict that describes the configuration format .
See ` ` DEFAULT_CONFIG_SCHEMA ` ` .
2020-01-13 15:51:10 +01:00
The content of ` ` schema ` ` must not change afterwards , it is kept
as an internal reference .
2019-06-17 04:13:25 +02:00
2020-01-12 23:32:28 +01:00
Use ` ` load ( ) ` ` to create an instance for use with
` ` radicale . app . Application ` ` .
2019-06-17 04:13:25 +02:00
"""
self . _schema = schema
self . _values = { }
self . _configs = [ ]
2020-02-19 09:50:27 +01:00
default = { section : { option : self . _schema [ section ] [ option ] [ " value " ]
for option in self . _schema [ section ]
if option not in INTERNAL_OPTIONS }
for section in self . _schema }
self . update ( default , " default config " , privileged = True )
2021-07-26 20:56:46 +02:00
def update ( self , config : types . CONFIG , source : Optional [ str ] = None ,
privileged : bool = False ) - > None :
2019-06-17 04:13:25 +02:00
""" Update the configuration.
` ` config ` ` a dict of the format { SECTION : { OPTION : VALUE , . . . } , . . . } .
2020-01-12 23:32:28 +01:00
The configuration is checked for errors according to the config schema .
2020-01-13 15:51:10 +01:00
The content of ` ` config ` ` must not change afterwards , it is kept
as an internal reference .
2019-06-17 04:13:25 +02:00
2020-01-12 23:32:28 +01:00
` ` source ` ` a description of the configuration source ( used in error
messages ) .
2019-06-17 04:13:25 +02:00
2020-02-19 09:50:27 +01:00
` ` privileged ` ` allows updating sections and options starting with " _ " .
2019-06-17 04:13:25 +02:00
"""
2021-07-26 20:56:46 +02:00
if source is None :
source = " unspecified config "
new_values : types . MUTABLE_CONFIG = { }
2019-06-17 04:13:25 +02:00
for section in config :
2020-02-19 09:50:27 +01:00
if ( section not in self . _schema or
section . startswith ( " _ " ) and not privileged ) :
raise ValueError (
2019-06-17 04:13:25 +02:00
" Invalid section %r in %s " % ( section , source ) )
new_values [ section ] = { }
2020-02-19 09:50:27 +01:00
extra_type = None
2020-02-19 09:50:30 +01:00
extra_type = self . _schema [ section ] . get ( " _allow_extra " )
2020-02-19 09:50:27 +01:00
if " type " in self . _schema [ section ] :
2019-06-17 04:13:25 +02:00
if " type " in config [ section ] :
2020-02-19 09:50:27 +01:00
plugin = config [ section ] [ " type " ]
2019-06-17 04:13:25 +02:00
else :
2020-02-19 09:50:27 +01:00
plugin = self . get ( section , " type " )
if plugin not in self . _schema [ section ] [ " type " ] [ " internal " ] :
2020-02-19 09:50:30 +01:00
extra_type = unspecified_type
2019-06-17 04:13:25 +02:00
for option in config [ section ] :
2020-02-19 09:50:27 +01:00
type_ = extra_type
2019-06-17 04:13:25 +02:00
if option in self . _schema [ section ] :
type_ = self . _schema [ section ] [ option ] [ " type " ]
2020-02-19 09:50:27 +01:00
if ( not type_ or option in INTERNAL_OPTIONS or
option . startswith ( " _ " ) and not privileged ) :
2019-06-17 04:13:25 +02:00
raise RuntimeError ( " Invalid option %r in section %r in "
" %s " % ( option , section , source ) )
raw_value = config [ section ] [ option ]
try :
2020-01-17 12:45:01 +01:00
if type_ == bool and not isinstance ( raw_value , bool ) :
2019-06-17 04:13:25 +02:00
raw_value = _convert_to_bool ( raw_value )
new_values [ section ] [ option ] = type_ ( raw_value )
except Exception as e :
raise RuntimeError (
" Invalid %s value for option %r in section %r in %s : "
" %r " % ( type_ . __name__ , option , section , source ,
raw_value ) ) from e
2020-02-19 09:50:27 +01:00
self . _configs . append ( ( config , source , bool ( privileged ) ) )
2019-06-17 04:13:25 +02:00
for section in new_values :
2020-02-19 09:50:27 +01:00
self . _values [ section ] = self . _values . get ( section , { } )
self . _values [ section ] . update ( new_values [ section ] )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def get ( self , section : str , option : str ) - > Any :
2019-06-17 04:13:25 +02:00
""" Get the value of ``option`` in ``section``. """
2020-02-19 09:50:25 +01:00
with contextlib . suppress ( KeyError ) :
return self . _values [ section ] [ option ]
raise KeyError ( section , option )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def get_raw ( self , section : str , option : str ) - > Any :
2019-06-17 04:13:25 +02:00
""" Get the raw value of ``option`` in ``section``. """
for config , _ , _ in reversed ( self . _configs ) :
2020-02-19 09:50:25 +01:00
if option in config . get ( section , { } ) :
return config [ section ] [ option ]
raise KeyError ( section , option )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def get_source ( self , section : str , option : str ) - > str :
2020-02-19 09:50:19 +01:00
""" Get the source that provides ``option`` in ``section``. """
for config , source , _ in reversed ( self . _configs ) :
if option in config . get ( section , { } ) :
return source
raise KeyError ( section , option )
2021-07-26 20:56:46 +02:00
def sections ( self ) - > List [ str ] :
2019-06-17 04:13:25 +02:00
""" List all sections. """
2021-07-26 20:56:46 +02:00
return list ( self . _values . keys ( ) )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def options ( self , section : str ) - > List [ str ] :
2019-06-17 04:13:25 +02:00
""" List all options in ``section`` """
2021-07-26 20:56:46 +02:00
return list ( self . _values [ section ] . keys ( ) )
2019-06-17 04:13:25 +02:00
2021-07-26 20:56:46 +02:00
def sources ( self ) - > List [ Tuple [ str , bool ] ] :
2020-02-19 09:48:42 +01:00
""" List all config sources. """
return [ ( source , config is self . SOURCE_MISSING ) for
config , source , _ in self . _configs ]
2021-07-26 20:56:46 +02:00
def copy ( self : _Self , plugin_schema : Optional [ types . CONFIG_SCHEMA ] = None
) - > _Self :
2019-06-17 04:13:25 +02:00
""" Create a copy of the configuration
` ` plugin_schema ` ` is a optional dict that contains additional options
for usage with a plugin . See ` ` DEFAULT_CONFIG_SCHEMA ` ` .
"""
if plugin_schema is None :
schema = self . _schema
else :
2021-07-26 20:56:46 +02:00
new_schema = dict ( self . _schema )
2019-06-17 04:13:25 +02:00
for section , options in plugin_schema . items ( ) :
2021-07-26 20:56:46 +02:00
if ( section not in new_schema or
" type " not in new_schema [ section ] or
" internal " not in new_schema [ section ] [ " type " ] ) :
2019-06-17 04:13:25 +02:00
raise ValueError ( " not a plugin section: %r " % section )
2021-07-26 20:56:46 +02:00
new_section = dict ( new_schema [ section ] )
new_type = dict ( new_section [ " type " ] )
new_type [ " internal " ] = ( self . get ( section , " type " ) , )
new_section [ " type " ] = new_type
2019-06-17 04:13:25 +02:00
for option , value in options . items ( ) :
2021-07-26 20:56:46 +02:00
if option in new_section :
raise ValueError ( " option already exists in %r : %r " %
( section , option ) )
new_section [ option ] = value
new_schema [ section ] = new_section
schema = new_schema
2020-01-12 23:32:27 +01:00
copy = type ( self ) ( schema )
2020-02-19 09:50:27 +01:00
for config , source , privileged in self . _configs :
copy . update ( config , source , privileged )
2019-06-17 04:13:25 +02:00
return copy