1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "icinga/legacytimeperiod.hpp"
4 #include "base/function.hpp"
5 #include "base/convert.hpp"
6 #include "base/exception.hpp"
7 #include "base/objectlock.hpp"
8 #include "base/logger.hpp"
9 #include "base/debug.hpp"
10 #include "base/utility.hpp"
11 
12 using namespace icinga;
13 
14 REGISTER_FUNCTION_NONCONST(Internal, LegacyTimePeriod, &LegacyTimePeriod::ScriptFunc, "tp:begin:end");
15 
16 /**
17  * Returns the same as mktime() but does not modify its argument and takes a const pointer.
18  *
19  * @param t struct tm to convert to time_t
20  * @return time_t representing the timestamp given by t
21  */
mktime_const(const tm * t)22 static time_t mktime_const(const tm *t) {
23 	tm copy = *t;
24 	return mktime(&copy);
25 }
26 
IsInTimeRange(const tm * begin,const tm * end,int stride,const tm * reference)27 bool LegacyTimePeriod::IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference)
28 {
29 	time_t tsbegin, tsend, tsref;
30 	tsbegin = mktime_const(begin);
31 	tsend = mktime_const(end);
32 	tsref = mktime_const(reference);
33 
34 	if (tsref < tsbegin || tsref > tsend)
35 		return false;
36 
37 	int daynumber = (tsref - tsbegin) / (24 * 60 * 60);
38 
39 	if (stride > 1 && daynumber % stride > 0)
40 		return false;
41 
42 	return true;
43 }
44 
45 /**
46  * Update all day-related fields of reference (tm_year, tm_mon, tm_mday, tm_wday, tm_yday) to reference the n-th
47  * occurrence of a weekday (given by wday) in the month represented by the original value of reference.
48  *
49  * If n is negative, counting is done from the end of the month, so for example with wday=1 and n=-1, the result will be
50  * the last Monday in the month given by reference.
51  *
52  * @param wday Weekday (0 = Sunday, 1 = Monday, ..., 6 = Saturday, like tm_wday)
53  * @param n Search the n-th weekday (given by wday) in the month given by reference
54  * @param reference Input for the current month and output for the given day of that moth
55  */
FindNthWeekday(int wday,int n,tm * reference)56 void LegacyTimePeriod::FindNthWeekday(int wday, int n, tm *reference)
57 {
58 	// Work on a copy to only update specific fields of reference (as documented).
59 	tm t = *reference;
60 
61 	int dir, seen = 0;
62 
63 	if (n > 0) {
64 		dir = 1;
65 	} else {
66 		n *= -1;
67 		dir = -1;
68 
69 		/* Negative days are relative to the next month. */
70 		t.tm_mon++;
71 	}
72 
73 	ASSERT(n > 0);
74 
75 	t.tm_mday = 1;
76 
77 	for (;;) {
78 		// Always operate on 00:00:00 with automatic DST detection, otherwise days could
79 		// be skipped or counted twice if +-24 hours is not on the next or previous day.
80 		t.tm_hour = 0;
81 		t.tm_min = 0;
82 		t.tm_sec = 0;
83 		t.tm_isdst = -1;
84 
85 		mktime(&t);
86 
87 		if (t.tm_wday == wday) {
88 			seen++;
89 
90 			if (seen == n)
91 				break;
92 		}
93 
94 		t.tm_mday += dir;
95 	}
96 
97 	reference->tm_year = t.tm_year;
98 	reference->tm_mon  = t.tm_mon;
99 	reference->tm_mday = t.tm_mday;
100 	reference->tm_wday = t.tm_wday;
101 	reference->tm_yday = t.tm_yday;
102 }
103 
WeekdayFromString(const String & daydef)104 int LegacyTimePeriod::WeekdayFromString(const String& daydef)
105 {
106 	if (daydef == "sunday")
107 		return 0;
108 	else if (daydef == "monday")
109 		return 1;
110 	else if (daydef == "tuesday")
111 		return 2;
112 	else if (daydef == "wednesday")
113 		return 3;
114 	else if (daydef == "thursday")
115 		return 4;
116 	else if (daydef == "friday")
117 		return 5;
118 	else if (daydef == "saturday")
119 		return 6;
120 	else
121 		return -1;
122 }
123 
MonthFromString(const String & monthdef)124 int LegacyTimePeriod::MonthFromString(const String& monthdef)
125 {
126 	if (monthdef == "january")
127 		return 0;
128 	else if (monthdef == "february")
129 		return 1;
130 	else if (monthdef == "march")
131 		return 2;
132 	else if (monthdef == "april")
133 		return 3;
134 	else if (monthdef == "may")
135 		return 4;
136 	else if (monthdef == "june")
137 		return 5;
138 	else if (monthdef == "july")
139 		return 6;
140 	else if (monthdef == "august")
141 		return 7;
142 	else if (monthdef == "september")
143 		return 8;
144 	else if (monthdef == "october")
145 		return 9;
146 	else if (monthdef == "november")
147 		return 10;
148 	else if (monthdef == "december")
149 		return 11;
150 	else
151 		return -1;
152 }
153 
GetEndOfMonthDay(int year,int month)154 boost::gregorian::date LegacyTimePeriod::GetEndOfMonthDay(int year, int month)
155 {
156 	boost::gregorian::date d(boost::gregorian::greg_year(year), boost::gregorian::greg_month(month), 1);
157 
158 	return d.end_of_month();
159 }
160 
161 /**
162  * Finds the first day on or after the day given by reference and writes the beginning and end time of that day to
163  * the output parameters begin and end.
164  *
165  * @param timespec Day to find, for example "2021-10-20", "sunday", ...
166  * @param begin if != nullptr, set to 00:00:00 on that day
167  * @param end if != nullptr, set to 24:00:00 on that day (i.e. 00:00:00 of the next day)
168  * @param reference Time to begin the search at
169  */
ParseTimeSpec(const String & timespec,tm * begin,tm * end,const tm * reference)170 void LegacyTimePeriod::ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference)
171 {
172 	/* YYYY-MM-DD */
173 	if (timespec.GetLength() == 10 && timespec[4] == '-' && timespec[7] == '-') {
174 		int year = Convert::ToLong(timespec.SubStr(0, 4));
175 		int month = Convert::ToLong(timespec.SubStr(5, 2));
176 		int day = Convert::ToLong(timespec.SubStr(8, 2));
177 
178 		if (month < 1 || month > 12)
179 			BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
180 		if (day < 1 || day > 31)
181 			BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid day in time specification: " + timespec));
182 
183 		if (begin) {
184 			*begin = *reference;
185 			begin->tm_year = year - 1900;
186 			begin->tm_mon = month - 1;
187 			begin->tm_mday = day;
188 			begin->tm_hour = 0;
189 			begin->tm_min = 0;
190 			begin->tm_sec = 0;
191 			begin->tm_isdst = -1;
192 		}
193 
194 		if (end) {
195 			*end = *reference;
196 			end->tm_year = year - 1900;
197 			end->tm_mon = month - 1;
198 			end->tm_mday = day;
199 			end->tm_hour = 24;
200 			end->tm_min = 0;
201 			end->tm_sec = 0;
202 			end->tm_isdst = -1;
203 		}
204 
205 		return;
206 	}
207 
208 	std::vector<String> tokens = timespec.Split(" ");
209 
210 	int mon = -1;
211 
212 	if (tokens.size() > 1 && (tokens[0] == "day" || (mon = MonthFromString(tokens[0])) != -1)) {
213 		if (mon == -1)
214 			mon = reference->tm_mon;
215 
216 		int mday = Convert::ToLong(tokens[1]);
217 
218 		if (begin) {
219 			*begin = *reference;
220 			begin->tm_mon = mon;
221 			begin->tm_mday = mday;
222 			begin->tm_hour = 0;
223 			begin->tm_min = 0;
224 			begin->tm_sec = 0;
225 			begin->tm_isdst = -1;
226 
227 			/* day -X: Negative days are relative to the next month. */
228 			if (mday < 0) {
229 				boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
230 
231 				//Depending on the number, we need to substract specific days (counting starts at 0).
232 				d = d - boost::gregorian::days(mday * -1 - 1);
233 
234 				*begin = boost::gregorian::to_tm(d);
235 				begin->tm_hour = 0;
236 				begin->tm_min = 0;
237 				begin->tm_sec = 0;
238 			}
239 		}
240 
241 		if (end) {
242 			*end = *reference;
243 			end->tm_mon = mon;
244 			end->tm_mday = mday;
245 			end->tm_hour = 24;
246 			end->tm_min = 0;
247 			end->tm_sec = 0;
248 			end->tm_isdst = -1;
249 
250 			/* day -X: Negative days are relative to the next month. */
251 			if (mday < 0) {
252 				boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
253 
254 				//Depending on the number, we need to substract specific days (counting starts at 0).
255 				d = d - boost::gregorian::days(mday * -1 - 1);
256 
257 				// End date is one day in the future, starting 00:00:00
258 				d = d + boost::gregorian::days(1);
259 
260 				*end = boost::gregorian::to_tm(d);
261 				end->tm_hour = 0;
262 				end->tm_min = 0;
263 				end->tm_sec = 0;
264 			}
265 		}
266 
267 		return;
268 	}
269 
270 	int wday;
271 
272 	if (tokens.size() >= 1 && (wday = WeekdayFromString(tokens[0])) != -1) {
273 		tm myref = *reference;
274 		myref.tm_isdst = -1;
275 
276 		if (tokens.size() > 2) {
277 			mon = MonthFromString(tokens[2]);
278 
279 			if (mon == -1)
280 				BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
281 
282 			myref.tm_mon = mon;
283 		}
284 
285 		int n = 0;
286 
287 		if (tokens.size() > 1)
288 			n = Convert::ToLong(tokens[1]);
289 
290 		if (begin) {
291 			*begin = myref;
292 
293 			if (tokens.size() > 1)
294 				FindNthWeekday(wday, n, begin);
295 			else
296 				begin->tm_mday += (7 - begin->tm_wday + wday) % 7;
297 
298 			begin->tm_hour = 0;
299 			begin->tm_min = 0;
300 			begin->tm_sec = 0;
301 		}
302 
303 		if (end) {
304 			*end = myref;
305 
306 			if (tokens.size() > 1)
307 				FindNthWeekday(wday, n, end);
308 			else
309 				end->tm_mday += (7 - end->tm_wday + wday) % 7;
310 
311 			end->tm_hour = 0;
312 			end->tm_min = 0;
313 			end->tm_sec = 0;
314 			end->tm_mday++;
315 		}
316 
317 		return;
318 	}
319 
320 	BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + timespec));
321 }
322 
323 /**
324  * Parse a range of days.
325  *
326  * The input can have the following formats:
327  *   begin
328  *   begin - end
329  *   begin / stride
330  *   begin - end / stride
331  *
332  * @param timerange Text representation of a day range or a single day, for example "2021-10-20", "monday - friday", ...
333  * @param begin Output parameter set to 00:00:00 of the first day of the range
334  * @param end Output parameter set to 24:00:00 of the last day of the range (i.e. 00:00:00 of the day after)
335  * @param stride Output parameter for the stride (for every n-th day)
336  * @param reference Expand the range relative to this timestamp
337  */
ParseTimeRange(const String & timerange,tm * begin,tm * end,int * stride,const tm * reference)338 void LegacyTimePeriod::ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference)
339 {
340 	String def = timerange;
341 
342 	/* Figure out the stride. */
343 	size_t pos = def.FindFirstOf('/');
344 
345 	if (pos != String::NPos) {
346 		String strStride = def.SubStr(pos + 1).Trim();
347 		*stride = Convert::ToLong(strStride);
348 
349 		/* Remove the stride parameter from the definition. */
350 		def = def.SubStr(0, pos);
351 	} else {
352 		*stride = 1; /* User didn't specify anything, assume default. */
353 	}
354 
355 	/* Figure out whether the user has specified two dates. */
356 	pos = def.Find("- ");
357 
358 	if (pos != String::NPos) {
359 		String first = def.SubStr(0, pos).Trim();
360 
361 		String second = def.SubStr(pos + 1).Trim();
362 
363 		ParseTimeSpec(first, begin, nullptr, reference);
364 
365 		/* If the second definition starts with a number we need
366 		 * to add the first word from the first definition, e.g.:
367 		 * day 1 - 15 --> "day 15" */
368 		bool is_number = true;
369 		size_t xpos = second.FindFirstOf(' ');
370 		String fword = second.SubStr(0, xpos);
371 
372 		try {
373 			Convert::ToLong(fword);
374 		} catch (...) {
375 			is_number = false;
376 		}
377 
378 		if (is_number) {
379 			xpos = first.FindFirstOf(' ');
380 			ASSERT(xpos != String::NPos);
381 			second = first.SubStr(0, xpos + 1) + second;
382 		}
383 
384 		ParseTimeSpec(second, nullptr, end, reference);
385 	} else {
386 		ParseTimeSpec(def, begin, end, reference);
387 	}
388 }
389 
IsInDayDefinition(const String & daydef,const tm * reference)390 bool LegacyTimePeriod::IsInDayDefinition(const String& daydef, const tm *reference)
391 {
392 	tm begin, end;
393 	int stride;
394 
395 	ParseTimeRange(daydef, &begin, &end, &stride, reference);
396 
397 	Log(LogDebug, "LegacyTimePeriod")
398 		<< "ParseTimeRange: '" << daydef << "' => " << mktime(&begin)
399 		<< " -> " << mktime(&end) << ", stride: " << stride;
400 
401 	return IsInTimeRange(&begin, &end, stride, reference);
402 }
403 
404 static inline
ProcessTimeRaw(const String & in,const tm * reference,tm * out)405 void ProcessTimeRaw(const String& in, const tm *reference, tm *out)
406 {
407 	*out = *reference;
408 
409 	auto hd (in.Split(":"));
410 
411 	switch (hd.size()) {
412 		case 2:
413 			out->tm_sec = 0;
414 			break;
415 		case 3:
416 			out->tm_sec = Convert::ToLong(hd[2]);
417 			break;
418 		default:
419 			BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + in));
420 	}
421 
422 	out->tm_hour = Convert::ToLong(hd[0]);
423 	out->tm_min = Convert::ToLong(hd[1]);
424 }
425 
ProcessTimeRangeRaw(const String & timerange,const tm * reference,tm * begin,tm * end)426 void LegacyTimePeriod::ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end)
427 {
428 	std::vector<String> times = timerange.Split("-");
429 
430 	if (times.size() != 2)
431 		BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timerange: " + timerange));
432 
433 	ProcessTimeRaw(times[0], reference, begin);
434 	ProcessTimeRaw(times[1], reference, end);
435 
436 	if (begin->tm_hour * 3600 + begin->tm_min * 60 + begin->tm_sec >=
437 		end->tm_hour * 3600 + end->tm_min * 60 + end->tm_sec)
438 		end->tm_hour += 24;
439 }
440 
ProcessTimeRange(const String & timestamp,const tm * reference)441 Dictionary::Ptr LegacyTimePeriod::ProcessTimeRange(const String& timestamp, const tm *reference)
442 {
443 	tm begin, end;
444 
445 	ProcessTimeRangeRaw(timestamp, reference, &begin, &end);
446 
447 	return new Dictionary({
448 		{ "begin", (long)mktime(&begin) },
449 		{ "end", (long)mktime(&end) }
450 	});
451 }
452 
453 /**
454  * Takes a list of timeranges end expands them to concrete timestamp based on a reference time.
455  *
456  * @param timeranges String of comma separated time ranges, for example "10:00-12:00", "12:15:30-12:23:43,16:00-18:00"
457  * @param reference Starting point for searching the segments
458  * @param result For each range, a dict with keys "begin" and "end" is added
459  */
ProcessTimeRanges(const String & timeranges,const tm * reference,const Array::Ptr & result)460 void LegacyTimePeriod::ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result)
461 {
462 	std::vector<String> ranges = timeranges.Split(",");
463 
464 	for (const String& range : ranges) {
465 		Dictionary::Ptr segment = ProcessTimeRange(range, reference);
466 
467 		if (segment->Get("begin") >= segment->Get("end"))
468 			continue;
469 
470 		result->Add(segment);
471 	}
472 }
473 
FindRunningSegment(const String & daydef,const String & timeranges,const tm * reference)474 Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference)
475 {
476 	tm begin, end, iter;
477 	time_t tsend, tsiter, tsref;
478 	int stride;
479 
480 	tsref = mktime_const(reference);
481 
482 	ParseTimeRange(daydef, &begin, &end, &stride, reference);
483 
484 	iter = begin;
485 
486 	tsend = mktime(&end);
487 
488 	do {
489 		if (IsInTimeRange(&begin, &end, stride, &iter)) {
490 			Array::Ptr segments = new Array();
491 			ProcessTimeRanges(timeranges, &iter, segments);
492 
493 			Dictionary::Ptr bestSegment;
494 			double bestEnd = 0.0;
495 
496 			ObjectLock olock(segments);
497 			for (const Dictionary::Ptr& segment : segments) {
498 				double begin = segment->Get("begin");
499 				double end = segment->Get("end");
500 
501 				if (begin >= tsref || end < tsref)
502 					continue;
503 
504 				if (!bestSegment || end > bestEnd) {
505 					bestSegment = segment;
506 					bestEnd = end;
507 				}
508 			}
509 
510 			if (bestSegment)
511 				return bestSegment;
512 		}
513 
514 		iter.tm_mday++;
515 		iter.tm_hour = 0;
516 		iter.tm_min = 0;
517 		iter.tm_sec = 0;
518 		tsiter = mktime(&iter);
519 	} while (tsiter < tsend);
520 
521 	return nullptr;
522 }
523 
FindNextSegment(const String & daydef,const String & timeranges,const tm * reference)524 Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const String& timeranges, const tm *reference)
525 {
526 	tm begin, end, iter, ref;
527 	time_t tsend, tsiter, tsref;
528 	int stride;
529 
530 	for (int pass = 1; pass <= 2; pass++) {
531 		if (pass == 1) {
532 			ref = *reference;
533 		} else {
534 			ref = end;
535 			ref.tm_mday++;
536 		}
537 
538 		tsref = mktime(&ref);
539 
540 		ParseTimeRange(daydef, &begin, &end, &stride, &ref);
541 
542 		iter = begin;
543 
544 		tsend = mktime(&end);
545 
546 		do {
547 			if (IsInTimeRange(&begin, &end, stride, &iter)) {
548 				Array::Ptr segments = new Array();
549 				ProcessTimeRanges(timeranges, &iter, segments);
550 
551 				Dictionary::Ptr bestSegment;
552 				double bestBegin;
553 
554 				ObjectLock olock(segments);
555 				for (const Dictionary::Ptr& segment : segments) {
556 					double begin = segment->Get("begin");
557 
558 					if (begin < tsref)
559 						continue;
560 
561 					if (!bestSegment || begin < bestBegin) {
562 						bestSegment = segment;
563 						bestBegin = begin;
564 					}
565 				}
566 
567 				if (bestSegment)
568 					return bestSegment;
569 			}
570 
571 			iter.tm_mday++;
572 			iter.tm_hour = 0;
573 			iter.tm_min = 0;
574 			iter.tm_sec = 0;
575 			tsiter = mktime(&iter);
576 		} while (tsiter < tsend);
577 	}
578 
579 	return nullptr;
580 }
581 
ScriptFunc(const TimePeriod::Ptr & tp,double begin,double end)582 Array::Ptr LegacyTimePeriod::ScriptFunc(const TimePeriod::Ptr& tp, double begin, double end)
583 {
584 	Array::Ptr segments = new Array();
585 
586 	Dictionary::Ptr ranges = tp->GetRanges();
587 
588 	if (ranges) {
589 		for (int i = 0; i <= (end - begin) / (24 * 60 * 60); i++) {
590 			time_t refts = begin + i * 24 * 60 * 60;
591 			tm reference = Utility::LocalTime(refts);
592 
593 #ifdef I2_DEBUG
594 			Log(LogDebug, "LegacyTimePeriod")
595 				<< "Checking reference time " << refts;
596 #endif /* I2_DEBUG */
597 
598 			ObjectLock olock(ranges);
599 			for (const Dictionary::Pair& kv : ranges) {
600 				if (!IsInDayDefinition(kv.first, &reference)) {
601 #ifdef I2_DEBUG
602 					Log(LogDebug, "LegacyTimePeriod")
603 						<< "Not in day definition '" << kv.first << "'.";
604 #endif /* I2_DEBUG */
605 					continue;
606 				}
607 
608 #ifdef I2_DEBUG
609 				Log(LogDebug, "LegacyTimePeriod")
610 					<< "In day definition '" << kv.first << "'.";
611 #endif /* I2_DEBUG */
612 
613 				ProcessTimeRanges(kv.second, &reference, segments);
614 			}
615 		}
616 	}
617 
618 	Log(LogDebug, "LegacyTimePeriod")
619 		<< "Legacy timeperiod update returned " << segments->GetLength() << " segments.";
620 
621 	return segments;
622 }
623