From 09775279a949f489264a13dd4a5a1008c79a9bc8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 27 Oct 2016 10:58:11 -0700 Subject: [PATCH] Support arbitary event invitees when importing events Summary: Ref T10747. When we import a ".ics" file, represent any attendees as simple external references. For consistency with other areas of the product, I've avoided disclosing email addresses. We'll try to get a real name if we can. (We store addresses and could expose or use them later, or do some kind of masking junk like "epr...ley@g...l.com" which is utterly impossible to figure out.) Test Plan: {F1888367} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16759 --- .../20161027.calendar.01.externalinvitee.sql | 12 ++ src/__phutil_library_map__.php | 9 ++ .../PhabricatorCalendarImportEngine.php | 107 ++++++++++++++++++ ...ricatorCalendarExternalInviteePHIDType.php | 40 +++++++ ...habricatorCalendarExternalInviteeQuery.php | 68 +++++++++++ .../PhabricatorCalendarExternalInvitee.php | 74 ++++++++++++ ...bricatorCalendarEventInviteTransaction.php | 5 +- 7 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql create mode 100644 src/applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php create mode 100644 src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php diff --git a/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql b/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql new file mode 100644 index 0000000000..0d3843c7d3 --- /dev/null +++ b/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_externalinvitee ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + nameIndex BINARY(12) NOT NULL, + uri LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + sourcePHID VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_name` (`nameIndex`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 14acf65f1c..cb4a6c15df 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2098,6 +2098,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php', 'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php', 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', + 'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php', + 'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php', + 'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php', @@ -6939,6 +6942,12 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExternalInvitee' => array( + 'PhabricatorCalendarDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine', diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 7f51b5f74e..1eae386869 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -207,6 +207,8 @@ abstract class PhabricatorCalendarImportEngine $xactions = array(); $update_map = array(); + $invitee_map = array(); + $attendee_map = array(); foreach ($node_map as $full_uid => $node) { $event = idx($events, $full_uid); if (!$event) { @@ -222,6 +224,66 @@ abstract class PhabricatorCalendarImportEngine $this->updateEventFromNode($viewer, $event, $node); $xactions[$full_uid] = $this->newUpdateTransactions($event, $node); $update_map[$full_uid] = $event; + + $attendees = $node->getAttendees(); + $private_index = 1; + foreach ($attendees as $attendee) { + // Generate a "name" for this attendee which is not an email address. + // We avoid disclosing email addresses to be consistent with the rest + // of the product. + $name = $attendee->getName(); + if (preg_match('/@/', $name)) { + $name = new PhutilEmailAddress($name); + $name = $name->getDisplayName(); + } + + // If we don't have a name or the name still looks like it's an + // email address, give them a dummy placeholder name. + if (!strlen($name) || preg_match('/@/', $name)) { + $name = pht('Private User %d', $private_index); + $private_index++; + } + + $attendee_map[$full_uid][$name] = $attendee; + } + } + + $attendee_names = array(); + foreach ($attendee_map as $full_uid => $event_attendees) { + foreach ($event_attendees as $name => $attendee) { + $attendee_names[$name] = $attendee; + } + } + + if ($attendee_names) { + $external_invitees = id(new PhabricatorCalendarExternalInviteeQuery()) + ->setViewer($viewer) + ->withNames($attendee_names) + ->execute(); + $external_invitees = mpull($external_invitees, null, 'getName'); + + foreach ($attendee_names as $name => $attendee) { + if (isset($external_invitees[$name])) { + continue; + } + + $external_invitee = id(new PhabricatorCalendarExternalInvitee()) + ->setName($name) + ->setURI($attendee->getURI()) + ->setSourcePHID($import->getPHID()); + + try { + $external_invitee->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + $external_invitee = + id(new PhabricatorCalendarExternalInviteeQuery()) + ->setViewer($viewer) + ->withNames(array($name)) + ->executeOne(); + } + + $external_invitees[$name] = $external_invitee; + } } // Reorder events so we create parents first. This allows us to populate @@ -288,6 +350,51 @@ abstract class PhabricatorCalendarImportEngine $editor->applyTransactions($event, $event_xactions); + // 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]; + $old_map = $event->getInvitees(); + $old_map = mpull($old_map, null, 'getInviteePHID'); + + $new_map = array(); + foreach ($attendees as $name => $attendee) { + $phid = $external_invitees[$name]->getPHID(); + + $invitee = idx($old_map, $phid); + if (!$invitee) { + $invitee = id(new PhabricatorCalendarEventInvitee()) + ->setEventPHID($event->getPHID()) + ->setInviteePHID($phid) + ->setInviterPHID($import->getPHID()); + } + + switch ($attendee->getStatus()) { + case PhutilCalendarUserNode::STATUS_ACCEPTED: + $status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; + break; + case PhutilCalendarUserNode::STATUS_DECLINED: + $status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; + break; + case PhutilCalendarUserNode::STATUS_INVITED: + default: + $status = PhabricatorCalendarEventInvitee::STATUS_INVITED; + break; + } + $invitee->setStatus($status); + $invitee->save(); + + $new_map[$phid] = $invitee; + } + + foreach ($old_map as $phid => $invitee) { + if (empty($new_map[$phid])) { + $invitee->delete(); + } + } + + $event->attachInvitees($new_map); + $import->newLogMessage( PhabricatorCalendarImportUpdateLogType::LOGTYPE, array( diff --git a/src/applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php b/src/applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php new file mode 100644 index 0000000000..a5e86893ac --- /dev/null +++ b/src/applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $invitee = $objects[$phid]; + + $name = $invitee->getName(); + $handle->setName($name); + } + } +} diff --git a/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php b/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php new file mode 100644 index 0000000000..ea7200e614 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php @@ -0,0 +1,68 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function newResultObject() { + return new PhabricatorCalendarExternalInvitee(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->names !== null) { + $name_indexes = array(); + foreach ($this->names as $name) { + $name_indexes[] = PhabricatorHash::digestForIndex($name); + } + $where[] = qsprintf( + $conn, + 'nameIndex IN (%Ls)', + $name_indexes); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php new file mode 100644 index 0000000000..b1a85cf520 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php @@ -0,0 +1,74 @@ +setInviterPHID($actor->getPHID()) + ->setStatus(self::STATUS_INVITED) + ->setEventPHID($event->getPHID()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'parameters' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text', + 'nameIndex' => 'bytes12', + 'uri' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_name' => array( + 'columns' => array('nameIndex'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorCalendarExternalInviteePHIDType::TYPECONST; + } + + public function save() { + $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); + return parent::save(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php index ea9355eb25..f1da2c1d77 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php @@ -30,10 +30,11 @@ final class PhabricatorCalendarEventInviteTransaction $map = array(); foreach ($add as $phid) { - $map[$phid] = $status_invited; + $map[$phid] = $status_invited; } + foreach ($rem as $phid) { - $map[$phid] = $status_uninvited; + $map[$phid] = $status_uninvited; } // If we're creating this event and the actor is inviting themselves,