diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index 36a9bfbbe9..88ffba9380 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -1,21 +1,40 @@ setViewer($viewer) ->setEvents($events) ->writeICSDocument(); return id(new AphrontFileResponse()) ->setDownload($file_name) ->setMimeType('text/calendar') ->setContent($ics_data); } + protected function newImportedEventResponse(PhabricatorCalendarEvent $event) { + if (!$event->isImportedEvent()) { + return null; + } + + // Give the user a specific, detailed message if they try to edit an + // imported event via common web paths. Other edits (including those via + // the API) are blocked by the normal policy system, but this makes it more + // clear exactly why the event can't be edited. + + return $this->newDialog() + ->setTitle(pht('Can Not Edit Imported Event')) + ->appendParagraph( + pht( + 'This event has been imported from an external source and '. + 'can not be edited.')) + ->addCancelButton($event->getURI(), pht('Done')); + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php index 63ce289970..fbf7f9d45e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php @@ -1,111 +1,119 @@ getViewer(); $id = $request->getURIData('id'); + // Just check CAN_VIEW first. Then we'll check if this is an import so + // we can raise a better error. $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->executeOne(); if (!$event) { return new Aphront404Response(); } + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + + // Now that we've done the import check, check for CAN_EDIT. + PhabricatorPolicyFilter::requireCapability( + $viewer, + $event, + PhabricatorPolicyCapability::CAN_EDIT); + $cancel_uri = $event->getURI(); $is_parent = $event->isParentEvent(); $is_child = $event->isChildEvent(); $is_cancelled = $event->getIsCancelled(); if ($is_child) { $is_parent_cancelled = $event->getParentEvent()->getIsCancelled(); } else { $is_parent_cancelled = false; } $validation_exception = null; if ($request->isFormPost()) { $xactions = array(); $xaction = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) ->setNewValue(!$is_cancelled); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); try { $editor->applyTransactions($event, array($xaction)); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } if ($is_cancelled) { if ($is_parent_cancelled) { $title = pht('Cannot Reinstate Instance'); $paragraph = pht( 'You cannot reinstate an instance of a cancelled recurring event.'); $cancel = pht('Back'); $submit = null; } else if ($is_child) { $title = pht('Reinstate Instance'); $paragraph = pht( 'Reinstate this instance of this recurring event?'); $cancel = pht('Back'); $submit = pht('Reinstate Instance'); } else if ($is_parent) { $title = pht('Reinstate Recurring Event'); $paragraph = pht( 'Reinstate all instances of this recurring event which have not '. 'been individually cancelled?'); $cancel = pht('Back'); $submit = pht('Reinstate Recurring Event'); } else { $title = pht('Reinstate Event'); $paragraph = pht('Reinstate this event?'); $cancel = pht('Back'); $submit = pht('Reinstate Event'); } } else { if ($is_child) { $title = pht('Cancel Instance'); $paragraph = pht('Cancel this instance of this recurring event?'); $cancel = pht('Back'); $submit = pht('Cancel Instance'); } else if ($is_parent) { $title = pht('Cancel Recurrin Event'); $paragraph = pht('Cancel this entire series of recurring events?'); $cancel = pht('Back'); $submit = pht('Cancel Recurring Event'); } else { $title = pht('Cancel Event'); $paragraph = pht( 'Cancel this event? You can always reinstate the event later.'); $cancel = pht('Back'); $submit = pht('Cancel Event'); } } return $this->newDialog() ->setTitle($title) ->setValidationException($validation_exception) ->appendParagraph($paragraph) ->addCancelButton($cancel_uri, $cancel) ->addSubmitButton($submit); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index b5beea9019..7c01e795b7 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -1,12 +1,26 @@ getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $event = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + } + return id(new PhabricatorCalendarEventEditEngine()) ->setController($this) ->buildResponse(); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php index 9b58d39dec..52ad8cb133 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php @@ -1,78 +1,83 @@ getViewer(); $id = $request->getURIData('id'); $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + $cancel_uri = $event->getURI(); $action = $request->getURIData('action'); switch ($action) { case 'accept': $is_join = true; break; case 'decline': $is_join = false; break; default: $is_join = !$event->getIsUserAttending($viewer->getPHID()); break; } $validation_exception = null; if ($request->isFormPost()) { if ($is_join) { $xaction_type = PhabricatorCalendarEventAcceptTransaction::TRANSACTIONTYPE; } else { $xaction_type = PhabricatorCalendarEventDeclineTransaction::TRANSACTIONTYPE; } $xaction = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType($xaction_type) ->setNewValue(true); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); try { $editor->applyTransactions($event, array($xaction)); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } if ($is_join) { $title = pht('Join Event'); $paragraph = pht('Would you like to join this event?'); $submit = pht('Join'); } else { $title = pht('Decline Event'); $paragraph = pht('Would you like to decline this event?'); $submit = pht('Decline'); } return $this->newDialog() ->setTitle($title) ->setValidationException($validation_exception) ->appendParagraph($paragraph) ->addCancelButton($cancel_uri) ->addSubmitButton($submit); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 89e8109a4c..c8bc9b5593 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,537 +1,573 @@ getViewer(); $event = $this->loadEvent(); if (!$event) { return new Aphront404Response(); } // If we looked up or generated a stub event, redirect to that event's // canonical URI. $id = $request->getURIData('id'); if ($event->getID() != $id) { $uri = $event->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } $monogram = $event->getMonogram(); $page_title = $monogram.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $start = $event->newStartDateTime() ->newPHPDateTime(); $crumbs->addTextCrumb( $start->format('F Y'), '/calendar/query/month/'.$start->format('Y/m/')); $crumbs->addTextCrumb( $start->format('D jS'), '/calendar/query/month/'.$start->format('Y/m/d/')); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $subheader = $this->buildSubheaderView($event); $curtain = $this->buildCurtain($event); $details = $this->buildPropertySection($event); $recurring = $this->buildRecurringSection($event); $description = $this->buildDescriptionView($event); $comment_view = id(new PhabricatorCalendarEventEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($event); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); $details_header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); $recurring_header = $this->buildRecurringHeader($event); + // NOTE: This is a bit hacky: for imported events, we're just hiding the + // comment form without actually preventing comments. Users could still + // submit a request to add comments to these events. This isn't really a + // major problem since they can't do anything truly bad and there isn't an + // easy way to selectively disable this or some other similar behaviors + // today, but it would probably be nice to fully disable these + // "pseudo-edits" (like commenting and probably subscribing and awarding + // tokens) at some point. + if ($event->isImportedEvent()) { + $comment_view = null; + $timeline->setShouldTerminate(true); + } + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setMainColumn( array( $timeline, $comment_view, )) ->setCurtain($curtain) ->addPropertySection(pht('Description'), $description) ->addPropertySection($recurring_header, $recurring) ->addPropertySection($details_header, $details); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($event->getPHID())) ->appendChild($view); } private function buildHeaderView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $id = $event->getID(); if ($event->isCancelledEvent()) { $icon = 'fa-ban'; $color = 'red'; $status = pht('Cancelled'); } else { $icon = 'fa-check'; $color = 'bluegrey'; $status = pht('Active'); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event) ->setHeaderIcon($event->getIcon()); + if ($event->isImportedEvent()) { + $header->addTag( + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setName(pht('Imported')) + ->setIcon('fa-download') + ->setHref($event->getImportSource()->getURI()) + ->setShade('orange')); + } + foreach ($this->buildRSVPActions($event) as $action) { $header->addActionLink($action); } return $header; } private function buildCurtain(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = "event/edit/{$id}/"; if ($event->isChildEvent()) { $edit_label = pht('Edit This Instance'); } else { $edit_label = pht('Edit Event'); } $curtain = $this->newCurtainView($event); if ($edit_label && $edit_uri) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($edit_label) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($edit_uri)) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); } + $can_attend = !$event->isImportedEvent(); + if ($is_attending) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) + ->setDisabled(!$can_attend) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) + ->setDisabled(!$can_attend) ->setWorkflow(true)); } $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); $cancel_disabled = !$can_edit; if ($event->isChildEvent()) { $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); if ($event->getParentEvent()->getIsCancelled()) { $cancel_disabled = true; } } else if ($event->isParentEvent()) { $cancel_label = pht('Cancel All'); $reinstate_label = pht('Reinstate All'); } else { $cancel_label = pht('Cancel Event'); $reinstate_label = pht('Reinstate Event'); } if ($event->isCancelledEvent()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($reinstate_label) ->setIcon('fa-plus') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName($cancel_label) ->setIcon('fa-times') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } $ics_name = $event->getICSFilename(); $export_uri = $this->getApplicationURI("event/export/{$id}/{$ics_name}"); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Export as .ics')) ->setIcon('fa-download') ->setHref($export_uri)); return $curtain; } private function buildPropertySection( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $invitees = $event->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } if ($invitees) { $invitee_list = new PHUIStatusListView(); $icon_invited = PHUIStatusItemView::ICON_OPEN; $icon_attending = PHUIStatusItemView::ICON_ACCEPT; $icon_declined = PHUIStatusItemView::ICON_REJECT; $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $icon_map = array( $status_invited => $icon_invited, $status_attending => $icon_attending, $status_declined => $icon_declined, ); $icon_color_map = array( $status_invited => null, $status_attending => 'green', $status_declined => 'red', ); foreach ($invitees as $invitee) { $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $status = $invitee->getStatus(); $target = $viewer->renderHandle($invitee_phid); $icon = $icon_map[$status]; $icon_color = $icon_color_map[$status]; $item->setIcon($icon, $icon_color) ->setTarget($target); $invitee_list->addItem($item); } } else { $invitee_list = phutil_tag( 'em', array(), pht('None')); } + if ($event->isImportedEvent()) { + $properties->addProperty( + pht('Imported By'), + pht( + '%s from %s', + $viewer->renderHandle($event->getImportAuthorPHID()), + $viewer->renderHandle($event->getImportSourcePHID()))); + } + $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); return $properties; } private function buildRecurringHeader(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); if (!$event->getIsRecurring()) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Recurring Event')); $sequence = $event->getSequenceIndex(); if ($event->isParentEvent()) { $parent = $event; } else { $parent = $event->getParentEvent(); } if ($parent->isValidSequenceIndex($viewer, $sequence + 1)) { $next_uri = $parent->getURI().'/'.($sequence + 1); $has_next = true; } else { $next_uri = null; $has_next = false; } if ($sequence) { if ($sequence > 1) { $previous_uri = $parent->getURI().'/'.($sequence - 1); } else { $previous_uri = $parent->getURI(); } $has_previous = true; } else { $has_previous = false; $previous_uri = null; } $prev_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-chevron-left') ->setHref($previous_uri) ->setDisabled(!$has_previous) ->setText(pht('Previous')); $next_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-chevron-right') ->setHref($next_uri) ->setDisabled(!$has_next) ->setText(pht('Next')); $header ->addActionLink($next_button) ->addActionLink($prev_button); return $header; } private function buildRecurringSection(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); if (!$event->getIsRecurring()) { return null; } $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $is_parent = $event->isParentEvent(); if ($is_parent) { $parent_link = null; } else { $parent = $event->getParentEvent(); $parent_link = $viewer ->renderHandle($parent->getPHID()) ->render(); } $rrule = $event->newRecurrenceRule(); if ($rrule) { $frequency = $rrule->getFrequency(); } else { $frequency = null; } switch ($frequency) { case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: if ($is_parent) { $message = pht('This event repeats every day.'); } else { $message = pht( 'This event is an instance of %s, and repeats every day.', $parent_link); } break; case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: if ($is_parent) { $message = pht('This event repeats every week.'); } else { $message = pht( 'This event is an instance of %s, and repeats every week.', $parent_link); } break; case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: if ($is_parent) { $message = pht('This event repeats every month.'); } else { $message = pht( 'This event is an instance of %s, and repeats every month.', $parent_link); } break; case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: if ($is_parent) { $message = pht('This event repeats every year.'); } else { $message = pht( 'This event is an instance of %s, and repeats every year.', $parent_link); } break; } $properties->addProperty(pht('Event Series'), $message); return $properties; } private function buildDescriptionView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if (strlen($event->getDescription())) { $description = new PHUIRemarkupView($viewer, $event->getDescription()); $properties->addTextContent($description); return $properties; } return null; } private function loadEvent() { $request = $this->getRequest(); $viewer = $this->getViewer(); $id = $request->getURIData('id'); $sequence = $request->getURIData('sequence'); // We're going to figure out which event you're trying to look at. Most of // the time this is simple, but you may be looking at an instance of a // recurring event which we haven't generated an object for. // If you are, we're going to generate a "stub" event so we have a real // ID and PHID to work with, since the rest of the infrastructure relies // on these identifiers existing. // Load the event identified by ID first. $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$event) { return null; } // If we aren't looking at an instance of this event, this is a completely // normal request and we can just return this event. if (!$sequence) { return $event; } // When you view "E123/999", E123 is normally the parent event. However, // you might visit a different instance first instead and then fiddle // with the URI. If the event we're looking at is a child, we are going // to act on the parent instead. if ($event->isChildEvent()) { $event = $event->getParentEvent(); } // Try to load the instance. If it already exists, we're all done and // can just return it. $instance = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInstanceSequencePairs( array( array($event->getPHID(), $sequence), )) ->executeOne(); if ($instance) { return $instance; } if (!$viewer->isLoggedIn()) { throw new Exception( pht( 'This event instance has not been created yet. Log in to create '. 'it.')); } if (!$event->isValidSequenceIndex($viewer, $sequence)) { return null; } return $event->newStub($viewer, $sequence); } private function buildSubheaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $host_phid = $event->getHostPHID(); $handles = $viewer->loadHandles(array($host_phid)); $handle = $handles[$host_phid]; $host = $viewer->renderHandle($host_phid); $host = phutil_tag('strong', array(), $host); $image_uri = $handles[$host_phid]->getImageURI(); $image_href = $handles[$host_phid]->getURI(); $date = $event->renderEventDate($viewer, true); $content = pht('Hosted by %s on %s.', $host, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildRSVPActions(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $id = $event->getID(); $invite_status = $event->getUserInviteStatus($viewer->getPHID()); $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $is_invite_pending = ($invite_status == $status_invited); if (!$is_invite_pending) { return array(); } $decline_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-times grey') ->setHref($this->getApplicationURI("/event/decline/{$id}/")) ->setWorkflow(true) ->setText(pht('Decline')); $accept_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-check green') ->setHref($this->getApplicationURI("/event/accept/{$id}/")) ->setWorkflow(true) ->setText(pht('Accept')); return array($decline_button, $accept_button); } } diff --git a/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php b/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php index 05d1cc0bb9..876ec7acf7 100644 --- a/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php +++ b/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php @@ -1,50 +1,50 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $import = $objects[$phid]; $id = $import->getID(); - $name = $import->getName(); + $name = $import->getDisplayName(); $uri = $import->getURI(); $handle ->setName($name) ->setFullName(pht('Calendar Import %s: %s', $id, $name)) ->setURI($uri); if ($import->getIsDisabled()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); } } } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 7ad6d85881..6baa54e008 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,1186 +1,1194 @@ 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_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( $now, $actor->getTimezoneIdentifier()); $datetime_end = $datetime_start->newRelativeDateTime('PT1H'); return id(new PhabricatorCalendarEvent()) ->setDescription('') ->setHostPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring(0) ->setIcon($default_icon) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) ->setDateFrom(0) ->setDateTo(0) ->setAllDayDateFrom(0) ->setAllDayDateTo(0) ->setStartDateTime($datetime_start) ->setEndDateTime($datetime_end) ->attachImportSource(null) ->applyViewerTimezone($actor); } 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!')); } $child = id(new self()) ->setIsCancelled(0) ->setIsStub(0) ->setInstanceOfEventPHID($this->getPHID()) ->setSequenceIndex($sequence) ->setIsRecurring(true) ->attachParentEvent($this) ->setAllDayDateFrom(0) ->setAllDayDateTo(0) ->setDateFrom(0) ->setDateTo(0); 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, ); // 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()); $sequence = $this->getSequenceIndex(); if ($start) { $start_datetime = $start; } else { $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); return $this; } public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) { return (bool)$this->newSequenceIndexDateTime($sequence); } public function newSequenceIndexDateTime($sequence) { $set = $this->newRecurrenceSet(); if (!$set) { return null; } $instances = $set->getEventsBetween( null, $this->newUntilDateTime(), $sequence + 1); 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', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', 'utcInitialEpoch' => 'epoch', 'utcUntilEpoch' => 'epoch?', 'utcInstanceEpoch' => 'epoch?', 'importAuthorPHID' => 'phid?', 'importSourcePHID' => 'phid?', 'importUIDIndex' => 'bytes12?', 'importUID' => 'text?', // TODO: DEPRECATED. 'allDayDateFrom' => 'epoch', 'allDayDateTo' => 'epoch', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'recurrenceEndDate' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_date' => array( 'columns' => array('dateFrom', 'dateTo'), ), '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, ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { return $this->assertAttached($this->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 getIsUserInvited($phid) { $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $status = $this->getUserInviteStatus($phid); if ($status == $uninvited_status || $status == $declined_status) { return false; } return true; } 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($event) { $this->parentEvent = $event; return $this; } public function isParentEvent() { return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID()); } public function isChildEvent() { return ($this->instanceOfEventPHID !== null); } public function isCancelledEvent() { if ($this->getIsCancelled()) { return true; } if ($this->isChildEvent()) { if ($this->getParentEvent()->getIsCancelled()) { return true; } } return false; } public function renderEventDate( PhabricatorUser $viewer, $show_end) { $start = $this->newStartDateTime(); $end = $this->newEndDateTime(); if ($show_end) { $min_date = $start->newPHPDateTime(); $max_date = $end->newPHPDateTime(); $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 = $start->getEpoch(); $max_epoch = $end->getEpoch(); 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->isCancelledEvent()) { return 'fa-times'; } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); 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'; } } + if ($this->isImportedEvent()) { + return 'fa-download'; + } + return $this->getIcon(); } public function getDisplayIconColor(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return 'red'; } + if ($this->isImportedEvent()) { + return 'orange'; + } + if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); 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->isCancelledEvent()) { 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(); $host_uri = $host_handle->getURI(); $host_uri = PhabricatorEnv::getURI($host_uri); $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'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getDateFrom(); return $this->newDateTimeFromEpoch($epoch); } public function getStartDateTimeEpoch() { return $this->newStartDateTime()->getEpoch(); } public function newEndDateTime() { $datetime = $this->getParameter('endDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getDateTo(); return $this->newDateTimeFromEpoch($epoch); } public function getEndDateTimeEpoch() { return $this->newEndDateTime()->getEpoch(); } public function newUntilDateTime() { $datetime = $this->getParameter('untilDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getRecurrenceEndDate(); if (!$epoch) { return null; } return $this->newDateTimeFromEpoch($epoch); } 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) { return $this->setParameter( 'untilDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } 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); } return $rrule; } public function newRecurrenceSet() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceSet(); } $set = new PhutilCalendarRecurrenceSet(); $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; } /* -( 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->getImportSource()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getEditPolicy(); } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getImportSource()) { 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; } public function describeAutomaticCapability($capability) { if ($this->getImportSource()) { return pht( 'Events imported from external sources can not be edited in '. 'Phabricator.'); } return pht( 'The host of an event can always view and edit it. Users who are '. 'invited to an event can always view it.'); } /* -( 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; } /* -( 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, ); } }