Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2894067
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
40 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
index 75d573c79e..5b64fdea53 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1,1429 +1,1433 @@
<?php
final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorPolicyCodexInterface,
PhabricatorProjectInterface,
PhabricatorMarkupInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $hostPHID;
protected $description;
protected $isCancelled;
protected $isAllDay;
protected $icon;
protected $mailKey;
protected $isStub;
protected $isRecurring = 0;
protected $seriesParentPHID;
protected $instanceOfEventPHID;
protected $sequenceIndex;
protected $viewPolicy;
protected $editPolicy;
protected $spacePHID;
protected $utcInitialEpoch;
protected $utcUntilEpoch;
protected $utcInstanceEpoch;
protected $parameters = array();
protected $importAuthorPHID;
protected $importSourcePHID;
protected $importUIDIndex;
protected $importUID;
private $parentEvent = self::ATTACHABLE;
private $invitees = self::ATTACHABLE;
private $importSource = self::ATTACHABLE;
private $rsvps = self::ATTACHABLE;
private $viewerTimezone;
private $isGhostEvent = false;
private $stubInvitees;
public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorCalendarApplication'))
->executeOne();
$view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY;
$edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY;
$view_policy = $app->getPolicy($view_default);
$edit_policy = $app->getPolicy($edit_default);
$now = PhabricatorTime::getNow();
$default_icon = 'fa-calendar';
$datetime_defaults = self::newDefaultEventDateTimes(
$actor,
$now);
list($datetime_start, $datetime_end) = $datetime_defaults;
// When importing events from a context like "bin/calendar reload", we may
// be acting as the omnipotent user.
$host_phid = $actor->getPHID();
if (!$host_phid) {
$host_phid = $app->getPHID();
}
return id(new PhabricatorCalendarEvent())
->setDescription('')
->setHostPHID($host_phid)
->setIsCancelled(0)
->setIsAllDay(0)
->setIsStub(0)
->setIsRecurring(0)
->setIcon($default_icon)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachInvitees(array())
->setStartDateTime($datetime_start)
->setEndDateTime($datetime_end)
->attachImportSource(null)
->applyViewerTimezone($actor);
}
public static function newDefaultEventDateTimes(
PhabricatorUser $viewer,
$now) {
$datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$now,
$viewer->getTimezoneIdentifier());
// Advance the time by an hour, then round downwards to the nearest hour.
// For example, if it is currently 3:25 PM, we suggest a default start time
// of 4 PM.
$datetime_start = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
$datetime_start->setMinute(0);
$datetime_start->setSecond(0);
// Default the end time to an hour after the start time.
$datetime_end = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
return array($datetime_start, $datetime_end);
}
private function newChild(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
if (!$this->isParentEvent()) {
throw new Exception(
pht(
'Unable to generate a new child event for an event which is not '.
'a recurring parent event!'));
}
$series_phid = $this->getSeriesParentPHID();
if (!$series_phid) {
$series_phid = $this->getPHID();
}
$child = id(new self())
->setIsCancelled(0)
->setIsStub(0)
->setInstanceOfEventPHID($this->getPHID())
->setSeriesParentPHID($series_phid)
->setSequenceIndex($sequence)
->setIsRecurring(true)
->attachParentEvent($this)
->attachImportSource(null);
return $child->copyFromParent($actor, $start);
}
protected function readField($field) {
static $inherit = array(
'hostPHID' => true,
'isAllDay' => true,
'icon' => true,
'spacePHID' => true,
'viewPolicy' => true,
'editPolicy' => true,
'name' => true,
'description' => true,
'isCancelled' => true,
);
// Read these fields from the parent event instead of this event. For
// example, we want any changes to the parent event's name to apply to
// the child.
if (isset($inherit[$field])) {
if ($this->getIsStub()) {
// TODO: This should be unconditional, but the execution order of
// CalendarEventQuery and applyViewerTimezone() are currently odd.
if ($this->parentEvent !== self::ATTACHABLE) {
return $this->getParentEvent()->readField($field);
}
}
}
return parent::readField($field);
}
public function copyFromParent(
PhabricatorUser $actor,
PhutilCalendarDateTime $start = null) {
if (!$this->isChildEvent()) {
throw new Exception(
pht(
'Unable to copy from parent event: this is not a child event.'));
}
$parent = $this->getParentEvent();
$this
->setHostPHID($parent->getHostPHID())
->setIsAllDay($parent->getIsAllDay())
->setIcon($parent->getIcon())
->setSpacePHID($parent->getSpacePHID())
->setViewPolicy($parent->getViewPolicy())
->setEditPolicy($parent->getEditPolicy())
->setName($parent->getName())
->setDescription($parent->getDescription())
->setIsCancelled($parent->getIsCancelled());
if ($start) {
$start_datetime = $start;
} else {
$sequence = $this->getSequenceIndex();
$start_datetime = $parent->newSequenceIndexDateTime($sequence);
if (!$start_datetime) {
throw new Exception(
pht(
'Sequence "%s" is not valid for event!',
$sequence));
}
}
$duration = $parent->newDuration();
$end_datetime = $start_datetime->newRelativeDateTime($duration);
$this
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
if ($parent->isImportedEvent()) {
$full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch();
// NOTE: We don't attach the import source because this gets called
// from CalendarEventQuery while building ghosts, before we've loaded
// and attached sources. Possibly this sequence should be flipped.
$this
->setImportAuthorPHID($parent->getImportAuthorPHID())
->setImportSourcePHID($parent->getImportSourcePHID())
->setImportUID($full_uid);
}
return $this;
}
public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) {
return (bool)$this->newSequenceIndexDateTime($sequence);
}
public function newSequenceIndexDateTime($sequence) {
$set = $this->newRecurrenceSet();
if (!$set) {
return null;
}
$limit = $sequence + 1;
$count = $this->getRecurrenceCount();
if ($count && ($count < $limit)) {
return null;
}
$instances = $set->getEventsBetween(
null,
$this->newUntilDateTime(),
$limit);
return idx($instances, $sequence, null);
}
public function newStub(PhabricatorUser $actor, $sequence) {
$stub = $this->newChild($actor, $sequence);
$stub->setIsStub(1);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$stub->save();
unset($unguarded);
$stub->applyViewerTimezone($actor);
return $stub;
}
public function newGhost(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
$ghost = $this->newChild($actor, $sequence, $start);
$ghost
->setIsGhostEvent(true)
->makeEphemeral();
$ghost->applyViewerTimezone($actor);
return $ghost;
}
public function applyViewerTimezone(PhabricatorUser $viewer) {
$this->viewerTimezone = $viewer->getTimezoneIdentifier();
return $this;
}
public function getDuration() {
return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch());
}
public function updateUTCEpochs() {
// The "intitial" epoch is the start time of the event, in UTC.
$start_date = $this->newStartDateTime()
->setViewerTimezone('UTC');
$start_epoch = $start_date->getEpoch();
$this->setUTCInitialEpoch($start_epoch);
// The "until" epoch is the last UTC epoch on which any instance of this
// event occurs. For infinitely recurring events, it is `null`.
if (!$this->getIsRecurring()) {
$end_date = $this->newEndDateTime()
->setViewerTimezone('UTC');
$until_epoch = $end_date->getEpoch();
} else {
$until_epoch = null;
$until_date = $this->newUntilDateTime();
if ($until_date) {
$until_date->setViewerTimezone('UTC');
$duration = $this->newDuration();
$until_epoch = id(new PhutilCalendarRelativeDateTime())
->setOrigin($until_date)
->setDuration($duration)
->getEpoch();
}
}
$this->setUTCUntilEpoch($until_epoch);
// The "instance" epoch is a property of instances of recurring events.
// It's the original UTC epoch on which the instance started. Usually that
// is the same as the start date, but they may be different if the instance
// has been edited.
// The ICS format uses this value (original start time) to identify event
// instances, and must do so because it allows additional arbitrary
// instances to be added (with "RDATE").
$instance_epoch = null;
$instance_date = $this->newInstanceDateTime();
if ($instance_date) {
$instance_epoch = $instance_date
->setViewerTimezone('UTC')
->getEpoch();
}
$this->setUTCInstanceEpoch($instance_epoch);
return $this;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$import_uid = $this->getImportUID();
if ($import_uid !== null) {
$index = PhabricatorHash::digestForIndex($import_uid);
} else {
$index = null;
}
$this->setImportUIDIndex($index);
$this->updateUTCEpochs();
return parent::save();
}
/**
* Get the event start epoch for evaluating invitee availability.
*
* When assessing availability, we pretend events start earlier than they
* really do. This allows us to mark users away for the entire duration of a
* series of back-to-back meetings, even if they don't strictly overlap.
*
* @return int Event start date for availability caches.
*/
public function getStartDateTimeEpochForCache() {
$epoch = $this->getStartDateTimeEpoch();
$window = phutil_units('15 minutes in seconds');
return ($epoch - $window);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'description' => 'text',
'isCancelled' => 'bool',
'isAllDay' => 'bool',
'icon' => 'text32',
'mailKey' => 'bytes20',
'isRecurring' => 'bool',
'seriesParentPHID' => 'phid?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
'utcInitialEpoch' => 'epoch',
'utcUntilEpoch' => 'epoch?',
'utcInstanceEpoch' => 'epoch?',
'importAuthorPHID' => 'phid?',
'importSourcePHID' => 'phid?',
'importUIDIndex' => 'bytes12?',
'importUID' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_instance' => array(
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
'unique' => true,
),
'key_epoch' => array(
'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
),
'key_rdate' => array(
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
'unique' => true,
),
'key_series' => array(
'columns' => array('seriesParentPHID', 'utcInitialEpoch'),
),
),
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorCalendarEventPHIDType::TYPECONST);
}
public function getMonogram() {
return 'E'.$this->getID();
}
public function getInvitees() {
if ($this->getIsGhostEvent() || $this->getIsStub()) {
if ($this->stubInvitees === null) {
$this->stubInvitees = $this->newStubInvitees();
}
return $this->stubInvitees;
}
return $this->assertAttached($this->invitees);
}
public function getInviteeForPHID($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
return idx($invitees, $phid);
}
public static function getFrequencyMap() {
return array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
'label' => pht('Daily'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array(
'label' => pht('Weekly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array(
'label' => pht('Monthly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array(
'label' => pht('Yearly'),
),
);
}
private function newStubInvitees() {
$parent = $this->getParentEvent();
$parent_invitees = $parent->getInvitees();
$stub_invitees = array();
foreach ($parent_invitees as $invitee) {
$stub_invitee = id(new PhabricatorCalendarEventInvitee())
->setInviteePHID($invitee->getInviteePHID())
->setInviterPHID($invitee->getInviterPHID())
->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED);
$stub_invitees[] = $stub_invitee;
}
return $stub_invitees;
}
public function attachInvitees(array $invitees) {
$this->invitees = $invitees;
return $this;
}
public function getInviteePHIDsForEdit() {
$invitees = array();
foreach ($this->getInvitees() as $invitee) {
if ($invitee->isUninvited()) {
continue;
}
$invitees[] = $invitee->getInviteePHID();
}
return $invitees;
}
public function getUserInviteStatus($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$invited = idx($invitees, $phid);
if (!$invited) {
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
$invited = $invited->getStatus();
return $invited;
}
public function getIsUserAttending($phid) {
$attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
$old_status = $this->getUserInviteStatus($phid);
$is_attending = ($old_status == $attending_status);
return $is_attending;
}
public function getIsGhostEvent() {
return $this->isGhostEvent;
}
public function setIsGhostEvent($is_ghost_event) {
$this->isGhostEvent = $is_ghost_event;
return $this;
}
public function getURI() {
if ($this->getIsGhostEvent()) {
$base = $this->getParentEvent()->getURI();
$sequence = $this->getSequenceIndex();
return "{$base}/{$sequence}/";
}
return '/'.$this->getMonogram();
}
public function getParentEvent() {
return $this->assertAttached($this->parentEvent);
}
public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
$this->parentEvent = $event;
return $this;
}
public function isParentEvent() {
return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID());
}
public function isChildEvent() {
return ($this->instanceOfEventPHID !== null);
}
public function renderEventDate(
PhabricatorUser $viewer,
$show_end) {
$start = $this->newStartDateTime();
$end = $this->newEndDateTime();
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
if ($this->getIsAllDay()) {
// Subtract one second since the stored date is exclusive.
$max_date = $max_date->modify('-1 second');
}
if ($show_end) {
$min_day = $min_date->format('Y m d');
$max_day = $max_date->format('Y m d');
$show_end_date = ($min_day != $max_day);
} else {
$show_end_date = false;
}
$min_epoch = $min_date->format('U');
$max_epoch = $max_date->format('U');
if ($this->getIsAllDay()) {
if ($show_end_date) {
return pht(
'%s - %s, All Day',
phabricator_date($min_epoch, $viewer),
phabricator_date($max_epoch, $viewer));
} else {
return pht(
'%s, All Day',
phabricator_date($min_epoch, $viewer));
}
} else if ($show_end_date) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_datetime($max_epoch, $viewer));
} else if ($show_end) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_time($max_epoch, $viewer));
} else {
return pht(
'%s',
phabricator_datetime($min_epoch, $viewer));
}
}
public function getDisplayIcon(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'fa-times';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'fa-users';
} else {
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'fa-check-circle';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'fa-user-plus';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'fa-times-circle';
}
}
}
if ($this->isImportedEvent()) {
return 'fa-download';
}
return $this->getIcon();
}
public function getDisplayIconColor(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'red';
}
if ($this->isImportedEvent()) {
return 'orange';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'green';
}
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'grey';
}
}
return 'bluegrey';
}
public function getDisplayIconLabel(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return pht('Cancelled');
}
if ($viewer->isLoggedIn()) {
$status = $this->getUserInviteStatus($viewer->getPHID());
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return pht('Attending');
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return pht('Invited');
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return pht('Declined');
}
}
return null;
}
public function getICSFilename() {
return $this->getMonogram().'.ics';
}
public function newIntermediateEventNode(
PhabricatorUser $viewer,
array $children) {
$base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$domain = $base_uri->getDomain();
// NOTE: For recurring events, all of the events in the series have the
// same UID (the UID of the parent). The child event instances are
// differentiated by the "RECURRENCE-ID" field.
if ($this->isChildEvent()) {
$parent = $this->getParentEvent();
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$this->getUTCInstanceEpoch());
$recurrence_id = $instance_datetime->getISO8601();
$rrule = null;
} else {
$parent = $this;
$recurrence_id = null;
$rrule = $this->newRecurrenceRule();
}
$uid = $parent->getPHID().'@'.$domain;
$created = $this->getDateCreated();
$created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created);
$modified = $this->getDateModified();
$modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
$date_start = $this->newStartDateTime();
$date_end = $this->newEndDateTime();
if ($this->getIsAllDay()) {
$date_start->setIsAllDay(true);
$date_end->setIsAllDay(true);
}
$host_phid = $this->getHostPHID();
$invitees = $this->getInvitees();
foreach ($invitees as $key => $invitee) {
if ($invitee->isUninvited()) {
unset($invitees[$key]);
}
}
$phids = array();
$phids[] = $host_phid;
foreach ($invitees as $invitee) {
$phids[] = $invitee->getInviteePHID();
}
$handles = $viewer->loadHandles($phids);
$host_handle = $handles[$host_phid];
$host_name = $host_handle->getFullName();
// NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does
// not look like an email address. Use a synthetic address so it shows
// the host name instead.
$install_uri = PhabricatorEnv::getProductionURI('/');
$install_uri = new PhutilURI($install_uri);
// This should possibly use "metamta.reply-handler-domain" instead, but
// we do not currently accept mail for users anyway, and that option may
// not be configured.
$mail_domain = $install_uri->getDomain();
$host_uri = "mailto:{$host_phid}@{$mail_domain}";
$organizer = id(new PhutilCalendarUserNode())
->setName($host_name)
->setURI($host_uri);
$attendees = array();
foreach ($invitees as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
$invitee_handle = $handles[$invitee_phid];
$invitee_name = $invitee_handle->getFullName();
$invitee_uri = $invitee_handle->getURI();
$invitee_uri = PhabricatorEnv::getURI($invitee_uri);
switch ($invitee->getStatus()) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
$status = PhutilCalendarUserNode::STATUS_ACCEPTED;
break;
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
$status = PhutilCalendarUserNode::STATUS_DECLINED;
break;
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
default:
$status = PhutilCalendarUserNode::STATUS_INVITED;
break;
}
$attendees[] = id(new PhutilCalendarUserNode())
->setName($invitee_name)
->setURI($invitee_uri)
->setStatus($status);
}
// TODO: Use $children to generate EXDATE/RDATE information.
$node = id(new PhutilCalendarEventNode())
->setUID($uid)
->setName($this->getName())
->setDescription($this->getDescription())
->setCreatedDateTime($created)
->setModifiedDateTime($modified)
->setStartDateTime($date_start)
->setEndDateTime($date_end)
->setOrganizer($organizer)
->setAttendees($attendees);
if ($rrule) {
$node->setRecurrenceRule($rrule);
}
if ($recurrence_id) {
$node->setRecurrenceID($recurrence_id);
}
return $node;
}
public function newStartDateTime() {
$datetime = $this->getParameter('startDateTime');
return $this->newDateTimeFromDictionary($datetime);
}
public function getStartDateTimeEpoch() {
return $this->newStartDateTime()->getEpoch();
}
public function newEndDateTimeForEdit() {
$datetime = $this->getParameter('endDateTime');
return $this->newDateTimeFromDictionary($datetime);
}
public function newEndDateTime() {
$datetime = $this->newEndDateTimeForEdit();
// If this is an all day event, we move the end date time forward to the
// first second of the following day. This is consistent with what users
// expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day.
// For imported events, the end date is already stored with this
// adjustment.
if ($this->getIsAllDay() && !$this->isImportedEvent()) {
$datetime = $datetime
->newAbsoluteDateTime()
->setHour(0)
->setMinute(0)
->setSecond(0)
->newRelativeDateTime('P1D')
->newAbsoluteDateTime();
}
return $datetime;
}
public function getEndDateTimeEpoch() {
return $this->newEndDateTime()->getEpoch();
}
public function newUntilDateTime() {
$datetime = $this->getParameter('untilDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
return null;
}
public function getUntilDateTimeEpoch() {
$datetime = $this->newUntilDateTime();
if (!$datetime) {
return null;
}
return $datetime->getEpoch();
}
public function newDuration() {
return id(new PhutilCalendarDuration())
->setSeconds($this->getDuration());
}
public function newInstanceDateTime() {
if (!$this->getIsRecurring()) {
return null;
}
$index = $this->getSequenceIndex();
if (!$index) {
return null;
}
return $this->newSequenceIndexDateTime($index);
}
private function newDateTimeFromEpoch($epoch) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch);
if ($this->getIsAllDay()) {
$datetime->setIsAllDay(true);
}
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDictionary(array $dict) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict);
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) {
$viewer_timezone = $this->viewerTimezone;
if ($viewer_timezone) {
$datetime->setViewerTimezone($viewer_timezone);
}
return $datetime;
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function setStartDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'startDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setEndDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'endDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setUntilDateTime(PhutilCalendarDateTime $datetime = null) {
if ($datetime) {
$value = $datetime->newAbsoluteDateTime()->toDictionary();
} else {
$value = null;
}
return $this->setParameter('untilDateTime', $value);
}
public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) {
return $this->setParameter(
'recurrenceRule',
$rrule->toDictionary());
}
public function newRecurrenceRule() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceRule();
}
if (!$this->getIsRecurring()) {
return null;
}
$dict = $this->getParameter('recurrenceRule');
if (!$dict) {
return null;
}
$rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict);
$start = $this->newStartDateTime();
$rrule->setStartDateTime($start);
$until = $this->newUntilDateTime();
if ($until) {
$rrule->setUntil($until);
}
$count = $this->getRecurrenceCount();
if ($count) {
$rrule->setCount($count);
}
return $rrule;
}
public function getRecurrenceCount() {
$count = (int)$this->getParameter('recurrenceCount');
if (!$count) {
return null;
}
return $count;
}
public function newRecurrenceSet() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceSet();
}
$set = new PhutilCalendarRecurrenceSet();
+ if ($this->viewerTimezone) {
+ $set->setViewerTimezone($this->viewerTimezone);
+ }
+
$rrule = $this->newRecurrenceRule();
if (!$rrule) {
return null;
}
$set->addSource($rrule);
return $set;
}
public function isImportedEvent() {
return (bool)$this->getImportSourcePHID();
}
public function getImportSource() {
return $this->assertAttached($this->importSource);
}
public function attachImportSource(
PhabricatorCalendarImport $import = null) {
$this->importSource = $import;
return $this;
}
public function loadForkTarget(PhabricatorUser $viewer) {
if (!$this->getIsRecurring()) {
// Can't fork an event which isn't recurring.
return null;
}
if ($this->isChildEvent()) {
// If this is a child event, this is the fork target.
return $this;
}
if (!$this->isValidSequenceIndex($viewer, 1)) {
// This appears to be a "recurring" event with no valid instances: for
// example, its "until" date is before the second instance would occur.
// This can happen if we already forked the event or if users entered
// silly stuff. Just edit the event directly without forking anything.
return null;
}
$next_event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withInstanceSequencePairs(
array(
array($this->getPHID(), 1),
))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$next_event) {
$next_event = $this->newStub($viewer, 1);
}
return $next_event;
}
public function loadFutureEvents(PhabricatorUser $viewer) {
// NOTE: If you can't edit some of the future events, we just
// don't try to update them. This seems like it's probably what
// users are likely to expect.
// NOTE: This only affects events that are currently in the same
// series, not all events that were ever in the original series.
// We could use series PHIDs instead of parent PHIDs to affect more
// events if this turns out to be counterintuitive. Other
// applications differ in their behavior.
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array($this->getPHID()))
->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
}
public function getNotificationPHIDs() {
$phids = array();
if ($this->getPHID()) {
$phids[] = $this->getPHID();
}
if ($this->getSeriesParentPHID()) {
$phids[] = $this->getSeriesParentPHID();
}
return $phids;
}
public function getRSVPs($phid) {
return $this->assertAttachedKey($this->rsvps, $phid);
}
public function attachRSVPs(array $rsvps) {
$this->rsvps = $rsvps;
return $this;
}
public function isRSVPInvited($phid) {
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
return ($this->getRSVPStatus($phid) == $status_invited);
}
public function hasRSVPAuthority($phid, $other_phid) {
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getInviteePHID() == $other_phid) {
return true;
}
}
return false;
}
public function getRSVPStatus($phid) {
// Check for an individual invitee record first.
$invitees = $this->invitees;
$invitees = mpull($invitees, null, 'getInviteePHID');
$invitee = idx($invitees, $phid);
if ($invitee) {
return $invitee->getStatus();
}
// If we don't have one, try to find an invited status for the user's
// projects.
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getStatus() == $status_invited) {
return $status_invited;
}
}
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "calendar:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newCalendarMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isImportedEvent()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getEditPolicy();
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isImportedEvent()) {
return false;
}
// The host of an event can always view and edit it.
$user_phid = $this->getHostPHID();
if ($user_phid) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid == $user_phid) {
return true;
}
}
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$status = $this->getUserInviteStatus($viewer->getPHID());
if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
$status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
$status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
return true;
}
}
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$import_source = $this->getImportSource();
if ($import_source) {
$extended[] = array(
$import_source,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
public function newPolicyCodex() {
return new PhabricatorCalendarEventPolicyCodex();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarEventEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarEventTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getHostPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getHostPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorCalendarEventFulltextEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The event description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isAllDay')
->setType('bool')
->setDescription(pht('True if the event is an all day event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('startDateTime')
->setType('datetime')
->setDescription(pht('Start date and time of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('endDateTime')
->setType('datetime')
->setDescription(pht('End date and time of the event.')),
);
}
public function getFieldValuesForConduit() {
$start_datetime = $this->newStartDateTime();
$end_datetime = $this->newEndDateTime();
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'isAllDay' => (bool)$this->getIsAllDay(),
'startDateTime' => $this->getConduitDateTime($start_datetime),
'endDateTime' => $this->getConduitDateTime($end_datetime),
);
}
public function getConduitSearchAttachments() {
return array();
}
private function getConduitDateTime($datetime) {
if (!$datetime) {
return null;
}
$epoch = $datetime->getEpoch();
// TODO: Possibly pass the actual viewer in from the Conduit stuff, or
// retain it when setting the viewer timezone?
$viewer = id(new PhabricatorUser())
->overrideTimezoneIdentifier($this->viewerTimezone);
return array(
'epoch' => (int)$epoch,
'display' => array(
'default' => phabricator_datetime($epoch, $viewer),
),
'iso8601' => $datetime->getISO8601(),
'timezone' => $this->viewerTimezone,
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 19:19 (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127906
Default Alt Text
(40 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment