Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/storage/PhabricatorCalendarEvent.php
12253 views
1
<?php
2
3
final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
4
implements
5
PhabricatorPolicyInterface,
6
PhabricatorExtendedPolicyInterface,
7
PhabricatorPolicyCodexInterface,
8
PhabricatorProjectInterface,
9
PhabricatorMarkupInterface,
10
PhabricatorApplicationTransactionInterface,
11
PhabricatorSubscribableInterface,
12
PhabricatorTokenReceiverInterface,
13
PhabricatorDestructibleInterface,
14
PhabricatorMentionableInterface,
15
PhabricatorFlaggableInterface,
16
PhabricatorSpacesInterface,
17
PhabricatorFulltextInterface,
18
PhabricatorFerretInterface,
19
PhabricatorConduitResultInterface {
20
21
protected $name;
22
protected $hostPHID;
23
protected $description;
24
protected $isCancelled;
25
protected $isAllDay;
26
protected $icon;
27
protected $isStub;
28
29
protected $isRecurring = 0;
30
31
protected $seriesParentPHID;
32
protected $instanceOfEventPHID;
33
protected $sequenceIndex;
34
35
protected $viewPolicy;
36
protected $editPolicy;
37
38
protected $spacePHID;
39
40
protected $utcInitialEpoch;
41
protected $utcUntilEpoch;
42
protected $utcInstanceEpoch;
43
protected $parameters = array();
44
45
protected $importAuthorPHID;
46
protected $importSourcePHID;
47
protected $importUIDIndex;
48
protected $importUID;
49
50
private $parentEvent = self::ATTACHABLE;
51
private $invitees = self::ATTACHABLE;
52
private $importSource = self::ATTACHABLE;
53
private $rsvps = self::ATTACHABLE;
54
55
private $viewerTimezone;
56
57
private $isGhostEvent = false;
58
private $stubInvitees;
59
60
public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
61
$app = id(new PhabricatorApplicationQuery())
62
->setViewer($actor)
63
->withClasses(array('PhabricatorCalendarApplication'))
64
->executeOne();
65
66
$view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY;
67
$edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY;
68
$view_policy = $app->getPolicy($view_default);
69
$edit_policy = $app->getPolicy($edit_default);
70
71
$now = PhabricatorTime::getNow();
72
73
$default_icon = 'fa-calendar';
74
75
$datetime_defaults = self::newDefaultEventDateTimes(
76
$actor,
77
$now);
78
list($datetime_start, $datetime_end) = $datetime_defaults;
79
80
// When importing events from a context like "bin/calendar reload", we may
81
// be acting as the omnipotent user.
82
$host_phid = $actor->getPHID();
83
if (!$host_phid) {
84
$host_phid = $app->getPHID();
85
}
86
87
return id(new PhabricatorCalendarEvent())
88
->setDescription('')
89
->setHostPHID($host_phid)
90
->setIsCancelled(0)
91
->setIsAllDay(0)
92
->setIsStub(0)
93
->setIsRecurring(0)
94
->setIcon($default_icon)
95
->setViewPolicy($view_policy)
96
->setEditPolicy($edit_policy)
97
->setSpacePHID($actor->getDefaultSpacePHID())
98
->attachInvitees(array())
99
->setStartDateTime($datetime_start)
100
->setEndDateTime($datetime_end)
101
->attachImportSource(null)
102
->applyViewerTimezone($actor);
103
}
104
105
public static function newDefaultEventDateTimes(
106
PhabricatorUser $viewer,
107
$now) {
108
109
$datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch(
110
$now,
111
$viewer->getTimezoneIdentifier());
112
113
// Advance the time by an hour, then round downwards to the nearest hour.
114
// For example, if it is currently 3:25 PM, we suggest a default start time
115
// of 4 PM.
116
$datetime_start = $datetime_start
117
->newRelativeDateTime('PT1H')
118
->newAbsoluteDateTime();
119
$datetime_start->setMinute(0);
120
$datetime_start->setSecond(0);
121
122
// Default the end time to an hour after the start time.
123
$datetime_end = $datetime_start
124
->newRelativeDateTime('PT1H')
125
->newAbsoluteDateTime();
126
127
return array($datetime_start, $datetime_end);
128
}
129
130
private function newChild(
131
PhabricatorUser $actor,
132
$sequence,
133
PhutilCalendarDateTime $start = null) {
134
if (!$this->isParentEvent()) {
135
throw new Exception(
136
pht(
137
'Unable to generate a new child event for an event which is not '.
138
'a recurring parent event!'));
139
}
140
141
$series_phid = $this->getSeriesParentPHID();
142
if (!$series_phid) {
143
$series_phid = $this->getPHID();
144
}
145
146
$child = id(new self())
147
->setIsCancelled(0)
148
->setIsStub(0)
149
->setInstanceOfEventPHID($this->getPHID())
150
->setSeriesParentPHID($series_phid)
151
->setSequenceIndex($sequence)
152
->setIsRecurring(true)
153
->attachParentEvent($this)
154
->attachImportSource(null);
155
156
return $child->copyFromParent($actor, $start);
157
}
158
159
protected function readField($field) {
160
static $inherit = array(
161
'hostPHID' => true,
162
'isAllDay' => true,
163
'icon' => true,
164
'spacePHID' => true,
165
'viewPolicy' => true,
166
'editPolicy' => true,
167
'name' => true,
168
'description' => true,
169
'isCancelled' => true,
170
);
171
172
// Read these fields from the parent event instead of this event. For
173
// example, we want any changes to the parent event's name to apply to
174
// the child.
175
if (isset($inherit[$field])) {
176
if ($this->getIsStub()) {
177
// TODO: This should be unconditional, but the execution order of
178
// CalendarEventQuery and applyViewerTimezone() are currently odd.
179
if ($this->parentEvent !== self::ATTACHABLE) {
180
return $this->getParentEvent()->readField($field);
181
}
182
}
183
}
184
185
return parent::readField($field);
186
}
187
188
189
public function copyFromParent(
190
PhabricatorUser $actor,
191
PhutilCalendarDateTime $start = null) {
192
193
if (!$this->isChildEvent()) {
194
throw new Exception(
195
pht(
196
'Unable to copy from parent event: this is not a child event.'));
197
}
198
199
$parent = $this->getParentEvent();
200
201
$this
202
->setHostPHID($parent->getHostPHID())
203
->setIsAllDay($parent->getIsAllDay())
204
->setIcon($parent->getIcon())
205
->setSpacePHID($parent->getSpacePHID())
206
->setViewPolicy($parent->getViewPolicy())
207
->setEditPolicy($parent->getEditPolicy())
208
->setName($parent->getName())
209
->setDescription($parent->getDescription())
210
->setIsCancelled($parent->getIsCancelled());
211
212
if ($start) {
213
$start_datetime = $start;
214
} else {
215
$sequence = $this->getSequenceIndex();
216
$start_datetime = $parent->newSequenceIndexDateTime($sequence);
217
218
if (!$start_datetime) {
219
throw new Exception(
220
pht(
221
'Sequence "%s" is not valid for event!',
222
$sequence));
223
}
224
}
225
226
$duration = $parent->newDuration();
227
$end_datetime = $start_datetime->newRelativeDateTime($duration);
228
229
$this
230
->setStartDateTime($start_datetime)
231
->setEndDateTime($end_datetime);
232
233
if ($parent->isImportedEvent()) {
234
$full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch();
235
236
// NOTE: We don't attach the import source because this gets called
237
// from CalendarEventQuery while building ghosts, before we've loaded
238
// and attached sources. Possibly this sequence should be flipped.
239
240
$this
241
->setImportAuthorPHID($parent->getImportAuthorPHID())
242
->setImportSourcePHID($parent->getImportSourcePHID())
243
->setImportUID($full_uid);
244
}
245
246
return $this;
247
}
248
249
public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) {
250
return (bool)$this->newSequenceIndexDateTime($sequence);
251
}
252
253
public function newSequenceIndexDateTime($sequence) {
254
$set = $this->newRecurrenceSet();
255
if (!$set) {
256
return null;
257
}
258
259
$limit = $sequence + 1;
260
$count = $this->getRecurrenceCount();
261
if ($count && ($count < $limit)) {
262
return null;
263
}
264
265
$instances = $set->getEventsBetween(
266
null,
267
$this->newUntilDateTime(),
268
$limit);
269
270
return idx($instances, $sequence, null);
271
}
272
273
public function newStub(PhabricatorUser $actor, $sequence) {
274
$stub = $this->newChild($actor, $sequence);
275
276
$stub->setIsStub(1);
277
278
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
279
$stub->save();
280
unset($unguarded);
281
282
$stub->applyViewerTimezone($actor);
283
284
return $stub;
285
}
286
287
public function newGhost(
288
PhabricatorUser $actor,
289
$sequence,
290
PhutilCalendarDateTime $start = null) {
291
292
$ghost = $this->newChild($actor, $sequence, $start);
293
294
$ghost
295
->setIsGhostEvent(true)
296
->makeEphemeral();
297
298
$ghost->applyViewerTimezone($actor);
299
300
return $ghost;
301
}
302
303
public function applyViewerTimezone(PhabricatorUser $viewer) {
304
$this->viewerTimezone = $viewer->getTimezoneIdentifier();
305
return $this;
306
}
307
308
public function getDuration() {
309
return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch());
310
}
311
312
public function updateUTCEpochs() {
313
// The "intitial" epoch is the start time of the event, in UTC.
314
$start_date = $this->newStartDateTime()
315
->setViewerTimezone('UTC');
316
$start_epoch = $start_date->getEpoch();
317
$this->setUTCInitialEpoch($start_epoch);
318
319
// The "until" epoch is the last UTC epoch on which any instance of this
320
// event occurs. For infinitely recurring events, it is `null`.
321
322
if (!$this->getIsRecurring()) {
323
$end_date = $this->newEndDateTime()
324
->setViewerTimezone('UTC');
325
$until_epoch = $end_date->getEpoch();
326
} else {
327
$until_epoch = null;
328
$until_date = $this->newUntilDateTime();
329
if ($until_date) {
330
$until_date->setViewerTimezone('UTC');
331
$duration = $this->newDuration();
332
$until_epoch = id(new PhutilCalendarRelativeDateTime())
333
->setOrigin($until_date)
334
->setDuration($duration)
335
->getEpoch();
336
}
337
}
338
$this->setUTCUntilEpoch($until_epoch);
339
340
// The "instance" epoch is a property of instances of recurring events.
341
// It's the original UTC epoch on which the instance started. Usually that
342
// is the same as the start date, but they may be different if the instance
343
// has been edited.
344
345
// The ICS format uses this value (original start time) to identify event
346
// instances, and must do so because it allows additional arbitrary
347
// instances to be added (with "RDATE").
348
349
$instance_epoch = null;
350
$instance_date = $this->newInstanceDateTime();
351
if ($instance_date) {
352
$instance_epoch = $instance_date
353
->setViewerTimezone('UTC')
354
->getEpoch();
355
}
356
$this->setUTCInstanceEpoch($instance_epoch);
357
358
return $this;
359
}
360
361
public function save() {
362
$import_uid = $this->getImportUID();
363
if ($import_uid !== null) {
364
$index = PhabricatorHash::digestForIndex($import_uid);
365
} else {
366
$index = null;
367
}
368
$this->setImportUIDIndex($index);
369
370
$this->updateUTCEpochs();
371
372
return parent::save();
373
}
374
375
/**
376
* Get the event start epoch for evaluating invitee availability.
377
*
378
* When assessing availability, we pretend events start earlier than they
379
* really do. This allows us to mark users away for the entire duration of a
380
* series of back-to-back meetings, even if they don't strictly overlap.
381
*
382
* @return int Event start date for availability caches.
383
*/
384
public function getStartDateTimeEpochForCache() {
385
$epoch = $this->getStartDateTimeEpoch();
386
$window = phutil_units('15 minutes in seconds');
387
return ($epoch - $window);
388
}
389
390
public function getEndDateTimeEpochForCache() {
391
return $this->getEndDateTimeEpoch();
392
}
393
394
protected function getConfiguration() {
395
return array(
396
self::CONFIG_AUX_PHID => true,
397
self::CONFIG_COLUMN_SCHEMA => array(
398
'name' => 'text',
399
'description' => 'text',
400
'isCancelled' => 'bool',
401
'isAllDay' => 'bool',
402
'icon' => 'text32',
403
'isRecurring' => 'bool',
404
'seriesParentPHID' => 'phid?',
405
'instanceOfEventPHID' => 'phid?',
406
'sequenceIndex' => 'uint32?',
407
'isStub' => 'bool',
408
'utcInitialEpoch' => 'epoch',
409
'utcUntilEpoch' => 'epoch?',
410
'utcInstanceEpoch' => 'epoch?',
411
412
'importAuthorPHID' => 'phid?',
413
'importSourcePHID' => 'phid?',
414
'importUIDIndex' => 'bytes12?',
415
'importUID' => 'text?',
416
),
417
self::CONFIG_KEY_SCHEMA => array(
418
'key_instance' => array(
419
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
420
'unique' => true,
421
),
422
'key_epoch' => array(
423
'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
424
),
425
'key_rdate' => array(
426
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
427
'unique' => true,
428
),
429
'key_series' => array(
430
'columns' => array('seriesParentPHID', 'utcInitialEpoch'),
431
),
432
),
433
self::CONFIG_SERIALIZATION => array(
434
'parameters' => self::SERIALIZATION_JSON,
435
),
436
) + parent::getConfiguration();
437
}
438
439
public function getPHIDType() {
440
return PhabricatorCalendarEventPHIDType::TYPECONST;
441
}
442
443
public function getMonogram() {
444
return 'E'.$this->getID();
445
}
446
447
public function getInvitees() {
448
if ($this->getIsGhostEvent() || $this->getIsStub()) {
449
if ($this->stubInvitees === null) {
450
$this->stubInvitees = $this->newStubInvitees();
451
}
452
return $this->stubInvitees;
453
}
454
455
return $this->assertAttached($this->invitees);
456
}
457
458
public function getInviteeForPHID($phid) {
459
$invitees = $this->getInvitees();
460
$invitees = mpull($invitees, null, 'getInviteePHID');
461
return idx($invitees, $phid);
462
}
463
464
public static function getFrequencyMap() {
465
return array(
466
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
467
'label' => pht('Daily'),
468
),
469
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array(
470
'label' => pht('Weekly'),
471
),
472
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array(
473
'label' => pht('Monthly'),
474
),
475
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array(
476
'label' => pht('Yearly'),
477
),
478
);
479
}
480
481
private function newStubInvitees() {
482
$parent = $this->getParentEvent();
483
484
$parent_invitees = $parent->getInvitees();
485
$stub_invitees = array();
486
487
foreach ($parent_invitees as $invitee) {
488
$stub_invitee = id(new PhabricatorCalendarEventInvitee())
489
->setInviteePHID($invitee->getInviteePHID())
490
->setInviterPHID($invitee->getInviterPHID())
491
->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED);
492
493
$stub_invitees[] = $stub_invitee;
494
}
495
496
return $stub_invitees;
497
}
498
499
public function attachInvitees(array $invitees) {
500
$this->invitees = $invitees;
501
return $this;
502
}
503
504
public function getInviteePHIDsForEdit() {
505
$invitees = array();
506
507
foreach ($this->getInvitees() as $invitee) {
508
if ($invitee->isUninvited()) {
509
continue;
510
}
511
$invitees[] = $invitee->getInviteePHID();
512
}
513
514
return $invitees;
515
}
516
517
public function getUserInviteStatus($phid) {
518
$invitees = $this->getInvitees();
519
$invitees = mpull($invitees, null, 'getInviteePHID');
520
521
$invited = idx($invitees, $phid);
522
if (!$invited) {
523
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
524
}
525
526
$invited = $invited->getStatus();
527
return $invited;
528
}
529
530
public function getIsUserAttending($phid) {
531
$attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
532
533
$old_status = $this->getUserInviteStatus($phid);
534
$is_attending = ($old_status == $attending_status);
535
536
return $is_attending;
537
}
538
539
public function getIsGhostEvent() {
540
return $this->isGhostEvent;
541
}
542
543
public function setIsGhostEvent($is_ghost_event) {
544
$this->isGhostEvent = $is_ghost_event;
545
return $this;
546
}
547
548
public function getURI() {
549
if ($this->getIsGhostEvent()) {
550
$base = $this->getParentEvent()->getURI();
551
$sequence = $this->getSequenceIndex();
552
return "{$base}/{$sequence}/";
553
}
554
555
return '/'.$this->getMonogram();
556
}
557
558
public function getParentEvent() {
559
return $this->assertAttached($this->parentEvent);
560
}
561
562
public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
563
$this->parentEvent = $event;
564
return $this;
565
}
566
567
public function isParentEvent() {
568
return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID());
569
}
570
571
public function isChildEvent() {
572
return ($this->instanceOfEventPHID !== null);
573
}
574
575
public function renderEventDate(
576
PhabricatorUser $viewer,
577
$show_end) {
578
579
$start = $this->newStartDateTime();
580
$end = $this->newEndDateTime();
581
582
$min_date = $start->newPHPDateTime();
583
$max_date = $end->newPHPDateTime();
584
585
if ($this->getIsAllDay()) {
586
// Subtract one second since the stored date is exclusive.
587
$max_date = $max_date->modify('-1 second');
588
}
589
590
if ($show_end) {
591
$min_day = $min_date->format('Y m d');
592
$max_day = $max_date->format('Y m d');
593
594
$show_end_date = ($min_day != $max_day);
595
} else {
596
$show_end_date = false;
597
}
598
599
$min_epoch = $min_date->format('U');
600
$max_epoch = $max_date->format('U');
601
602
if ($this->getIsAllDay()) {
603
if ($show_end_date) {
604
return pht(
605
'%s - %s, All Day',
606
phabricator_date($min_epoch, $viewer),
607
phabricator_date($max_epoch, $viewer));
608
} else {
609
return pht(
610
'%s, All Day',
611
phabricator_date($min_epoch, $viewer));
612
}
613
} else if ($show_end_date) {
614
return pht(
615
'%s - %s',
616
phabricator_datetime($min_epoch, $viewer),
617
phabricator_datetime($max_epoch, $viewer));
618
} else if ($show_end) {
619
return pht(
620
'%s - %s',
621
phabricator_datetime($min_epoch, $viewer),
622
phabricator_time($max_epoch, $viewer));
623
} else {
624
return pht(
625
'%s',
626
phabricator_datetime($min_epoch, $viewer));
627
}
628
}
629
630
631
public function getDisplayIcon(PhabricatorUser $viewer) {
632
if ($this->getIsCancelled()) {
633
return 'fa-times';
634
}
635
636
if ($viewer->isLoggedIn()) {
637
$viewer_phid = $viewer->getPHID();
638
if ($this->isRSVPInvited($viewer_phid)) {
639
return 'fa-users';
640
} else {
641
$status = $this->getUserInviteStatus($viewer_phid);
642
switch ($status) {
643
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
644
return 'fa-check-circle';
645
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
646
return 'fa-user-plus';
647
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
648
return 'fa-times-circle';
649
}
650
}
651
}
652
653
if ($this->isImportedEvent()) {
654
return 'fa-download';
655
}
656
657
return $this->getIcon();
658
}
659
660
public function getDisplayIconColor(PhabricatorUser $viewer) {
661
if ($this->getIsCancelled()) {
662
return 'red';
663
}
664
665
if ($this->isImportedEvent()) {
666
return 'orange';
667
}
668
669
if ($viewer->isLoggedIn()) {
670
$viewer_phid = $viewer->getPHID();
671
if ($this->isRSVPInvited($viewer_phid)) {
672
return 'green';
673
}
674
675
$status = $this->getUserInviteStatus($viewer_phid);
676
switch ($status) {
677
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
678
return 'green';
679
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
680
return 'green';
681
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
682
return 'grey';
683
}
684
}
685
686
return 'bluegrey';
687
}
688
689
public function getDisplayIconLabel(PhabricatorUser $viewer) {
690
if ($this->getIsCancelled()) {
691
return pht('Cancelled');
692
}
693
694
if ($viewer->isLoggedIn()) {
695
$status = $this->getUserInviteStatus($viewer->getPHID());
696
switch ($status) {
697
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
698
return pht('Attending');
699
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
700
return pht('Invited');
701
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
702
return pht('Declined');
703
}
704
}
705
706
return null;
707
}
708
709
public function getICSFilename() {
710
return $this->getMonogram().'.ics';
711
}
712
713
public function newIntermediateEventNode(
714
PhabricatorUser $viewer,
715
array $children) {
716
717
$base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
718
$domain = $base_uri->getDomain();
719
720
// NOTE: For recurring events, all of the events in the series have the
721
// same UID (the UID of the parent). The child event instances are
722
// differentiated by the "RECURRENCE-ID" field.
723
if ($this->isChildEvent()) {
724
$parent = $this->getParentEvent();
725
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
726
$this->getUTCInstanceEpoch());
727
$recurrence_id = $instance_datetime->getISO8601();
728
$rrule = null;
729
} else {
730
$parent = $this;
731
$recurrence_id = null;
732
$rrule = $this->newRecurrenceRule();
733
}
734
$uid = $parent->getPHID().'@'.$domain;
735
736
$created = $this->getDateCreated();
737
$created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created);
738
739
$modified = $this->getDateModified();
740
$modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
741
742
$date_start = $this->newStartDateTime();
743
$date_end = $this->newEndDateTime();
744
745
if ($this->getIsAllDay()) {
746
$date_start->setIsAllDay(true);
747
$date_end->setIsAllDay(true);
748
}
749
750
$host_phid = $this->getHostPHID();
751
752
$invitees = $this->getInvitees();
753
foreach ($invitees as $key => $invitee) {
754
if ($invitee->isUninvited()) {
755
unset($invitees[$key]);
756
}
757
}
758
759
$phids = array();
760
$phids[] = $host_phid;
761
foreach ($invitees as $invitee) {
762
$phids[] = $invitee->getInviteePHID();
763
}
764
765
$handles = $viewer->loadHandles($phids);
766
767
$host_handle = $handles[$host_phid];
768
$host_name = $host_handle->getFullName();
769
770
// NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does
771
// not look like an email address. Use a synthetic address so it shows
772
// the host name instead.
773
$install_uri = PhabricatorEnv::getProductionURI('/');
774
$install_uri = new PhutilURI($install_uri);
775
776
// This should possibly use "metamta.reply-handler-domain" instead, but
777
// we do not currently accept mail for users anyway, and that option may
778
// not be configured.
779
$mail_domain = $install_uri->getDomain();
780
$host_uri = "mailto:{$host_phid}@{$mail_domain}";
781
782
$organizer = id(new PhutilCalendarUserNode())
783
->setName($host_name)
784
->setURI($host_uri);
785
786
$attendees = array();
787
foreach ($invitees as $invitee) {
788
$invitee_phid = $invitee->getInviteePHID();
789
$invitee_handle = $handles[$invitee_phid];
790
$invitee_name = $invitee_handle->getFullName();
791
$invitee_uri = $invitee_handle->getURI();
792
$invitee_uri = PhabricatorEnv::getURI($invitee_uri);
793
794
switch ($invitee->getStatus()) {
795
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
796
$status = PhutilCalendarUserNode::STATUS_ACCEPTED;
797
break;
798
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
799
$status = PhutilCalendarUserNode::STATUS_DECLINED;
800
break;
801
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
802
default:
803
$status = PhutilCalendarUserNode::STATUS_INVITED;
804
break;
805
}
806
807
$attendees[] = id(new PhutilCalendarUserNode())
808
->setName($invitee_name)
809
->setURI($invitee_uri)
810
->setStatus($status);
811
}
812
813
// TODO: Use $children to generate EXDATE/RDATE information.
814
815
$node = id(new PhutilCalendarEventNode())
816
->setUID($uid)
817
->setName($this->getName())
818
->setDescription($this->getDescription())
819
->setCreatedDateTime($created)
820
->setModifiedDateTime($modified)
821
->setStartDateTime($date_start)
822
->setEndDateTime($date_end)
823
->setOrganizer($organizer)
824
->setAttendees($attendees);
825
826
if ($rrule) {
827
$node->setRecurrenceRule($rrule);
828
}
829
830
if ($recurrence_id) {
831
$node->setRecurrenceID($recurrence_id);
832
}
833
834
return $node;
835
}
836
837
public function newStartDateTime() {
838
$datetime = $this->getParameter('startDateTime');
839
return $this->newDateTimeFromDictionary($datetime);
840
}
841
842
public function getStartDateTimeEpoch() {
843
return $this->newStartDateTime()->getEpoch();
844
}
845
846
public function newEndDateTimeForEdit() {
847
$datetime = $this->getParameter('endDateTime');
848
return $this->newDateTimeFromDictionary($datetime);
849
}
850
851
public function newEndDateTime() {
852
$datetime = $this->newEndDateTimeForEdit();
853
854
// If this is an all day event, we move the end date time forward to the
855
// first second of the following day. This is consistent with what users
856
// expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day.
857
858
// For imported events, the end date is already stored with this
859
// adjustment.
860
861
if ($this->getIsAllDay() && !$this->isImportedEvent()) {
862
$datetime = $datetime
863
->newAbsoluteDateTime()
864
->setHour(0)
865
->setMinute(0)
866
->setSecond(0)
867
->newRelativeDateTime('P1D')
868
->newAbsoluteDateTime();
869
}
870
871
return $datetime;
872
}
873
874
public function getEndDateTimeEpoch() {
875
return $this->newEndDateTime()->getEpoch();
876
}
877
878
public function newUntilDateTime() {
879
$datetime = $this->getParameter('untilDateTime');
880
if ($datetime) {
881
return $this->newDateTimeFromDictionary($datetime);
882
}
883
884
return null;
885
}
886
887
public function getUntilDateTimeEpoch() {
888
$datetime = $this->newUntilDateTime();
889
890
if (!$datetime) {
891
return null;
892
}
893
894
return $datetime->getEpoch();
895
}
896
897
public function newDuration() {
898
return id(new PhutilCalendarDuration())
899
->setSeconds($this->getDuration());
900
}
901
902
public function newInstanceDateTime() {
903
if (!$this->getIsRecurring()) {
904
return null;
905
}
906
907
$index = $this->getSequenceIndex();
908
if (!$index) {
909
return null;
910
}
911
912
return $this->newSequenceIndexDateTime($index);
913
}
914
915
private function newDateTimeFromEpoch($epoch) {
916
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch);
917
918
if ($this->getIsAllDay()) {
919
$datetime->setIsAllDay(true);
920
}
921
922
return $this->newDateTimeFromDateTime($datetime);
923
}
924
925
private function newDateTimeFromDictionary(array $dict) {
926
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict);
927
return $this->newDateTimeFromDateTime($datetime);
928
}
929
930
private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) {
931
$viewer_timezone = $this->viewerTimezone;
932
if ($viewer_timezone) {
933
$datetime->setViewerTimezone($viewer_timezone);
934
}
935
936
return $datetime;
937
}
938
939
public function getParameter($key, $default = null) {
940
return idx($this->parameters, $key, $default);
941
}
942
943
public function setParameter($key, $value) {
944
$this->parameters[$key] = $value;
945
return $this;
946
}
947
948
public function setStartDateTime(PhutilCalendarDateTime $datetime) {
949
return $this->setParameter(
950
'startDateTime',
951
$datetime->newAbsoluteDateTime()->toDictionary());
952
}
953
954
public function setEndDateTime(PhutilCalendarDateTime $datetime) {
955
return $this->setParameter(
956
'endDateTime',
957
$datetime->newAbsoluteDateTime()->toDictionary());
958
}
959
960
public function setUntilDateTime(PhutilCalendarDateTime $datetime = null) {
961
if ($datetime) {
962
$value = $datetime->newAbsoluteDateTime()->toDictionary();
963
} else {
964
$value = null;
965
}
966
967
return $this->setParameter('untilDateTime', $value);
968
}
969
970
public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) {
971
return $this->setParameter(
972
'recurrenceRule',
973
$rrule->toDictionary());
974
}
975
976
public function newRecurrenceRule() {
977
if ($this->isChildEvent()) {
978
return $this->getParentEvent()->newRecurrenceRule();
979
}
980
981
if (!$this->getIsRecurring()) {
982
return null;
983
}
984
985
$dict = $this->getParameter('recurrenceRule');
986
if (!$dict) {
987
return null;
988
}
989
990
$rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict);
991
992
$start = $this->newStartDateTime();
993
$rrule->setStartDateTime($start);
994
995
$until = $this->newUntilDateTime();
996
if ($until) {
997
$rrule->setUntil($until);
998
}
999
1000
$count = $this->getRecurrenceCount();
1001
if ($count) {
1002
$rrule->setCount($count);
1003
}
1004
1005
return $rrule;
1006
}
1007
1008
public function getRecurrenceCount() {
1009
$count = (int)$this->getParameter('recurrenceCount');
1010
1011
if (!$count) {
1012
return null;
1013
}
1014
1015
return $count;
1016
}
1017
1018
public function newRecurrenceSet() {
1019
if ($this->isChildEvent()) {
1020
return $this->getParentEvent()->newRecurrenceSet();
1021
}
1022
1023
$set = new PhutilCalendarRecurrenceSet();
1024
1025
if ($this->viewerTimezone) {
1026
$set->setViewerTimezone($this->viewerTimezone);
1027
}
1028
1029
$rrule = $this->newRecurrenceRule();
1030
if (!$rrule) {
1031
return null;
1032
}
1033
1034
$set->addSource($rrule);
1035
1036
return $set;
1037
}
1038
1039
public function isImportedEvent() {
1040
return (bool)$this->getImportSourcePHID();
1041
}
1042
1043
public function getImportSource() {
1044
return $this->assertAttached($this->importSource);
1045
}
1046
1047
public function attachImportSource(
1048
PhabricatorCalendarImport $import = null) {
1049
$this->importSource = $import;
1050
return $this;
1051
}
1052
1053
public function loadForkTarget(PhabricatorUser $viewer) {
1054
if (!$this->getIsRecurring()) {
1055
// Can't fork an event which isn't recurring.
1056
return null;
1057
}
1058
1059
if ($this->isChildEvent()) {
1060
// If this is a child event, this is the fork target.
1061
return $this;
1062
}
1063
1064
if (!$this->isValidSequenceIndex($viewer, 1)) {
1065
// This appears to be a "recurring" event with no valid instances: for
1066
// example, its "until" date is before the second instance would occur.
1067
// This can happen if we already forked the event or if users entered
1068
// silly stuff. Just edit the event directly without forking anything.
1069
return null;
1070
}
1071
1072
1073
$next_event = id(new PhabricatorCalendarEventQuery())
1074
->setViewer($viewer)
1075
->withInstanceSequencePairs(
1076
array(
1077
array($this->getPHID(), 1),
1078
))
1079
->requireCapabilities(
1080
array(
1081
PhabricatorPolicyCapability::CAN_VIEW,
1082
PhabricatorPolicyCapability::CAN_EDIT,
1083
))
1084
->executeOne();
1085
1086
if (!$next_event) {
1087
$next_event = $this->newStub($viewer, 1);
1088
}
1089
1090
return $next_event;
1091
}
1092
1093
public function loadFutureEvents(PhabricatorUser $viewer) {
1094
// NOTE: If you can't edit some of the future events, we just
1095
// don't try to update them. This seems like it's probably what
1096
// users are likely to expect.
1097
1098
// NOTE: This only affects events that are currently in the same
1099
// series, not all events that were ever in the original series.
1100
// We could use series PHIDs instead of parent PHIDs to affect more
1101
// events if this turns out to be counterintuitive. Other
1102
// applications differ in their behavior.
1103
1104
return id(new PhabricatorCalendarEventQuery())
1105
->setViewer($viewer)
1106
->withParentEventPHIDs(array($this->getPHID()))
1107
->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null)
1108
->requireCapabilities(
1109
array(
1110
PhabricatorPolicyCapability::CAN_VIEW,
1111
PhabricatorPolicyCapability::CAN_EDIT,
1112
))
1113
->execute();
1114
}
1115
1116
public function getNotificationPHIDs() {
1117
$phids = array();
1118
if ($this->getPHID()) {
1119
$phids[] = $this->getPHID();
1120
}
1121
1122
if ($this->getSeriesParentPHID()) {
1123
$phids[] = $this->getSeriesParentPHID();
1124
}
1125
1126
return $phids;
1127
}
1128
1129
public function getRSVPs($phid) {
1130
return $this->assertAttachedKey($this->rsvps, $phid);
1131
}
1132
1133
public function attachRSVPs(array $rsvps) {
1134
$this->rsvps = $rsvps;
1135
return $this;
1136
}
1137
1138
public function isRSVPInvited($phid) {
1139
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
1140
return ($this->getRSVPStatus($phid) == $status_invited);
1141
}
1142
1143
public function hasRSVPAuthority($phid, $other_phid) {
1144
foreach ($this->getRSVPs($phid) as $rsvp) {
1145
if ($rsvp->getInviteePHID() == $other_phid) {
1146
return true;
1147
}
1148
}
1149
1150
return false;
1151
}
1152
1153
public function getRSVPStatus($phid) {
1154
// Check for an individual invitee record first.
1155
$invitees = $this->invitees;
1156
$invitees = mpull($invitees, null, 'getInviteePHID');
1157
$invitee = idx($invitees, $phid);
1158
if ($invitee) {
1159
return $invitee->getStatus();
1160
}
1161
1162
// If we don't have one, try to find an invited status for the user's
1163
// projects.
1164
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
1165
foreach ($this->getRSVPs($phid) as $rsvp) {
1166
if ($rsvp->getStatus() == $status_invited) {
1167
return $status_invited;
1168
}
1169
}
1170
1171
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
1172
}
1173
1174
1175
1176
/* -( Markup Interface )--------------------------------------------------- */
1177
1178
1179
/**
1180
* @task markup
1181
*/
1182
public function getMarkupFieldKey($field) {
1183
$content = $this->getMarkupText($field);
1184
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
1185
}
1186
1187
1188
/**
1189
* @task markup
1190
*/
1191
public function getMarkupText($field) {
1192
return $this->getDescription();
1193
}
1194
1195
1196
/**
1197
* @task markup
1198
*/
1199
public function newMarkupEngine($field) {
1200
return PhabricatorMarkupEngine::newCalendarMarkupEngine();
1201
}
1202
1203
1204
/**
1205
* @task markup
1206
*/
1207
public function didMarkupText(
1208
$field,
1209
$output,
1210
PhutilMarkupEngine $engine) {
1211
return $output;
1212
}
1213
1214
1215
/**
1216
* @task markup
1217
*/
1218
public function shouldUseMarkupCache($field) {
1219
return (bool)$this->getID();
1220
}
1221
1222
/* -( PhabricatorPolicyInterface )----------------------------------------- */
1223
1224
1225
public function getCapabilities() {
1226
return array(
1227
PhabricatorPolicyCapability::CAN_VIEW,
1228
PhabricatorPolicyCapability::CAN_EDIT,
1229
);
1230
}
1231
1232
public function getPolicy($capability) {
1233
switch ($capability) {
1234
case PhabricatorPolicyCapability::CAN_VIEW:
1235
return $this->getViewPolicy();
1236
case PhabricatorPolicyCapability::CAN_EDIT:
1237
if ($this->isImportedEvent()) {
1238
return PhabricatorPolicies::POLICY_NOONE;
1239
} else {
1240
return $this->getEditPolicy();
1241
}
1242
}
1243
}
1244
1245
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1246
if ($this->isImportedEvent()) {
1247
return false;
1248
}
1249
1250
// The host of an event can always view and edit it.
1251
$user_phid = $this->getHostPHID();
1252
if ($user_phid) {
1253
$viewer_phid = $viewer->getPHID();
1254
if ($viewer_phid == $user_phid) {
1255
return true;
1256
}
1257
}
1258
1259
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
1260
$status = $this->getUserInviteStatus($viewer->getPHID());
1261
if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
1262
$status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
1263
$status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
1264
return true;
1265
}
1266
}
1267
1268
return false;
1269
}
1270
1271
1272
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
1273
1274
1275
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
1276
$extended = array();
1277
1278
switch ($capability) {
1279
case PhabricatorPolicyCapability::CAN_VIEW:
1280
$import_source = $this->getImportSource();
1281
if ($import_source) {
1282
$extended[] = array(
1283
$import_source,
1284
PhabricatorPolicyCapability::CAN_VIEW,
1285
);
1286
}
1287
break;
1288
}
1289
1290
return $extended;
1291
}
1292
1293
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
1294
1295
public function newPolicyCodex() {
1296
return new PhabricatorCalendarEventPolicyCodex();
1297
}
1298
1299
1300
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
1301
1302
1303
public function getApplicationTransactionEditor() {
1304
return new PhabricatorCalendarEventEditor();
1305
}
1306
1307
public function getApplicationTransactionTemplate() {
1308
return new PhabricatorCalendarEventTransaction();
1309
}
1310
1311
1312
/* -( PhabricatorSubscribableInterface )----------------------------------- */
1313
1314
1315
public function isAutomaticallySubscribed($phid) {
1316
return ($phid == $this->getHostPHID());
1317
}
1318
1319
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
1320
1321
1322
public function getUsersToNotifyOfTokenGiven() {
1323
return array($this->getHostPHID());
1324
}
1325
1326
/* -( PhabricatorDestructibleInterface )----------------------------------- */
1327
1328
1329
public function destroyObjectPermanently(
1330
PhabricatorDestructionEngine $engine) {
1331
1332
$this->openTransaction();
1333
$invitees = id(new PhabricatorCalendarEventInvitee())->loadAllWhere(
1334
'eventPHID = %s',
1335
$this->getPHID());
1336
foreach ($invitees as $invitee) {
1337
$invitee->delete();
1338
}
1339
1340
$notifications = id(new PhabricatorCalendarNotification())->loadAllWhere(
1341
'eventPHID = %s',
1342
$this->getPHID());
1343
foreach ($notifications as $notification) {
1344
$notification->delete();
1345
}
1346
1347
$this->delete();
1348
$this->saveTransaction();
1349
}
1350
1351
/* -( PhabricatorSpacesInterface )----------------------------------------- */
1352
1353
1354
public function getSpacePHID() {
1355
return $this->spacePHID;
1356
}
1357
1358
1359
/* -( PhabricatorFulltextInterface )--------------------------------------- */
1360
1361
1362
public function newFulltextEngine() {
1363
return new PhabricatorCalendarEventFulltextEngine();
1364
}
1365
1366
1367
/* -( PhabricatorFerretInterface )----------------------------------------- */
1368
1369
1370
public function newFerretEngine() {
1371
return new PhabricatorCalendarEventFerretEngine();
1372
}
1373
1374
1375
/* -( PhabricatorConduitResultInterface )---------------------------------- */
1376
1377
1378
public function getFieldSpecificationsForConduit() {
1379
return array(
1380
id(new PhabricatorConduitSearchFieldSpecification())
1381
->setKey('name')
1382
->setType('string')
1383
->setDescription(pht('The name of the event.')),
1384
id(new PhabricatorConduitSearchFieldSpecification())
1385
->setKey('description')
1386
->setType('string')
1387
->setDescription(pht('The event description.')),
1388
id(new PhabricatorConduitSearchFieldSpecification())
1389
->setKey('isAllDay')
1390
->setType('bool')
1391
->setDescription(pht('True if the event is an all day event.')),
1392
id(new PhabricatorConduitSearchFieldSpecification())
1393
->setKey('startDateTime')
1394
->setType('datetime')
1395
->setDescription(pht('Start date and time of the event.')),
1396
id(new PhabricatorConduitSearchFieldSpecification())
1397
->setKey('endDateTime')
1398
->setType('datetime')
1399
->setDescription(pht('End date and time of the event.')),
1400
);
1401
}
1402
1403
public function getFieldValuesForConduit() {
1404
$start_datetime = $this->newStartDateTime();
1405
$end_datetime = $this->newEndDateTime();
1406
1407
return array(
1408
'name' => $this->getName(),
1409
'description' => $this->getDescription(),
1410
'isAllDay' => (bool)$this->getIsAllDay(),
1411
'startDateTime' => $this->getConduitDateTime($start_datetime),
1412
'endDateTime' => $this->getConduitDateTime($end_datetime),
1413
);
1414
}
1415
1416
public function getConduitSearchAttachments() {
1417
return array();
1418
}
1419
1420
private function getConduitDateTime($datetime) {
1421
if (!$datetime) {
1422
return null;
1423
}
1424
1425
$epoch = $datetime->getEpoch();
1426
1427
// TODO: Possibly pass the actual viewer in from the Conduit stuff, or
1428
// retain it when setting the viewer timezone?
1429
$viewer = id(new PhabricatorUser())
1430
->overrideTimezoneIdentifier($this->viewerTimezone);
1431
1432
return array(
1433
'epoch' => (int)$epoch,
1434
'display' => array(
1435
'default' => phabricator_datetime($epoch, $viewer),
1436
),
1437
'iso8601' => $datetime->getISO8601(),
1438
'timezone' => $this->viewerTimezone,
1439
);
1440
}
1441
1442
}
1443
1444