-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcaldav2ics.php
302 lines (276 loc) · 10.4 KB
/
caldav2ics.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#!/usr/bin/env php
<?php
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\DomCrawler\Crawler;
// Standalone CalDav2ics (e.g. for cron job...)
// stores Logfile and Calendar File in same Directory as script
// can fetch/process multiple calendars at once, just put these as a json encoded array in $CalendarsFile, a sample File is included here for convenience
// from V 1.1.0, this program includes and uses Symfony\Component\Yaml class, so it can read native yaml Files even when used on a server that has php-yaml not installed.
// so, no more need to use 'fake' yaml files that contain json data :-)
// Config goes here:
$verbose = true;
$LogEnabled = true;
$RequestTimeout = '20'; // default Timeout for curl Request
$LogFile = pathinfo(__FILE__, PATHINFO_DIRNAME)."/logfile.txt";
$CalendarsFile = pathinfo(__FILE__, PATHINFO_DIRNAME)."/caldav2ics.yaml"; // config file name, yaml Format for security reasons !
// feel free to use your own $CalendarsFile PATH - just be sure it is correct :-)
// alternatively, you can provide the Calendars File Name via commandline, Argument 1, see code below. Same goes for $ICSpath (optional Argument 2)
$ICSpath = pathinfo(__FILE__, PATHINFO_DIRNAME); // default
// here you can still use the old, hardcoded parameter definition, this must, however, be in array notation, like follows (just comment out the 'die..' in the line above):
$Config = array (
"calendars" => array (
array (
"Name"=>"TestCalendar",
"Url"=>"https://yourcaldavserver.net/remote.php/dav/calendars/myuser/pubcal",
"User"=>"myuser",
"Pass"=>"my#secret!Password",
),
)
);
// end Config
require_once './vendor/autoload.php';
if ($argc > 1)
$CalendarsFile = $argv[1];
if ($argc > 2)
$ICSpath = $argv[2]; // currently unused, just ignore :-)
if ( file_exists($CalendarsFile) ) {
$Config = Yaml::parseFile($CalendarsFile); // now, this is a real yaml file ;-)
if ($verbose) var_dump($Config);
if (is_array($Config['calendars'])) {
$calendars = $Config['calendars'];
// var_dump($calendars);
foreach($calendars as $calendar) {
// var_dump($calendar);
}
} else {
die("invalid Calendar data, aborted!");
}
// exit;
} else {
echo("Calendars File not found, using default Config data !");
}
// commonly used ics Properties, see https://en.wikipedia.org/wiki/ICalendar
$Properties = ["DTSTAMP","URL","URL;VALUE=URI","CREATED","UID","LAST-MODIFIED","SUMMARY","LOCATION","DTSTAMP","DTSTART","DTSTART;VALUE=DATE","DTEND","DTEND;VALUE=DATE","TRANSP","DESCRIPTION","UID","CLASS","STATUS","SEQUENCE","RRULE","RDATE","EXDATE","BEGIN","END","TRIGGER","ACTION","CATEGORIES","GEO","ATTENDEE","ROLE","EMAIL","CN"];
foreach ($calendars as $calendar) {
if ($verbose) var_dump($calendar);
$cal = $calendar; // (array) $calendars;
$name = $cal["Name"];
$calendar_url = $cal['Url'];
$calendar_user = $cal['User'];
$calendar_password = $cal['Pass'];
$ICalFile = $ICSpath."/".$name.".ics"; // ical file name
if ($verbose) {
echo "\n";
echo "$name\n";
echo "$calendar_url\n";
echo "$calendar_user\n";
echo "$calendar_password\n";
echo "$ICalFile\n";
}
// continue; // NOT break;
$fmdelay = 60; // seconds
if ($LogEnabled) {
$loghandle = fopen($LogFile, 'w') or die('Cannot open file: '.$LogFile);
}
if (empty($calendar_url) || empty($calendar_user) || empty($calendar_password)) {
if (!$LogEnabled) {
$loghandle = fopen($LogFile, 'w') or die('Cannot open file: '.$LogFile);
}
fwrite($loghandle, "Invalid Settings !\n");
fwrite($loghandle, "Calendar URL: ".$calendar_url." must be specified\n");
fwrite($loghandle, "Username: ".$calendar_user." must be specified\n");
fwrite($loghandle, "Password: ".$calendar_password." must be specified\n");
fclose($loghandle);
return;
}
if (filter_var($calendar_url, FILTER_VALIDATE_URL) === false) {
print_r("Invalid Calendar URL: ", $calendar_url);
if (!$LogEnabled) {
$loghandle = fopen($LogFile, 'w') or die('Cannot open file: '.$LogFile);
fwrite($loghandle, "Invalid Calendar URL: ", $calendar_url);
fclose($loghandle);
}
return;
}
if ($LogEnabled) {
print_r($calendar_url);
fwrite($loghandle, $calendar_url."\n");
fwrite($loghandle, $calendar_user."\n");
fwrite($loghandle, $calendar_password."\n");
fwrite($loghandle, "Delay:".$fmdelay."\n");
fwrite($loghandle, "EnableLog:".$LogEnabled."\n");
}
// Simple caching system, feel free to change the delay
if (file_exists($ICalFile)) {
$last_update = filemtime($ICalFile);
} else {
$last_update = 0;
}
if ($last_update + $fmdelay < time()) {
// Get events
$headers = array(
'Content-Type: application/xml; charset=utf-8',
'Depth: 1',
'Prefer: return-minimal'
);
// see https://uname.pingveno.net/blog/index.php/post/2016/07/30/Sample-public-calendar-for-ownCloud-using-ICS-parser
// Prepare request body, MANDATORY !
$doc = new DOMDocument('1.0', 'utf-8');
$doc->formatOutput = true;
$query = $doc->createElement('c:calendar-query');
$query->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:c', 'urn:ietf:params:xml:ns:caldav');
$query->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:d', 'DAV:');
$prop = $doc->createElement('d:prop');
$prop->appendChild($doc->createElement('d:getetag'));
$prop->appendChild($doc->createElement('c:calendar-data'));
$query->appendChild($prop);
$prop = $doc->createElement('c:filter');
$filter = $doc->createElement('c:comp-filter');
$filter->setAttribute('name', 'VCALENDAR');
$prop->appendChild($filter);
$query->appendChild($prop);
$doc->appendChild($query);
$body = $doc->saveXML();
// Debugging purpose
if ($LogEnabled) {
echo htmlspecialchars($body);
fwrite($loghandle, htmlspecialchars($body));
}
// Prepare cURL request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $calendar_url);
curl_setopt($ch, CURLOPT_USERPWD, $calendar_user . ':' . $calendar_password);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, $RequestTimeout); // DONE: set Timeout, see https://thisinterestsme.com/php-setting-curl-timeout/
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'REPORT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$response = curl_exec($ch);
if (curl_error($ch)) {
if ($LogEnabled) {
echo curl_error($ch);
fwrite($loghandle, curl_error($ch));
fclose($loghandle);
}
return;
}
curl_close($ch);
// Debugging purpose
if ($LogEnabled) {
echo htmlspecialchars($response);
fwrite($loghandle, htmlspecialchars($response));
}
// Get the useful part of the response
/* skip any xml conversion/parsing
*/
// Parse events
$calendar_events = array();
$handle = fopen($ICalFile, 'w') or die('Cannot open file: '.$ICalFile);
// create valid ICS File with only ONE Vcalendar !
// write VCALENDAR header
fwrite($handle, 'BEGIN:VCALENDAR'."\r\n");
fwrite($handle, 'VERSION:2.0'."\r\n");
fwrite($handle, 'PRODID:-//hoernerfranzracing/caldav2ics.php'."\r\n");
// find and write TIMEZONE data, new feature, 27.12.19
$skip = true;
$wroteTZ = false;
$lines = explode("\n", $response);
foreach ($lines as $line) {
$line = trim($line);
if ( !$wroteTZ ) {
if (startswith($line,'BEGIN:VTIMEZONE')) {
$skip = false;
}
if ( !$skip ) {
fwrite($handle, $line."\r\n"); // write everything between 'BEGIN:VTIMEZONE' and 'END:VTIMEZONE'
// echo $line."\n";
}
if (startswith($line,'END:VTIMEZONE')) {
$skip = true;
$wroteTZ = true; // only write VTIMEZONE entry once
}
}
}
// parse $response, do NOT write VCALENDAR header for each one, just the event data
foreach ($lines as $line) {
$line = trim($line,"\n\r\t\v\x00"); // mod. 17.07.23 WJ, see https://wordpress.org/support/topic/converted-openxchange-calendar-subscibe-to-google-didnt-work/
if (strlen($line) > 0) { // mod. 17.07.23 WJ
if (strstr($line,'BEGIN:VCALENDAR')) { // first occurrence might not be at line start
$skip = true;
}
if (startswith($line,'PRODID:')) {
$skip = true;
}
if (strstr($line,'VERSION:')) {
$skip = true; // VERSION can appear in different places
}
if (startswith($line,'CALSCALE:')) {
$skip = true;
}
if (startswith($line,'BEGIN:VEVENT')) {
$skip = false;
//fwrite($handle, "\r\n"); // improves readability, but triggers warning in validator :)
}
if (startswith($line,'END:VCALENDAR')) {
$skip = true;
}
// more validations 20.09.24
//if (!str_contains($line, ':')) { // skip all lines that do not contain ':' (Keyword)
if (!strpos($line, ':')) { // TODO: better join lines to previous one :-)
$skip = true;
} else {
$parts = explode(":",$line);
$keyword = $parts[0];
if (in_array($keyword, $Properties)) {
$skip = false;
if (startswith($line,'END:VCALENDAR')) {
$skip = true;
}
} else {
if (startswith($line,'ORGANIZER;CN=')) {
$skip = false;
} else {
$skip = true;
}
}
}
if ( !$skip ) {
if ((startswith($line,'URL:')) || (startswith($line,'URL;VALUE=URI:'))) { // check if 'URL:..' Line contains valid URL
$url = str_replace("URL:", "", $line);
$url = str_replace("URL;VALUE=URI:", "", $url);
$url = trim($url);
if (filter_var($url, FILTER_VALIDATE_URL) === FALSE) {
if ($LogEnabled) {
fwrite($loghandle, $url.' is Not a valid URL'."\r\n");
fwrite($loghandle, "Line: ".$line."\r\n");
}
} else {
fwrite($handle, $line."\r\n");
}
} else {
fwrite($handle, $line."\r\n");
}
}
}
}
fwrite($handle, 'END:VCALENDAR'."\r\n");
fclose($handle);
if ($LogEnabled) {
fclose($loghandle);
}
}
}
function startswith ($string, $stringToSearchFor) {
if (substr(trim($string),0,strlen($stringToSearchFor)) == $stringToSearchFor) {
// the string starts with the string you're looking for
return true;
} else {
// the string does NOT start with the string you're looking for
return false;
}
}
?>