blazor-calendar/BlazorApp/Data/Calendar.cs

182 lines
5.8 KiB
C#
Raw Normal View History

2021-07-03 12:19:53 +00:00
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace BlazorApp.Data
{
public class Event
{
public string Uid { get; set; }
public string Summary { get; set; }
public DateTime? DtStart { get; set; }
public DateTime? DtEnd { get; set; }
public string Description { get; set; }
}
public class Calendar
{
public int Id { get; set; }
[Required] public string Url { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Name { get; set; }
private string AuthorizationHeader =>
$"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))}";
private static DateTime _parseDateTime(string s)
{
var format = s.Contains('T') ? "yyyyMMddTHHmmssK" : "yyyyMMdd";
return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture);
}
private static List<Event> ParseICal(string iCal)
{
Event @event = null;
var events = new List<Event>();
// Un-wrap all lines from the input
var body = iCal
.Replace("\n ", "")
.Replace("\n\t", "")
.Replace("\r", "");
foreach (var line in body.Split("\n"))
{
var trimmed = line.Trim();
var split = trimmed.Split(':', 2);
if (split.Length != 2)
continue;
var left = split[0].Trim().Split(';');
var name = left[0];
// TODO Turn this into a regex
var value = split[1]
.Replace(@"\n", "\n")
.Replace(@"\N", "\n")
.Replace(@"\,", ",")
.Replace(@"\;", ";")
.Replace(@"\\", @"\");
if (name == "BEGIN" && value == "VEVENT")
{
// Start of a new event
@event = new Event();
continue;
}
if (@event == null)
{
// Event hasn't started yet, ignore the property.
continue;
}
switch (name)
{
case "END":
if (value == "VEVENT")
{
// End of the current event, add it to the list
events.Add(@event);
@event = null;
}
break;
case "UID":
@event.Uid = value;
break;
case "SUMMARY":
@event.Summary = value;
break;
case "DESCRIPTION":
@event.Description = value;
break;
case "DTSTART":
@event.DtStart = _parseDateTime(value);
break;
case "DTEND":
@event.DtEnd = _parseDateTime(value);
break;
}
}
return events;
}
/**
* <summary>Retrieves and parses all events from the CalDav server.</summary>
*/
public async Task<List<Event>> GetEvents()
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
var body = await client.GetStringAsync(Url);
var events = ParseICal(body);
events.Sort((x, y) =>
{
if (y.DtStart == null)
return -1;
if (x.DtStart == null)
return 1;
return DateTime.Compare(y.DtStart.Value, x.DtStart.Value);
});
return events;
}
/**
* <summary>Retrieves and parses an event with UID <code>uid</code> from the CalDav server.</summary>
*/
public async Task<Event> GetEventByUid(string uid)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
request.Headers.Add("Depth", "1");
// TODO Use XML generator to safely add UID - this is sensitive to scary injection stuff
request.Content = new StringContent(@$"<?xml version=""1.0"" encoding=""utf-8"" ?>
<C:calendar-query xmlns:C=""urn:ietf:params:xml:ns:caldav"">
<D:prop xmlns:D=""DAV:"">
<D:getetag/>
<C:calendar-data/>
</D:prop>
<C:filter>
<C:comp-filter name=""VCALENDAR"">
<C:comp-filter name=""VEVENT"">
<C:prop-filter name=""UID"">
<C:text-match collation=""i;octet""
>{uid}</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
", Encoding.UTF8, "application/xml");
var result = await client.SendAsync(request);
var body = await result.Content.ReadAsStreamAsync();
var reader = new XmlTextReader(body);
var document = new XmlDocument();
document.Load(reader);
var node = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
var events = ParseICal(node);
return events.Count > 0 ? events[0] : null;
}
}
}