From 8ddba38bd51fc7a9ed367a8ecdab3b5c052b04a0 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 28 Nov 2024 23:08:24 +0100 Subject: [PATCH] [task-192] [int] geht zum Teil --- conf/example.json | 2 +- source/api/actions/caldav_get.ts | 139 +++---- source/api/actions/caldav_probe.ts | 137 +++---- .../actions/caldav_probe_via_well_known.ts | 115 ++++++ source/api/actions/caldav_projects.ts | 91 +++++ source/api/actions/caldav_put.ts | 32 ++ source/api/actions/meta_ping.ts | 8 +- source/api/actions/meta_spec.ts | 6 +- source/api/actions/session_begin.ts | 6 +- source/api/actions/session_prepare.ts | 6 +- source/api/base.ts | 91 ++++- source/api/functions.ts | 5 +- source/services/caldal.ts | 5 + source/services/caldav.ts | 386 ++++++++++++++++++ tools/makefile | 4 + 15 files changed, 865 insertions(+), 168 deletions(-) create mode 100644 source/api/actions/caldav_probe_via_well_known.ts create mode 100644 source/api/actions/caldav_projects.ts create mode 100644 source/api/actions/caldav_put.ts create mode 100644 source/services/caldal.ts create mode 100644 source/services/caldav.ts diff --git a/conf/example.json b/conf/example.json index ea0df88..d4d149c 100644 --- a/conf/example.json +++ b/conf/example.json @@ -5,7 +5,7 @@ "kind": "stdout", "data": { "threshold": "info", - "format": "human_readable" + "format": "jsonl_structured" } } ], diff --git a/source/api/actions/caldav_get.ts b/source/api/actions/caldav_get.ts index 8812e8a..f66b99e 100644 --- a/source/api/actions/caldav_get.ts +++ b/source/api/actions/caldav_get.ts @@ -11,14 +11,16 @@ namespace _zeitbild.api register< null, ( - lib_plankton.ical.type_vcalendar + null | string + | + lib_plankton.ical.type_vcalendar ) >( rest_subject, lib_plankton.caldav.enum_method.report, - "/caldav", + "/caldav/project/:id", { "description": "trägt Veranstaltungen aus verschiedenen Kalendern zusammen im ical-Format", "query_parameters": () => ([ @@ -48,89 +50,82 @@ namespace _zeitbild.api "type": "string", }), "response_body_mimetype": "text/calendar", - "response_body_encode": (output) => Buffer.from( - (typeof(output) === "string") + "response_body_encode": (output) => ( + (output === null) ? - output + null : - lib_plankton.ical.ics_encode(output) + Buffer.from( + (typeof(output) === "string") + ? + output + : + lib_plankton.ical.ics_encode(output) + ) ), "restriction": restriction_none, + /** + * @todo use stuff.path_parameters["id"] + */ "execution": async (stuff) => { - const user_id : (null | _zeitbild.type_user_id) = await ( - session_from_stuff(stuff) - .then( - (session : {key : string; value : lib_plankton.session.type_session;}) => ( - _zeitbild.service.user.identify(session.value.name) - .catch(x => Promise.resolve(null)) - ) - ) - .catch(x => Promise.resolve(null)) - ); - - const from : lib_plankton.pit.type_pit = ( - ("from" in stuff.query_parameters) - ? - parseInt(stuff.query_parameters["from"]) - : - lib_plankton.pit.shift_week( - lib_plankton.pit.now(), - -2 - ) - ); - const to : lib_plankton.pit.type_pit = ( - ("to" in stuff.query_parameters) - ? - parseInt(stuff.query_parameters["to"]) - : - lib_plankton.pit.shift_week( - lib_plankton.pit.now(), - +6 - ) - ); - const calendar_ids_wanted : (null | Array<_zeitbild.type_calendar_id>) = ( - ( - ("calendar_ids" in stuff.query_parameters) - && - (stuff.query_parameters["calendar_ids"] !== null) - ) - ? - lib_plankton.call.convey( - stuff.query_parameters["calendar_ids"], - [ - (x : string) => x.split(","), - (x : Array) => x.map(parseInt), - (x : Array) => x.filter(y => (! isNaN(y))) - ] - ) - : + const user_id : (null | type_user_id) = await _zeitbild.api.web_auth( + stuff.headers["Authorization"] + ?? + stuff.headers["authorization"] + ?? null ); - - const auth_hash_shall : string = lib_plankton.sha256.get( - (stuff.query_parameters["calendar_ids"] ?? ""), - _zeitbild.conf.get()["misc"]["auth_salt"] - ); - const auth_hash_is : string = stuff.query_parameters["auth"]; - /** - * @todo remove - */ - lib_plankton.log.info( - "auth_hashes", - { - "shall": auth_hash_shall, - "is": auth_hash_is, - } - ); - if (! (auth_hash_is === auth_hash_shall)) { + if (user_id === null) { return Promise.resolve( { - "status_code": 403, - "data": "not authorized", + "status_code": 401, + "data": null, + "extra_headers": { + "WWW-Authenticate": "Basic realm=Restricted", + } } ); } else { + const from : lib_plankton.pit.type_pit = ( + ("from" in stuff.query_parameters) + ? + parseInt(stuff.query_parameters["from"]) + : + lib_plankton.pit.shift_week( + lib_plankton.pit.now(), + -2 + ) + ); + const to : lib_plankton.pit.type_pit = ( + ("to" in stuff.query_parameters) + ? + parseInt(stuff.query_parameters["to"]) + : + lib_plankton.pit.shift_week( + lib_plankton.pit.now(), + +6 + ) + ); + const calendar_ids_wanted : (null | Array<_zeitbild.type_calendar_id>) = ( + ( + ("calendar_ids" in stuff.query_parameters) + && + (stuff.query_parameters["calendar_ids"] !== null) + ) + ? + lib_plankton.call.convey( + stuff.query_parameters["calendar_ids"], + [ + (x : string) => x.split(","), + (x : Array) => x.map(parseInt), + (x : Array) => x.filter(y => (! isNaN(y))) + ] + ) + : + null + ); + return ( _zeitbild.service.calendar.gather_events( calendar_ids_wanted, diff --git a/source/api/actions/caldav_probe.ts b/source/api/actions/caldav_probe.ts index f6f886d..4a08d52 100644 --- a/source/api/actions/caldav_probe.ts +++ b/source/api/actions/caldav_probe.ts @@ -9,46 +9,45 @@ namespace _zeitbild.api ) : void { register< - null, - string + ( + null + | + lib_plankton.xml.type_node_data + ), + lib_plankton.xml.type_node_data >( rest_subject, lib_plankton.caldav.enum_method.propfind, "/caldav", { - "query_parameters": () => ([ - { - "name": "from", - "required": false, - "description": "UNIX timestamp", - }, - { - "name": "to", - "required": false, - "description": "UNIX timestamp", - }, - { - "name": "calendar_ids", - "required": false, - "description": "comma separated", - }, - { - "name": "auth", - "required": true, - "description": "", - }, - ]), + "request_body_mimetype": () => "text/xml", + "request_body_decode": () => async (body, header_content_type) => ( + ( + (header_content_type !== null) + && + ( + (header_content_type.startsWith("text/xml")) + || + (header_content_type.startsWith("application/xml")) + ) + ) + ? + lib_plankton.xml.parse(body.toString()) + : + Promise.resolve(null) + ), "output_schema": () => ({ "nullable": false, "type": "string", }), - "response_body_mimetype": "text/xml; charset=utf-8", - "response_body_encode": output => Buffer.from(output), + "response_body_mimetype": "text/xml", + "response_body_encode": output => Promise.resolve( + Buffer.from( + lib_plankton.xml.get_node_logic(output).compile(0) + ) + ), // "restriction": restriction_basic_auth, "restriction": restriction_none, - /** - * @todo examine body - */ "execution": async (stuff) => { const user_id : (null | type_user_id) = await _zeitbild.api.web_auth( stuff.headers["Authorization"] @@ -61,7 +60,10 @@ namespace _zeitbild.api return Promise.resolve( { "status_code": 401, - "data": "", + "data": { + "kind": "text", + "data": "" + }, "extra_headers": { "WWW-Authenticate": "Basic realm=Restricted", } @@ -69,54 +71,35 @@ namespace _zeitbild.api ); } else { - return ( - _zeitbild.service.calendar.overview(user_id) - .then( - (data_raw) => Promise.resolve( - data_raw - .map( - (entry) => ({ - "id": entry.id, - "name": entry.name, - "access_level": _zeitbild.value_object.access_level.to_string(entry.access_level), - }) - ) - ) - ) - .then( - (data) => Promise.resolve({ - "status_code": 207, - "data": ( - "\n" - + - lib_plankton.webdav.data_multistatus_encode( - { - "responses": [ - { - "href": "/caldav/events", - "body": { - "propstats": data.map( - (entry) => ({ - "prop": [ - {"name": "displayname", "value": entry.name}, - // {"name": "cs:getctag", "value": "47"}, // TODO correct value - // {"name": "current-user-privilege-set", "value": ""}, - ], - "status": "HTTP/2.0 200 OK", - "description": entry.access_level, - }) - ), - }, - "description": null, - } - ], - "description": null, - } - ) - ), - }) - ) + const output : (null | lib_plankton.xml.type_node_data) = _zeitbild.service.caldav.probe( + stuff.input ); + if (output !== null) { + return Promise.resolve( + { + "status_code": 207, + "data": output + } + ); + } + else { + return Promise.resolve( + { + "status_code": 501, + "data": { + "kind": "root", + "data": { + "version": "1.0", + "encoding": "utf-8", + "content": { + "kind": "text", + "data": "" + } + } + } + } + ); + } } } } diff --git a/source/api/actions/caldav_probe_via_well_known.ts b/source/api/actions/caldav_probe_via_well_known.ts new file mode 100644 index 0000000..1ce6034 --- /dev/null +++ b/source/api/actions/caldav_probe_via_well_known.ts @@ -0,0 +1,115 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_caldav_probe_via_well_known( + rest_subject : lib_plankton.rest_caldav.type_rest + ) : void + { + register< + ( + null + | + lib_plankton.xml.type_node_data + ), + lib_plankton.xml.type_node_data + >( + rest_subject, + lib_plankton.caldav.enum_method.propfind, + "/.well-known/caldav", + { + "request_body_mimetype": () => "text/xml", + "request_body_decode": () => async (body, header_content_type) => ( + ( + (header_content_type !== null) + && + ( + (header_content_type.startsWith("text/xml")) + || + (header_content_type.startsWith("application/xml")) + ) + ) + ? + lib_plankton.xml.parse(body.toString()) + : + Promise.resolve(null) + ), + "output_schema": () => ({ + "nullable": false, + "type": "string", + }), + "response_body_mimetype": "text/xml", + "response_body_encode": output => Promise.resolve( + Buffer.from( + lib_plankton.xml.get_node_logic(output).compile(0) + ) + ), + // "restriction": restriction_basic_auth, + "restriction": restriction_none, + "execution": async (stuff) => { + const user_id : (null | type_user_id) = await _zeitbild.api.web_auth( + stuff.headers["Authorization"] + ?? + stuff.headers["authorization"] + ?? + null + ); + if (user_id === null) { + return Promise.resolve( + { + "status_code": 401, + "data": { + "kind": "text", + "data": "" + }, + "extra_headers": { + "WWW-Authenticate": "Basic realm=Restricted", + } + } + ); + } + else { + const output : (null | lib_plankton.xml.type_node_data) = _zeitbild.service.caldav.probe( + stuff.input, + { + "force_props": [ + "D:displayname", + "D:resourcetype", + ] + } + ); + if (output !== null) { + return Promise.resolve( + { + "status_code": 207, + "data": output + } + ); + } + else { + return Promise.resolve( + { + "status_code": 501, + "data": { + "kind": "root", + "data": { + "version": "1.0", + "encoding": "utf-8", + "content": { + "kind": "text", + "data": "" + } + } + } + } + ); + } + } + } + } + ); + } + +} diff --git a/source/api/actions/caldav_projects.ts b/source/api/actions/caldav_projects.ts new file mode 100644 index 0000000..f530a84 --- /dev/null +++ b/source/api/actions/caldav_projects.ts @@ -0,0 +1,91 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_caldav_projects( + rest_subject : lib_plankton.rest_caldav.type_rest + ) : void + { + register< + ( + null + | + lib_plankton.xml.type_node_data + ), + lib_plankton.xml.type_node_data + >( + rest_subject, + lib_plankton.caldav.enum_method.propfind, + "/caldav/project", + { + "output_schema": () => ({ + "nullable": false, + "type": "string", + }), + "request_body_mimetype": () => "text/xml", + "request_body_decode": () => async (body, header_content_type) => ( + ( + (header_content_type !== null) + && + ( + (header_content_type.startsWith("text/xml")) + || + (header_content_type.startsWith("application/xml")) + ) + ) + ? + lib_plankton.xml.parse(body.toString()) + : + Promise.resolve(null) + ), + "response_body_mimetype": "text/xml", + "response_body_encode": output => Promise.resolve( + Buffer.from( + lib_plankton.xml.get_node_logic(output).compile(0) + ) + ), + // "restriction": restriction_basic_auth, + "restriction": restriction_none, + "execution": async (stuff) => { + const user_id : (null | type_user_id) = await _zeitbild.api.web_auth( + stuff.headers["Authorization"] + ?? + stuff.headers["authorization"] + ?? + null + ); + if (user_id === null) { + return Promise.resolve( + { + "status_code": 401, + "data": { + "kind": "text", + "data": "" + }, + "extra_headers": { + "WWW-Authenticate": "Basic realm=Restricted", + } + } + ); + } + else { + return ( + _zeitbild.service.caldav.projects(user_id) + .then( + (output) => Promise.resolve( + { + "status_code": 207, + "data": output, + } + ) + ) + ); + } + } + } + ); + } + +} diff --git a/source/api/actions/caldav_put.ts b/source/api/actions/caldav_put.ts new file mode 100644 index 0000000..1d49725 --- /dev/null +++ b/source/api/actions/caldav_put.ts @@ -0,0 +1,32 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_caldav_put( + rest_subject : lib_plankton.rest_caldav.type_rest + ) : void + { + register< + null, + null + >( + rest_subject, + lib_plankton.caldav.enum_method.put, + "/caldav", + { + "restriction": restriction_none, + "execution": async (stuff) => { + return Promise.resolve( + { + "status_code": 200, + "data": null + } + ); + } + } + ); + } + +} diff --git a/source/api/actions/meta_ping.ts b/source/api/actions/meta_ping.ts index 77f8258..5a1dcca 100644 --- a/source/api/actions/meta_ping.ts +++ b/source/api/actions/meta_ping.ts @@ -17,7 +17,7 @@ namespace _zeitbild.api lib_plankton.http.enum_method.get, _zeitbild.conf.get().server.path_base + "/meta/ping", { - "description": "sendet ein 'pong' zurück; gedacht um die Erreichbarkeit des Backends zu prüfen", + "description": () => "sendet ein 'pong' zurück; gedacht um die Erreichbarkeit des Backends zu prüfen", "input_schema": () => ({ "nullable": true, }), @@ -25,8 +25,10 @@ namespace _zeitbild.api "nullable": false, "type": "string", }), - "restriction": restriction_none, - "execution": () => { + "response_body_encode": () => (body) => Promise.resolve(body), + "response_body_mimetype": () => "text/plain", + "restriction": () => restriction_none, + "execution": () => () => { return Promise.resolve({ "status_code": 200, "data": "pong", diff --git a/source/api/actions/meta_spec.ts b/source/api/actions/meta_spec.ts index 5dca37d..a0d81dd 100644 --- a/source/api/actions/meta_spec.ts +++ b/source/api/actions/meta_spec.ts @@ -17,14 +17,14 @@ namespace _zeitbild.api lib_plankton.http.enum_method.get, _zeitbild.conf.get().server.path_base + "/meta/spec", { - "description": "gibt die API-Spezifikation im OpenAPI-Format aus", + "description": () => "gibt die API-Spezifikation im OpenAPI-Format aus", "input_schema": () => ({ "nullable": true, }), "output_schema": () => ({ }), - "restriction": restriction_none, - "execution": () => { + "restriction": () => restriction_none, + "execution": () => () => { return Promise.resolve({ "status_code": 200, "data": lib_plankton.rest_caldav.to_oas(rest_subject), diff --git a/source/api/actions/session_begin.ts b/source/api/actions/session_begin.ts index 62ab793..bba0b31 100644 --- a/source/api/actions/session_begin.ts +++ b/source/api/actions/session_begin.ts @@ -23,7 +23,7 @@ namespace _zeitbild.api lib_plankton.http.enum_method.post, "/session/begin", { - "description": "führt die Anmeldung am System aus um geschützte Aktionen nutzen zu können", + "description": () => "führt die Anmeldung am System aus um geschützte Aktionen nutzen zu können", "input_schema": () => ({ "type": "object", "properties": { @@ -44,8 +44,8 @@ namespace _zeitbild.api "type": "string", "description": "der Sitzungs-Schlüssel, der als Header 'X-Session-Key' gesetzt werden muss um Erlaubnis zur Ausführung geschützter Aktionen zu erhalten", }), - "restriction": restriction_none, - "execution": async ({"input": input}) => { + "restriction": () => restriction_none, + "execution": () => async ({"input": input}) => { if (input === null) { return Promise.reject(new Error("impossible")); } diff --git a/source/api/actions/session_prepare.ts b/source/api/actions/session_prepare.ts index 8753106..965c262 100644 --- a/source/api/actions/session_prepare.ts +++ b/source/api/actions/session_prepare.ts @@ -19,15 +19,15 @@ namespace _zeitbild.api lib_plankton.http.enum_method.post, "/session/prepare", { - "description": "gibt die nötigen Werkzeuge für eine Anmeldung aus", + "description": () => "gibt die nötigen Werkzeuge für eine Anmeldung aus", "input_schema": () => ({ "nullable": true, }), "output_schema": () => ({ "nullable": false }), - "restriction": restriction_none, - "execution": async (stuff) => { + "restriction": () => restriction_none, + "execution": () => async (stuff) => { const preparation = await _zeitbild.auth.prepare(stuff.input); return Promise.resolve({ "status_code": 200, diff --git a/source/api/base.ts b/source/api/base.ts index 8039c23..5132c31 100644 --- a/source/api/base.ts +++ b/source/api/base.ts @@ -141,7 +141,7 @@ namespace _zeitbild.api http_method : lib_plankton.caldav.enum_method, path : string, options : { - active ?: ((version : string) => boolean); + active ?: ((version : (null | string)) => boolean); restriction ?: (null | lib_plankton.rest_caldav.type_restriction); execution ?: lib_plankton.rest_caldav.type_execution; title ?: (null | string); @@ -155,10 +155,24 @@ namespace _zeitbild.api >); input_schema ?: ((version: (null | string)) => lib_plankton.rest_caldav.type_oas_schema); output_schema ?: ((version: (null | string)) => lib_plankton.rest_caldav.type_oas_schema); - request_body_mimetype ?: string; - request_body_decode ?: ((http_request_body : Buffer, http_request_header_content_type : (null | string)) => any); + request_body_mimetype ?: ( + (version : (null | string)) + => + string + ); + request_body_decode ?: ( + (version : (null | string)) + => + (http_request_body : Buffer, http_request_header_content_type : (null | string)) + => + Promise + ); response_body_mimetype ?: string; - response_body_encode ?: ((output : any) => Buffer); + response_body_encode ?: ( + (output : any) + => + Promise + ); } = {} ) : void { @@ -171,7 +185,74 @@ namespace _zeitbild.api rest_subject, http_method, (_zeitbild.conf.get().server.path_base + path), - options + { + "active": options.active, + /** + * @todo heed version + */ + "restriction": ( + ((options.restriction === undefined) || (options.restriction === null)) + ? + undefined + : + (version) => (options.restriction as lib_plankton.rest_caldav.type_restriction) + ), + /** + * @todo heed version + */ + "execution": ( + ((options.execution === undefined) || (options.execution === null)) + ? + undefined + : + (version) => (options.execution as lib_plankton.rest_caldav.type_execution) + ), + /** + * @todo heed version + */ + "title": ( + ((options.title === undefined) || (options.title === null)) + ? + (version) => null + : + (version) => (options.title as string) + ), + /** + * @todo heed version + */ + "description": ( + ((options.description === undefined) || (options.description === null)) + ? + (version) => null + : + (version) => (options.description as string) + ), + "query_parameters": options.query_parameters, + "input_schema": options.input_schema, + "output_schema": options.output_schema, + "request_body_mimetype": options.request_body_mimetype, + "request_body_decode": options.request_body_decode, + /** + * @todo heed version + */ + "response_body_mimetype": ( + ((options.response_body_mimetype === undefined) || (options.response_body_mimetype === null)) + ? + undefined + : + (version) => (options.response_body_mimetype as string) + ), + /** + * @todo heed version + */ + "response_body_encode": ( + ((options.response_body_encode === undefined) || (options.response_body_encode === null)) + ? + undefined + : + (version) => (options.response_body_encode as ((output : any) => Promise)) + ), + } ); } diff --git a/source/api/functions.ts b/source/api/functions.ts index 3b346bb..9504555 100644 --- a/source/api/functions.ts +++ b/source/api/functions.ts @@ -15,7 +15,7 @@ namespace _zeitbild.api "set_access_control_headers": true, "authentication": { "kind": "key_header", - "parameters": {"name": "X-Session-Key"} + "data": {"name": "X-Session-Key"} }, } ); @@ -53,7 +53,10 @@ namespace _zeitbild.api // caldav { _zeitbild.api.register_caldav_sniff(rest_subject); + _zeitbild.api.register_caldav_put(rest_subject); _zeitbild.api.register_caldav_probe(rest_subject); + _zeitbild.api.register_caldav_probe_via_well_known(rest_subject); + _zeitbild.api.register_caldav_projects(rest_subject); _zeitbild.api.register_caldav_get(rest_subject); } // misc diff --git a/source/services/caldal.ts b/source/services/caldal.ts new file mode 100644 index 0000000..c20fac7 --- /dev/null +++ b/source/services/caldal.ts @@ -0,0 +1,5 @@ + +namespace _zeitbild.service.caldav +{ + +} diff --git a/source/services/caldav.ts b/source/services/caldav.ts new file mode 100644 index 0000000..1e101f2 --- /dev/null +++ b/source/services/caldav.ts @@ -0,0 +1,386 @@ + +namespace _zeitbild.service.caldav +{ + + /** + * @todo use pod for output + * @todo get api paths in props from config + * @todo consider to outsorce to plankton + */ + export function probe( + input : (null | lib_plankton.xml.type_node_data), + { + "force_props": force_props = null, + } : { + force_props ?: (null | Array); + } = { + } + ) : (null | lib_plankton.xml.type_node_data) + { + const http_protocol : string = "HTTP/1.1"; + let props : (null | Array); + if (force_props) { + props = force_props; + } + else { + if ( + (input !== null) + && + (input.kind === "complex") + && + ( + (input.data.tag.toLowerCase() === "d:propfind") + || + (input.data.tag.toLowerCase() === "propfind") + ) + && + (input.data.children.length === 1) + && + (input.data.children[0].kind === "complex") + && + ( + (input.data.children[0].data.tag.toLowerCase() === "d:prop") + || + (input.data.children[0].data.tag.toLowerCase() === "prop") + ) + ) { + props = input.data.children[0].data.children.map( + node => { + switch (node.kind) { + case "complex": { + return node.data.tag; + break; + } + default: { + throw (new Error("unexpected node type for prop")); + break; + } + } + } + ); + props.sort(); + } + else { + props = null; + } + } + if (props === null) { + lib_plankton.log.notice( + "service.caldav.probe.unexpected_input", + { + "input": input, + } + ); + return null; + } + else { + const answers : Record< + string, + lib_plankton.webdav.type_data_prop_value + > = { + // webdav 1 + + /** + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-13.2 + */ + "displayname": { + "kind": "primitive", + "data": "projects" + }, + /** + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-13.4 + */ + /* + "getcontentlength": { + "kind": "none", + "data": null + }, + */ + /** + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-13.5 + */ + /* + "getcontenttype": { + "kind": "none", + "data": null + }, + */ + /** + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-13.7 + */ + /* + "getlastmodified": { + "kind": "none", + "data": null + }, + */ + /** + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-13.9 + */ + "resourcetype": { + "kind": "resourcetype", + "data": { + "kind": "collection", + "type": "calendar", + } + }, + + // webdav 2 + + /** + * @see https://datatracker.ietf.org/doc/html/rfc3744#section-4.2 + */ + "principal-url": { + "kind": "href", + "data": "/caldav" + }, + /** + * @see https://datatracker.ietf.org/doc/html/rfc3744#section-5.1 + */ + "owner": { + "kind": "primitive", + "data": "" + }, + /** + * @see https://datatracker.ietf.org/doc/html/rfc3744#section-5.4 + */ + "current-user-privilege-set": { + "kind": "privileges", + "data": [ + "read" + ] + }, + + // caldav + + /** + * @see https://datatracker.ietf.org/doc/html/rfc4791#section-6.2.1 + */ + "calendar-home-set": { + "kind": "href", + "data": "/caldav/project" + }, + + // WebDAV Current Principal Extension + + /** + * @see https://datatracker.ietf.org/doc/html/rfc5397#section-3 + */ + "current-user-principal": { + "kind": "href", + "data": "/caldav" + }, + + // unknown + + /* + "calendar-color": { + "kind": "none", + "data": null + }, + "executable": { + "kind": "none", + "data": null + }, + "checked-in": { + "kind": "none", + "data": null + }, + "checked-out": { + "kind": "none", + "data": null + }, + "calendar-user-address-set": { + "kind": "none", + "data": null + }, + */ + }; + return { + "kind": "root", + "data": { + "version": "1.0", + "encoding": "utf-8", + "content": lib_plankton.webdav.data_multistatus_encode_xml( + { + "responses": [ + { + "href": "/caldav/project", + "body": { + /** + * @todo maybe propstats needs to be filled with props (.map …) + */ + "propstats": ( + false + ? + [ + { + "prop": (props ?? []).map( + (prop) => { + const prop_parts : Array = prop.toLowerCase().split(":"); + const prop_normalized : string = ((prop_parts.length <= 1) ? prop_parts[0] : prop_parts.slice(1).join(":")); + if (! (prop_normalized in answers)) { + lib_plankton.log.error( + "api.caldav_probe.unhandled_prop", + prop_normalized + ); + throw (new Error("unhandled prop: " + prop_normalized)); + } + else { + return { + "name": prop, + "value": ( + answers[prop_normalized] + ?? + { + "kind": "none", + "data": null, + } + ), + }; + } + } + ), + "status": (http_protocol + " 200 OK"), + "description": null, + }, + ] + : + props.map( + (prop) => { + const prop_parts : Array = prop.toLowerCase().split(":"); + const prop_normalized : string = ( + (prop_parts.length <= 1) + ? + prop_parts[0] + : + prop_parts.slice(1).join(":") + ); + if (! (prop_normalized in answers)) { + lib_plankton.log.notice( + "api.caldav_probe.unhandled_prop", + prop_normalized + ); + /* + throw (new Error("unhandled prop: " + prop_normalized)); + */ + return { + "prop": [ + { + "name": prop, + "value": { + "kind": "none", + "data": null, + } + }, + ], + "status": (http_protocol + " 404 Not Found"), + "description": null, + }; + } + else { + return { + "prop": [ + { + "name": prop, + "value": answers[prop_normalized], + }, + ], + "status": (http_protocol + " 200 OK"), + "description": null, + }; + }; + } + ) + ) + }, + "description": null, + } + ], + "description": null, + } + ) + } + }; + } + } + + + /** + */ + export function projects( + user_id : type_user_id + ) : Promise + { + return ( + _zeitbild.service.calendar.overview(user_id) + .then( + (data_raw) => Promise.resolve( + data_raw + .map( + (entry) => ({ + "id": entry.id, + "name": entry.name, + "access_level": _zeitbild.value_object.access_level.to_string(entry.access_level), + }) + ) + ) + ) + .then( + (data) => Promise.resolve( + { + "kind": "root", + "data": { + "version": "1.0", + "encoding": "utf-8", + "content": lib_plankton.webdav.data_multistatus_encode_xml( + { + "responses": data.map( + (entry) => ({ + "href": lib_plankton.string.coin( + "/caldav/project/{{id}}", + { + "id": entry.id.toFixed(0), + } + ), + "body": { + "propstats": [ + { + "prop": [ + { + "name": "D:displayname", + "value": { + "kind": "primitive", + "data": entry.name, + }, + }, + { + "name": "D:resourcetype", + "value": { + "kind": "resourcetype", + "data": { + "kind": "collection", + "type": "calendar", + } + } + }, + ], + "status": "HTTP/1.1 200 OK", + "description": null, + } + ], + }, + "description": null, + }) + ), + "description": null, + } + ) + } + } + ) + ) + ); + } + +} diff --git a/tools/makefile b/tools/makefile index 6d5ca33..c7de353 100644 --- a/tools/makefile +++ b/tools/makefile @@ -56,6 +56,7 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/services/user.ts \ ${dir_source}/services/resource.ts \ ${dir_source}/services/calendar.ts \ + ${dir_source}/services/caldav.ts \ ${dir_source}/api/base.ts \ ${dir_source}/api/transformations/datetime.ts \ ${dir_source}/api/actions/meta_ping.ts \ @@ -77,7 +78,10 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/api/actions/events.ts \ ${dir_source}/api/actions/export_ical.ts \ ${dir_source}/api/actions/caldav_sniff.ts \ + ${dir_source}/api/actions/caldav_put.ts \ ${dir_source}/api/actions/caldav_probe.ts \ + ${dir_source}/api/actions/caldav_probe_via_well_known.ts \ + ${dir_source}/api/actions/caldav_projects.ts \ ${dir_source}/api/actions/caldav_get.ts \ ${dir_source}/api/functions.ts \ ${dir_source}/main.ts