1 /* http_tzdist.c -- Routines for handling tzdist service requests in httpd
2 *
3 * Copyright (c) 1994-2014 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43
44 /*
45 * TODO:
46 * - Implement localized names / handle Accept-Language header field?
47 */
48
49 #include <config.h>
50
51 #ifdef HAVE_UNISTD_H
52 #include <unistd.h>
53 #endif
54 #include <ctype.h>
55 #include <math.h>
56 #include <string.h>
57 #include <syslog.h>
58 #include <assert.h>
59 #include <errno.h>
60
61 #include "global.h"
62 #include "hash.h"
63 #include "httpd.h"
64 #include "http_dav.h"
65 #include "http_proxy.h"
66 #include "ical_support.h"
67 #include "jcal.h"
68 #include "map.h"
69 #include "strhash.h"
70 #include "times.h"
71 #include "tok.h"
72 #include "util.h"
73 #include "version.h"
74 #include "xcal.h"
75 #include "xstrlcpy.h"
76 #include "zoneinfo_db.h"
77
78 /* generated headers are not necessarily in current directory */
79 #include "imap/http_err.h"
80 #include "imap/tz_err.h"
81
82 #define TZDIST_WELLKNOWN_URI "/.well-known/timezone"
83
84 static time_t compile_time;
85 static unsigned synctoken_prefix;
86 static ptrarray_t *leap_seconds = NULL;
87 static int geo_enabled = 0;
88 static const char *zoneinfo_dir = NULL;
89 static void tzdist_init(struct buf *serverinfo);
90 static void tzdist_shutdown(void);
91 static int meth_get(struct transaction_t *txn, void *params);
92 static int action_capa(struct transaction_t *txn);
93 static int action_leap(struct transaction_t *txn);
94 static int action_list(struct transaction_t *txn);
95 static int action_get(struct transaction_t *txn);
96 static int action_expand(struct transaction_t *txn);
97 static int json_response(int code, struct transaction_t *txn, json_t *root,
98 char **resp);
99 static int json_error_response(struct transaction_t *txn, long tz_code,
100 struct strlist *param, icaltimetype *time);
101 static struct buf *icaltimezone_as_tzif(icalcomponent* comp);
102 static struct buf *icaltimezone_as_tzif_leap(icalcomponent* comp);
103 static struct buf *_icaltimezone_as_tzif(icalcomponent* ical, bit32 leapcnt,
104 icaltimetype *startp, icaltimetype *endp);
105
106 struct observance {
107 const char *name;
108 icaltimetype onset;
109 int offset_from;
110 int offset_to;
111 int is_daylight;
112 int is_std;
113 int is_gmt;
114 };
115
116 static struct mime_type_t tz_mime_types[] = {
117 /* First item MUST be the default type and storage format */
118 { "text/calendar; charset=utf-8", "2.0", "ics",
119 (struct buf* (*)(void *)) &my_icalcomponent_as_ical_string,
120 NULL, NULL, NULL, NULL
121 },
122 { "application/calendar+xml; charset=utf-8", NULL, "xcs",
123 (struct buf* (*)(void *)) &icalcomponent_as_xcal_string,
124 NULL, NULL, NULL, NULL
125 },
126 { "application/calendar+json; charset=utf-8", NULL, "jcs",
127 (struct buf* (*)(void *)) &icalcomponent_as_jcal_string,
128 NULL, NULL, NULL, NULL
129 },
130 { "application/tzif", NULL, NULL,
131 (struct buf* (*)(void *)) &icaltimezone_as_tzif,
132 NULL, NULL, NULL, NULL
133 },
134 { "application/tzif-leap", NULL, NULL,
135 (struct buf* (*)(void *)) &icaltimezone_as_tzif_leap,
136 NULL, NULL, NULL, NULL
137 },
138 { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
139 };
140
141
142 /* Namespace for tzdist service */
143 struct namespace_t namespace_tzdist = {
144 URL_NS_TZDIST, 0, "tzdist", "/tzdist", TZDIST_WELLKNOWN_URI,
145 http_allow_noauth, /*authschemes*/0,
146 /*mbtype*/0,
147 ALLOW_READ,
148 tzdist_init, NULL, NULL, tzdist_shutdown, NULL, NULL,
149 {
150 { NULL, NULL }, /* ACL */
151 { NULL, NULL }, /* BIND */
152 { NULL, NULL }, /* CONNECT */
153 { NULL, NULL }, /* COPY */
154 { NULL, NULL }, /* DELETE */
155 { &meth_get, NULL }, /* GET */
156 { &meth_get, NULL }, /* HEAD */
157 { NULL, NULL }, /* LOCK */
158 { NULL, NULL }, /* MKCALENDAR */
159 { NULL, NULL }, /* MKCOL */
160 { NULL, NULL }, /* MOVE */
161 { &meth_options, NULL }, /* OPTIONS */
162 { NULL, NULL }, /* POST */
163 { NULL, NULL }, /* PATCH */
164 { NULL, NULL }, /* PROPFIND */
165 { NULL, NULL }, /* PROPPATCH */
166 { NULL, NULL }, /* PUT */
167 { NULL, NULL }, /* REPORT */
168 { &meth_trace, NULL }, /* TRACE */
169 { NULL, NULL }, /* UNBIND */
170 { NULL, NULL } /* UNLOCK */
171 }
172 };
173
174
175 #ifdef HAVE_SHAPELIB
176 #include <shapefil.h>
177
178 struct tz_shape_t {
179 int valid;
180 SHPHandle shp;
181 DBFHandle dbf;
182 };
183
184 static struct tz_shape_t tz_world = { 0, NULL, NULL };
185 static struct tz_shape_t tz_aq = { 0, NULL, NULL };
186
open_shape_file(struct buf * serverinfo)187 static void open_shape_file(struct buf *serverinfo)
188 {
189 char buf[1024];
190 int nrecords, shapetype;
191 double minbound[4], maxbound[4];
192 DBFFieldType fieldtype;
193
194 buf_printf(serverinfo, " ShapeLib/%s", SHAPELIB_VERSION);
195
196 /* Open the tz_world shape files */
197 snprintf(buf, sizeof(buf), "%s%s", zoneinfo_dir, FNAME_WORLD_SHAPEFILE);
198 if (!(tz_world.shp = SHPOpen(buf, "rb"))) {
199 syslog(LOG_ERR, "Failed to open file %s", buf);
200 return;
201 }
202
203 if (!(tz_world.dbf = DBFOpen(buf, "rb"))) {
204 syslog(LOG_ERR, "Failed to open file %s", buf);
205 return;
206 }
207
208 /* Sanity check the shape files */
209 SHPGetInfo(tz_world.shp, &nrecords, &shapetype, minbound, maxbound);
210 if (!nrecords || shapetype != SHPT_POLYGON || /* polygons */
211 minbound[0] < -180.0 || maxbound[0] > 180.0 || /* longitude range */
212 minbound[1] < -90.0 || maxbound[1] > 90.0 || /* latitude range */
213 nrecords != DBFGetRecordCount(tz_world.dbf)) { /* record counts */
214 syslog(LOG_ERR, "%s appears to contain invalid data", buf);
215 return;
216 }
217
218 fieldtype = DBFGetFieldInfo(tz_world.dbf, 0 /* column 1 */, buf, NULL, NULL);
219 if (fieldtype != FTString || strcasecmp(buf, "TZID")) { /* TZIDs */
220 syslog(LOG_ERR, "%s appears to contain invalid data", buf);
221 return;
222 }
223
224 geo_enabled = tz_world.valid = 1;
225
226 /* Open the tz_antarctica shape files (optional) */
227 snprintf(buf, sizeof(buf), "%s%s", zoneinfo_dir, FNAME_AQ_SHAPEFILE);
228 if (!(tz_aq.shp = SHPOpen(buf, "rb"))) {
229 syslog(LOG_NOTICE, "Failed to open file %s", buf);
230 return;
231 }
232
233 if (!(tz_aq.dbf = DBFOpen(buf, "rb"))) {
234 syslog(LOG_NOTICE, "Failed to open file %s", buf);
235 SHPClose(tz_aq.shp);
236 return;
237 }
238
239 /* Sanity check the shape files */
240 SHPGetInfo(tz_aq.shp, &nrecords, &shapetype, minbound, maxbound);
241 if (!nrecords || shapetype != SHPT_POINT || /* points */
242 minbound[0] < -180.0 || maxbound[0] > 180.0 || /* longitude range */
243 minbound[1] < -90.0 || maxbound[1] > -60.0 || /* latitude range */
244 nrecords != DBFGetRecordCount(tz_aq.dbf)) { /* record counts */
245 syslog(LOG_ERR, "%s appears to contain invalid data", buf);
246 return;
247 }
248
249 fieldtype = DBFGetFieldInfo(tz_aq.dbf, 1 /* column 2 */, buf, NULL, NULL);
250 if (fieldtype != FTString || strcasecmp(buf, "TZID")) { /* TZIDs */
251 syslog(LOG_ERR, "%s appears to contain invalid data", buf);
252 return;
253 }
254
255 tz_aq.valid = 1;
256 }
257
close_shape_file()258 static void close_shape_file()
259 {
260 if (tz_world.dbf) DBFClose(tz_world.dbf);
261 if (tz_world.shp) SHPClose(tz_world.shp);
262 if (tz_aq.dbf) DBFClose(tz_aq.dbf);
263 if (tz_aq.shp) SHPClose(tz_aq.shp);
264 }
265
pt_in_poly(int nvert,double * vx,double * vy,double px,double py)266 static int pt_in_poly(int nvert, double *vx, double *vy, double px, double py)
267 {
268 int i, j, in = 0;
269
270 for (i = 0, j = nvert - 1; i < nvert; j = i++) {
271 if (((vy[i] > py) != (vy[j] > py)) &&
272 (px < (vx[j] - vx[i]) * (py - vy[i]) / (vy[j] - vy[i]) + vx[i])) {
273 in = !in;
274 }
275 }
276
277 return in;
278 }
279
280
281 #define M_EARTH_RADIUS 6371008.7 /* mean radius (meters) */
282
283 #define M_PI_180 0.01745329251994329547 /* pi / 180 */
284
285 #define deg2rad(deg) (deg * M_PI_180) /* degrees -> radians */
286
287 #define vec_normal(v) vec_mult(1 / vec_mag(v), v) /* normalize vector */
288
289 #define vec_diff(v1, v2) acos(vec_dot_prod(v1, v2)) /* angular difference */
290
291 struct vector {
292 double x, y, z;
293 };
294
geo2vec(double lat,double lon,struct vector * p)295 static struct vector *geo2vec(double lat, double lon, struct vector *p)
296 {
297 /* Convert lat/lon to radians */
298 lat = deg2rad(lat);
299 lon = deg2rad(lon);
300
301 /* Convert lat/lon to unit vector */
302 p->x = cos(lat) * cos(lon);
303 p->y = cos(lat) * sin(lon);
304 p->z = sin(lat);
305
306 return p;
307 }
308
vec_mag(const struct vector * v)309 static double vec_mag(const struct vector *v)
310 {
311 return sqrt(v->x * v->x + v->y * v->y + v->z * v->z);
312 }
313
vec_mult(double m,struct vector * v)314 static struct vector *vec_mult(double m, struct vector *v)
315 {
316 v->x *= m;
317 v->y *= m;
318 v->z *= m;
319
320 return v;
321 }
322
vec_dot_prod(const struct vector * v1,const struct vector * v2)323 static double vec_dot_prod(const struct vector *v1, const struct vector *v2)
324 {
325 return (v1->x * v2->x + v1->y * v2->y + v1->z * v2->z);
326 }
327
vec_cross_prod(const struct vector * v1,const struct vector * v2,struct vector * r)328 static struct vector *vec_cross_prod(const struct vector *v1,
329 const struct vector *v2,
330 struct vector *r)
331 {
332 r->x = v1->y * v2->z - v1->z * v2->y;
333 r->y = v1->z * v2->x - v1->x * v2->z;
334 r->z = v1->x * v2->y - v1->y * v2->x;
335
336 return r;
337 }
338
pt_near_poly(int nvert,double * vx,double * vy,struct vector * p,double range)339 static int pt_near_poly(int nvert, double *vx, double *vy,
340 struct vector *p, double range)
341 {
342 int i, j;
343
344 for (i = 0, j = nvert - 1; i < nvert; j = i++) {
345 struct vector a, b, n;
346
347 geo2vec(vy[j], vx[j], &a);
348 geo2vec(vy[i], vx[i], &b);
349
350 /* Check if either end point of the line is within range */
351 if (vec_diff(p, &a) <= range || vec_diff(p, &b) <= range) return 1;
352
353 /* Find unit normal vector (n) for plane passing through a & b */
354 vec_normal(vec_cross_prod(&a, &b, &n));
355
356 /* Shortest distance between p and geodesic passing through a & b (ab) */
357 if (asin(fabs(vec_dot_prod(&n, p))) <= range) {
358 struct vector c, d;
359 double ab_len;
360
361 /* Find perpendicular geodesic (d) through p to ab */
362 vec_cross_prod(p, &n, &d);
363
364 /* Find intersection point (c) of d and ab */
365 vec_normal(vec_cross_prod(&n, &d, &c));
366
367 /* Make sure intersection point (c) lies between a & b */
368 ab_len = vec_diff(&a, &b);
369 if (vec_diff(&a, &c) <= ab_len && vec_diff(&b, &c) <= ab_len) {
370 return 1;
371 }
372 }
373 }
374
375 return 0;
376 }
377
tzid_from_geo(struct transaction_t * txn,double latitude,double longitude,double uncertainty)378 static strarray_t *tzid_from_geo(struct transaction_t *txn,
379 double latitude, double longitude,
380 double uncertainty)
381 {
382 strarray_t *tzids = strarray_new();
383 const char *tzid;
384 struct vector p, a;
385 int i, npoly;
386 double minbound[4], maxbound[4];
387
388 /* using unit vectors */
389 uncertainty /= M_EARTH_RADIUS;
390 geo2vec(latitude, longitude, &p); /* vector for point */
391 geo2vec(-60, longitude, &a); /* perpendicular vector to Antarctic */
392
393 if (tz_aq.valid &&
394 /* Check if point is within or near Antarctic region */
395 (latitude <= -60 || (uncertainty && vec_diff(&p, &a) <= uncertainty))) {
396
397 /* check if point is near an Antarctic base */
398 double dist = uncertainty;
399
400 if (!dist) {
401 /* default to 10km radius */
402 dist = 10000 / M_EARTH_RADIUS;
403 }
404
405 for (i = 0; i < tz_aq.shp->nRecords; i++) {
406 SHPObject *base = SHPReadObject(tz_aq.shp, i);
407 struct vector b;
408
409 geo2vec(base->padfY[0], base->padfX[0], &b); /* vector for base */
410
411 if (vec_diff(&p, &b) <= dist) {
412 /* Point is near a base, check if it has a known time zone */
413 tzid = DBFReadStringAttribute(tz_aq.dbf, i, 1 /* column 2 */);
414 if (strcmp(tzid, "unknown")) strarray_append(tzids, tzid);
415 }
416
417 SHPDestroyObject(base);
418
419 keepalive_response(txn);
420 }
421 }
422
423 /* Check if point is within or near bounding box of tz_world */
424 SHPGetInfo(tz_world.shp, &npoly, NULL, minbound, maxbound);
425
426 double WbbX[5] =
427 { minbound[0], minbound[0], maxbound[0], maxbound[0], minbound[0] };
428 double WbbY[5] =
429 { minbound[1], maxbound[1], maxbound[1], minbound[1], minbound[1] };
430
431 if (pt_in_poly(5, WbbX, WbbY, longitude, latitude) ||
432 (uncertainty && pt_near_poly(5, WbbX, WbbY, &p, uncertainty))) {
433 /* Check if point is within or near a time zone boundary */
434
435 for (i = 0; i < npoly; i++) {
436 SHPObject *poly = SHPReadObject(tz_world.shp, i);
437 double bbX[5] = { poly->dfXMin, poly->dfXMin,
438 poly->dfXMax, poly->dfXMax, poly->dfXMin };
439 double bbY[5] = { poly->dfYMin, poly->dfYMax,
440 poly->dfYMax, poly->dfYMin, poly->dfYMin };
441
442 /* Check if point is within or near bounding box of boundary */
443 int within = pt_in_poly(5, bbX, bbY, longitude, latitude);
444 int near = uncertainty && pt_near_poly(5, bbX, bbY, &p, uncertainty);
445 int r = 0;
446
447 if (within || near) {
448 if (within) {
449 /* Check if point is within boundary */
450 r = pt_in_poly(poly->nVertices, poly->padfX, poly->padfY,
451 longitude, latitude);
452 }
453
454 if (!r && uncertainty) {
455 /* Check if point is near boundary */
456 r = pt_near_poly(poly->nVertices, poly->padfX, poly->padfY,
457 &p, uncertainty);
458 }
459 }
460
461 if (r) {
462 tzid = DBFReadStringAttribute(tz_world.dbf, i, 0 /* column 1 */);
463 strarray_append(tzids, tzid);
464 }
465
466 SHPDestroyObject(poly);
467
468 keepalive_response(txn);
469 }
470 }
471
472 if (!strarray_size(tzids)) {
473 /* No tzids found in shapefile(s) */
474 char tzid_buf[20];
475
476 if (latitude <= -60) {
477 /* Antarctic region - guess-timate offset from GMT based on:
478
479 https://en.wikipedia.org/wiki/Time_in_Antarctica
480 https://en.wikipedia.org/wiki/Territorial_claims_in_Antarctica
481 https://en.wikipedia.org/wiki/Australian_Antarctic_Territory
482 https://en.wikipedia.org/wiki/Queen_Maud_Land
483 https://en.wikipedia.org/wiki/Princess_Martha_Coast
484 https://en.wikipedia.org/wiki/Princess_Astrid_Coast
485 https://en.wikipedia.org/wiki/Princess_Ragnhild_Coast
486 https://en.wikipedia.org/wiki/Prince_Harald_Coast
487 https://en.wikipedia.org/wiki/Prince_Olav_Coast
488 */
489 if (latitude <= -89 || (longitude >= 160 || longitude <= -150)) {
490 /* South Pole and New Zealand Claim (Ross Dependency) */
491 tzid = "Antarctica/South_Pole";
492 }
493 else if (longitude >= -20 && latitude <= -80) {
494 /* Uninhabited */
495 tzid = "Etc/GMT";
496 }
497 else if (longitude >= 142.033333) { /* 142° 2' */
498 /* Australian Claim (George V / Oates Lands) */
499 tzid = "Etc/GMT+10";
500 }
501 else if (longitude >= 136.183333) { /* 136° 11' */
502 /* French Claim (Adelie Land) */
503 tzid = "Etc/GMT+10";
504 }
505 else if (longitude >= 44.633333) { /* 44° 38' */
506 /* Australian Claim */
507 if (longitude >= 100.5) { /* 100° 30' */
508 /* Wilkes Land */
509 tzid = "Etc/GMT+8";
510 }
511 else if (longitude >= 72.583333) { /* 72° 35' */
512 /* Princess Elizabeth / Kaiser Wilhelm II / Queen Mary Lands */
513 tzid = "Etc/GMT+7";
514 }
515 else {
516 /* Enderby / Kemp / Mac. Robertson Lands */
517 tzid = "Etc/GMT+6";
518 }
519 }
520 else if (longitude >= -20) {
521 /* Norwegian Claim (Queen Maud Land) */
522 if (longitude >= 20) {
523 /* Princess Ragnhild / Prince Harald / Prince Olav Coasts */
524 tzid = "Etc/GMT+3";
525 }
526 else {
527 /* Princess Martha / Princess Astrid Coasts */
528 tzid = "Etc/GMT";
529 }
530 }
531 else if (longitude >= -90) {
532 /* British / Argentine / Chilean Claims */
533 if (longitude >= -74) {
534 /* Argentine / British Claims */
535 tzid = "Etc/GMT-3";
536 }
537 /* XXX Is there a GMT-4 + DST region? */
538 else {
539 /* Chilean / British Claims */
540 tzid = "Etc/GMT-4";
541 }
542 }
543 else {
544 /* Unclaimed */
545 if (latitude <= -80) {
546 /* Uninhabited */
547 tzid = "Etc/GMT";
548 }
549 else tzid = "Etc/GMT-6";
550 }
551 }
552 else {
553 /* Assume international waters -
554 calculate offset from GMT based on longitude
555
556 XXX Which offset does an exact multiple of +/- 7.5
557 and +/- 180 degrees belong to?
558 */
559 snprintf(tzid_buf, sizeof(tzid_buf), "Etc/GMT%+d",
560 (short) (longitude + copysign(1.0, longitude) * 7.5) / 15);
561 tzid = tzid_buf;
562 }
563
564 strarray_append(tzids, tzid);
565 }
566
567 return tzids;
568 }
569 #else
570
open_shape_file(struct buf * serverinfo)571 static void open_shape_file(struct buf *serverinfo __attribute__((unused)))
572 {
573 return;
574 }
575
close_shape_file()576 static void close_shape_file()
577 {
578 return;
579 }
580
tzid_from_geo(struct transaction_t * txn,double latitude,double longitude,double uncertainty)581 static strarray_t *tzid_from_geo(struct transaction_t *txn __attribute__((unused)),
582 double latitude __attribute__((unused)),
583 double longitude __attribute__((unused)),
584 double uncertainty __attribute__((unused)))
585 {
586 return NULL;
587 }
588
589 #endif /* HAVE_SHAPELIB */
590
591
592 struct leapsec {
593 long long int t; /* transition time */
594 long int sec; /* leap seconds */
595 };
596
read_leap_seconds()597 static void read_leap_seconds()
598 {
599 FILE *fp;
600 char buf[1024];
601 struct leapsec *leap;
602
603 snprintf(buf, sizeof(buf), "%s%s", zoneinfo_dir, FNAME_LEAPSECFILE);
604 if (!(fp = fopen(buf, "r"))) {
605 syslog(LOG_ERR, "Failed to open file %s", buf);
606 return;
607 }
608
609 /* expires record is always at idx=0, if exists */
610 leap_seconds = ptrarray_new();
611 leap = xzmalloc(sizeof(struct leapsec));
612 ptrarray_append(leap_seconds, leap);
613
614 while (fgets(buf, sizeof(buf), fp)) {
615 if (buf[0] == '#') {
616 /* comment line */
617
618 if (buf[1] == '@') {
619 /* expires */
620 leap = ptrarray_nth(leap_seconds, 0);
621 sscanf(buf+2, "\t%lld", &leap->t);
622 leap->t -= NIST_EPOCH_OFFSET;
623 }
624 }
625 else if (isdigit(buf[0])) {
626 /* leap second */
627 leap = xmalloc(sizeof(struct leapsec));
628 ptrarray_append(leap_seconds, leap);
629 sscanf(buf, "%lld\t%ld", &leap->t, &leap->sec);
630 leap->t -= NIST_EPOCH_OFFSET;
631 }
632 }
633 fclose(fp);
634 }
635
636
tzdist_init(struct buf * serverinfo)637 static void tzdist_init(struct buf *serverinfo __attribute__((unused)))
638 {
639 struct buf buf = BUF_INITIALIZER;
640
641 namespace_tzdist.enabled =
642 config_httpmodules & IMAP_ENUM_HTTPMODULES_TZDIST;
643
644 if (!namespace_tzdist.enabled) return;
645
646 /* Open zoneinfo db */
647 if (zoneinfo_open(NULL)) {
648 namespace_tzdist.enabled = 0;
649 return;
650 }
651
652 /* Find configured zoneinfo_zir */
653 zoneinfo_dir = config_getstring(IMAPOPT_ZONEINFO_DIR);
654 if (!zoneinfo_dir) {
655 syslog(LOG_ERR, "zoneinfo_dir must be set for tzdist service");
656 namespace_tzdist.enabled = 0;
657 return;
658 }
659
660 compile_time = calc_compile_time(__TIME__, __DATE__);
661
662 buf_printf(&buf, "Cyrus TZdist: %s", config_servername);
663 synctoken_prefix = strhash(buf_cstring(&buf));
664 buf_free(&buf);
665
666 initialize_tz_error_table();
667
668 open_shape_file(serverinfo);
669
670 read_leap_seconds();
671 if (!leap_seconds || leap_seconds->count < 2) {
672 /* Disable application/tzif-leap */
673 struct mime_type_t *mime;
674
675 for (mime = tz_mime_types; mime->content_type; mime++) {
676 if (!strcmp(mime->content_type, "application/tzif-leap")) {
677 mime->content_type = NULL;
678 break;
679 }
680 }
681 }
682 }
683
684
tzdist_shutdown(void)685 static void tzdist_shutdown(void)
686 {
687 struct leapsec *leap;
688
689 zoneinfo_close(NULL);
690
691 close_shape_file();
692
693 if (!leap_seconds) return;
694
695 while ((leap = ptrarray_pop(leap_seconds))) free(leap);
696 ptrarray_free(leap_seconds);
697 }
698
699
700 /* Perform a GET/HEAD request */
meth_get(struct transaction_t * txn,void * params)701 static int meth_get(struct transaction_t *txn,
702 void *params __attribute__((unused)))
703 {
704 struct request_target_t *tgt = &txn->req_tgt;
705 int (*action)(struct transaction_t *txn) = NULL;
706 unsigned levels = 0;
707 char *p;
708
709 /* Make a working copy of target path */
710 strlcpy(tgt->path, txn->req_uri->path, sizeof(tgt->path));
711 p = tgt->path;
712
713 /* Skip namespace */
714 p += strlen(namespace_tzdist.prefix);
715 if (*p == '/') *p++ = '\0';
716
717 /* Check for path after prefix */
718 if (*p) {
719 /* Get collection (action) */
720 tgt->collection = p;
721 p += strcspn(p, "/");
722 if (*p == '/') *p++ = '\0';
723
724 if (!strcmp(tgt->collection, "capabilities")) {
725 if (!*p) action = &action_capa;
726 }
727 else if (!strcmp(tgt->collection, "leapseconds")) {
728 if (!*p) action = &action_leap;
729 }
730 else if (!strcmp(tgt->collection, "zones")) {
731 if (!*p) {
732 action = &action_list;
733 }
734 else {
735 /* Get resource (tzid) */
736 tgt->resource = p;
737 p += strlen(p);
738 if (p[-1] == '/') *--p = '\0';
739
740 /* Check for sub-action */
741 p = strstr(tgt->resource, "observances");
742 if (!p) {
743 action = &action_get;
744 }
745 else if (p[-1] == '/') {
746 *--p = '\0';
747 action = &action_expand;
748 }
749
750 /* XXX Hack - probably need to check for %2F vs '/'
751 Count the number of "levels". Current tzid have max of 3. */
752 for (p = tgt->resource; p && ++levels; (p = strchr(p+1, '/')));
753 }
754 }
755 }
756
757 if (!action || levels > 3)
758 return json_error_response(txn, TZ_INVALID_ACTION, NULL, NULL);
759
760 if (tgt->resource && strchr(tgt->resource, '.')) /* paranoia */
761 return json_error_response(txn, TZ_NOT_FOUND, NULL, NULL);
762
763 return action(txn);
764 }
765
766
767 /* Perform a capabilities action */
action_capa(struct transaction_t * txn)768 static int action_capa(struct transaction_t *txn)
769 {
770 int precond;
771 struct message_guid guid;
772 const char *etag;
773 static time_t lastmod = 0;
774 static char *resp = NULL;
775 json_t *root = NULL;
776
777 /* Generate ETag based on compile date/time of this source file.
778 * Extend this to include config file size/mtime if we add run-time options.
779 */
780 assert(!buf_len(&txn->buf));
781 buf_printf(&txn->buf, TIME_T_FMT, compile_time);
782 message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
783 etag = message_guid_encode(&guid);
784
785 /* Check any preconditions, including range request */
786 txn->flags.ranges = 1;
787 precond = check_precond(txn, etag, compile_time);
788
789 switch (precond) {
790 case HTTP_OK:
791 case HTTP_PARTIAL:
792 case HTTP_NOT_MODIFIED:
793 /* Fill in Etag, Last-Modified, Expires */
794 txn->resp_body.etag = etag;
795 txn->resp_body.lastmod = compile_time;
796 txn->resp_body.maxage = 86400; /* 24 hrs */
797 txn->flags.cc |= CC_MAXAGE;
798 if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
799
800 if (precond != HTTP_NOT_MODIFIED) break;
801
802 GCC_FALLTHROUGH
803
804 default:
805 /* We failed a precondition - don't perform the request */
806 return precond;
807 }
808
809 if (txn->resp_body.lastmod > lastmod) {
810 struct zoneinfo info;
811 struct mime_type_t *mime;
812 json_t *formats;
813 int r;
814
815 /* Get info record from the database */
816 if ((r = zoneinfo_lookup_info(&info))) return HTTP_SERVER_ERROR;
817
818 buf_reset(&txn->buf);
819 buf_printf(&txn->buf, "%s:%s", info.data->s, info.data->next->s);
820
821 /* Construct our response */
822 root = json_pack("{ s:i" /* version */
823 " s:{" /* info */
824 " s:s" /* primary-source */
825 " s:[]" /* formats */
826 " s:{s:b s:b}" /* truncated */
827 // " s:s" /* provider-details */
828 // " s:[]" /* contacts */
829 " }"
830 " s:[" /* actions */
831 " {s:s s:s s:[" /* capabilities */
832 " ]}"
833 " {s:s s:s s:[" /* list */
834 " {s:s}" /* changedsince */
835 " ]}"
836 " {s:s s:s s:[" /* get */
837 " {s:s}" /* start */
838 " {s:s}" /* end */
839 " ]}"
840 " {s:s s:s s:[" /* expand */
841 " {s:s s:b}" /* start */
842 " {s:s s:b}" /* end */
843 " ]}"
844 " {s:s s:s s:[" /* find */
845 " {s:s s:b}" /* pattern */
846 " ]}"
847 " {s:s s:s s:[" /* leapseconds */
848 " ]}"
849 " ]}",
850
851 "version", 1,
852
853 "info",
854 "primary-source", buf_cstring(&txn->buf), "formats",
855 "truncated", "any", 1, "untruncated", 1,
856 // "provider-details", "", "contacts",
857
858 "actions",
859 "name", "capabilities",
860 "uri-template", "/capabilities", "parameters",
861
862 "name", "list",
863 "uri-template", "/zones{?changedsince}", "parameters",
864 "name", "changedsince",
865
866 "name", "get", "uri-template",
867 "/zones{/tzid}{?start,end}", "parameters",
868 "name", "start",
869 "name", "end",
870
871 "name", "expand", "uri-template",
872 "/zones{/tzid}/observances{?start,end}",
873 "parameters",
874 "name", "start", "required", 1,
875 "name", "end", "required", 1,
876
877 "name", "find",
878 "uri-template", "/zones{?pattern}", "parameters",
879 "name", "pattern", "required", 1,
880
881 "name", "leapseconds",
882 "uri-template", "/leapseconds", "parameters");
883
884 freestrlist(info.data);
885
886 if (!root) {
887 txn->error.desc = "Unable to create JSON response";
888 return HTTP_SERVER_ERROR;
889 }
890
891 if (geo_enabled) {
892 /* Add geolocate action */
893 json_t *actions = json_object_get(root, "actions");
894
895 json_array_append_new(actions,
896 json_pack("{s:s s:s s:["
897 " {s:s s:b}"
898 "]}",
899 "name", "geolocate", "uri-template",
900 "/zones{?location}",
901 "parameters",
902 "name", "location", "required", 1));
903 }
904
905 /* Add supported formats */
906 formats = json_object_get(json_object_get(root, "info"), "formats");
907 for (mime = tz_mime_types; mime->content_type; mime++) {
908 buf_setcstr(&txn->buf, mime->content_type);
909 buf_truncate(&txn->buf, strcspn(mime->content_type, ";"));
910 json_array_append_new(formats, json_string(buf_cstring(&txn->buf)));
911 }
912 buf_reset(&txn->buf);
913
914 /* Update lastmod */
915 lastmod = txn->resp_body.lastmod;
916 }
917
918 /* Output the JSON object */
919 return json_response(precond, txn, root, &resp);
920 }
921
922 /* Perform a leapseconds action */
action_leap(struct transaction_t * txn)923 static int action_leap(struct transaction_t *txn)
924 {
925 int r, ret = 0, precond;
926 struct resp_body_t *resp_body = &txn->resp_body;
927 struct zoneinfo info, leap;
928
929 /* Get info record from the database */
930 if ((r = zoneinfo_lookup_info(&info))) return HTTP_SERVER_ERROR;
931
932 /* Get leap record from the database */
933 if ((r = zoneinfo_lookup_leap(&leap))) {
934 ret = (r == CYRUSDB_NOTFOUND ? HTTP_NOT_FOUND : HTTP_SERVER_ERROR);
935 goto done;
936 }
937
938 /* Check any preconditions, including range request */
939 txn->flags.ranges = 1;
940 precond = check_precond(txn, leap.data->s, leap.dtstamp);
941
942 switch (precond) {
943 case HTTP_OK:
944 case HTTP_PARTIAL:
945 case HTTP_NOT_MODIFIED:
946 /* Fill in ETag, Last-Modified, and Expires */
947 resp_body->etag = leap.data->s;
948 resp_body->lastmod = leap.dtstamp;
949 resp_body->maxage = 86400; /* 24 hrs */
950 txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;
951 if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
952
953 if (precond != HTTP_NOT_MODIFIED) break;
954
955 GCC_FALLTHROUGH
956
957 default:
958 /* We failed a precondition - don't perform the request */
959 resp_body->type = NULL;
960 ret = precond;
961 goto done;
962 }
963
964
965 if (txn->meth != METH_HEAD) {
966 json_t *root, *expires, *leapseconds;
967 struct leapsec *leapsec;
968 char buf[1024];
969 int n;
970
971 if (!leap_seconds) {
972 ret = HTTP_NOT_FOUND;
973 goto done;
974 }
975
976 /* Construct our response */
977 root = json_pack("{s:s s:s s:s s:[]}",
978 "expires", "", "publisher", info.data->s,
979 "version", info.data->next->s, "leapseconds");
980 if (!root) {
981 txn->error.desc = "Unable to create JSON response";
982 ret = HTTP_SERVER_ERROR;
983 goto done;
984 }
985
986 expires = json_object_get(root, "expires");
987 leapseconds = json_object_get(root, "leapseconds");
988
989 leapsec = ptrarray_nth(leap_seconds, 0);
990 if (leapsec->t) {
991 time_to_rfc3339(leapsec->t, buf, 11 /* clip time */);
992 json_string_set(expires, buf);
993 }
994
995 for (n = 1; n < leap_seconds->count; n++) {
996 json_t *leap;
997
998 leapsec = ptrarray_nth(leap_seconds, n);
999 time_to_rfc3339(leapsec->t, buf, 11 /* clip time */);
1000 leap = json_pack("{s:i s:s}",
1001 "utc-offset", leapsec->sec, "onset", buf);
1002 json_array_append_new(leapseconds, leap);
1003 }
1004
1005 /* Output the JSON object */
1006 ret = json_response(precond, txn, root, NULL);
1007 }
1008
1009 done:
1010 freestrlist(leap.data);
1011 freestrlist(info.data);
1012 return ret;
1013 }
1014
1015
1016 struct list_rock {
1017 struct strlist *meta;
1018 json_t *tzarray;
1019 struct hash_table *tztable;
1020 };
1021
list_cb(const char * tzid,int tzidlen,struct zoneinfo * zi,void * rock)1022 static int list_cb(const char *tzid, int tzidlen,
1023 struct zoneinfo *zi, void *rock)
1024 {
1025 struct list_rock *lrock = (struct list_rock *) rock;
1026 char tzidbuf[200], etag[32], lastmod[RFC3339_DATETIME_MAX];
1027 json_t *tz;
1028
1029 snprintf(tzidbuf, sizeof(tzidbuf), "%.*s", tzidlen, tzid);
1030
1031 if (lrock->tztable) {
1032 if (hash_lookup(tzidbuf, lrock->tztable)) return 0;
1033 hash_insert(tzidbuf, (void *) 0xDEADBEEF, lrock->tztable);
1034 }
1035
1036 sprintf(etag, "%u-" TIME_T_FMT, strhash(tzidbuf), zi->dtstamp);
1037 time_to_rfc3339(zi->dtstamp, lastmod, RFC3339_DATETIME_MAX);
1038
1039 tz = json_pack("{s:s s:s s:s s:s s:s}",
1040 "tzid", tzidbuf, "etag", etag, "last-modified", lastmod,
1041 "publisher", lrock->meta->s, "version", lrock->meta->next->s);
1042 json_array_append_new(lrock->tzarray, tz);
1043
1044 if (zi->data) {
1045 struct strlist *sl;
1046 json_t *aliases = json_array();
1047
1048 json_object_set_new(tz, "aliases", aliases);
1049
1050 for (sl = zi->data; sl; sl = sl->next)
1051 json_array_append_new(aliases, json_string(sl->s));
1052 }
1053
1054 return 0;
1055 }
1056
1057 /* Perform a list action */
action_list(struct transaction_t * txn)1058 static int action_list(struct transaction_t *txn)
1059 {
1060 int r, ret, precond;
1061 struct strlist *param;
1062 const char *pattern = NULL;
1063 struct resp_body_t *resp_body = &txn->resp_body;
1064 struct zoneinfo info;
1065 time_t changedsince = 0, lastmod;
1066 double latitude = 99.9, longitude = 0.0;
1067 double altitude = 0.0, uncertainty = 0.0;
1068 strarray_t *geo_tzids = NULL;
1069 json_t *root = NULL;
1070
1071 /* Get info record from the database */
1072 if ((r = zoneinfo_lookup_info(&info))) return HTTP_SERVER_ERROR;
1073
1074 /* Sanity check the parameters */
1075 if ((param = hash_lookup("pattern", &txn->req_qparams))) {
1076 if (param->next /* once only */
1077 || !param->s || !*param->s /* not empty */
1078 || strspn(param->s, "*") == strlen(param->s)) { /* not (*)+ */
1079 return json_error_response(txn, TZ_INVALID_PATTERN, param, NULL);
1080 }
1081 pattern = param->s;
1082 }
1083 else if (geo_enabled &&
1084 (param = hash_lookup("location", &txn->req_qparams))) {
1085 /* Parse 'geo' URI */
1086 char *endptr;
1087
1088 if (param->next /* once only */
1089 || strncmp(param->s, "geo:", 4)) { /* value value */
1090 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1091 }
1092
1093 latitude = strtod(param->s + 4, &endptr);
1094 if (errno || *endptr != ','
1095 || latitude < -90.0 || latitude > 90.0) { /* valid value */
1096 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1097 }
1098
1099 longitude = strtod(++endptr, &endptr);
1100 if (errno || (*endptr && !strchr(",;", *endptr))
1101 || longitude < -180.0 || longitude > 180.0) { /* valid value */
1102 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1103 }
1104
1105 if (*endptr == ',') {
1106 altitude = strtod(++endptr, &endptr);
1107 if (*endptr && *endptr != ';') { /* valid value */
1108 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1109 }
1110 (void) altitude;
1111 }
1112
1113 if (!strncmp(endptr, ";crs=", 5)) {
1114 char *crs = endptr + 5;
1115 size_t len = strcspn(crs, ";");
1116
1117 if (len != 5 || strncmp(crs, "wgs84", 5)) { /* unsupported value */
1118 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1119 }
1120 endptr = crs + len;
1121 }
1122
1123 if (!strncmp(endptr, ";u=", 3)) {
1124 uncertainty = strtod(endptr + 3, &endptr);
1125 if (errno || uncertainty < 0) { /* valid value */
1126 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1127 }
1128 }
1129
1130 if (*endptr && *endptr != ';') { /* valid value */
1131 return json_error_response(txn, TZ_INVALID_LOCATION, param, NULL);
1132 }
1133 }
1134 else if ((param = hash_lookup("changedsince", &txn->req_qparams))) {
1135 unsigned prefix = 0;
1136
1137 if (param->next) { /* once only */
1138 return json_error_response(txn, TZ_INVALID_CHANGEDSINCE,
1139 param, NULL);
1140 }
1141
1142 /* Parse and sanity check the changedsince token */
1143 sscanf(param->s, "%u-" TIME_T_FMT, &prefix, &changedsince);
1144 if (prefix != synctoken_prefix || changedsince > info.dtstamp) {
1145 changedsince = 0;
1146 }
1147 }
1148 else if (hash_numrecords(&txn->req_qparams)) {
1149 return json_error_response(txn, TZ_INVALID_ACTION, NULL, NULL);
1150 }
1151
1152 /* Generate ETag & Last-Modified from info record */
1153 assert(!buf_len(&txn->buf));
1154 buf_printf(&txn->buf, "%u-" TIME_T_FMT, synctoken_prefix, info.dtstamp);
1155 lastmod = info.dtstamp;
1156
1157 /* Check any preconditions, including range request */
1158 txn->flags.ranges = 1;
1159 precond = check_precond(txn, buf_cstring(&txn->buf), lastmod);
1160
1161 switch (precond) {
1162 case HTTP_OK:
1163 case HTTP_PARTIAL:
1164 case HTTP_NOT_MODIFIED:
1165 /* Fill in ETag, Last-Modified, and Expires */
1166 resp_body->etag = buf_cstring(&txn->buf);
1167 resp_body->lastmod = lastmod;
1168 resp_body->maxage = 86400; /* 24 hrs */
1169 txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;
1170 if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
1171
1172 if (precond != HTTP_NOT_MODIFIED) break;
1173
1174 GCC_FALLTHROUGH
1175
1176 default:
1177 /* We failed a precondition - don't perform the request */
1178 resp_body->type = NULL;
1179 ret = precond;
1180 goto done;
1181 }
1182
1183
1184 if (txn->meth != METH_HEAD) {
1185 struct list_rock lrock = { NULL, NULL, NULL };
1186 struct hash_table tzids = HASH_TABLE_INITIALIZER;
1187 int i = 0;
1188
1189 /* Start constructing our response */
1190 root = json_pack("{s:s s:[]}",
1191 "synctoken", resp_body->etag, "timezones");
1192 if (!root) {
1193 txn->error.desc = "Unable to create JSON response";
1194 ret = HTTP_SERVER_ERROR;
1195 goto done;
1196 }
1197
1198 lrock.meta = info.data;
1199 lrock.tzarray = json_object_get(root, "timezones");
1200
1201 if (latitude <= 90) {
1202 geo_tzids = tzid_from_geo(txn, latitude, longitude, uncertainty);
1203 pattern = strarray_nth(geo_tzids, 0);
1204 if (!pattern) pattern = "/"; /* force lookup failure */
1205 }
1206
1207 if (pattern) {
1208 construct_hash_table(&tzids, 500, 1);
1209 lrock.tztable = &tzids;
1210 }
1211
1212 /* Add timezones to array */
1213 do {
1214 zoneinfo_find(pattern, !pattern, changedsince, &list_cb, &lrock);
1215
1216 } while (geo_tzids && (pattern = strarray_nth(geo_tzids, ++i)));
1217
1218 free_hash_table(&tzids, NULL);
1219 }
1220
1221 /* Output the JSON object */
1222 ret = json_response(precond, txn, root, NULL);
1223
1224 done:
1225 strarray_free(geo_tzids);
1226 freestrlist(info.data);
1227 return ret;
1228 }
1229
1230
check_tombstone(struct observance * tombstone,struct observance * obs)1231 static void check_tombstone(struct observance *tombstone,
1232 struct observance *obs)
1233 {
1234 if (icaltime_compare(obs->onset, tombstone->onset) > 0) {
1235 /* onset is closer to cutoff than existing tombstone */
1236 tombstone->name = icalmemory_tmp_copy(obs->name);
1237 tombstone->offset_from = tombstone->offset_to = obs->offset_to;
1238 tombstone->is_daylight = obs->is_daylight;
1239 tombstone->onset = obs->onset;
1240 }
1241 }
1242
1243 struct rdate {
1244 icalproperty *prop;
1245 struct icaldatetimeperiodtype date;
1246 };
1247
rdate_compare(const void * rdate1,const void * rdate2)1248 static int rdate_compare(const void *rdate1, const void *rdate2)
1249 {
1250 return icaltime_compare(((struct rdate *) rdate1)->date.time,
1251 ((struct rdate *) rdate2)->date.time);
1252 }
1253
observance_compare(const void * obs1,const void * obs2)1254 static int observance_compare(const void *obs1, const void *obs2)
1255 {
1256 return icaltime_compare(((struct observance *) obs1)->onset,
1257 ((struct observance *) obs2)->onset);
1258 }
1259
icalproperty_get_isstd_isgmt(icalproperty * prop,struct observance * obs)1260 static void icalproperty_get_isstd_isgmt(icalproperty *prop,
1261 struct observance *obs)
1262 {
1263 const char *time_type =
1264 icalproperty_get_parameter_as_string(prop, "X-OBSERVED-AT");
1265
1266 if (!time_type) time_type = "W";
1267
1268 switch (time_type[0]) {
1269 case 'G': case 'g':
1270 case 'U': case 'u':
1271 case 'Z': case 'z':
1272 obs->is_gmt = obs->is_std = 1;
1273 break;
1274 case 'S': case 's':
1275 obs->is_gmt = 0;
1276 obs->is_std = 1;
1277 break;
1278 case 'W': case 'w':
1279 default:
1280 obs->is_gmt = obs->is_std = 0;
1281 break;
1282 }
1283 }
1284
truncate_vtimezone(icalcomponent * vtz,icaltimetype * startp,icaltimetype * endp,icalarray * obsarray,struct observance ** proleptic,icalcomponent ** eternal_std,icalcomponent ** eternal_dst,icaltimetype * last_dtstart,int ms_compatible)1285 static void truncate_vtimezone(icalcomponent *vtz,
1286 icaltimetype *startp, icaltimetype *endp,
1287 icalarray *obsarray,
1288 struct observance **proleptic,
1289 icalcomponent **eternal_std,
1290 icalcomponent **eternal_dst,
1291 icaltimetype *last_dtstart,
1292 int ms_compatible)
1293 {
1294 icaltimetype start = *startp, end = *endp;
1295 icalcomponent *comp, *nextc, *tomb_std = NULL, *tomb_day = NULL;
1296 icalproperty *prop, *proleptic_prop = NULL;
1297 static struct observance tombstone;
1298 unsigned need_tomb = !icaltime_is_null_time(start);
1299 unsigned adjust_start = !icaltime_is_null_time(start);
1300 unsigned adjust_end = !icaltime_is_null_time(end);
1301
1302 if (last_dtstart) *last_dtstart = icaltime_null_time();
1303
1304 /* See if we have a proleptic tzname in VTIMEZONE */
1305 for (prop = icalcomponent_get_first_property(vtz, ICAL_X_PROPERTY);
1306 prop;
1307 prop = icalcomponent_get_next_property(vtz, ICAL_X_PROPERTY)) {
1308 if (!strcmp("X-PROLEPTIC-TZNAME", icalproperty_get_x_name(prop))) {
1309 proleptic_prop = prop;
1310 break;
1311 }
1312 }
1313
1314 memset(&tombstone, 0, sizeof(struct observance));
1315 tombstone.name = icalmemory_tmp_copy(proleptic_prop ?
1316 icalproperty_get_x(proleptic_prop) :
1317 "LMT");
1318 if (!proleptic_prop ||
1319 !icalproperty_get_parameter_as_string(prop, "X-NO-BIG-BANG"))
1320 tombstone.onset.year = -1;
1321
1322 /* Process each VTMEZONE STANDARD/DAYLIGHT subcomponent */
1323 for (comp = icalcomponent_get_first_component(vtz, ICAL_ANY_COMPONENT);
1324 comp; comp = nextc) {
1325 icalproperty *dtstart_prop = NULL, *rrule_prop = NULL;
1326 icalarray *rdate_array = icalarray_new(sizeof(struct rdate), 10);
1327 icaltimetype dtstart;
1328 struct observance obs;
1329 unsigned n, trunc_dtstart = 0;
1330 int r;
1331
1332 nextc = icalcomponent_get_next_component(vtz, ICAL_ANY_COMPONENT);
1333
1334 memset(&obs, 0, sizeof(struct observance));
1335 obs.offset_from = obs.offset_to = INT_MAX;
1336 obs.is_daylight = (icalcomponent_isa(comp) == ICAL_XDAYLIGHT_COMPONENT);
1337
1338 /* Grab the properties that we require to expand recurrences */
1339 for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY);
1340 prop;
1341 prop = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY)) {
1342
1343 switch (icalproperty_isa(prop)) {
1344 case ICAL_TZNAME_PROPERTY:
1345 obs.name = icalproperty_get_tzname(prop);
1346 break;
1347
1348 case ICAL_DTSTART_PROPERTY:
1349 dtstart_prop = prop;
1350 obs.onset = dtstart = icalproperty_get_dtstart(prop);
1351 icalproperty_get_isstd_isgmt(prop, &obs);
1352 if (last_dtstart && icaltime_compare(dtstart, *last_dtstart))
1353 *last_dtstart = dtstart;
1354 break;
1355
1356 case ICAL_TZOFFSETFROM_PROPERTY:
1357 obs.offset_from = icalproperty_get_tzoffsetfrom(prop);
1358 break;
1359
1360 case ICAL_TZOFFSETTO_PROPERTY:
1361 obs.offset_to = icalproperty_get_tzoffsetto(prop);
1362 break;
1363
1364 case ICAL_RRULE_PROPERTY:
1365 rrule_prop = prop;
1366 break;
1367
1368 case ICAL_RDATE_PROPERTY: {
1369 struct rdate rdate = { prop, icalproperty_get_rdate(prop) };
1370
1371 icalarray_append(rdate_array, &rdate);
1372 break;
1373 }
1374
1375 default:
1376 /* ignore all other properties */
1377 break;
1378 }
1379 }
1380
1381 /* We MUST have DTSTART, TZNAME, TZOFFSETFROM, and TZOFFSETTO */
1382 if (!dtstart_prop || !obs.name ||
1383 obs.offset_from == INT_MAX || obs.offset_to == INT_MAX) {
1384 icalarray_free(rdate_array);
1385 continue;
1386 }
1387
1388 /* Adjust DTSTART observance to UTC */
1389 icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from);
1390 icaltime_set_utc(&obs.onset, 1);
1391
1392 /* Check DTSTART vs window close */
1393 if (!icaltime_is_null_time(end) &&
1394 icaltime_compare(obs.onset, end) >= 0) {
1395 /* All observances occur on/after window close - remove component */
1396 icalcomponent_remove_component(vtz, comp);
1397 icalcomponent_free(comp);
1398
1399 /* Actual range end == request range end */
1400 adjust_end = 0;
1401
1402 /* Nothing else to do */
1403 icalarray_free(rdate_array);
1404 continue;
1405 }
1406
1407 /* Check DTSTART vs window open */
1408 r = icaltime_compare(obs.onset, start);
1409 if (r < 0) {
1410 /* DTSTART is prior to our window open - check it vs tombstone */
1411 if (need_tomb) check_tombstone(&tombstone, &obs);
1412
1413 /* Adjust it */
1414 trunc_dtstart = 1;
1415
1416 /* Actual range start == request range start */
1417 adjust_start = 0;
1418 }
1419 else {
1420 /* DTSTART is on/after our window open */
1421 if (r == 0) need_tomb = 0;
1422
1423 if (obsarray && !rrule_prop) {
1424 /* Add the DTSTART observance to our array */
1425 icalarray_append(obsarray, &obs);
1426 }
1427 }
1428
1429 if (rrule_prop) {
1430 struct icalrecurrencetype rrule =
1431 icalproperty_get_rrule(rrule_prop);
1432 icalrecur_iterator *ritr = NULL;
1433 unsigned eternal = icaltime_is_null_time(rrule.until);
1434 unsigned trunc_until = 0;
1435
1436 if (eternal) {
1437 if (obs.is_daylight) {
1438 if (eternal_dst) *eternal_dst = comp;
1439 }
1440 else if (eternal_std) *eternal_std = comp;
1441 }
1442
1443 /* Check RRULE duration */
1444 if (!eternal && icaltime_compare(rrule.until, start) < 0) {
1445 /* RRULE ends prior to our window open -
1446 check UNTIL vs tombstone */
1447 obs.onset = rrule.until;
1448 if (need_tomb) check_tombstone(&tombstone, &obs);
1449
1450 /* Remove RRULE */
1451 icalcomponent_remove_property(comp, rrule_prop);
1452 icalproperty_free(rrule_prop);
1453 }
1454 else {
1455 /* RRULE ends on/after our window open */
1456 if (!icaltime_is_null_time(end) &&
1457 (eternal || icaltime_compare(rrule.until, end) >= 0)) {
1458 /* RRULE ends after our window close - need to adjust it */
1459 trunc_until = 1;
1460 }
1461
1462 if (!eternal) {
1463 /* Adjust UNTIL to local time (for iterator) */
1464 icaltime_adjust(&rrule.until, 0, 0, 0, obs.offset_from);
1465 icaltime_set_utc(&rrule.until, 0);
1466 }
1467
1468 if (trunc_dtstart) {
1469 /* Bump RRULE start to 1 year prior to our window open */
1470 dtstart.year = start.year - 1;
1471 dtstart.month = start.month;
1472 dtstart.day = start.day;
1473 icaltime_normalize(dtstart);
1474 }
1475
1476 ritr = icalrecur_iterator_new(rrule, dtstart);
1477 }
1478
1479 /* Process any RRULE observances within our window */
1480 if (ritr) {
1481 icaltimetype recur, prev_onset;
1482
1483 /* Mark original DTSTART (UTC) */
1484 dtstart = obs.onset;
1485
1486 while (!icaltime_is_null_time(obs.onset = recur =
1487 icalrecur_iterator_next(ritr))) {
1488 unsigned ydiff;
1489
1490 /* Adjust observance to UTC */
1491 icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from);
1492 icaltime_set_utc(&obs.onset, 1);
1493
1494 if (trunc_until && icaltime_compare(obs.onset, end) >= 0) {
1495 /* Observance is on/after window close */
1496
1497 /* Actual range end == request range end */
1498 adjust_end = 0;
1499
1500 /* Check if DSTART is within 1yr of prev onset */
1501 ydiff = prev_onset.year - dtstart.year;
1502 if (ydiff <= 1) {
1503 /* Remove RRULE */
1504 icalcomponent_remove_property(comp, rrule_prop);
1505 icalproperty_free(rrule_prop);
1506
1507 if (ydiff) {
1508 /* Add previous onset as RDATE */
1509 struct icaldatetimeperiodtype rdate = {
1510 prev_onset,
1511 icalperiodtype_null_period()
1512 };
1513 prop = icalproperty_new_rdate(rdate);
1514 icalcomponent_add_property(comp, prop);
1515 }
1516 }
1517 else if (!eternal) {
1518 /* Set UNTIL to previous onset */
1519 rrule.until = prev_onset;
1520 icalproperty_set_rrule(rrule_prop, rrule);
1521 }
1522
1523 /* We're done */
1524 break;
1525 }
1526
1527 /* Check observance vs our window open */
1528 r = icaltime_compare(obs.onset, start);
1529 if (r < 0) {
1530 /* Observance is prior to our window open -
1531 check it vs tombstone */
1532 if (ms_compatible) {
1533 /* XXX We don't want to move DTSTART of the RRULE
1534 as Outlook/Exchange doesn't appear to like
1535 truncating the frontend of RRULEs */
1536 need_tomb = 0;
1537 trunc_dtstart = 0;
1538 if (proleptic_prop) {
1539 icalcomponent_remove_property(vtz,
1540 proleptic_prop);
1541 icalproperty_free(proleptic_prop);
1542 proleptic_prop = NULL;
1543 }
1544 }
1545 if (need_tomb) check_tombstone(&tombstone, &obs);
1546 }
1547 else {
1548 /* Observance is on/after our window open */
1549 if (r == 0) need_tomb = 0;
1550
1551 if (trunc_dtstart) {
1552 /* Make this observance the new DTSTART */
1553 icalproperty_set_dtstart(dtstart_prop, recur);
1554 dtstart = obs.onset;
1555 trunc_dtstart = 0;
1556
1557 if (last_dtstart &&
1558 icaltime_compare(dtstart, *last_dtstart) > 0) {
1559 *last_dtstart = dtstart;
1560 }
1561
1562 /* Check if new DSTART is within 1yr of UNTIL */
1563 ydiff = rrule.until.year - recur.year;
1564 if (!trunc_until && ydiff <= 1) {
1565 /* Remove RRULE */
1566 icalcomponent_remove_property(comp, rrule_prop);
1567 icalproperty_free(rrule_prop);
1568
1569 if (ydiff) {
1570 /* Add UNTIL as RDATE */
1571 struct icaldatetimeperiodtype rdate = {
1572 rrule.until,
1573 icalperiodtype_null_period()
1574 };
1575 prop = icalproperty_new_rdate(rdate);
1576 icalcomponent_add_property(comp, prop);
1577 }
1578 }
1579 }
1580
1581 if (obsarray) {
1582 /* Add the observance to our array */
1583 icalarray_append(obsarray, &obs);
1584 }
1585 else if (!trunc_until) {
1586 /* We're done */
1587 break;
1588 }
1589 }
1590 prev_onset = obs.onset;
1591 }
1592 icalrecur_iterator_free(ritr);
1593 }
1594 }
1595
1596 /* Sort the RDATEs by onset */
1597 icalarray_sort(rdate_array, &rdate_compare);
1598
1599 /* Check RDATEs */
1600 for (n = 0; n < rdate_array->num_elements; n++) {
1601 struct rdate *rdate = icalarray_element_at(rdate_array, n);
1602
1603 if (n == 0 && icaltime_compare(rdate->date.time, dtstart) == 0) {
1604 /* RDATE is same as DTSTART - remove it */
1605 icalcomponent_remove_property(comp, rdate->prop);
1606 icalproperty_free(rdate->prop);
1607 continue;
1608 }
1609
1610 obs.onset = rdate->date.time;
1611 icalproperty_get_isstd_isgmt(rdate->prop, &obs);
1612
1613 /* Adjust observance to UTC */
1614 icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from);
1615 icaltime_set_utc(&obs.onset, 1);
1616
1617 if (!icaltime_is_null_time(end) &&
1618 icaltime_compare(obs.onset, end) >= 0) {
1619 /* RDATE is after our window close - remove it */
1620 icalcomponent_remove_property(comp, rdate->prop);
1621 icalproperty_free(rdate->prop);
1622
1623 /* Actual range end == request range end */
1624 adjust_end = 0;
1625
1626 continue;
1627 }
1628
1629 r = icaltime_compare(obs.onset, start);
1630 if (r < 0) {
1631 /* RDATE is prior to window open - check it vs tombstone */
1632 if (need_tomb) check_tombstone(&tombstone, &obs);
1633
1634 /* Remove it */
1635 icalcomponent_remove_property(comp, rdate->prop);
1636 icalproperty_free(rdate->prop);
1637
1638 /* Actual range start == request range start */
1639 adjust_start = 0;
1640 }
1641 else {
1642 /* RDATE is on/after our window open */
1643 if (r == 0) need_tomb = 0;
1644
1645 if (trunc_dtstart) {
1646 /* Make this RDATE the new DTSTART */
1647 icalproperty_set_dtstart(dtstart_prop,
1648 rdate->date.time);
1649 trunc_dtstart = 0;
1650
1651 icalcomponent_remove_property(comp, rdate->prop);
1652 icalproperty_free(rdate->prop);
1653 }
1654
1655 if (obsarray) {
1656 /* Add the observance to our array */
1657 icalarray_append(obsarray, &obs);
1658 }
1659 }
1660 }
1661 icalarray_free(rdate_array);
1662
1663 /* Final check */
1664 if (trunc_dtstart) {
1665 /* All observances in comp occur prior to window open, remove it
1666 unless we haven't saved a tombstone comp of this type yet */
1667 if (icalcomponent_isa(comp) == ICAL_XDAYLIGHT_COMPONENT) {
1668 if (!tomb_day) {
1669 tomb_day = comp;
1670 comp = NULL;
1671 }
1672 }
1673 else if (!tomb_std) {
1674 tomb_std = comp;
1675 comp = NULL;
1676 }
1677
1678 if (comp) {
1679 icalcomponent_remove_component(vtz, comp);
1680 icalcomponent_free(comp);
1681 }
1682 }
1683 }
1684
1685 if (need_tomb && !icaltime_is_null_time(tombstone.onset)) {
1686 /* Need to add tombstone component/observance starting at window open
1687 as long as its not prior to start of TZ data */
1688 icalcomponent *tomb;
1689 icalproperty *prop, *nextp;
1690
1691 if (obsarray) {
1692 /* Add the tombstone to our array */
1693 tombstone.onset = start;
1694 tombstone.is_gmt = tombstone.is_std = 1;
1695 icalarray_append(obsarray, &tombstone);
1696 }
1697
1698 /* Determine which tombstone component we need */
1699 if (tombstone.is_daylight) {
1700 tomb = tomb_day;
1701 tomb_day = NULL;
1702 }
1703 else {
1704 tomb = tomb_std;
1705 tomb_std = NULL;
1706 }
1707
1708 /* Set property values on our tombstone */
1709 for (prop = icalcomponent_get_first_property(tomb, ICAL_ANY_PROPERTY);
1710 prop; prop = nextp) {
1711
1712 nextp = icalcomponent_get_next_property(tomb, ICAL_ANY_PROPERTY);
1713
1714 switch (icalproperty_isa(prop)) {
1715 case ICAL_TZNAME_PROPERTY:
1716 icalproperty_set_tzname(prop, tombstone.name);
1717 break;
1718 case ICAL_TZOFFSETFROM_PROPERTY:
1719 icalproperty_set_tzoffsetfrom(prop, tombstone.offset_from);
1720 break;
1721 case ICAL_TZOFFSETTO_PROPERTY:
1722 icalproperty_set_tzoffsetto(prop, tombstone.offset_to);
1723 break;
1724 case ICAL_DTSTART_PROPERTY:
1725 /* Adjust window open to local time */
1726 icaltime_adjust(&start, 0, 0, 0, tombstone.offset_from);
1727 icaltime_set_utc(&start, 0);
1728
1729 icalproperty_set_dtstart(prop, start);
1730 break;
1731 default:
1732 icalcomponent_remove_property(tomb, prop);
1733 icalproperty_free(prop);
1734 break;
1735 }
1736 }
1737
1738 /* Remove X-PROLEPTIC-TZNAME as it no longer applies */
1739 if (proleptic_prop) {
1740 icalcomponent_remove_property(vtz, proleptic_prop);
1741 icalproperty_free(proleptic_prop);
1742 }
1743 }
1744
1745 /* Remove any unused tombstone components */
1746 if (tomb_std) {
1747 icalcomponent_remove_component(vtz, tomb_std);
1748 icalcomponent_free(tomb_std);
1749 }
1750 if (tomb_day) {
1751 icalcomponent_remove_component(vtz, tomb_day);
1752 icalcomponent_free(tomb_day);
1753 }
1754
1755 if (obsarray) {
1756 struct observance *obs;
1757
1758 /* Sort the observances by onset */
1759 icalarray_sort(obsarray, &observance_compare);
1760
1761 /* Set offset_to for tombstone, if necessary */
1762 obs = icalarray_element_at(obsarray, 0);
1763 if (!tombstone.offset_to) tombstone.offset_to = obs->offset_from;
1764
1765 /* Adjust actual range if necessary */
1766 if (adjust_start) {
1767 *startp = obs->onset;
1768 }
1769 if (adjust_end) {
1770 obs = icalarray_element_at(obsarray, obsarray->num_elements-1);
1771 *endp = obs->onset;
1772 icaltime_adjust(endp, 0, 0, 0, 1);
1773 }
1774 }
1775
1776 if (proleptic) *proleptic = &tombstone;
1777 }
1778
1779 /* Perform a get action */
action_get(struct transaction_t * txn)1780 static int action_get(struct transaction_t *txn)
1781 {
1782 int r, precond;
1783 struct strlist *param;
1784 const char *tzid = txn->req_tgt.resource;
1785 struct zoneinfo zi;
1786 time_t lastmod;
1787 icaltimetype start = icaltime_null_time(), end = icaltime_null_time();
1788 char *data = NULL;
1789 unsigned long datalen = 0;
1790 struct resp_body_t *resp_body = &txn->resp_body;
1791 struct mime_type_t *mime = NULL;
1792 const char **hdr;
1793
1794 /* Check/find requested MIME type:
1795 1st entry in gparams->mime_types array MUST be default MIME type */
1796 if ((param = hash_lookup("format", &txn->req_qparams))) {
1797 for (mime = tz_mime_types;
1798 mime->content_type && !is_mediatype(mime->content_type, param->s);
1799 mime++);
1800 }
1801 else if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
1802 mime = get_accept_type(hdr, tz_mime_types);
1803 else mime = tz_mime_types;
1804
1805 if (!mime || !mime->content_type)
1806 return json_error_response(txn, TZ_INVALID_FORMAT, NULL, NULL);
1807
1808 /* Sanity check the parameters */
1809 if ((param = hash_lookup("start", &txn->req_qparams))) {
1810 start = icaltime_from_string(param->s);
1811 if (param->next || !icaltime_is_utc(start)) { /* once only, UTC */
1812 return json_error_response(txn, TZ_INVALID_START, param, &start);
1813 }
1814 }
1815
1816 if ((param = hash_lookup("end", &txn->req_qparams))) {
1817 end = icaltime_from_string(param->s);
1818 if (param->next || !icaltime_is_utc(end) /* once only, UTC */
1819 || icaltime_compare(end, start) <= 0) { /* end MUST be > start */
1820 return json_error_response(txn, TZ_INVALID_END, param, &end);
1821 }
1822 }
1823
1824 /* Get info record from the database */
1825 if ((r = zoneinfo_lookup(tzid, &zi))) {
1826 return (r == CYRUSDB_NOTFOUND ?
1827 json_error_response(txn, TZ_NOT_FOUND, NULL, NULL)
1828 : HTTP_SERVER_ERROR);
1829 }
1830
1831 /* Generate ETag & Last-Modified from info record */
1832 assert(!buf_len(&txn->buf));
1833 buf_printf(&txn->buf, "%u-" TIME_T_FMT, strhash(tzid), zi.dtstamp);
1834 lastmod = zi.dtstamp;
1835 freestrlist(zi.data);
1836
1837 /* Check any preconditions, including range request */
1838 txn->flags.ranges = 1;
1839 precond = check_precond(txn, buf_cstring(&txn->buf), lastmod);
1840
1841 switch (precond) {
1842 case HTTP_OK:
1843 case HTTP_PARTIAL:
1844 case HTTP_NOT_MODIFIED:
1845 /* Fill in Content-Type, ETag, Last-Modified, and Expires */
1846 resp_body->type = mime->content_type;
1847 resp_body->etag = buf_cstring(&txn->buf);
1848 resp_body->lastmod = lastmod;
1849 resp_body->maxage = 86400; /* 24 hrs */
1850 txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;
1851 if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
1852
1853 if (precond != HTTP_NOT_MODIFIED) break;
1854
1855 GCC_FALLTHROUGH
1856
1857 default:
1858 /* We failed a precondition - don't perform the request */
1859 resp_body->type = NULL;
1860 return precond;
1861 }
1862
1863
1864 if (txn->meth != METH_HEAD) {
1865 static struct buf pathbuf = BUF_INITIALIZER;
1866 const char *p, *path, *proto, *host, *msg_base = NULL;
1867 size_t msg_size = 0;
1868 icalcomponent *ical, *vtz;
1869 icalproperty *prop;
1870 struct buf *buf = NULL;
1871 int fd;
1872
1873 /* Open, mmap, and parse the file */
1874 buf_reset(&pathbuf);
1875 buf_printf(&pathbuf, "%s/%s.ics", zoneinfo_dir, tzid);
1876 path = buf_cstring(&pathbuf);
1877 if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR;
1878
1879 map_refresh(fd, 1, &msg_base, &msg_size, MAP_UNKNOWN_LEN, path, NULL);
1880 if (!msg_base) return HTTP_SERVER_ERROR;
1881
1882 ical = icalparser_parse_string(msg_base);
1883 map_free(&msg_base, &msg_size);
1884 close(fd);
1885
1886 vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
1887 prop = icalcomponent_get_first_property(vtz, ICAL_TZID_PROPERTY);
1888
1889 if ((zi.type == ZI_LINK) &&
1890 !icalcomponent_get_first_property(vtz, ICAL_TZIDALIASOF_PROPERTY)) {
1891 /* Add TZID-ALIAS-OF */
1892 const char *aliasof = icalproperty_get_tzid(prop);
1893 icalproperty *atzid = icalproperty_new_tzidaliasof(aliasof);
1894
1895 icalcomponent_add_property(vtz, atzid);
1896
1897 /* Substitute TZID alias */
1898 icalproperty_set_tzid(prop, tzid);
1899 }
1900
1901 /* Start constructing TZURL */
1902 buf_reset(&pathbuf);
1903 http_proto_host(txn->req_hdrs, &proto, &host);
1904 buf_printf(&pathbuf, "%s://%s%s/zones/",
1905 proto, host, namespace_tzdist.prefix);
1906
1907 /* Escape '/' and ' ' in tzid */
1908 for (p = tzid; *p; p++) {
1909 switch (*p) {
1910 case '/':
1911 case ' ':
1912 buf_printf(&pathbuf, "%%%02X", *p);
1913 break;
1914
1915 default:
1916 buf_putc(&pathbuf, *p);
1917 break;
1918 }
1919 }
1920
1921 if (!icaltime_is_null_time(start) || !icaltime_is_null_time(end)) {
1922
1923 if (!icaltime_is_null_time(end)) {
1924 /* Add TZUNTIL to VTIMEZONE */
1925 icalproperty *tzuntil = icalproperty_new_tzuntil(end);
1926 icalcomponent_add_property(vtz, tzuntil);
1927 }
1928
1929 /* Add truncation parameter(s) to TZURL */
1930 buf_printf(&pathbuf, "?%s", URI_QUERY(txn->req_uri));
1931
1932 if (!strncmp(mime->content_type, "application/tzif", 16)) {
1933 /* Truncate and convert the VTIMEZONE */
1934 bit32 leapcnt = 0;
1935
1936 if (!strcmp(mime->content_type + 16, "-leap"))
1937 leapcnt = leap_seconds->count - 2;
1938
1939 buf =_icaltimezone_as_tzif(ical, leapcnt, &start, &end);
1940 }
1941 else {
1942 /* Truncate the VTIMEZONE */
1943 truncate_vtimezone(vtz, &start, &end, NULL, NULL,
1944 NULL, NULL, NULL, 0);
1945 }
1946 }
1947
1948 /* Set TZURL property */
1949 prop = icalproperty_new_tzurl(buf_cstring(&pathbuf));
1950 icalcomponent_add_property(vtz, prop);
1951
1952 /* Convert to requested MIME type */
1953 if (!buf) buf = mime->from_object(ical);
1954 datalen = buf_len(buf);
1955 data = buf_release(buf);
1956 buf_destroy(buf);
1957
1958 /* Set Content-Disposition filename */
1959 buf_setcstr(&pathbuf, tzid);
1960 if (mime->file_ext) buf_printf(&pathbuf, ".%s", mime->file_ext);
1961 resp_body->dispo.fname = buf_cstring(&pathbuf);
1962
1963 txn->flags.vary |= VARY_ACCEPT;
1964
1965 icalcomponent_free(ical);
1966 }
1967
1968 write_body(precond, txn, data, datalen);
1969
1970 if (data) free(data);
1971
1972 return 0;
1973 }
1974
1975
1976 #define CTIME_FMT "%s %s %2d %02d:%02d:%02d %4d"
1977 #define CTIME_ARGS(tt) \
1978 wday[icaltime_day_of_week(tt)-1], monthname[tt.month-1], \
1979 tt.day, tt.hour, tt.minute, tt.second, tt.year
1980
1981
1982 /* Perform an expand action */
action_expand(struct transaction_t * txn)1983 static int action_expand(struct transaction_t *txn)
1984 {
1985 int r, precond, zdump = 0;
1986 struct strlist *param;
1987 const char *tzid = txn->req_tgt.resource;
1988 struct zoneinfo zi;
1989 time_t lastmod;
1990 icaltimetype start, end;
1991 struct resp_body_t *resp_body = &txn->resp_body;
1992 json_t *root = NULL;
1993
1994 /* Sanity check the parameters */
1995 param = hash_lookup("start", &txn->req_qparams);
1996 if (!param || param->next) /* mandatory, once only */
1997 return json_error_response(txn, TZ_INVALID_START, param, NULL);
1998
1999 start = icaltime_from_string(param->s);
2000 if (!icaltime_is_utc(start)) /* MUST be UTC */
2001 return json_error_response(txn, TZ_INVALID_START, param, &start);
2002
2003 param = hash_lookup("end", &txn->req_qparams);
2004 if (!param || param->next) /* mandatory, once only */
2005 return json_error_response(txn, TZ_INVALID_END, param, NULL);
2006
2007 end = icaltime_from_string(param->s);
2008 if (!icaltime_is_utc(end) /* MUST be UTC */
2009 || icaltime_compare(end, start) <= 0) { /* end MUST be > start */
2010 return json_error_response(txn, TZ_INVALID_END, param, &end);
2011 }
2012
2013 /* Check requested format (debugging only) */
2014 if ((param = hash_lookup("format", &txn->req_qparams)) &&
2015 !strcmp(param->s, "application/zdump")) {
2016 /* Mimic zdump(8) -V output for comparison:
2017
2018 For each zonename, print the times both one second before and
2019 exactly at each detected time discontinuity, the time at one day
2020 less than the highest possible time value, and the time at the
2021 highest possible time value. Each line is followed by isdst=D
2022 where D is positive, zero, or negative depending on whether the
2023 given time is daylight saving time, standard time, or an unknown
2024 time type, respectively. Each line is also followed by gmtoff=N
2025 if the given local time is known to be N seconds east of Green‐
2026 wich.
2027 */
2028 zdump = 1;
2029 }
2030
2031 /* Get info record from the database */
2032 if ((r = zoneinfo_lookup(tzid, &zi))) {
2033 return (r == CYRUSDB_NOTFOUND ?
2034 json_error_response(txn, TZ_NOT_FOUND, NULL, NULL)
2035 : HTTP_SERVER_ERROR);
2036 }
2037
2038 /* Generate ETag & Last-Modified from info record */
2039 assert(!buf_len(&txn->buf));
2040 buf_printf(&txn->buf, "%u-" TIME_T_FMT, strhash(tzid), zi.dtstamp);
2041 lastmod = zi.dtstamp;
2042 freestrlist(zi.data);
2043
2044 /* Check any preconditions, including range request */
2045 txn->flags.ranges = 1;
2046 precond = check_precond(txn, buf_cstring(&txn->buf), lastmod);
2047
2048 switch (precond) {
2049 case HTTP_OK:
2050 case HTTP_PARTIAL:
2051 case HTTP_NOT_MODIFIED:
2052 /* Fill in ETag, Last-Modified, and Expires */
2053 resp_body->etag = buf_cstring(&txn->buf);
2054 resp_body->lastmod = lastmod;
2055 resp_body->maxage = 86400; /* 24 hrs */
2056 txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;
2057 if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
2058
2059 if (precond != HTTP_NOT_MODIFIED) break;
2060
2061 GCC_FALLTHROUGH
2062
2063 default:
2064 /* We failed a precondition - don't perform the request */
2065 resp_body->type = NULL;
2066 return precond;
2067 }
2068
2069
2070 if (txn->meth != METH_HEAD) {
2071 static struct buf pathbuf = BUF_INITIALIZER;
2072 const char *path, *msg_base = NULL;
2073 size_t msg_size = 0;
2074 icalcomponent *ical, *vtz;
2075 struct observance *proleptic;
2076 icalarray *obsarray;
2077 json_t *jobsarray;
2078 unsigned n;
2079 int fd;
2080
2081 /* Open, mmap, and parse the file */
2082 buf_reset(&pathbuf);
2083 buf_printf(&pathbuf, "%s/%s.ics", zoneinfo_dir, tzid);
2084 path = buf_cstring(&pathbuf);
2085 if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR;
2086
2087 map_refresh(fd, 1, &msg_base, &msg_size, MAP_UNKNOWN_LEN, path, NULL);
2088 if (!msg_base) return HTTP_SERVER_ERROR;
2089
2090 ical = icalparser_parse_string(msg_base);
2091 map_free(&msg_base, &msg_size);
2092 close(fd);
2093
2094
2095 /* Create an array of observances */
2096 obsarray = icalarray_new(sizeof(struct observance), 20);
2097 vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
2098 truncate_vtimezone(vtz, &start, &end, obsarray, &proleptic,
2099 NULL, NULL, NULL, 0);
2100
2101 if (zdump) {
2102 struct buf *body = &txn->resp_body.payload;
2103 struct icaldurationtype off = icaldurationtype_null_duration();
2104 const char *prev_name = proleptic->name;
2105 int prev_isdst = proleptic->is_daylight;
2106
2107 for (n = 0; n < obsarray->num_elements; n++) {
2108 struct observance *obs = icalarray_element_at(obsarray, n);
2109 struct icaltimetype local, ut;
2110
2111 /* Skip any no-ops as zdump doesn't output them */
2112 if (obs->offset_from == obs->offset_to
2113 && prev_isdst == obs->is_daylight
2114 && !strcmp(prev_name, obs->name)) continue;
2115
2116 /* UT and local time 1 second before onset */
2117 off.seconds = -1;
2118 ut = icaltime_add(obs->onset, off);
2119
2120 off.seconds = obs->offset_from;
2121 local = icaltime_add(ut, off);
2122
2123 buf_printf(body,
2124 "%s " CTIME_FMT " UT = " CTIME_FMT " %s"
2125 " isdst=%d gmtoff=%d\n",
2126 tzid, CTIME_ARGS(ut), CTIME_ARGS(local),
2127 prev_name, prev_isdst, obs->offset_from);
2128
2129 /* UT and local time at onset */
2130 icaltime_adjust(&ut, 0, 0, 0, 1);
2131
2132 off.seconds = obs->offset_to;
2133 local = icaltime_add(ut, off);
2134
2135 buf_printf(body,
2136 "%s " CTIME_FMT " UT = " CTIME_FMT " %s"
2137 " isdst=%d gmtoff=%d\n",
2138 tzid, CTIME_ARGS(ut), CTIME_ARGS(local),
2139 obs->name, obs->is_daylight, obs->offset_to);
2140
2141 prev_name = obs->name;
2142 prev_isdst = obs->is_daylight;
2143 }
2144 }
2145 else {
2146 /* Start constructing our response */
2147 root = json_pack("{s:s}", "tzid", tzid);
2148 if (!root) {
2149 txn->error.desc = "Unable to create JSON response";
2150 return HTTP_SERVER_ERROR;
2151 }
2152
2153 json_object_set_new(root, "start",
2154 json_string(icaltime_as_iso_string(start)));
2155 json_object_set_new(root, "end",
2156 json_string(icaltime_as_iso_string(end)));
2157
2158 /* Add observances to JSON array */
2159 jobsarray = json_array();
2160 for (n = 0; n < obsarray->num_elements; n++) {
2161 struct observance *obs = icalarray_element_at(obsarray, n);
2162
2163 json_array_append_new(jobsarray,
2164 json_pack(
2165 "{s:s s:s s:i s:i}",
2166 "name", obs->name,
2167 "onset",
2168 icaltime_as_iso_string(obs->onset),
2169 "utc-offset-from", obs->offset_from,
2170 "utc-offset-to", obs->offset_to));
2171 }
2172 json_object_set_new(root, "observances", jobsarray);
2173 }
2174 icalarray_free(obsarray);
2175
2176 icalcomponent_free(ical);
2177 }
2178
2179 if (zdump) {
2180 struct resp_body_t *body = &txn->resp_body;
2181
2182 body->type = "text/plain; charset=us-ascii";
2183
2184 write_body(precond, txn,
2185 buf_cstring(&body->payload), buf_len(&body->payload));
2186
2187 return 0;
2188 }
2189 else {
2190 /* Output the JSON object */
2191 return json_response(precond, txn, root, NULL);
2192 }
2193 }
2194
2195
json_response(int code,struct transaction_t * txn,json_t * root,char ** resp)2196 static int json_response(int code, struct transaction_t *txn, json_t *root,
2197 char **resp)
2198 {
2199 size_t flags = JSON_PRESERVE_ORDER;
2200 static char *buf = NULL; /* keep generated data until next call */
2201 char *json = NULL;
2202
2203 free(buf);
2204
2205 if (root) {
2206 /* Dump JSON object into a text buffer */
2207 flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
2208 json = buf = json_dumps(root, flags);
2209 json_decref(root);
2210
2211 if (!buf) {
2212 txn->error.desc = "Error dumping JSON object";
2213 return HTTP_SERVER_ERROR;
2214 }
2215 else if (resp) {
2216 if (*resp) free(*resp);
2217 *resp = buf;
2218 buf = NULL;
2219 }
2220 }
2221 else if (resp) json = *resp;
2222
2223 /* Output the JSON object */
2224 if (code == HTTP_OK)
2225 txn->resp_body.type = "application/json; charset=utf-8";
2226 else
2227 txn->resp_body.type = "application/problem+json; charset=utf-8";
2228 write_body(code, txn, json, json ? strlen(json) : 0);
2229
2230 return 0;
2231 }
2232
2233
2234 /* Array of parameter names - MUST be kept in sync with tz_err.et */
2235 static const char *param_names[] = {
2236 "action",
2237 "pattern",
2238 "format",
2239 "start",
2240 "end",
2241 "changedsince",
2242 "latitude",
2243 "longitude",
2244 "tzid"
2245 };
2246
json_error_response(struct transaction_t * txn,long tz_code,struct strlist * param,icaltimetype * time)2247 static int json_error_response(struct transaction_t *txn, long tz_code,
2248 struct strlist *param, icaltimetype *time)
2249 {
2250 long http_code = HTTP_BAD_REQUEST;
2251 const char *param_name, *fmt = NULL;
2252 json_t *root;
2253
2254 param_name = param_names[tz_code - tz_err_base];
2255
2256 if (!param) {
2257 switch (tz_code) {
2258 case TZ_INVALID_ACTION:
2259 fmt = "Request URI doesn't map to a known action";
2260 break;
2261
2262 case TZ_INVALID_FORMAT:
2263 http_code = HTTP_NOT_ACCEPTABLE;
2264 fmt = "Unsupported media type";
2265 break;
2266
2267 case TZ_NOT_FOUND:
2268 http_code = HTTP_NOT_FOUND;
2269 fmt = "Time zone identifier not found";
2270 break;
2271
2272 default:
2273 fmt = "Missing %s parameter";
2274 break;
2275 }
2276 }
2277 else if (param->next) fmt = "Multiple %s parameters";
2278 else if (!param->s || !param->s[0]) fmt = "Missing %s value";
2279 else if (!time) fmt = "Invalid %s value";
2280 else if (!icaltime_is_utc(*time)) fmt = "Invalid %s UTC value";
2281 else fmt = "End date-time <= start date-time";
2282
2283 assert(!buf_len(&txn->buf));
2284 buf_printf(&txn->buf, fmt, param_name);
2285
2286 root = json_pack("{s:s s:s s:i}", "title", buf_cstring(&txn->buf),
2287 "type", error_message(tz_code),
2288 "status", atoi(error_message(http_code)));
2289 if (!root) {
2290 txn->error.desc = "Unable to create JSON response";
2291 return HTTP_SERVER_ERROR;
2292 }
2293
2294 return json_response(http_code, txn, root, NULL);
2295 }
2296
2297
2298 #ifndef BIG_BANG
2299 #define BIG_BANG (- (1LL << 59)) /* from zic.c */
2300 #endif
2301
2302 #ifndef INT32_MAX
2303 #define INT32_MAX 0x7fffffff
2304 #endif
2305 #ifndef INT32_MIN
2306 #define INT32_MIN (-INT32_MAX - 1)
2307 #endif
2308
2309 #define NUM_LEAP_DAYS(y) ((y-1) / 4 - (y-1) / 100 + (y-1) / 400)
2310 #define NUM_YEAR_DAYS(y) (365 * y + NUM_LEAP_DAYS(y))
2311
2312 /* Day of year offsets for each month. Second array is for leap years. */
2313 static const int month_doy_offsets[2][12] = {
2314 /* jan feb mar apr may jun jul aug sep oct nov dec */
2315 { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 },
2316 { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 }
2317 };
2318
2319 /* Convert icaltimetype to 64-bit time_t. 0 = Jan 1 00:00:00 1970 UTC */
icaltime_to_gmtime64(const struct icaltimetype tt)2320 static long long int icaltime_to_gmtime64(const struct icaltimetype tt)
2321 {
2322 long long int days;
2323
2324 days = NUM_YEAR_DAYS(tt.year) - NUM_YEAR_DAYS(1970);
2325 days += month_doy_offsets[icaltime_is_leap_year(tt.year)][tt.month - 1];
2326 days += tt.day - 1;
2327
2328 return (((days * 24 + tt.hour) * 60 + tt.minute) * 60 + tt.second);
2329 }
2330
2331 struct ttinfo {
2332 long int offset; /* offset from GMT */
2333 unsigned char isdst; /* transition time is for DST */
2334 unsigned char idx; /* index into 'abbrev' buffer */
2335 unsigned char isstd; /* transition time is in standard time */
2336 unsigned char isgmt; /* transition time is in GMT */
2337 };
2338
set_ttinfo(struct ttinfo * ttinfo,const struct observance * obs,unsigned char idx)2339 static void set_ttinfo(struct ttinfo *ttinfo,
2340 const struct observance *obs, unsigned char idx)
2341 {
2342 ttinfo->offset = obs->offset_to;
2343 ttinfo->isdst = obs->is_daylight;
2344 ttinfo->isstd = obs->is_std;
2345 ttinfo->isgmt = obs->is_gmt;
2346 ttinfo->idx = idx;
2347 }
2348
buf_append_utcoffset_as_iso_string(struct buf * buf,int off)2349 static void buf_append_utcoffset_as_iso_string(struct buf *buf, int off)
2350 {
2351 int h, m, s;
2352
2353 h = -off/3600;
2354 m = (abs(off) % 3600) / 60;
2355 s = abs(off) % 60;
2356 buf_printf(buf, "%d", h);
2357 if (m || s) buf_printf(buf, ":%02d", m);
2358 if (s) buf_printf(buf, ":%02d", s);
2359 }
2360
2361 /* Generate a POSIX rule from iCal RRULE.
2362 We assume that the RRULE parts are sane for a VTIMEZONE
2363 and all rules refer to a day of week in a single month. */
buf_append_rrule_as_posix_string(struct buf * buf,icalcomponent * comp)2364 static unsigned buf_append_rrule_as_posix_string(struct buf *buf,
2365 icalcomponent *comp)
2366 {
2367 icalproperty *prop;
2368 icaltimetype at;
2369 struct icalrecurrencetype rrule;
2370 unsigned ver = '2';
2371 int hour;
2372
2373 prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
2374 rrule = icalproperty_get_rrule(prop);
2375
2376 #ifdef HAVE_RSCALE
2377 if (rrule.rscale && strcasecmp(rrule.rscale, "GREGORIAN")) {
2378 /* POSIX rules are based on Gregorian calendar only */
2379 return 0;
2380 }
2381 #endif
2382
2383 prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
2384 at = icalproperty_get_dtstart(prop);
2385 hour = at.hour;
2386
2387 if (rrule.by_day[0] == ICAL_RECURRENCE_ARRAY_MAX) {
2388 /* date - Julian yday */
2389 buf_printf(buf, ",J%u", month_doy_offsets[0][at.month - 1] + at.day);
2390 }
2391 else {
2392 /* BYDAY */
2393 unsigned month;
2394 int week = icalrecurrencetype_day_position(rrule.by_day[0]);
2395 int wday = icalrecurrencetype_day_day_of_week(rrule.by_day[0]);
2396 int yday = rrule.by_year_day[0];
2397
2398 if (yday != ICAL_RECURRENCE_ARRAY_MAX) {
2399 /* BYYEARDAY */
2400
2401 if (yday >= 0 || hour) {
2402 /* Bogus? Either way, we can't handle this */
2403 return 0;
2404 }
2405
2406 /* Rewrite as last (wday-1) @ 24:00 */
2407 week = -1;
2408 wday--;
2409 hour = 24;
2410
2411 /* Find month that contains this yday */
2412 yday += 365;
2413 for (month = 0; month < 12; month++) {
2414 if (yday <= month_doy_offsets[0][month]) break;
2415 }
2416 }
2417 else {
2418 /* BYMONTH */
2419 int mday = rrule.by_month_day[0];
2420
2421 month = rrule.by_month[0];
2422
2423 if (mday != ICAL_RECURRENCE_ARRAY_MAX) {
2424 /* MONTHDAY: wday >= mday */
2425
2426 /* Need to use an extension to POSIX: -167 <= hour <= 167 */
2427 ver = '3';
2428
2429 if (mday + 7 == icaltime_days_in_month(month, 0)) {
2430 /* Rewrite as last (wday+1) @ hour < 0 */
2431 week = -1;
2432 wday++;
2433 hour -= 24;
2434 }
2435 else {
2436 /* Rewrite as nth (wday-offset) @ hour > 24 */
2437 unsigned mday_offset;
2438
2439 week = (mday - 1) / 7 + 1;
2440 mday_offset = mday - ((week - 1) * 7 + 1);
2441 wday -= mday_offset;
2442 hour += 24 * mday_offset;
2443 }
2444 }
2445 }
2446
2447 /* date - month, week, wday */
2448 buf_printf(buf, ",M%u.%u.%u", month,
2449 (week + 6) % 6, /* normalize; POSIX uses 5 for last (-1) */
2450 (wday + 6) % 7); /* normalize; POSIX is 0-based */
2451 }
2452
2453 /* time - default is 02:00:00 */
2454 if (hour != 2 || at.minute || at.second) {
2455 buf_printf(buf, "/%d", hour);
2456 if (at.minute || at.second) buf_printf(buf, ":%02u", at.minute);
2457 if (at.second) buf_printf(buf, ":%02u", at.second);
2458 }
2459
2460 return ver;
2461 }
2462
2463 /* Convert VTIMEZONE into tzif format (draft-murchison-tzdist-tzif) */
_icaltimezone_as_tzif(icalcomponent * ical,bit32 leapcnt,icaltimetype * startp,icaltimetype * endp)2464 static struct buf *_icaltimezone_as_tzif(icalcomponent* ical, bit32 leapcnt,
2465 icaltimetype *startp, icaltimetype *endp)
2466 {
2467 icalcomponent *vtz, *eternal_std = NULL, *eternal_dst = NULL;
2468 icalarray *obsarray;
2469 struct observance *proleptic;
2470 icaltimetype start = icaltime_null_time();
2471 icaltimetype end = icaltime_from_day_of_year(1, 2100);
2472 icaltimetype last_dtstart = icaltime_null_time();
2473 char header[] = {
2474 'T', 'Z', 'i', 'f', /* magic */
2475 '2', /* version */
2476 0, 0, 0, 0, 0, /* reserved */
2477 0, 0, 0, 0, 0, /* reserved */
2478 0, 0, 0, 0, 0 /* reserved */
2479 };
2480 struct transition {
2481 long long int t; /* transition time */
2482 unsigned char idx; /* index into 'types' array */
2483 } *times = NULL;
2484 struct ttinfo types[256]; /* only indexed by unsigned char */
2485 struct buf *tzif, posix = BUF_INITIALIZER, abbrev = BUF_INITIALIZER;
2486 struct observance *obs;
2487 unsigned do_bit64;
2488 struct leapsec *leap = NULL;
2489 bit32 leap_init = 0, leap_sec = 0;
2490
2491 tzif = buf_new();
2492
2493 vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
2494 if (!vtz) return tzif;
2495
2496 if (leapcnt) {
2497 leap = ptrarray_nth(leap_seconds, 1);
2498 leap_init = leap->sec;
2499 }
2500
2501 if (!startp) startp = &start;
2502 if (!endp || icaltime_is_null_time(*endp)) endp = &end;
2503
2504 /* Create an array of observances */
2505 obsarray = icalarray_new(sizeof(struct observance), 100);
2506 truncate_vtimezone(vtz, startp, endp, obsarray,
2507 &proleptic, &eternal_std, &eternal_dst, &last_dtstart, 0);
2508
2509 /* Create an array of transitions */
2510 times = xmalloc((obsarray->num_elements+1) * sizeof(struct transition));
2511
2512 /* Try to create POSIX tz rule */
2513 if (eternal_dst) {
2514 unsigned d_ver, s_ver;
2515
2516 if ((d_ver = buf_append_rrule_as_posix_string(&posix, eternal_dst)) &&
2517 (s_ver = buf_append_rrule_as_posix_string(&posix, eternal_std))) {
2518 /* Set format version */
2519 header[4] = d_ver | s_ver;
2520 }
2521 else {
2522 /* Can't create rule */
2523 buf_reset(&posix);
2524 }
2525 }
2526
2527 /* Add two tzif datasets:
2528 The first using 32-bit times and the second using 64-bit times. */
2529 for (do_bit64 = 0; do_bit64 <= 1; do_bit64++) {
2530 long long int epoch = do_bit64 ? BIG_BANG : INT32_MIN;
2531 struct observance *prev_obs = proleptic;
2532 bit32 timecnt = 0, typecnt = 0;
2533 int leapidx = 2;
2534 size_t n;
2535
2536 buf_reset(&abbrev);
2537
2538 leap_sec = 0;
2539 if (leapcnt) leap = ptrarray_nth(leap_seconds, leapidx);
2540
2541 /* Populate array of transitions & types */
2542 for (n = 0; n < obsarray->num_elements; n++) {
2543 long long int t;
2544 unsigned typeidx;
2545 icaltimetype tt_1601 = icaltime_from_string("1601-01-01T00:00:00Z");
2546
2547 obs = icalarray_element_at(obsarray, n);
2548 t = icaltime_to_gmtime64(obs->onset);
2549 icaltime_adjust(&tt_1601, 0, 0, 0, -obs->offset_to);
2550
2551 if (obs->onset.year > 2037 &&
2552 (!do_bit64 || obs->onset.year > last_dtstart.year)) {
2553 /* tzdata doesn't seem to go any further */
2554 break;
2555 }
2556 else if (!timecnt) {
2557 if (t > epoch && proleptic->onset.year < 0) {
2558 /* Insert a tombstone prior to first real transition */
2559 t = epoch;
2560 obs = prev_obs;
2561
2562 /* Need to reprocess current observance */
2563 n--;
2564 }
2565 else {
2566 /* Reset types and abbreviations */
2567 typecnt = 0;
2568 buf_reset(&abbrev);
2569 }
2570 }
2571 else if (!icaltime_compare(obs->onset, tt_1601)) {
2572 /* Skip vzic tombstone for YEAR_MINIMUM */
2573 continue;
2574 }
2575 else if (obs->offset_from == obs->offset_to
2576 && prev_obs->is_daylight == obs->is_daylight
2577 && prev_obs->is_std == obs->is_std
2578 && prev_obs->is_gmt == obs->is_gmt
2579 && !strcmp(prev_obs->name, obs->name)) {
2580 /* Skip any no-ops */
2581 continue;
2582 }
2583 prev_obs = obs;
2584
2585 /* Check for existing type */
2586 for (typeidx = 0; typeidx < typecnt; typeidx++) {
2587 if ((obs->offset_to == types[typeidx].offset) &&
2588 (obs->is_daylight == types[typeidx].isdst) &&
2589 (obs->is_std == types[typeidx].isstd) &&
2590 (obs->is_gmt == types[typeidx].isgmt) &&
2591 !strcmp(obs->name, buf_cstring(&abbrev) + types[typeidx].idx))
2592 break;
2593 }
2594
2595 if (typeidx == typecnt) {
2596 /* Didn't find existing type */
2597 const char *p = buf_base(&abbrev);
2598 const char *endp = p + buf_len(&abbrev);
2599
2600 /* Check for existing abbreviation */
2601 while (p < endp) {
2602 if (!strcmp(p, obs->name)) break;
2603 p += strlen(p) + 1;
2604 }
2605
2606 /* Add new type */
2607 set_ttinfo(&types[typecnt++], obs, p - buf_base(&abbrev));
2608
2609 if (p == endp) {
2610 /* Add new abbreviation (including the NUL) */
2611 buf_appendmap(&abbrev, obs->name, strlen(obs->name) + 1);
2612 }
2613 }
2614
2615 if (t < epoch) {
2616 /* Skip transitions earlier than our epoch */
2617 continue;
2618 }
2619
2620 /* Add transition */
2621 if (leapcnt) {
2622 while (t >= leap->t && leapidx < leap_seconds->count) {
2623 leap_sec = leap->sec - leap_init;
2624 if (++leapidx < leap_seconds->count)
2625 leap = ptrarray_nth(leap_seconds, leapidx);
2626 }
2627 t += leap_sec;
2628 }
2629 times[timecnt].t = t;
2630 times[timecnt].idx = typeidx;
2631 timecnt++;
2632 }
2633
2634
2635 /* Output dataset */
2636
2637 /* Header */
2638 buf_appendmap(tzif, header, sizeof(header));
2639 buf_appendbit32(tzif, typecnt); /* isgmtcnt */
2640 buf_appendbit32(tzif, typecnt); /* isstdcnt */
2641 buf_appendbit32(tzif, leapcnt); /* leapcnt */
2642 buf_appendbit32(tzif, timecnt); /* timecnt */
2643 buf_appendbit32(tzif, typecnt); /* typecnt */
2644 buf_appendbit32(tzif, buf_len(&abbrev)); /* charcnt */
2645
2646 /* Transition times */
2647 for (n = 0; n < timecnt; n++) {
2648 if (do_bit64) buf_appendbit64(tzif, times[n].t);
2649 else buf_appendbit32(tzif, times[n].t);
2650 }
2651
2652 /* Transition time indices */
2653 for (n = 0; n < timecnt; n++) buf_putc(tzif, times[n].idx);
2654
2655 /* Types structures */
2656 for (n = 0; n < typecnt; n++) {
2657 buf_appendbit32(tzif, types[n].offset);
2658 buf_putc(tzif, types[n].isdst);
2659 buf_putc(tzif, types[n].idx);
2660 }
2661
2662 /* Abbreviation array */
2663 buf_append(tzif, &abbrev);
2664
2665 /* Leap second records */
2666 if (leapcnt) {
2667 leap_sec = 0;
2668
2669 for (leapidx = 2; leapidx < leap_seconds->count; leapidx++) {
2670 long long int t;
2671
2672 leap = ptrarray_nth(leap_seconds, leapidx);
2673 t = leap->t + leap_sec;
2674 if (do_bit64) buf_appendbit64(tzif, t);
2675 else buf_appendbit32(tzif, t);
2676
2677 leap_sec = leap->sec - leap_init;
2678 buf_appendbit32(tzif, leap_sec);
2679 }
2680 }
2681
2682 /* Standard/wall indicators */
2683 for (n = 0; n < typecnt; n++) buf_putc(tzif, types[n].isstd);
2684
2685 /* GMT/local indicators */
2686 for (n = 0; n < typecnt; n++) buf_putc(tzif, types[n].isgmt);
2687 }
2688
2689 free(times);
2690 buf_free(&abbrev);
2691
2692
2693 /* POSIX timezone string */
2694 buf_putc(tzif, '\n');
2695
2696 /* std offset [dst [offset] [,rule] ] */
2697 if (buf_len(&posix)) {
2698 /* Use POSIX rule */
2699 icalproperty *prop;
2700 int stdoff, dstoff;
2701
2702 /* std name */
2703 prop = icalcomponent_get_first_property(eternal_std,
2704 ICAL_TZNAME_PROPERTY);
2705 buf_appendcstr(tzif, icalproperty_get_tzname(prop));
2706
2707 /* std offset */
2708 prop = icalcomponent_get_first_property(eternal_std,
2709 ICAL_TZOFFSETTO_PROPERTY);
2710 stdoff = icalproperty_get_tzoffsetto(prop);
2711 buf_append_utcoffset_as_iso_string(tzif, stdoff);
2712
2713 /* dst name */
2714 prop = icalcomponent_get_first_property(eternal_dst,
2715 ICAL_TZNAME_PROPERTY);
2716 buf_appendcstr(tzif, icalproperty_get_tzname(prop));
2717
2718 /* dst offset */
2719 prop = icalcomponent_get_first_property(eternal_dst,
2720 ICAL_TZOFFSETTO_PROPERTY);
2721 dstoff = icalproperty_get_tzoffsetto(prop);
2722 if (dstoff - stdoff != 3600) { /* default is 1hr from std */
2723 buf_append_utcoffset_as_iso_string(tzif, dstoff);
2724 }
2725
2726 /* rule */
2727 buf_append(tzif, &posix);
2728 }
2729 else if (!eternal_dst &&
2730 !icalcomponent_get_tzuntil_property(vtz)) {
2731 /* Use last observance as fixed offset */
2732 obs = icalarray_element_at(obsarray, obsarray->num_elements - 1);
2733
2734 /* std name */
2735 if (obs->name[0] == ':' ||
2736 strcspn(obs->name, ",+-0123456789") < strlen(obs->name)) {
2737 buf_printf(tzif, "<%s>", obs->name);
2738 }
2739 else buf_appendcstr(tzif, obs->name);
2740
2741 /* std offset */
2742 buf_append_utcoffset_as_iso_string(tzif, obs->offset_to);
2743 }
2744 buf_putc(tzif, '\n');
2745
2746 buf_free(&posix);
2747 icalarray_free(obsarray);
2748
2749 return tzif;
2750 }
2751
tzdist_truncate_vtimezone(icalcomponent * vtz,icaltimetype * startp,icaltimetype * endp)2752 static void tzdist_truncate_vtimezone(icalcomponent *vtz,
2753 icaltimetype *startp, icaltimetype *endp)
2754 {
2755 truncate_vtimezone(vtz, startp, endp, NULL, NULL, NULL, NULL, NULL,
2756 1 /* ms_compatible */);
2757 }
2758
icaltimezone_as_tzif(icalcomponent * ical)2759 static struct buf *icaltimezone_as_tzif(icalcomponent* ical)
2760 {
2761 return _icaltimezone_as_tzif(ical, 0, NULL, NULL);
2762 }
2763
icaltimezone_as_tzif_leap(icalcomponent * ical)2764 static struct buf *icaltimezone_as_tzif_leap(icalcomponent* ical)
2765 {
2766 return _icaltimezone_as_tzif(ical, leap_seconds->count - 2, NULL, NULL);
2767 }
2768
tz_from_tzid(const char * tzid)2769 static icaltimezone *tz_from_tzid(const char *tzid)
2770 {
2771 if (!tzid)
2772 return NULL;
2773
2774 /* libical doesn't return the UTC singleton for Etc/UTC */
2775 if (!strcmp(tzid, "Etc/UTC") || !strcmp(tzid, "UTC"))
2776 return icaltimezone_get_utc_timezone();
2777
2778 return icaltimezone_get_builtin_timezone(tzid);
2779 }
2780
collect_timezones_cb(icalparameter * param,void * data)2781 static void collect_timezones_cb(icalparameter *param, void *data)
2782 {
2783 ptrarray_t *tzs = (ptrarray_t*) data;
2784 int i;
2785 icaltimezone *tz;
2786
2787 tz = tz_from_tzid(icalparameter_get_tzid(param));
2788 if (!tz) {
2789 return;
2790 }
2791 for (i = 0; i < tzs->count; i++) {
2792 if (ptrarray_nth(tzs, i) == tz) {
2793 return;
2794 }
2795 }
2796 ptrarray_push(tzs, tz);
2797 }
2798
icalcomponent_add_required_timezones(icalcomponent * ical)2799 EXPORTED void icalcomponent_add_required_timezones(icalcomponent *ical)
2800 {
2801 icalcomponent *comp, *tzcomp, *next;
2802 icalproperty *prop;
2803 struct icalperiodtype span;
2804 ptrarray_t tzs = PTRARRAY_INITIALIZER;
2805
2806 /* Determine recurrence span. */
2807 comp = icalcomponent_get_first_real_component(ical);
2808 span = icalrecurrenceset_get_utc_timespan(ical, icalcomponent_isa(comp),
2809 NULL, NULL, NULL, NULL);
2810
2811 /* Remove all VTIMEZONE components for known TZIDs. This operation is
2812 * a bit hairy: we could expunge a timezone which is in use by an ical
2813 * property that is unknown to us. But since we don't know what to
2814 * look for, we can't make sure to preserve these timezones. */
2815 for (tzcomp = icalcomponent_get_first_component(ical,
2816 ICAL_VTIMEZONE_COMPONENT);
2817 tzcomp;
2818 tzcomp = next) {
2819
2820 next = icalcomponent_get_next_component(ical,
2821 ICAL_VTIMEZONE_COMPONENT);
2822
2823 prop = icalcomponent_get_first_property(tzcomp, ICAL_TZID_PROPERTY);
2824 if (prop) {
2825 const char *tzid = icalproperty_get_tzid(prop);
2826 if (tzid && tz_from_tzid(tzid)) {
2827 icalcomponent_remove_component(ical, tzcomp);
2828 icalcomponent_free(tzcomp);
2829 }
2830 }
2831 }
2832
2833 /* Collect timezones by TZID */
2834 icalcomponent_foreach_tzid(ical, collect_timezones_cb, &tzs);
2835
2836 /* Now add each timezone, truncated by this events span. */
2837 int i;
2838 for (i = 0; i < tzs.count; i++) {
2839 icaltimezone *tz = ptrarray_nth(&tzs, i);
2840
2841 /* Clone tz to overwrite its TZID property. */
2842 icalcomponent *tzcomp =
2843 icalcomponent_clone(icaltimezone_get_component(tz));
2844 icalproperty *tzprop =
2845 icalcomponent_get_first_property(tzcomp, ICAL_TZID_PROPERTY);
2846 icalproperty_set_tzid(tzprop, icaltimezone_get_location(tz));
2847
2848 /* Truncate the timezone to the events timespan. */
2849 tzdist_truncate_vtimezone(tzcomp, &span.start, &span.end);
2850
2851 if (icaltime_as_timet_with_zone(span.end, NULL) < caldav_eternity) {
2852 /* Add TZUNTIL to timezone */
2853 icalproperty *tzuntil = icalproperty_new_tzuntil(span.end);
2854 icalcomponent_add_property(tzcomp, tzuntil);
2855 }
2856
2857 /* Strip any COMMENT property */
2858 /* XXX These were added by KSM in a previous version of vzic,
2859 but libical doesn't allow them in its restrictions checks */
2860 tzprop = icalcomponent_get_first_property(tzcomp, ICAL_COMMENT_PROPERTY);
2861 if (tzprop) {
2862 icalcomponent_remove_property(tzcomp, tzprop);
2863 icalproperty_free(tzprop);
2864 }
2865
2866 /* Add the truncated timezone. */
2867 icalcomponent_add_component(ical, tzcomp);
2868 }
2869
2870 ptrarray_fini(&tzs);
2871 }
2872