| 190 | | sub BinarySearchForTZEntry { |
|---|
| 191 | | # $tzList is assumed to be sorted, $dateTime is |
|---|
| 192 | | my($tzList, $dateTime) = @_; |
|---|
| 193 | | my ($l, $u) = (0, @$tzList - 1); # lower, upper end of search interval |
|---|
| 194 | | my $i; # index of probe |
|---|
| 195 | | my $final = 0; |
|---|
| 196 | | while ($l <= $u) { |
|---|
| 197 | | $i = int(($l + $u)/2); |
|---|
| 198 | | my $compareVal = DateTime->compare($tzList->[$i]{date}, $dateTime); |
|---|
| 199 | | if ($compareVal < 0) { |
|---|
| 200 | | $l = $i+1; |
|---|
| 201 | | $final = $i; |
|---|
| 202 | | } elsif ($compareVal > 0) { |
|---|
| 203 | | $u = $i-1; |
|---|
| 204 | | } else { |
|---|
| 205 | | return $tzList->[$i]; # found, won't happen often |
|---|
| 206 | | } |
|---|
| 207 | | } |
|---|
| 208 | | return $tzList->[$final]; # not found, go down one lower |
|---|
| 209 | | } |
|---|
| 210 | | ############################################################################### |
|---|
| 211 | | sub PrivatizeMergeAndTZIcalFile ($$$$$$) { |
|---|
| 212 | | my($icsPrivate, $icsPublic, $icsFull, $tzList, $user, $errorUser) = @_; |
|---|
| 213 | | |
|---|
| 214 | | my %calendar; |
|---|
| 215 | | $calendar{private} = Data::ICal->new(filename => $icsPrivate); |
|---|
| 216 | | $calendar{public} = Data::ICal->new(filename => $icsPublic); |
|---|
| 217 | | my $newCalendar = Data::ICal->new(data => <<END_ICAL |
|---|
| 218 | | BEGIN:VCALENDAR |
|---|
| 219 | | VERSION:2.0 |
|---|
| 220 | | PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN |
|---|
| 221 | | END:VCALENDAR |
|---|
| 222 | | END_ICAL |
|---|
| 223 | | ); |
|---|
| 224 | | my $type = "public"; |
|---|
| 225 | | while (1) { |
|---|
| 226 | | my $entries = (defined $calendar{$type}) ? $calendar{$type}->entries : []; |
|---|
| 227 | | foreach my $entry (@{$entries}) { |
|---|
| 228 | | $entry->add_property(class => "PRIVATE") if ($type eq "private"); |
|---|
| 229 | | |
|---|
| 230 | | # Let's shift some timezones around. |
|---|
| 231 | | foreach my $dateType (qw/DTSTART DTEND/) { |
|---|
| 232 | | my $datePropList = $entry->property($dateType); |
|---|
| 233 | | next unless @$datePropList > 0; |
|---|
| 234 | | |
|---|
| 235 | | WarnLog($errorUser, "Strange that the entry below for $icsFull had more " . |
|---|
| 236 | | "than one $dateType:\n" . Data::Dumper->Dumper($entry) ) |
|---|
| 237 | | unless @$datePropList == 1; |
|---|
| 238 | | |
|---|
| 239 | | my $dateProp = $datePropList->[0]; |
|---|
| 240 | | # Only continue processing date if we have this property. (Duh) |
|---|
| 241 | | next unless defined $dateProp; |
|---|
| 242 | | my $params = $dateProp->parameters(); |
|---|
| 243 | | |
|---|
| 244 | | # Only continue if it is a DATE-TIME property. This is a bit of a |
|---|
| 245 | | # judgement call but I think it's the right one. When someone |
|---|
| 246 | | # creates an all-day event, we don't want to allow it to drift to |
|---|
| 247 | | # antoher day merely because the user has moved time zones. |
|---|
| 248 | | |
|---|
| 249 | | next unless defined $params and defined $params->{VALUE} |
|---|
| 250 | | and $params->{VALUE} =~ /DATE\-TIME/i; |
|---|
| 251 | | my $nyTime = DateTime::Format::ICal->parse_datetime($dateProp->value); |
|---|
| 252 | | my $newDate = DateTime::Format::ICal->parse_datetime($dateProp->value); |
|---|
| 253 | | $nyTime->set_time_zone("America/New_York") |
|---|
| 254 | | if $nyTime->time_zone->name =~ /floating/; |
|---|
| 255 | | my $val = BinarySearchForTZEntry($tzList, $nyTime); |
|---|
| 256 | | $newDate->set_time_zone($val->{newTZ}); |
|---|
| 257 | | $newDate->set_time_zone("America/New_York"); |
|---|
| 258 | | $newDate->set_time_zone("floating"); |
|---|
| 259 | | my $newICalDate = DateTime::Format::ICal->format_datetime($newDate); |
|---|
| 260 | | $dateProp->value($newICalDate); |
|---|
| 261 | | |
|---|
| 262 | | } |
|---|
| 263 | | $newCalendar->add_entry($entry); |
|---|
| 264 | | } |
|---|
| 265 | | last if ($type eq "private"); |
|---|
| 266 | | $type = "private"; |
|---|
| 267 | | } |
|---|
| 268 | | foreach my $tzEntry (@$tzList) { $tzEntry->{date}->set_time_zone("floating"); } |
|---|
| 269 | | for (my $ii = 0; $ii < @$tzList; $ii++) { |
|---|
| 270 | | my $tzEntry = $tzList->[$ii]; |
|---|
| 271 | | |
|---|
| 272 | | next unless defined $tzEntry->{location} and |
|---|
| 273 | | $tzEntry->{location} !~ /^\s*NYC\s*$/i; |
|---|
| 274 | | |
|---|
| 275 | | my $startDate = DateTime::Format::ICal->format_datetime($tzEntry->{date}); |
|---|
| 276 | | |
|---|
| 277 | | |
|---|
| 278 | | my $nextDate = ($ii+1 < @$tzList) ? |
|---|
| 279 | | DateTime::Format::ICal->format_datetime($tzList->[$ii+1]{date}) : $startDate; |
|---|
| 280 | | |
|---|
| 281 | | $nextDate =~ s/T\d+Z?$//; $startDate =~ s/T\d+Z?$//; |
|---|
| 282 | | |
|---|
| 283 | | my $whereEvent = Data::ICal::Entry::Event->new(); |
|---|
| 284 | | $whereEvent->add_properties(summary => "$user Travel: ". $tzEntry->{location}, |
|---|
| 285 | | description => $tzEntry->{location}, |
|---|
| 286 | | dtstart => [ $startDate, { VALUE => 'DATE' } ], |
|---|
| 287 | | dtend => [ $nextDate,{ VALUE => 'DATE' } ], |
|---|
| 288 | | uid => 'bkuhnScript' . (time() + $ii)); |
|---|
| 289 | | $newCalendar->add_entry($whereEvent); |
|---|
| 290 | | } |
|---|
| 291 | | open(MERGED_CAL, ">$icsFull") or |
|---|
| 292 | | DieLog("Unable to overwrite $icsFull: $!", $LOCK_CLEANUP_CODE); |
|---|
| 293 | | print MERGED_CAL $newCalendar->as_string; |
|---|
| 294 | | close MERGED_CAL; |
|---|
| 295 | | DieLog("Error when writing $icsFull: $!", $LOCK_CLEANUP_CODE) |
|---|
| 296 | | unless $? == 0; |
|---|
| 297 | | } |
|---|
| 298 | | ############################################################################### |
|---|
| 299 | | sub FilterEmacsToICal ($$$$$) { |
|---|
| 300 | | my ($publicCalendarFile, $privateCalendarFile, $outputFile, |
|---|
| 301 | | $emacsSettings, $user) = @_; |
|---|
| 302 | | |
|---|
| 303 | | my @tzList = BuildTZList($emacsSettings->{reportProblems}, |
|---|
| 304 | | $publicCalendarFile, $privateCalendarFile); |
|---|
| 305 | | |
|---|
| 306 | | my($elispFH, $elispFile) = tempfile(); |
|---|
| 307 | | my $icsWillBePrivatizedFile = tmpnam(); |
|---|
| 308 | | my $icsPublicFile = tmpnam(); |
|---|
| 309 | | print $elispFH "(setq-default european-calendar-style t)\n" |
|---|
| 310 | | if $emacsSettings->{calendarStyle} =~ /european/i; |
|---|
| 311 | | print $elispFH <<ELISP_END |
|---|
| 312 | | (icalendar-export-file "$privateCalendarFile" "$icsWillBePrivatizedFile") |
|---|
| 313 | | (icalendar-export-file "$publicCalendarFile" "$icsPublicFile") |
|---|
| 314 | | ELISP_END |
|---|
| 315 | | ; |
|---|
| 316 | | $elispFH->close(); |
|---|
| 317 | | my @emacsOutput = read_from_process($EMACS, '--no-windows', |
|---|
| 318 | | '--batch', '--no-site-file', '-l', $elispFile); |
|---|
| 319 | | DieLog("Emacs process for exporting $privateCalendarFile and " . |
|---|
| 320 | | "$publicCalendarFile exited with non-zero exit status of " . |
|---|
| 321 | | "$? ($!), and output of:\n " . join("\n ", @emacsOutput), |
|---|
| 322 | | $LOCK_CLEANUP_CODE) |
|---|
| 323 | | if ($? != 0); |
|---|
| 324 | | my $goodCount =0; |
|---|
| 325 | | foreach my $line (@emacsOutput) { |
|---|
| 326 | | $goodCount++ |
|---|
| 327 | | if $line =~ /^\s*Wrote\s+($icsPublicFile|$icsWillBePrivatizedFile)\s*$/; |
|---|
| 328 | | } |
|---|
| 329 | | DieLog("Unexpected Emacs output: " . join("\n ", @emacsOutput), |
|---|
| 330 | | $LOCK_CLEANUP_CODE) |
|---|
| 331 | | if ($goodCount != 2); |
|---|
| 332 | | |
|---|
| 333 | | my $icsFullFile = tmpnam(); |
|---|
| 334 | | PrivatizeMergeAndTZIcalFile($icsWillBePrivatizedFile, $icsPublicFile, |
|---|
| 335 | | $icsFullFile, \@tzList, $user, |
|---|
| 336 | | $emacsSettings->{reportProblems}); |
|---|
| 337 | | |
|---|
| 338 | | PrivacyFilterICalFile($icsFullFile, $outputFile); |
|---|
| 339 | | DieLog("Unable to remove temporary files") |
|---|
| 340 | | unless unlink($icsPublicFile, $icsWillBePrivatizedFile, $icsFullFile) == 3; |
|---|
| 341 | | } |
|---|
| 356 | | sub BuildTZList ($$$) { |
|---|
| 357 | | my($user, $pubEmacsFile, $privEmacsFile) = @_; |
|---|
| 358 | | |
|---|
| 359 | | my @tzList; |
|---|
| 360 | | |
|---|
| 361 | | foreach my $file ($pubEmacsFile, $privEmacsFile) { |
|---|
| 362 | | open(DATA, "<$file") or DieLog("unable to read $file: $!", |
|---|
| 363 | | $LOCK_CLEANUP_CODE); |
|---|
| 364 | | while (my $line = <DATA>) { |
|---|
| 365 | | if ($line =~ /^\s*;[;\s]*TZ\s*=([^\s,]+)\s*(?:,+\s*LOCA?T?I?O?N?\s*=\"([^"]+)\")? |
|---|
| 366 | | \s+(?:at|on)\s*(.*)\s+in\s+(\S+)\s*$/ix) { |
|---|
| 367 | | my($newTZstr, $location, $dateStartStr, $dateStartTZstr) = ($1, $2, $3, $4); |
|---|
| 368 | | my $newTZ; |
|---|
| 369 | | eval { $newTZ = DateTime::TimeZone->new( name => $newTZstr ); }; |
|---|
| 370 | | if ($@ or not defined $newTZ) { |
|---|
| 371 | | WarnLog($user, |
|---|
| 372 | | "Invalid time zone of \"$newTZstr\" in $line from $file: $@"); |
|---|
| 373 | | next; |
|---|
| 374 | | } |
|---|
| 375 | | my $dateStartTZ; |
|---|
| 376 | | eval { |
|---|
| 377 | | $dateStartTZ = DateTime::TimeZone->new( name => $dateStartTZstr ); }; |
|---|
| 378 | | if ($@ or not defined $dateStartTZ) { |
|---|
| 379 | | WarnLog($user, |
|---|
| 380 | | "Invalid time zone of \"$dateStartTZstr\" in $line from $file: $@"); |
|---|
| 381 | | next; |
|---|
| 382 | | } |
|---|
| 383 | | my(@data) = UnixDate("$dateStartStr", qw(%Y %m %d %H %M %S)); |
|---|
| 384 | | if (@data != 6) { |
|---|
| 385 | | WarnLog($user, "Unparseable date string of \"$dateStartStr\"" . |
|---|
| 386 | | "in $line from $file"); |
|---|
| 387 | | next; |
|---|
| 388 | | } |
|---|
| 389 | | my @args = MergeLists([qw( year month day hour minute second)], \@data); |
|---|
| 390 | | my $startDate; |
|---|
| 391 | | eval { |
|---|
| 392 | | $startDate = DateTime->new(@args, time_zone => $dateStartTZstr); |
|---|
| 393 | | }; |
|---|
| 394 | | if ($@ or not defined $startDate) { |
|---|
| 395 | | WarnLog($user, "Trouble parsing \"$dateStartStr $dateStartTZstr\" " . |
|---|
| 396 | | "in $line from $file\n\n" . |
|---|
| 397 | | "Most likely $dateStartTZstr was a bad time zone.: $@ "); |
|---|
| 398 | | next; |
|---|
| 399 | | } |
|---|
| 400 | | push(@tzList, { date => $startDate, newTZ => $newTZ, location => $location}); |
|---|
| 401 | | } |
|---|
| 402 | | } |
|---|
| 403 | | } |
|---|
| 404 | | # If we found nothing, everything is NYC |
|---|
| 405 | | if (@tzList == 0) { |
|---|
| 406 | | push(@tzList, { date => DateTime->new(year => 2006, month => 11, day => 03, |
|---|
| 407 | | hour => 11, minute => 00, second => 00, |
|---|
| 408 | | time_zone => "America/New_York"), |
|---|
| 409 | | newTZ => "America/New_York", location => undef }); |
|---|
| 410 | | |
|---|
| 411 | | } |
|---|
| 412 | | return sort { DateTime->compare($a->{date}, $b->{date}); } @tzList; |
|---|
| 413 | | } |
|---|
| 414 | | ############################################################################### |
|---|
| 415 | | sub PrivacyFilterICalFile ($$) { |
|---|
| 416 | | my($inputFile, $outputFile) = @_; |
|---|
| 417 | | |
|---|
| 418 | | my $oldCalendar = Data::ICal->new(filename => $inputFile); |
|---|
| 419 | | my $newCalendar = Data::ICal->new(data => <<END_ICAL |
|---|
| 420 | | BEGIN:VCALENDAR |
|---|
| 421 | | VERSION:2.0 |
|---|
| 422 | | PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN |
|---|
| 423 | | END:VCALENDAR |
|---|
| 424 | | END_ICAL |
|---|
| 425 | | ); |
|---|
| 426 | | my $entries = (defined $oldCalendar) ? $oldCalendar->entries : []; |
|---|
| 427 | | my $x =0; |
|---|
| 428 | | foreach my $entry (@{$entries}) { |
|---|
| 429 | | my @newSubEntries; |
|---|
| 430 | | foreach my $subEntry (@{$entry->{entries}}) { |
|---|
| 431 | | my $refVal = ref $subEntry; |
|---|
| 432 | | if (defined $refVal and $refVal =~ /Alarm/i) { |
|---|
| 433 | | # Don't put it in the list in the public version if is an alarm |
|---|
| 434 | | } else { |
|---|
| 435 | | push(@newSubEntries, $subEntry); |
|---|
| 436 | | } |
|---|
| 437 | | } |
|---|
| 438 | | $entry->{entries} = \@newSubEntries; |
|---|
| 439 | | |
|---|
| 440 | | my $classes = $entry->property('class'); |
|---|
| 441 | | my $class; |
|---|
| 442 | | foreach my $classProp (@{$classes}) { |
|---|
| 443 | | $class = $classProp->value; |
|---|
| 444 | | last if defined $class and |
|---|
| 445 | | $class =~ /^\s*(?:PUBLIC|PRIVATE|CONFIDENTIAL)\s*/i; |
|---|
| 446 | | } |
|---|
| 447 | | if (defined $class and $class =~ /CONFIDENTIAL/i) { |
|---|
| 448 | | # do not put this event in the output at all |
|---|
| 449 | | next; |
|---|
| 450 | | } elsif (defined $class and $class =~ /PRIVATE/i){ |
|---|
| 451 | | foreach my $prop (qw/location summary description/) { |
|---|
| 452 | | my $propList = $entry->property($prop); |
|---|
| 453 | | $entry->add_property($prop => "Private") |
|---|
| 454 | | if (defined $propList and @{$propList} > 0); |
|---|
| 455 | | } |
|---|
| 456 | | } |
|---|
| 457 | | $newCalendar->add_entry($entry); |
|---|
| 458 | | } |
|---|
| 459 | | open(SCRUBBED_CAL, ">$outputFile") or |
|---|
| 460 | | DieLog("Unable to overwrite $outputFile: $!", $LOCK_CLEANUP_CODE); |
|---|
| 461 | | print SCRUBBED_CAL $newCalendar->as_string; |
|---|
| 462 | | close SCRUBBED_CAL; |
|---|
| 463 | | DieLog("Error when writing $outputFile: $!", $LOCK_CLEANUP_CODE) |
|---|
| 464 | | unless $? == 0; |
|---|
| 465 | | } |
|---|
| 466 | | ############################################################################### |
|---|