3 abstract class PhabricatorCalendarImportEngine
6 const QUEUE_BYTE_LIMIT
= 524288;
8 final public function getImportEngineType() {
9 return $this->getPhobjectClassConstant('ENGINETYPE', 64);
12 abstract public function getImportEngineName();
13 abstract public function getImportEngineTypeName();
14 abstract public function getImportEngineHint();
16 public function appendImportProperties(
17 PhabricatorUser
$viewer,
18 PhabricatorCalendarImport
$import,
19 PHUIPropertyListView
$properties) {
23 abstract public function newEditEngineFields(
24 PhabricatorEditEngine
$engine,
25 PhabricatorCalendarImport
$import);
27 abstract public function getDisplayName(PhabricatorCalendarImport
$import);
29 abstract public function importEventsFromSource(
30 PhabricatorUser
$viewer,
31 PhabricatorCalendarImport
$import,
34 abstract public function canDisable(
35 PhabricatorUser
$viewer,
36 PhabricatorCalendarImport
$import);
38 public function explainCanDisable(
39 PhabricatorUser
$viewer,
40 PhabricatorCalendarImport
$import) {
41 throw new PhutilMethodNotImplementedException();
44 abstract public function supportsTriggers(
45 PhabricatorCalendarImport
$import);
47 final public static function getAllImportEngines() {
48 return id(new PhutilClassMapQuery())
49 ->setAncestorClass(__CLASS__
)
50 ->setUniqueMethod('getImportEngineType')
51 ->setSortMethod('getImportEngineName')
55 final protected function importEventDocument(
56 PhabricatorUser
$viewer,
57 PhabricatorCalendarImport
$import,
58 PhutilCalendarRootNode
$root = null) {
60 $event_type = PhutilCalendarEventNode
::NODETYPE
;
64 foreach ($root->getChildren() as $document) {
65 foreach ($document->getChildren() as $node) {
66 $node_type = $node->getNodeType();
67 if ($node_type != $event_type) {
68 $import->newLogMessage(
69 PhabricatorCalendarImportIgnoredNodeLogType
::LOGTYPE
,
71 'node.type' => $node_type,
81 // Reject events which have dates outside of the range of a signed
82 // 32-bit integer. We'll need to accommodate a wider range of events
83 // eventually, but have about 20 years until it's an issue and we'll
84 // all be dead by then.
85 foreach ($nodes as $key => $node) {
87 $dates[] = $node->getStartDateTime();
88 $dates[] = $node->getEndDateTime();
89 $dates[] = $node->getCreatedDateTime();
90 $dates[] = $node->getModifiedDateTime();
91 $rrule = $node->getRecurrenceRule();
93 $dates[] = $rrule->getUntil();
97 foreach ($dates as $date) {
102 $year = $date->getYear();
103 if ($year < 1970 ||
$year > 2037) {
110 $import->newLogMessage(
111 PhabricatorCalendarImportEpochLogType
::LOGTYPE
,
117 // Reject events which occur too frequently. Users do not normally define
118 // these events and the UI and application make many assumptions which are
119 // incompatible with events recurring once per second.
120 foreach ($nodes as $key => $node) {
121 $rrule = $node->getRecurrenceRule();
123 // This is not a recurring event, so we don't need to check the
127 $scale = $rrule->getFrequencyScale();
128 if ($scale >= PhutilCalendarRecurrenceRule
::SCALE_DAILY
) {
129 // This is a daily, weekly, monthly, or yearly event. These are
132 // This is an hourly, minutely, or secondly event.
133 $import->newLogMessage(
134 PhabricatorCalendarImportFrequencyLogType
::LOGTYPE
,
136 'frequency' => $rrule->getFrequency(),
143 foreach ($nodes as $node) {
144 $full_uid = $this->getFullNodeUID($node);
145 if (isset($node_map[$full_uid])) {
146 $import->newLogMessage(
147 PhabricatorCalendarImportDuplicateLogType
::LOGTYPE
,
149 'uid.full' => $full_uid,
153 $node_map[$full_uid] = $node;
156 // If we already know about some of these events and they were created
157 // here, we're not going to import it again. This can happen if a user
158 // exports an event and then tries to import it again. This is probably
159 // not what they meant to do and this pathway generally leads to madness.
160 $likely_phids = array();
161 foreach ($node_map as $full_uid => $node) {
162 $uid = $node->getUID();
164 if (preg_match('/^(PHID-.*)@(.*)\z/', $uid, $matches)) {
165 $likely_phids[$full_uid] = $matches[1];
170 // NOTE: We're using the omnipotent viewer here because we don't want
171 // to collide with events that already exist, even if you can't see
173 $events = id(new PhabricatorCalendarEventQuery())
174 ->setViewer(PhabricatorUser
::getOmnipotentUser())
175 ->withPHIDs($likely_phids)
177 $events = mpull($events, null, 'getPHID');
178 foreach ($node_map as $full_uid => $node) {
179 $phid = idx($likely_phids, $full_uid);
184 $event = idx($events, $phid);
189 $import->newLogMessage(
190 PhabricatorCalendarImportOriginalLogType
::LOGTYPE
,
192 'phid' => $event->getPHID(),
195 unset($node_map[$full_uid]);
200 $events = id(new PhabricatorCalendarEventQuery())
202 ->withImportAuthorPHIDs(array($import->getAuthorPHID()))
203 ->withImportUIDs(array_keys($node_map))
205 $events = mpull($events, null, 'getImportUID');
211 $update_map = array();
212 $invitee_map = array();
213 $attendee_map = array();
214 foreach ($node_map as $full_uid => $node) {
215 $event = idx($events, $full_uid);
217 $event = PhabricatorCalendarEvent
::initializeNewCalendarEvent($viewer);
221 ->setImportAuthorPHID($import->getAuthorPHID())
222 ->setImportSourcePHID($import->getPHID())
223 ->setImportUID($full_uid)
224 ->attachImportSource($import);
226 $this->updateEventFromNode($viewer, $event, $node);
227 $xactions[$full_uid] = $this->newUpdateTransactions($event, $node);
228 $update_map[$full_uid] = $event;
230 $attendee_map[$full_uid] = array();
231 $attendees = $node->getAttendees();
233 foreach ($attendees as $attendee) {
234 // Generate a "name" for this attendee which is not an email address.
235 // We avoid disclosing email addresses to be consistent with the rest
237 $name = $attendee->getName();
238 if (preg_match('/@/', $name)) {
239 $name = new PhutilEmailAddress($name);
240 $name = $name->getDisplayName();
243 // If we don't have a name or the name still looks like it's an
244 // email address, give them a dummy placeholder name.
245 if (!strlen($name) ||
preg_match('/@/', $name)) {
246 $name = pht('Private User %d', $private_index);
250 $attendee_map[$full_uid][$name] = $attendee;
254 $attendee_names = array();
255 foreach ($attendee_map as $full_uid => $event_attendees) {
256 foreach ($event_attendees as $name => $attendee) {
257 $attendee_names[$name] = $attendee;
261 if ($attendee_names) {
262 $external_invitees = id(new PhabricatorCalendarExternalInviteeQuery())
264 ->withNames(array_keys($attendee_names))
266 $external_invitees = mpull($external_invitees, null, 'getName');
268 foreach ($attendee_names as $name => $attendee) {
269 if (isset($external_invitees[$name])) {
273 $external_invitee = id(new PhabricatorCalendarExternalInvitee())
275 ->setURI($attendee->getURI())
276 ->setSourcePHID($import->getPHID());
279 $external_invitee->save();
280 } catch (AphrontDuplicateKeyQueryException
$ex) {
282 id(new PhabricatorCalendarExternalInviteeQuery())
284 ->withNames(array($name))
288 $external_invitees[$name] = $external_invitee;
292 // Reorder events so we create parents first. This allows us to populate
293 // "instanceOfEventPHID" correctly.
294 $insert_order = array();
295 foreach ($update_map as $full_uid => $event) {
296 $parent_uid = $this->getParentNodeUID($node_map[$full_uid]);
297 if ($parent_uid === null) {
298 $insert_order[$full_uid] = $full_uid;
302 if (empty($update_map[$parent_uid])) {
303 // The parent was not present in this import, which means it either
304 // does not exist or we're going to delete it anyway. We just drop
307 $import->newLogMessage(
308 PhabricatorCalendarImportOrphanLogType
::LOGTYPE
,
310 'uid.full' => $full_uid,
311 'uid.parent' => $parent_uid,
317 // Otherwise, we're going to insert the parent first, then insert
319 $insert_order[$parent_uid] = $parent_uid;
320 $insert_order[$full_uid] = $full_uid;
323 // TODO: Define per-engine content sources so this can say "via Upload" or
325 $content_source = PhabricatorContentSource
::newForSource(
326 PhabricatorWebContentSource
::SOURCECONST
);
328 // NOTE: We're using the omnipotent user here because imported events are
329 // otherwise immutable.
330 $edit_actor = PhabricatorUser
::getOmnipotentUser();
332 $update_map = array_select_keys($update_map, $insert_order);
333 foreach ($update_map as $full_uid => $event) {
334 $parent_uid = $this->getParentNodeUID($node_map[$full_uid]);
336 $parent_phid = $update_map[$parent_uid]->getPHID();
341 $event->setInstanceOfEventPHID($parent_phid);
343 $event_xactions = $xactions[$full_uid];
345 $editor = id(new PhabricatorCalendarEventEditor())
346 ->setActor($edit_actor)
347 ->setActingAsPHID($import->getPHID())
348 ->setContentSource($content_source)
349 ->setContinueOnNoEffect(true)
350 ->setContinueOnMissingFields(true);
352 $is_new = !$event->getID();
354 $editor->applyTransactions($event, $event_xactions);
356 // We're just forcing attendees to the correct values here because
357 // transactions intentionally don't let you RSVP for other users. This
358 // might need to be turned into a special type of transaction eventually.
359 $attendees = $attendee_map[$full_uid];
360 $old_map = $event->getInvitees();
361 $old_map = mpull($old_map, null, 'getInviteePHID');
364 foreach ($attendees as $name => $attendee) {
365 $phid = $external_invitees[$name]->getPHID();
367 $invitee = idx($old_map, $phid);
369 $invitee = id(new PhabricatorCalendarEventInvitee())
370 ->setEventPHID($event->getPHID())
371 ->setInviteePHID($phid)
372 ->setInviterPHID($import->getPHID());
375 switch ($attendee->getStatus()) {
376 case PhutilCalendarUserNode
::STATUS_ACCEPTED
:
377 $status = PhabricatorCalendarEventInvitee
::STATUS_ATTENDING
;
379 case PhutilCalendarUserNode
::STATUS_DECLINED
:
380 $status = PhabricatorCalendarEventInvitee
::STATUS_DECLINED
;
382 case PhutilCalendarUserNode
::STATUS_INVITED
:
384 $status = PhabricatorCalendarEventInvitee
::STATUS_INVITED
;
387 $invitee->setStatus($status);
390 $new_map[$phid] = $invitee;
393 foreach ($old_map as $phid => $invitee) {
394 if (empty($new_map[$phid])) {
399 $event->attachInvitees($new_map);
401 $import->newLogMessage(
402 PhabricatorCalendarImportUpdateLogType
::LOGTYPE
,
405 'phid' => $event->getPHID(),
410 $import->newLogMessage(
411 PhabricatorCalendarImportEmptyLogType
::LOGTYPE
,
415 // Delete any events which are no longer present in the source.
416 $updated_events = mpull($update_map, null, 'getPHID');
417 $source_events = id(new PhabricatorCalendarEventQuery())
419 ->withImportSourcePHIDs(array($import->getPHID()))
422 $engine = new PhabricatorDestructionEngine();
423 foreach ($source_events as $source_event) {
424 if (isset($updated_events[$source_event->getPHID()])) {
425 // We imported and updated this event, so keep it around.
429 $import->newLogMessage(
430 PhabricatorCalendarImportDeleteLogType
::LOGTYPE
,
432 'name' => $source_event->getName(),
435 $engine->destroyObject($source_event);
439 private function getFullNodeUID(PhutilCalendarEventNode
$node) {
440 $uid = $node->getUID();
441 $instance_epoch = $this->getNodeInstanceEpoch($node);
442 $full_uid = $uid.'/'.$instance_epoch;
447 private function getParentNodeUID(PhutilCalendarEventNode
$node) {
448 $recurrence_id = $node->getRecurrenceID();
450 if (!strlen($recurrence_id)) {
454 return $node->getUID().'/';
457 private function getNodeInstanceEpoch(PhutilCalendarEventNode
$node) {
458 $instance_iso = $node->getRecurrenceID();
459 if (strlen($instance_iso)) {
460 $instance_datetime = PhutilCalendarAbsoluteDateTime
::newFromISO8601(
462 $instance_epoch = $instance_datetime->getEpoch();
464 $instance_epoch = null;
467 return $instance_epoch;
470 private function newUpdateTransactions(
471 PhabricatorCalendarEvent
$event,
472 PhutilCalendarEventNode
$node) {
475 $uid = $node->getUID();
477 if (!$event->getID()) {
478 $xactions[] = id(new PhabricatorCalendarEventTransaction())
479 ->setTransactionType(PhabricatorTransactions
::TYPE_CREATE
)
483 $name = $node->getName();
484 if (!strlen($name)) {
486 $name = pht('Unnamed Event "%s"', $uid);
488 $name = pht('Unnamed Imported Event');
491 $xactions[] = id(new PhabricatorCalendarEventTransaction())
492 ->setTransactionType(
493 PhabricatorCalendarEventNameTransaction
::TRANSACTIONTYPE
)
494 ->setNewValue($name);
496 $description = $node->getDescription();
497 $xactions[] = id(new PhabricatorCalendarEventTransaction())
498 ->setTransactionType(
499 PhabricatorCalendarEventDescriptionTransaction
::TRANSACTIONTYPE
)
500 ->setNewValue((string)$description);
502 $is_recurring = (bool)$node->getRecurrenceRule();
503 $xactions[] = id(new PhabricatorCalendarEventTransaction())
504 ->setTransactionType(
505 PhabricatorCalendarEventRecurringTransaction
::TRANSACTIONTYPE
)
506 ->setNewValue($is_recurring);
511 private function updateEventFromNode(
512 PhabricatorUser
$actor,
513 PhabricatorCalendarEvent
$event,
514 PhutilCalendarEventNode
$node) {
516 $instance_epoch = $this->getNodeInstanceEpoch($node);
517 $event->setUTCInstanceEpoch($instance_epoch);
519 $timezone = $actor->getTimezoneIdentifier();
521 // TODO: These should be transactional, but the transaction only accepts
522 // epoch timestamps right now.
523 $start_datetime = $node->getStartDateTime()
524 ->setViewerTimezone($timezone);
525 $end_datetime = $node->getEndDateTime()
526 ->setViewerTimezone($timezone);
529 ->setStartDateTime($start_datetime)
530 ->setEndDateTime($end_datetime);
532 $event->setIsAllDay((int)$start_datetime->getIsAllDay());
534 // TODO: This should be transactional, but the transaction only accepts
535 // simple frequency rules right now.
536 $rrule = $node->getRecurrenceRule();
538 $event->setRecurrenceRule($rrule);
540 $until_datetime = $rrule->getUntil();
541 if ($until_datetime) {
542 $until_datetime->setViewerTimezone($timezone);
543 $event->setUntilDateTime($until_datetime);
546 $count = $rrule->getCount();
547 $event->setParameter('recurrenceCount', $count);
553 public function canDeleteAnyEvents(
554 PhabricatorUser
$viewer,
555 PhabricatorCalendarImport
$import) {
557 $table = new PhabricatorCalendarEvent();
558 $conn = $table->establishConnection('r');
560 // Using a CalendarEventQuery here was failing oddly in a way that was
561 // difficult to reproduce locally (see T11808). Just check the table
562 // directly; this is significantly more efficient anyway.
564 $any_event = queryfx_all(
566 'SELECT phid FROM %T WHERE importSourcePHID = %s LIMIT 1',
567 $table->getTableName(),
570 return (bool)$any_event;
573 final protected function shouldQueueDataImport($data) {
574 return (strlen($data) > self
::QUEUE_BYTE_LIMIT
);
577 final protected function queueDataImport(
578 PhabricatorCalendarImport
$import,
581 $import->newLogMessage(
582 PhabricatorCalendarImportQueueLogType
::LOGTYPE
,
584 'data.size' => strlen($data),
585 'data.limit' => self
::QUEUE_BYTE_LIMIT
,
588 // When we queue on this pathway, we're queueing in response to an explicit
589 // user action (like uploading a big `.ics` file), so we queue at normal
590 // priority instead of bulk/import priority.
592 PhabricatorWorker
::scheduleTask(
593 'PhabricatorCalendarImportReloadWorker',
595 'importPHID' => $import->getPHID(),
596 'via' => PhabricatorCalendarImportReloadWorker
::VIA_BACKGROUND
,
599 'objectPHID' => $import->getPHID(),