Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/import/PhabricatorCalendarImportEngine.php
12256 views
1
<?php
2
3
abstract class PhabricatorCalendarImportEngine
4
extends Phobject {
5
6
const QUEUE_BYTE_LIMIT = 524288;
7
8
final public function getImportEngineType() {
9
return $this->getPhobjectClassConstant('ENGINETYPE', 64);
10
}
11
12
abstract public function getImportEngineName();
13
abstract public function getImportEngineTypeName();
14
abstract public function getImportEngineHint();
15
16
public function appendImportProperties(
17
PhabricatorUser $viewer,
18
PhabricatorCalendarImport $import,
19
PHUIPropertyListView $properties) {
20
return;
21
}
22
23
abstract public function newEditEngineFields(
24
PhabricatorEditEngine $engine,
25
PhabricatorCalendarImport $import);
26
27
abstract public function getDisplayName(PhabricatorCalendarImport $import);
28
29
abstract public function importEventsFromSource(
30
PhabricatorUser $viewer,
31
PhabricatorCalendarImport $import,
32
$should_queue);
33
34
abstract public function canDisable(
35
PhabricatorUser $viewer,
36
PhabricatorCalendarImport $import);
37
38
public function explainCanDisable(
39
PhabricatorUser $viewer,
40
PhabricatorCalendarImport $import) {
41
throw new PhutilMethodNotImplementedException();
42
}
43
44
abstract public function supportsTriggers(
45
PhabricatorCalendarImport $import);
46
47
final public static function getAllImportEngines() {
48
return id(new PhutilClassMapQuery())
49
->setAncestorClass(__CLASS__)
50
->setUniqueMethod('getImportEngineType')
51
->setSortMethod('getImportEngineName')
52
->execute();
53
}
54
55
final protected function importEventDocument(
56
PhabricatorUser $viewer,
57
PhabricatorCalendarImport $import,
58
PhutilCalendarRootNode $root = null) {
59
60
$event_type = PhutilCalendarEventNode::NODETYPE;
61
62
$nodes = array();
63
if ($root) {
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,
70
array(
71
'node.type' => $node_type,
72
));
73
continue;
74
}
75
76
$nodes[] = $node;
77
}
78
}
79
}
80
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) {
86
$dates = array();
87
$dates[] = $node->getStartDateTime();
88
$dates[] = $node->getEndDateTime();
89
$dates[] = $node->getCreatedDateTime();
90
$dates[] = $node->getModifiedDateTime();
91
$rrule = $node->getRecurrenceRule();
92
if ($rrule) {
93
$dates[] = $rrule->getUntil();
94
}
95
96
$bad_date = false;
97
foreach ($dates as $date) {
98
if ($date === null) {
99
continue;
100
}
101
102
$year = $date->getYear();
103
if ($year < 1970 || $year > 2037) {
104
$bad_date = true;
105
break;
106
}
107
}
108
109
if ($bad_date) {
110
$import->newLogMessage(
111
PhabricatorCalendarImportEpochLogType::LOGTYPE,
112
array());
113
unset($nodes[$key]);
114
}
115
}
116
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();
122
if (!$rrule) {
123
// This is not a recurring event, so we don't need to check the
124
// frequency.
125
continue;
126
}
127
$scale = $rrule->getFrequencyScale();
128
if ($scale >= PhutilCalendarRecurrenceRule::SCALE_DAILY) {
129
// This is a daily, weekly, monthly, or yearly event. These are
130
// supported.
131
} else {
132
// This is an hourly, minutely, or secondly event.
133
$import->newLogMessage(
134
PhabricatorCalendarImportFrequencyLogType::LOGTYPE,
135
array(
136
'frequency' => $rrule->getFrequency(),
137
));
138
unset($nodes[$key]);
139
}
140
}
141
142
$node_map = array();
143
foreach ($nodes as $node) {
144
$full_uid = $this->getFullNodeUID($node);
145
if (isset($node_map[$full_uid])) {
146
$import->newLogMessage(
147
PhabricatorCalendarImportDuplicateLogType::LOGTYPE,
148
array(
149
'uid.full' => $full_uid,
150
));
151
continue;
152
}
153
$node_map[$full_uid] = $node;
154
}
155
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();
163
$matches = null;
164
if (preg_match('/^(PHID-.*)@(.*)\z/', $uid, $matches)) {
165
$likely_phids[$full_uid] = $matches[1];
166
}
167
}
168
169
if ($likely_phids) {
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
172
// them.
173
$events = id(new PhabricatorCalendarEventQuery())
174
->setViewer(PhabricatorUser::getOmnipotentUser())
175
->withPHIDs($likely_phids)
176
->execute();
177
$events = mpull($events, null, 'getPHID');
178
foreach ($node_map as $full_uid => $node) {
179
$phid = idx($likely_phids, $full_uid);
180
if (!$phid) {
181
continue;
182
}
183
184
$event = idx($events, $phid);
185
if (!$event) {
186
continue;
187
}
188
189
$import->newLogMessage(
190
PhabricatorCalendarImportOriginalLogType::LOGTYPE,
191
array(
192
'phid' => $event->getPHID(),
193
));
194
195
unset($node_map[$full_uid]);
196
}
197
}
198
199
if ($node_map) {
200
$events = id(new PhabricatorCalendarEventQuery())
201
->setViewer($viewer)
202
->withImportAuthorPHIDs(array($import->getAuthorPHID()))
203
->withImportUIDs(array_keys($node_map))
204
->execute();
205
$events = mpull($events, null, 'getImportUID');
206
} else {
207
$events = null;
208
}
209
210
$xactions = array();
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);
216
if (!$event) {
217
$event = PhabricatorCalendarEvent::initializeNewCalendarEvent($viewer);
218
}
219
220
$event
221
->setImportAuthorPHID($import->getAuthorPHID())
222
->setImportSourcePHID($import->getPHID())
223
->setImportUID($full_uid)
224
->attachImportSource($import);
225
226
$this->updateEventFromNode($viewer, $event, $node);
227
$xactions[$full_uid] = $this->newUpdateTransactions($event, $node);
228
$update_map[$full_uid] = $event;
229
230
$attendee_map[$full_uid] = array();
231
$attendees = $node->getAttendees();
232
$private_index = 1;
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
236
// of the product.
237
$name = $attendee->getName();
238
if (preg_match('/@/', $name)) {
239
$name = new PhutilEmailAddress($name);
240
$name = $name->getDisplayName();
241
}
242
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);
247
$private_index++;
248
}
249
250
$attendee_map[$full_uid][$name] = $attendee;
251
}
252
}
253
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;
258
}
259
}
260
261
if ($attendee_names) {
262
$external_invitees = id(new PhabricatorCalendarExternalInviteeQuery())
263
->setViewer($viewer)
264
->withNames(array_keys($attendee_names))
265
->execute();
266
$external_invitees = mpull($external_invitees, null, 'getName');
267
268
foreach ($attendee_names as $name => $attendee) {
269
if (isset($external_invitees[$name])) {
270
continue;
271
}
272
273
$external_invitee = id(new PhabricatorCalendarExternalInvitee())
274
->setName($name)
275
->setURI($attendee->getURI())
276
->setSourcePHID($import->getPHID());
277
278
try {
279
$external_invitee->save();
280
} catch (AphrontDuplicateKeyQueryException $ex) {
281
$external_invitee =
282
id(new PhabricatorCalendarExternalInviteeQuery())
283
->setViewer($viewer)
284
->withNames(array($name))
285
->executeOne();
286
}
287
288
$external_invitees[$name] = $external_invitee;
289
}
290
}
291
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;
299
continue;
300
}
301
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
305
// this node.
306
307
$import->newLogMessage(
308
PhabricatorCalendarImportOrphanLogType::LOGTYPE,
309
array(
310
'uid.full' => $full_uid,
311
'uid.parent' => $parent_uid,
312
));
313
314
continue;
315
}
316
317
// Otherwise, we're going to insert the parent first, then insert
318
// the child.
319
$insert_order[$parent_uid] = $parent_uid;
320
$insert_order[$full_uid] = $full_uid;
321
}
322
323
// TODO: Define per-engine content sources so this can say "via Upload" or
324
// whatever.
325
$content_source = PhabricatorContentSource::newForSource(
326
PhabricatorWebContentSource::SOURCECONST);
327
328
// NOTE: We're using the omnipotent user here because imported events are
329
// otherwise immutable.
330
$edit_actor = PhabricatorUser::getOmnipotentUser();
331
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]);
335
if ($parent_uid) {
336
$parent_phid = $update_map[$parent_uid]->getPHID();
337
} else {
338
$parent_phid = null;
339
}
340
341
$event->setInstanceOfEventPHID($parent_phid);
342
343
$event_xactions = $xactions[$full_uid];
344
345
$editor = id(new PhabricatorCalendarEventEditor())
346
->setActor($edit_actor)
347
->setActingAsPHID($import->getPHID())
348
->setContentSource($content_source)
349
->setContinueOnNoEffect(true)
350
->setContinueOnMissingFields(true);
351
352
$is_new = !$event->getID();
353
354
$editor->applyTransactions($event, $event_xactions);
355
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');
362
363
$new_map = array();
364
foreach ($attendees as $name => $attendee) {
365
$phid = $external_invitees[$name]->getPHID();
366
367
$invitee = idx($old_map, $phid);
368
if (!$invitee) {
369
$invitee = id(new PhabricatorCalendarEventInvitee())
370
->setEventPHID($event->getPHID())
371
->setInviteePHID($phid)
372
->setInviterPHID($import->getPHID());
373
}
374
375
switch ($attendee->getStatus()) {
376
case PhutilCalendarUserNode::STATUS_ACCEPTED:
377
$status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
378
break;
379
case PhutilCalendarUserNode::STATUS_DECLINED:
380
$status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
381
break;
382
case PhutilCalendarUserNode::STATUS_INVITED:
383
default:
384
$status = PhabricatorCalendarEventInvitee::STATUS_INVITED;
385
break;
386
}
387
$invitee->setStatus($status);
388
$invitee->save();
389
390
$new_map[$phid] = $invitee;
391
}
392
393
foreach ($old_map as $phid => $invitee) {
394
if (empty($new_map[$phid])) {
395
$invitee->delete();
396
}
397
}
398
399
$event->attachInvitees($new_map);
400
401
$import->newLogMessage(
402
PhabricatorCalendarImportUpdateLogType::LOGTYPE,
403
array(
404
'new' => $is_new,
405
'phid' => $event->getPHID(),
406
));
407
}
408
409
if (!$update_map) {
410
$import->newLogMessage(
411
PhabricatorCalendarImportEmptyLogType::LOGTYPE,
412
array());
413
}
414
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())
418
->setViewer($viewer)
419
->withImportSourcePHIDs(array($import->getPHID()))
420
->execute();
421
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.
426
continue;
427
}
428
429
$import->newLogMessage(
430
PhabricatorCalendarImportDeleteLogType::LOGTYPE,
431
array(
432
'name' => $source_event->getName(),
433
));
434
435
$engine->destroyObject($source_event);
436
}
437
}
438
439
private function getFullNodeUID(PhutilCalendarEventNode $node) {
440
$uid = $node->getUID();
441
$instance_epoch = $this->getNodeInstanceEpoch($node);
442
$full_uid = $uid.'/'.$instance_epoch;
443
444
return $full_uid;
445
}
446
447
private function getParentNodeUID(PhutilCalendarEventNode $node) {
448
$recurrence_id = $node->getRecurrenceID();
449
450
if (!strlen($recurrence_id)) {
451
return null;
452
}
453
454
return $node->getUID().'/';
455
}
456
457
private function getNodeInstanceEpoch(PhutilCalendarEventNode $node) {
458
$instance_iso = $node->getRecurrenceID();
459
if (strlen($instance_iso)) {
460
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601(
461
$instance_iso);
462
$instance_epoch = $instance_datetime->getEpoch();
463
} else {
464
$instance_epoch = null;
465
}
466
467
return $instance_epoch;
468
}
469
470
private function newUpdateTransactions(
471
PhabricatorCalendarEvent $event,
472
PhutilCalendarEventNode $node) {
473
474
$xactions = array();
475
$uid = $node->getUID();
476
477
if (!$event->getID()) {
478
$xactions[] = id(new PhabricatorCalendarEventTransaction())
479
->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
480
->setNewValue(true);
481
}
482
483
$name = $node->getName();
484
if (!strlen($name)) {
485
if (strlen($uid)) {
486
$name = pht('Unnamed Event "%s"', $uid);
487
} else {
488
$name = pht('Unnamed Imported Event');
489
}
490
}
491
$xactions[] = id(new PhabricatorCalendarEventTransaction())
492
->setTransactionType(
493
PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE)
494
->setNewValue($name);
495
496
$description = $node->getDescription();
497
$xactions[] = id(new PhabricatorCalendarEventTransaction())
498
->setTransactionType(
499
PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE)
500
->setNewValue((string)$description);
501
502
$is_recurring = (bool)$node->getRecurrenceRule();
503
$xactions[] = id(new PhabricatorCalendarEventTransaction())
504
->setTransactionType(
505
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE)
506
->setNewValue($is_recurring);
507
508
return $xactions;
509
}
510
511
private function updateEventFromNode(
512
PhabricatorUser $actor,
513
PhabricatorCalendarEvent $event,
514
PhutilCalendarEventNode $node) {
515
516
$instance_epoch = $this->getNodeInstanceEpoch($node);
517
$event->setUTCInstanceEpoch($instance_epoch);
518
519
$timezone = $actor->getTimezoneIdentifier();
520
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);
527
528
$event
529
->setStartDateTime($start_datetime)
530
->setEndDateTime($end_datetime);
531
532
$event->setIsAllDay((int)$start_datetime->getIsAllDay());
533
534
// TODO: This should be transactional, but the transaction only accepts
535
// simple frequency rules right now.
536
$rrule = $node->getRecurrenceRule();
537
if ($rrule) {
538
$event->setRecurrenceRule($rrule);
539
540
$until_datetime = $rrule->getUntil();
541
if ($until_datetime) {
542
$until_datetime->setViewerTimezone($timezone);
543
$event->setUntilDateTime($until_datetime);
544
}
545
546
$count = $rrule->getCount();
547
$event->setParameter('recurrenceCount', $count);
548
}
549
550
return $event;
551
}
552
553
public function canDeleteAnyEvents(
554
PhabricatorUser $viewer,
555
PhabricatorCalendarImport $import) {
556
557
$table = new PhabricatorCalendarEvent();
558
$conn = $table->establishConnection('r');
559
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.
563
564
$any_event = queryfx_all(
565
$conn,
566
'SELECT phid FROM %T WHERE importSourcePHID = %s LIMIT 1',
567
$table->getTableName(),
568
$import->getPHID());
569
570
return (bool)$any_event;
571
}
572
573
final protected function shouldQueueDataImport($data) {
574
return (strlen($data) > self::QUEUE_BYTE_LIMIT);
575
}
576
577
final protected function queueDataImport(
578
PhabricatorCalendarImport $import,
579
$data) {
580
581
$import->newLogMessage(
582
PhabricatorCalendarImportQueueLogType::LOGTYPE,
583
array(
584
'data.size' => strlen($data),
585
'data.limit' => self::QUEUE_BYTE_LIMIT,
586
));
587
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.
591
592
PhabricatorWorker::scheduleTask(
593
'PhabricatorCalendarImportReloadWorker',
594
array(
595
'importPHID' => $import->getPHID(),
596
'via' => PhabricatorCalendarImportReloadWorker::VIA_BACKGROUND,
597
),
598
array(
599
'objectPHID' => $import->getPHID(),
600
));
601
}
602
603
604
}
605
606