diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index ce825ce55..148af9605 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -1,4 +1,5 @@ 7.10.9 + - fixed #12030: Calendar Feed Time Zone Issue 7.10.8 - rfe #12016 for the top story as well diff --git a/docs/gotcha.txt b/docs/gotcha.txt index 09b9ae108..dd6a4fc72 100644 --- a/docs/gotcha.txt +++ b/docs/gotcha.txt @@ -7,6 +7,11 @@ upgrading from one version to the next, or even between multiple versions. Be sure to heed the warnings contained herein as they will save you many hours of grief. +7.10.9 +-------------------------------------------------------------------- + * WebGUI now depends on Data::ICal for making and reading iCal feeds + for the Calendar. + 7.10.4 -------------------------------------------------------------------- * WebGUI now depends on Monkey::Patch for doing sanely scoped diff --git a/lib/WebGUI/Asset/Event.pm b/lib/WebGUI/Asset/Event.pm index 3dbe0d0e5..9f4bb9184 100644 --- a/lib/WebGUI/Asset/Event.pm +++ b/lib/WebGUI/Asset/Event.pm @@ -28,6 +28,7 @@ use WebGUI::Storage; use Test::Deep::NoTest qw(eq_deeply); use DateTime::Event::ICal; use DateTime::Set; +use Data::ICal::Entry::Event; use base 'WebGUI::Asset'; @@ -74,6 +75,54 @@ sub addRevision { return $newRev; } +#################################################################### + +=head2 add_to_calendar ($iCal) + +Build a Data::ICal::Entry::Event object that contains the information for this +event and add it to the Data::ICal calendar + +=head3 $iCal + +A Data::ICal object, representing the top-level calendar instance. + +=cut + +sub add_to_calendar { + my $self = shift; + my $session = $self->session; + my $calendar = shift; + my $event = Data::ICal::Entry::Event->new(); + $event->add_properties( + 'last-modified' => WebGUI::DateTime->new($session, $event->get("revisionDate"))->toIcal, + created => WebGUI::DateTime->new($session, $event->get("creationDate"))->toIcal, + sequence => $self->get('iCalSequenceNumber'), + summary => $self->get('title'), + description => $self->get('description'), + location => $self->get('location'), + uid => $self->get('feedUid') + ? $self->get('feedUid') + : $self->get('assetId') . '@'. $session->config->get("sitename")->[0], + ); + ##WebGUI Specific fields + foreach my $prop (qw/groupIdView groupIdEdit url menuTitle timeZone/) { + $event->add_property( 'x-webgui-'.lc($prop) => $self->get($prop)); + } + my $eventStart = $self->getIcalStart; + my $start_parameters = {}; + if (! $eventStart =~ /T/) { + $start_parameters->{VALUE} = 'DATE'; + } + $event->add_property(dtstart => [ $eventStart, $start_parameters ]); + my $eventEnd = $self->getIcalEnd; + my $end_parameters = {}; + if (! $eventEnd =~ /T/) { + $end_parameters->{VALUE} = 'DATE'; + } + $event->add_property(dtend => [ $eventEnd, $end_parameters ]); + $calendar->add_entry($event); +} + { my %dayNamesToICal = ( diff --git a/lib/WebGUI/Asset/Wobject/Calendar.pm b/lib/WebGUI/Asset/Wobject/Calendar.pm index b2be4eeb7..e21b237bf 100644 --- a/lib/WebGUI/Asset/Wobject/Calendar.pm +++ b/lib/WebGUI/Asset/Wobject/Calendar.pm @@ -19,6 +19,7 @@ use WebGUI::International; use WebGUI::Search; use WebGUI::Form; use WebGUI::HTML; +use WebGUI::ICal; use WebGUI::DateTime; use Class::C3; @@ -1736,97 +1737,20 @@ sub www_ical { $dt_end = $dt_start->clone->add( seconds => $self->get('icalInterval') ); } + my $ical = WebGUI::ICal->new(); + # Get all the events we're going to display my @events = $self->getEventsIn($dt_start->toMysql,$dt_end->toMysql); - - my $ical = qq{BEGIN:VCALENDAR\r\n} - . qq{PRODID:WebGUI }.$WebGUI::VERSION."-".$WebGUI::STATUS.qq{\r\n} - . qq{VERSION:2.0\r\n}; - - # VEVENT: EVENT: for my $event (@events) { next EVENT unless $event->canView(); - $ical .= qq{BEGIN:VEVENT\r\n}; - - ### UID - # Use feed's UID to prevent over-propagation - if ($event->get("feedUid")) { - $ical .= qq{UID:}.$event->get("feedUid")."\r\n"; - } - # Create a UID for feeds native to this calendar - else { - my $domain = $session->config->get("sitename")->[0]; - $ical .= qq{UID:}.$event->get("assetId").'@'.$domain."\r\n"; - } - - # LAST-MODIFIED (revisionDate) - $ical .= qq{LAST-MODIFIED:} - . WebGUI::DateTime->new($self->session, $event->get("revisionDate"))->toIcal - . "\r\n"; - - # CREATED (creationDate) - $ical .= qq{CREATED:} - . WebGUI::DateTime->new($self->session, $event->get("creationDate"))->toIcal - . "\r\n"; - - # SEQUENCE - my $sequenceNumber = $event->get("iCalSequenceNumber"); - if (defined $sequenceNumber) { - $ical .= qq{SEQUENCE:} - . $event->get("iCalSequenceNumber") - . "\r\n"; - } - - # DTSTART - my $eventStart = $event->getIcalStart; - $ical .= 'DTSTART'; - if ($eventStart !~ /T/) { - $ical .= ';VALUE=DATE'; - } - $ical .= ":$eventStart\r\n"; - - # DTEND - my $eventEnd = $event->getIcalEnd; - $ical .= 'DTEND'; - if ($eventEnd !~ /T/) { - $ical .= ';VALUE=DATE'; - } - $ical .= ":$eventEnd\r\n"; - - # Summary (the title) - # Wrapped at 75 columns - $ical .= $self->wrapIcal("SUMMARY:".$event->get("title"))."\r\n"; - - # Description (the text) - # Wrapped at 75 columns - $ical .= $self->wrapIcal("DESCRIPTION:".$event->get("description"))."\r\n"; - - # Location (the text) - # Wrapped at 75 columns - $ical .= $self->wrapIcal("LOCATION:".$event->get("location"))."\r\n"; - - # X-WEBGUI lines - if ($event->get("groupIdView")) { - $ical .= "X-WEBGUI-GROUPIDVIEW:".$event->get("groupIdView")."\r\n"; - } - if ($event->get("groupIdEdit")) { - $ical .= "X-WEBGUI-GROUPIDEDIT:".$event->get("groupIdEdit")."\r\n"; - } - $ical .= "X-WEBGUI-URL:".$event->get("url")."\r\n"; - $ical .= "X-WEBGUI-MENUTITLE:".$event->get("menuTitle")."\r\n"; - - $ical .= qq{END:VEVENT\r\n}; + $event->add_to_calendar($ical); } - # ENDVEVENT - - $ical .= qq{END:VCALENDAR\r\n}; - # Set mime of text/icalendar #$self->session->http->setMimeType("text/plain"); $self->session->http->setFilename("feed.ics","text/calendar"); - return $ical; + return $ical->as_string; } #---------------------------------------------------------------------------- diff --git a/lib/WebGUI/ICal.pm b/lib/WebGUI/ICal.pm new file mode 100644 index 000000000..bf2291d23 --- /dev/null +++ b/lib/WebGUI/ICal.pm @@ -0,0 +1,10 @@ +package WebGUI::ICal; + +use WebGUI; +use parent qw/Data::ICal/; + +sub product_id { + return 'WebGUI '. $WebGUI::VERSION . '-' . $WebGUI::STATUS; +} + +1; diff --git a/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm index 795192bc6..8e8428443 100644 --- a/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm +++ b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm @@ -23,6 +23,7 @@ use WebGUI::Asset::Event; use WebGUI::DateTime; use DateTime::TimeZone; use Data::Dumper; +use Data::ICal; use LWP::UserAgent; use JSON (); @@ -142,88 +143,52 @@ sub execute { next FEED; } - my $data = $response->content; - # If doesn't start with BEGIN:VCALENDAR then error - unless ($data =~ /^BEGIN:VCALENDAR/i) { + my $data = $response->content; + my $cal = Data::ICal->new( data => $data ); + if (!$cal) { # Update the result and last updated fields - $feed->{lastResult} = "Not an iCalendar feed"; + $feed->{lastResult} = "Error parsing iCal feed"; $feed->{lastUpdated} = $dt; $calendar->setFeed($feed->{feedId}, $feed); - next FEED; + #next FEED; } - - my $active = 0; # Parser on/off - my %current_event = (); - my %events; - my $line_number = 0; - $data =~ s/[ \t]?[\r\n]+[ \t]+/ /msg; #Process line continuations - LINE: for my $line (split /[\r\n]+/,$data) { - chomp $line; - $line_number++; - next unless $line =~ /\w/; - - #warn "LINE $line_number: $line\n"; - - if ($line =~ /^BEGIN:VEVENT$/i) { - $active = 1; - next LINE; - } - elsif ($line =~ /^END:VEVENT$/i) { - $active = 0; - # Flush event - my $uid = lc $current_event{uid}[1]; - delete $current_event{uid}; - $events{$uid} = {%current_event}; - $session->log->info( "Found event $uid from feed " . $feed->{feedId} ); - %current_event = (); - next LINE; - } - else { - # Flush old entry - # KEY;ATTRIBUTE=VALUE;ATTRIBUTE=VALUE:KEYVALUE - my ($key_attrs,$value) = split /:/,$line,2; - my @attrs = $key_attrs ? (split /;/, $key_attrs) : (); - my $key = shift @attrs; - my %attrs; - while (my $attribute = shift @attrs) { - my ($attr_key, $attr_value) = split /=/, $attribute, 2; - $attrs{lc $attr_key} = $attr_value; - } - - $current_event{lc $key} = [\%attrs,$value]; - } - } - my $feedData = $feedList->{$feed->{feedId}} = { added => 0, updated => 0, errored => 0, assetId => $calendar->getId, }; - EVENT: for my $id (keys %events) { + EVENT: foreach my $entry (@{ $cal->entries }) { + next EVENT unless $entry->ical_entry_type eq 'VEVENT'; #use Data::Dumper; #warn "EVENT: $id; ".Dumper $events{$id}; + my $event_properties = $entry->properties; # Prepare event data my $properties = { - feedUid => $id, feedId => $feed->{feedId}, - description => _unwrapIcalText($events{$id}->{description}->[1]), - title => _unwrapIcalText($events{$id}->{summary}->[1]), - location => _unwrapIcalText($events{$id}->{location}->[1]), - menuTitle => substr($events{$id}->{summary}->[1],0,15), className => 'WebGUI::Asset::Event', isHidden => 1, }; + PROPERTY: foreach my $property (qw/uid description summary location/) { + next property unless exists $event_properties->{$property}; + $properties->{$property} = $event_properties->{$property}->[0]->value; + } + ##Fixup + $properties->{title} = delete $properties->{summary}; + $properties->{feedUid} = delete $properties->{uid}; # Prepare the date - my $dtstart = $events{$id}->{dtstart}->[1]; + my $dtstart = $event_properties->{dtstart}->[0]->value; if ($dtstart =~ /T/) { my ($date, $time) = split /T/, $dtstart; my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; - my $tz = $events{$id}->{dtstart}->[0]->{tzid}; + my $tz = ''; + if ($event_properties->{dtstart}->[0]->properties->{tzid}) { + $tz = $event_properties->{dtstart}->[0]->properties->{tzid}; + } if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { $tz = "UTC"; } @@ -253,14 +218,14 @@ sub execute { next EVENT; } - my $dtend = $events{$id}->{dtend}->[1]; - my $duration = $events{$id}->{duration}->[1]; + my $dtend = exists $event_properties->{dtend} ? $event_properties->{dtend}->[0]->value : undef; + my $duration = exists $event_properties->{duration} ? $event_properties->{duration}->[0]->value : undef; if ($dtend =~ /T/) { my ($date, $time) = split /T/, $dtend; my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; - my $tz = $events{$id}->{dtend}->[0]->{tzid}; + my $tz = ''; if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { $tz = "UTC"; } @@ -330,28 +295,15 @@ sub execute { } # If there are X-WebGUI-* fields - for my $key (grep /^x-webgui-/, keys %{$events{$id}}) { - my $property_name = $key; - $property_name =~ s/^x-webgui-//; - $property_name = lc $property_name; - - if ($property_name eq "groupidedit") { - $properties->{groupIdEdit} = $events{$id}->{$key}->[1]; - } - elsif ($property_name eq "groupidview") { - $properties->{groupIdView} = $events{$id}->{$key}->[1]; - } - elsif ($property_name eq "url") { - $properties->{url} = $events{$id}->{$key}->[1]; - } - elsif ($property_name eq "menutitle") { - $properties->{menuTitle} = $events{$id}->{$key}->[1]; - } + PROPERTY: foreach my $key (qw/groupIdEdit groupIdView url menuTitle timeZone/) { + my $property_name = 'x-webgui-'.lc $key; + next PROPERTY unless exists $event_properties->{$property_name}; + $properties->{$key} = $event_properties->{$property_name}->[0]->value; } my $recur; - if ($events{$id}->{rrule}) { - $recur = _icalToRecur($session, $properties->{startDate}, $events{$id}->{rrule}->[1]); + if (exists $event_properties->{rrule}) { + $recur = _icalToRecur($session, $properties->{startDate}, $event_properties->{rrule}->[0]->value); } # save events for later diff --git a/sbin/testEnvironment.pl b/sbin/testEnvironment.pl index 581b616d3..b42f31b9f 100755 --- a/sbin/testEnvironment.pl +++ b/sbin/testEnvironment.pl @@ -67,6 +67,7 @@ checkModule("HTTP::Headers", 1.61 ); checkModule("Test::More", 0.82, 2 ); checkModule("Test::MockObject", 1.02, 2 ); checkModule("Test::Deep", 0.095, ); +checkModule("Test::LongString", 0.13, 2 ); checkModule("Test::Exception", 0.27, 2 ); checkModule("Test::Differences", 0.5, 2 ); checkModule("Test::Class", 0.31, 2 ); @@ -149,6 +150,7 @@ checkModule('IO::Socket::SSL', ); checkModule('Net::Twitter', "3.13006" ); checkModule('PerlIO::eol', "0.14" ); checkModule('Monkey::Patch', '0.03' ); +checkModule('Data::ICal', '0.16' ); failAndExit("Required modules are missing, running no more checks.") if $missingModule; diff --git a/t/Workflow/Activity/CalendarUpdateFeeds.t b/t/Workflow/Activity/CalendarUpdateFeeds.t index 9f97f5e7c..f00832d52 100644 --- a/t/Workflow/Activity/CalendarUpdateFeeds.t +++ b/t/Workflow/Activity/CalendarUpdateFeeds.t @@ -20,12 +20,13 @@ use WebGUI::Asset::Wobject::Calendar; use Test::More; use Test::Deep; +use Test::LongString; use Data::Dumper; plan skip_all => 'set WEBGUI_LIVE to enable this test' unless $ENV{WEBGUI_LIVE}; -plan tests => 14; # increment this value for each test you create +plan tests => 19; # increment this value for each test you create my $session = WebGUI::Test->session; @@ -105,7 +106,12 @@ is($anniversary->get('menuTitle'), $party->get('menuTitle'), '... menuTitl is($anniversary->get('groupIdView'), $party->get('groupIdView'), '... groupIdView'); is($anniversary->get('groupIdEdit'), $party->get('groupIdEdit'), '... groupIdEdit'); is($anniversary->get('url'), $party->get('url').'2', '... url (accounting for duplicate)'); -is($anniversary->get('description'), $party->get('description'), '... description, checks for line wrapping'); +is($anniversary->get('timeZone'), $party->get('timeZone'), '... timeZone'); +is($anniversary->get('startDate'), $party->get('startDate'), '... startDate'); +is($anniversary->get('startTime'), $party->get('startTime'), '... startTime'); +is($anniversary->get('endDate'), $party->get('endDate'), '... endDate'); +is($anniversary->get('endTime'), $party->get('endTime'), '... endTime'); +is_string($anniversary->get('description'), $party->get('description'), '... description, checks for line wrapping'); $party->update({description => "one line\nsecond line"});