diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -166,6 +166,7 @@ } $availability_select->setDropdownMenu($dropdown); + $availability_select->setDisabled($event->isImportedEvent()); $header->addActionLink($availability_select); } @@ -629,6 +630,7 @@ ->setIcon('fa-times grey') ->setHref($this->getApplicationURI("/event/decline/{$id}/")) ->setWorkflow(true) + ->setDisabled($event->isImportedEvent()) ->setText(pht('Decline')); $accept_button = id(new PHUIButtonView()) @@ -636,6 +638,7 @@ ->setIcon('fa-check green') ->setHref($this->getApplicationURI("/event/accept/{$id}/")) ->setWorkflow(true) + ->setDisabled($event->isImportedEvent()) ->setText(pht('Accept')); return array($decline_button, $accept_button); diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -207,10 +207,24 @@ $events = null; } + // Verified emails of the Event Uploader, to be eventually matched. + // Phorge loves privacy, so emails are generally private. + // This just covers a corner case: yourself importing yourself. + // NOTE: We are using the omnipotent user since we already have + // withUserPHIDs() limiting to a specific person (you). + $author_verified_emails = id(new PhabricatorPeopleUserEmailQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUserPHIDs(array($import->getAuthorPHID())) + ->withIsVerified(true) + ->execute(); + $author_verified_emails = mpull($author_verified_emails, 'getAddress'); + $author_verified_emails = array_fuse($author_verified_emails); + $xactions = array(); $update_map = array(); $invitee_map = array(); - $attendee_map = array(); + $attendee_name_map = array(); // map[eventUID][email from] = Attendee + $attendee_user_map = array(); // map[eventUID][userPHID ] = Attendee foreach ($node_map as $full_uid => $node) { $event = idx($events, $full_uid); if (!$event) { @@ -227,7 +241,8 @@ $xactions[$full_uid] = $this->newUpdateTransactions($event, $node); $update_map[$full_uid] = $event; - $attendee_map[$full_uid] = array(); + $attendee_name_map[$full_uid] = array(); + $attendee_user_map[$full_uid] = array(); $attendees = $node->getAttendees(); $private_index = 1; foreach ($attendees as $attendee) { @@ -236,8 +251,16 @@ // of the product. $name = $attendee->getName(); if (phutil_nonempty_string($name) && preg_match('/@/', $name)) { - $name = new PhutilEmailAddress($name); - $name = $name->getDisplayName(); + $attendee_mail = new PhutilEmailAddress($name); + $name = $attendee_mail->getDisplayName(); + $address = $attendee_mail->getAddress(); + + // Skip creation of dummy "Private User" if it's me, the uploader. + if ($address && isset($author_verified_emails[$address])) { + $attendee_user_map[$full_uid][$import->getAuthorPHID()] = + $attendee; + continue; + } } // If we don't have a name or the name still looks like it's an @@ -247,12 +270,12 @@ $private_index++; } - $attendee_map[$full_uid][$name] = $attendee; + $attendee_name_map[$full_uid][$name] = $attendee; } } $attendee_names = array(); - foreach ($attendee_map as $full_uid => $event_attendees) { + foreach ($attendee_name_map as $full_uid => $event_attendees) { foreach ($event_attendees as $name => $attendee) { $attendee_names[$name] = $attendee; } @@ -331,7 +354,8 @@ $update_map = array_select_keys($update_map, $insert_order); foreach ($update_map as $full_uid => $event) { - $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); + $node = $node_map[$full_uid]; + $parent_uid = $this->getParentNodeUID($node); if ($parent_uid) { $parent_phid = $update_map[$parent_uid]->getPHID(); } else { @@ -356,19 +380,28 @@ // We're just forcing attendees to the correct values here because // transactions intentionally don't let you RSVP for other users. This // might need to be turned into a special type of transaction eventually. - $attendees = $attendee_map[$full_uid]; + $attendees_name = $attendee_name_map[$full_uid]; + $attendees_user = $attendee_user_map[$full_uid]; $old_map = $event->getInvitees(); $old_map = mpull($old_map, null, 'getInviteePHID'); + $phid_invitees = array(); + foreach ($attendees_name as $name => $attendee) { + $attendee_phid = $external_invitees[$name]->getPHID(); + $phid_invitees[$attendee_phid] = $attendee; + } + foreach ($attendees_user as $phid_user_attendee => $attendee) { + $phid_invitees[$phid_user_attendee] = $attendee; + } + $new_map = array(); - foreach ($attendees as $name => $attendee) { - $phid = $external_invitees[$name]->getPHID(); + foreach ($phid_invitees as $phid_invitee => $attendee) { - $invitee = idx($old_map, $phid); + $invitee = idx($old_map, $phid_invitee); if (!$invitee) { $invitee = id(new PhabricatorCalendarEventInvitee()) ->setEventPHID($event->getPHID()) - ->setInviteePHID($phid) + ->setInviteePHID($phid_invitee) ->setInviterPHID($import->getPHID()); } @@ -381,15 +414,26 @@ break; case PhutilCalendarUserNode::STATUS_INVITED: default: - $status = PhabricatorCalendarEventInvitee::STATUS_INVITED; + // Is me importing myself? I'm coming! + if ($phid_invitee === $import->getAuthorPHID()) { + $status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; + } else { + $status = PhabricatorCalendarEventInvitee::STATUS_INVITED; + } break; } $invitee->setStatus($status); + // Import "busy/available", very useful for myself to tell this + // to coworkers. This is probably somehow very un-useful for most + // "Private user(s)", but let's add it for them too since it + // doesn't hurt them. + $invitee->importAvailabilityFromTimeTransparency( + $node->getTimeTransparency()); $invitee->save(); - $new_map[$phid] = $invitee; + $new_map[$phid_invitee] = $invitee; } - + // Remove old Invitees if they are not invited anymore. foreach ($old_map as $phid => $invitee) { if (empty($new_map[$phid])) { $invitee->delete(); diff --git a/src/applications/calendar/import/__tests__/CalendarImportTestCase.php b/src/applications/calendar/import/__tests__/CalendarImportTestCase.php --- a/src/applications/calendar/import/__tests__/CalendarImportTestCase.php +++ b/src/applications/calendar/import/__tests__/CalendarImportTestCase.php @@ -56,7 +56,7 @@ 'fileAuthor' => $lincoln_verified, 'expectedInvitees' => 3, 'expectedInviteesTests' => array( -// array($lincoln_verified, true), // Self-invitation. T15564 + array($lincoln_verified, true), // Self-invitation. T15564 array($alice_unverified, false), array($alien_unverified, false), array($alien_verified, false), diff --git a/src/applications/calendar/parser/data/PhutilCalendarEventNode.php b/src/applications/calendar/parser/data/PhutilCalendarEventNode.php --- a/src/applications/calendar/parser/data/PhutilCalendarEventNode.php +++ b/src/applications/calendar/parser/data/PhutilCalendarEventNode.php @@ -15,6 +15,7 @@ private $modifiedDateTime; private $organizer; private $attendees = array(); + private $timeTransparency; private $recurrenceRule; private $recurrenceExceptions = array(); private $recurrenceDates = array(); @@ -130,6 +131,24 @@ return $this; } + /** + * Get the "time transparency" as described by RFC 5545 3.8.2.7. + * @return string|null + */ + public function getTimeTransparency() { + return $this->timeTransparency; + } + + /** + * Set the "time transparency" as described by RFC 5545 3.8.2.7. + * @param string|null $time_transparency + * @return self + */ + public function setTimeTransparency($time_transparency) { + $this->timeTransparency = $time_transparency; + return $this; + } + public function setRecurrenceRule( PhutilCalendarRecurrenceRule $recurrence_rule) { $this->recurrenceRule = $recurrence_rule; diff --git a/src/applications/calendar/parser/ics/PhutilICSParser.php b/src/applications/calendar/parser/ics/PhutilICSParser.php --- a/src/applications/calendar/parser/ics/PhutilICSParser.php +++ b/src/applications/calendar/parser/ics/PhutilICSParser.php @@ -673,6 +673,10 @@ $attendee = $this->newAttendeeFromProperty($parameters, $value); $node->addAttendee($attendee); break; + case 'TRANSP': + $transp = $this->newTextFromProperty($parameters, $value); + $node->setTimeTransparency($transp); + break; } } diff --git a/src/applications/calendar/parser/ics/PhutilICSWriter.php b/src/applications/calendar/parser/ics/PhutilICSWriter.php --- a/src/applications/calendar/parser/ics/PhutilICSWriter.php +++ b/src/applications/calendar/parser/ics/PhutilICSWriter.php @@ -213,6 +213,13 @@ } } + $transp = $event->getTimeTransparency(); + if ($transp) { + $properties[] = $this->newTextProperty( + 'TRANSP', + $transp); + } + $rrule = $event->getRecurrenceRule(); if ($rrule) { $properties[] = $this->newRRULEProperty( diff --git a/src/applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php b/src/applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php --- a/src/applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php +++ b/src/applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php @@ -93,6 +93,17 @@ 'raw' => 'This is a simple event.', ), ), + array( + 'name' => 'TRANSP', + 'parameters' => array(), + 'value' => array( + 'type' => 'TEXT', + 'value' => array( + 'OPAQUE', + ), + 'raw' => 'OPAQUE', + ), + ), ), $event->getAttribute('ics.properties')); diff --git a/src/applications/calendar/parser/ics/__tests__/data/simple.ics b/src/applications/calendar/parser/ics/__tests__/data/simple.ics --- a/src/applications/calendar/parser/ics/__tests__/data/simple.ics +++ b/src/applications/calendar/parser/ics/__tests__/data/simple.ics @@ -8,5 +8,6 @@ DTEND;TZID=America/Los_Angeles:20160915T100000 SUMMARY:Simple Event DESCRIPTION:This is a simple event. +TRANSP:OPAQUE END:VEVENT END:VCALENDAR diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php --- a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php @@ -69,6 +69,33 @@ } } + /** + * Import the invitee availability from the Time Transparency + * field in an ICS calendar event as per RFC 5545 section 3.8.2.7. + * @param wild $time_transp Time transparency like 'OPAQUE' + * or 'TRANSPARENT' or null. + * @return void + */ + public function importAvailabilityFromTimeTransparency($time_transp) { + // How to understand RFC 5545 suburbs. Example conversation: + // "Hey dude + // I'm a bit *opaque* on this event so I'm not *transparent*" + // Means: + // "Good morning Sir, + // I'm a bit *busy* on this business so I'm not *available*" + static $transparency_2_availability = array( + 'OPAQUE' => self::AVAILABILITY_BUSY, + 'TRANSPARENT' => self::AVAILABILITY_AVAILABLE, + ); + + // Note that idx($array, $key) likes a null $key. + $availability = idx($transparency_2_availability, $time_transp); + if ($availability) { + $this->setAvailability($availability); + } + } + + public static function getAvailabilityMap() { return array( self::AVAILABILITY_AVAILABLE => array( diff --git a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php --- a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php +++ b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php @@ -5,6 +5,8 @@ private $ids; private $phids; + private $userPhids; + private $isVerified; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,6 +18,24 @@ return $this; } + /** + * With the specified User PHIDs. + * @param null|array $phids User PHIDs + */ + public function withUserPHIDs(array $phids) { + $this->userPhids = $phids; + return $this; + } + + /** + * With a verified email or not. + * @param bool|null $isVerified + */ + public function withIsVerified($verified) { + $this->isVerified = $verified; + return $this; + } + public function newResultObject() { return new PhabricatorUserEmail(); } @@ -41,6 +61,20 @@ $this->phids); } + if ($this->userPhids !== null) { + $where[] = qsprintf( + $conn, + 'email.userPHID IN (%Ls)', + $this->userPhids); + } + + if ($this->isVerified !== null) { + $where[] = qsprintf( + $conn, + 'email.isVerified = %d', + (int)$this->isVerified); + } + return $where; }