Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php
index 96069e0ee1..bf2200b981 100644
--- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php
+++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php
@@ -1,563 +1,571 @@
<?php
abstract class PhabricatorCalendarImportEngine
extends Phobject {
final public function getImportEngineType() {
return $this->getPhobjectClassConstant('ENGINETYPE', 64);
}
abstract public function getImportEngineName();
abstract public function getImportEngineTypeName();
abstract public function getImportEngineHint();
public function appendImportProperties(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import,
PHUIPropertyListView $properties) {
return;
}
abstract public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorCalendarImport $import);
abstract public function getDisplayName(PhabricatorCalendarImport $import);
abstract public function importEventsFromSource(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import);
abstract public function canDisable(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import);
public function explainCanDisable(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import) {
throw new PhutilMethodNotImplementedException();
}
abstract public function supportsTriggers(
PhabricatorCalendarImport $import);
final public static function getAllImportEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getImportEngineType')
->setSortMethod('getImportEngineName')
->execute();
}
final protected function importEventDocument(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import,
PhutilCalendarRootNode $root = null) {
$event_type = PhutilCalendarEventNode::NODETYPE;
$nodes = array();
if ($root) {
foreach ($root->getChildren() as $document) {
foreach ($document->getChildren() as $node) {
$node_type = $node->getNodeType();
if ($node_type != $event_type) {
$import->newLogMessage(
PhabricatorCalendarImportIgnoredNodeLogType::LOGTYPE,
array(
'node.type' => $node_type,
));
continue;
}
$nodes[] = $node;
}
}
}
// Reject events which have dates outside of the range of a signed
// 32-bit integer. We'll need to accommodate a wider range of events
// eventually, but have about 20 years until it's an issue and we'll
// all be dead by then.
foreach ($nodes as $key => $node) {
$dates = array();
$dates[] = $node->getStartDateTime();
$dates[] = $node->getEndDateTime();
$dates[] = $node->getCreatedDateTime();
$dates[] = $node->getModifiedDateTime();
$rrule = $node->getRecurrenceRule();
if ($rrule) {
$dates[] = $rrule->getUntil();
}
$bad_date = false;
foreach ($dates as $date) {
if ($date === null) {
continue;
}
$year = $date->getYear();
if ($year < 1970 || $year > 2037) {
$bad_date = true;
break;
}
}
if ($bad_date) {
$import->newLogMessage(
PhabricatorCalendarImportEpochLogType::LOGTYPE,
array());
unset($nodes[$key]);
}
}
// Reject events which occur too frequently. Users do not normally define
// these events and the UI and application make many assumptions which are
// incompatible with events recurring once per second.
foreach ($nodes as $key => $node) {
$rrule = $node->getRecurrenceRule();
if (!$rrule) {
// This is not a recurring event, so we don't need to check the
// frequency.
continue;
}
$scale = $rrule->getFrequencyScale();
if ($scale >= PhutilCalendarRecurrenceRule::SCALE_DAILY) {
// This is a daily, weekly, monthly, or yearly event. These are
// supported.
} else {
// This is an hourly, minutely, or secondly event.
$import->newLogMessage(
PhabricatorCalendarImportFrequencyLogType::LOGTYPE,
array(
'frequency' => $rrule->getFrequency(),
));
unset($nodes[$key]);
}
}
$node_map = array();
foreach ($nodes as $node) {
$full_uid = $this->getFullNodeUID($node);
if (isset($node_map[$full_uid])) {
$import->newLogMessage(
PhabricatorCalendarImportDuplicateLogType::LOGTYPE,
array(
'uid.full' => $full_uid,
));
continue;
}
$node_map[$full_uid] = $node;
}
// If we already know about some of these events and they were created
// here, we're not going to import it again. This can happen if a user
// exports an event and then tries to import it again. This is probably
// not what they meant to do and this pathway generally leads to madness.
$likely_phids = array();
foreach ($node_map as $full_uid => $node) {
$uid = $node->getUID();
$matches = null;
if (preg_match('/^(PHID-.*)@(.*)\z/', $uid, $matches)) {
$likely_phids[$full_uid] = $matches[1];
}
}
if ($likely_phids) {
// NOTE: We're using the omnipotent viewer here because we don't want
// to collide with events that already exist, even if you can't see
// them.
$events = id(new PhabricatorCalendarEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($likely_phids)
->execute();
$events = mpull($events, null, 'getPHID');
foreach ($node_map as $full_uid => $node) {
$phid = idx($likely_phids, $full_uid);
if (!$phid) {
continue;
}
$event = idx($events, $phid);
if (!$event) {
continue;
}
$import->newLogMessage(
PhabricatorCalendarImportOriginalLogType::LOGTYPE,
array(
'phid' => $event->getPHID(),
));
unset($node_map[$full_uid]);
}
}
if ($node_map) {
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withImportAuthorPHIDs(array($viewer->getPHID()))
->withImportUIDs(array_keys($node_map))
->execute();
$events = mpull($events, null, 'getImportUID');
} else {
$events = null;
}
$xactions = array();
$update_map = array();
$invitee_map = array();
$attendee_map = array();
foreach ($node_map as $full_uid => $node) {
$event = idx($events, $full_uid);
if (!$event) {
$event = PhabricatorCalendarEvent::initializeNewCalendarEvent($viewer);
}
$event
->setImportAuthorPHID($viewer->getPHID())
->setImportSourcePHID($import->getPHID())
->setImportUID($full_uid)
->attachImportSource($import);
$this->updateEventFromNode($viewer, $event, $node);
$xactions[$full_uid] = $this->newUpdateTransactions($event, $node);
$update_map[$full_uid] = $event;
+ $attendee_map[$full_uid] = array();
$attendees = $node->getAttendees();
$private_index = 1;
foreach ($attendees as $attendee) {
// Generate a "name" for this attendee which is not an email address.
// We avoid disclosing email addresses to be consistent with the rest
// of the product.
$name = $attendee->getName();
if (preg_match('/@/', $name)) {
$name = new PhutilEmailAddress($name);
$name = $name->getDisplayName();
}
// If we don't have a name or the name still looks like it's an
// email address, give them a dummy placeholder name.
if (!strlen($name) || preg_match('/@/', $name)) {
$name = pht('Private User %d', $private_index);
$private_index++;
}
$attendee_map[$full_uid][$name] = $attendee;
}
}
$attendee_names = array();
foreach ($attendee_map as $full_uid => $event_attendees) {
foreach ($event_attendees as $name => $attendee) {
$attendee_names[$name] = $attendee;
}
}
if ($attendee_names) {
$external_invitees = id(new PhabricatorCalendarExternalInviteeQuery())
->setViewer($viewer)
->withNames($attendee_names)
->execute();
$external_invitees = mpull($external_invitees, null, 'getName');
foreach ($attendee_names as $name => $attendee) {
if (isset($external_invitees[$name])) {
continue;
}
$external_invitee = id(new PhabricatorCalendarExternalInvitee())
->setName($name)
->setURI($attendee->getURI())
->setSourcePHID($import->getPHID());
try {
$external_invitee->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$external_invitee =
id(new PhabricatorCalendarExternalInviteeQuery())
->setViewer($viewer)
->withNames(array($name))
->executeOne();
}
$external_invitees[$name] = $external_invitee;
}
}
// Reorder events so we create parents first. This allows us to populate
// "instanceOfEventPHID" correctly.
$insert_order = array();
foreach ($update_map as $full_uid => $event) {
$parent_uid = $this->getParentNodeUID($node_map[$full_uid]);
if ($parent_uid === null) {
$insert_order[$full_uid] = $full_uid;
continue;
}
if (empty($update_map[$parent_uid])) {
// The parent was not present in this import, which means it either
// does not exist or we're going to delete it anyway. We just drop
// this node.
$import->newLogMessage(
PhabricatorCalendarImportOrphanLogType::LOGTYPE,
array(
'uid.full' => $full_uid,
'uid.parent' => $parent_uid,
));
continue;
}
// Otherwise, we're going to insert the parent first, then insert
// the child.
$insert_order[$parent_uid] = $parent_uid;
$insert_order[$full_uid] = $full_uid;
}
// TODO: Define per-engine content sources so this can say "via Upload" or
// whatever.
$content_source = PhabricatorContentSource::newForSource(
PhabricatorWebContentSource::SOURCECONST);
// NOTE: We're using the omnipotent user here because imported events are
// otherwise immutable.
$edit_actor = PhabricatorUser::getOmnipotentUser();
$update_map = array_select_keys($update_map, $insert_order);
foreach ($update_map as $full_uid => $event) {
$parent_uid = $this->getParentNodeUID($node_map[$full_uid]);
if ($parent_uid) {
$parent_phid = $update_map[$full_uid]->getPHID();
} else {
$parent_phid = null;
}
$event->setInstanceOfEventPHID($parent_phid);
$event_xactions = $xactions[$full_uid];
$editor = id(new PhabricatorCalendarEventEditor())
->setActor($edit_actor)
->setActingAsPHID($import->getPHID())
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$is_new = !$event->getID();
$editor->applyTransactions($event, $event_xactions);
// We're just forcing attendees to the correct values here because
// transactions intentionally don't let you RSVP for other users. This
// might need to be turned into a special type of transaction eventually.
$attendees = $attendee_map[$full_uid];
$old_map = $event->getInvitees();
$old_map = mpull($old_map, null, 'getInviteePHID');
$new_map = array();
foreach ($attendees as $name => $attendee) {
$phid = $external_invitees[$name]->getPHID();
$invitee = idx($old_map, $phid);
if (!$invitee) {
$invitee = id(new PhabricatorCalendarEventInvitee())
->setEventPHID($event->getPHID())
->setInviteePHID($phid)
->setInviterPHID($import->getPHID());
}
switch ($attendee->getStatus()) {
case PhutilCalendarUserNode::STATUS_ACCEPTED:
$status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
break;
case PhutilCalendarUserNode::STATUS_DECLINED:
$status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
break;
case PhutilCalendarUserNode::STATUS_INVITED:
default:
$status = PhabricatorCalendarEventInvitee::STATUS_INVITED;
break;
}
$invitee->setStatus($status);
$invitee->save();
$new_map[$phid] = $invitee;
}
foreach ($old_map as $phid => $invitee) {
if (empty($new_map[$phid])) {
$invitee->delete();
}
}
$event->attachInvitees($new_map);
$import->newLogMessage(
PhabricatorCalendarImportUpdateLogType::LOGTYPE,
array(
'new' => $is_new,
'phid' => $event->getPHID(),
));
}
if (!$update_map) {
$import->newLogMessage(
PhabricatorCalendarImportEmptyLogType::LOGTYPE,
array());
}
// Delete any events which are no longer present in the source.
$updated_events = mpull($update_map, null, 'getPHID');
$source_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withImportSourcePHIDs(array($import->getPHID()))
->execute();
$engine = new PhabricatorDestructionEngine();
foreach ($source_events as $source_event) {
if (isset($updated_events[$source_event->getPHID()])) {
// We imported and updated this event, so keep it around.
continue;
}
$import->newLogMessage(
PhabricatorCalendarImportDeleteLogType::LOGTYPE,
array(
'name' => $source_event->getName(),
));
$engine->destroyObject($source_event);
}
}
private function getFullNodeUID(PhutilCalendarEventNode $node) {
$uid = $node->getUID();
$instance_epoch = $this->getNodeInstanceEpoch($node);
$full_uid = $uid.'/'.$instance_epoch;
return $full_uid;
}
private function getParentNodeUID(PhutilCalendarEventNode $node) {
$recurrence_id = $node->getRecurrenceID();
if (!strlen($recurrence_id)) {
return null;
}
return $node->getUID().'/';
}
private function getNodeInstanceEpoch(PhutilCalendarEventNode $node) {
$instance_iso = $node->getRecurrenceID();
if (strlen($instance_iso)) {
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601(
$instance_iso);
$instance_epoch = $instance_datetime->getEpoch();
} else {
$instance_epoch = null;
}
return $instance_epoch;
}
private function newUpdateTransactions(
PhabricatorCalendarEvent $event,
PhutilCalendarEventNode $node) {
$xactions = array();
$uid = $node->getUID();
if (!$event->getID()) {
$xactions[] = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
->setNewValue(true);
}
$name = $node->getName();
if (!strlen($name)) {
if (strlen($uid)) {
$name = pht('Unnamed Event "%s"', $uid);
} else {
$name = pht('Unnamed Imported Event');
}
}
$xactions[] = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$description = $node->getDescription();
$xactions[] = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE)
->setNewValue((string)$description);
$is_recurring = (bool)$node->getRecurrenceRule();
$xactions[] = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE)
->setNewValue($is_recurring);
return $xactions;
}
private function updateEventFromNode(
PhabricatorUser $actor,
PhabricatorCalendarEvent $event,
PhutilCalendarEventNode $node) {
$instance_epoch = $this->getNodeInstanceEpoch($node);
$event->setUTCInstanceEpoch($instance_epoch);
$timezone = $actor->getTimezoneIdentifier();
// TODO: These should be transactional, but the transaction only accepts
// epoch timestamps right now.
$start_datetime = $node->getStartDateTime()
->setViewerTimezone($timezone);
$end_datetime = $node->getEndDateTime()
->setViewerTimezone($timezone);
$event
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
- $event->setIsAllDay($start_datetime->getIsAllDay());
+ $event->setIsAllDay((int)$start_datetime->getIsAllDay());
// TODO: This should be transactional, but the transaction only accepts
// simple frequency rules right now.
$rrule = $node->getRecurrenceRule();
if ($rrule) {
$event->setRecurrenceRule($rrule);
$until_datetime = $rrule->getUntil();
if ($until_datetime) {
$until_datetime->setViewerTimezone($timezone);
$event->setUntilDateTime($until_datetime);
}
$count = $rrule->getCount();
$event->setParameter('recurrenceCount', $count);
}
return $event;
}
public function canDeleteAnyEvents(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import) {
- $any_event = id(new PhabricatorCalendarEventQuery())
- ->setViewer($viewer)
- ->withImportSourcePHIDs(array($import->getPHID()))
- ->setLimit(1)
- ->execute();
+ $table = new PhabricatorCalendarEvent();
+ $conn = $table->establishConnection('r');
+
+ // Using a CalendarEventQuery here was failing oddly in a way that was
+ // difficult to reproduce locally (see T11808). Just check the table
+ // directly; this is significantly more efficient anyway.
+
+ $any_event = queryfx_all(
+ $conn,
+ 'SELECT phid FROM %T WHERE importSourcePHID = %s LIMIT 1',
+ $table->getTableName(),
+ $import->getPHID());
return (bool)$any_event;
}
}
diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
index 68d4f37a2c..b0a762b577 100644
--- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
+++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
@@ -1,303 +1,304 @@
<?php
final class PhabricatorCalendarNotificationEngine
extends Phobject {
private $cursor;
private $notifyWindow;
public function getCursor() {
if (!$this->cursor) {
$now = PhabricatorTime::getNow();
$this->cursor = $now - phutil_units('10 minutes in seconds');
}
return $this->cursor;
}
public function setCursor($cursor) {
$this->cursor = $cursor;
return $this;
}
public function setNotifyWindow($notify_window) {
$this->notifyWindow = $notify_window;
return $this;
}
public function getNotifyWindow() {
if (!$this->notifyWindow) {
return phutil_units('15 minutes in seconds');
}
return $this->notifyWindow;
}
public function publishNotifications() {
$cursor = $this->getCursor();
$now = PhabricatorTime::getNow();
if ($cursor > $now) {
return;
}
$calendar_class = 'PhabricatorCalendarApplication';
if (!PhabricatorApplication::isClassInstalled($calendar_class)) {
return;
}
try {
$lock = PhabricatorGlobalLock::newLock('calendar.notify')
->lock(5);
} catch (PhutilLockException $ex) {
return;
}
$caught = null;
try {
$this->sendNotifications();
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
// Wait a little while before checking for new notifications to send.
$this->setCursor($cursor + phutil_units('1 minute in seconds'));
if ($caught) {
throw $caught;
}
}
private function sendNotifications() {
$cursor = $this->getCursor();
$window_min = $cursor - phutil_units('16 hours in seconds');
$window_max = $cursor + phutil_units('16 hours in seconds');
$viewer = PhabricatorUser::getOmnipotentUser();
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withDateRange($window_min, $window_max)
->withIsCancelled(false)
->withIsImported(false)
->setGenerateGhosts(true)
->execute();
if (!$events) {
// No events are starting soon in any timezone, so there is nothing
// left to be done.
return;
}
$attendee_map = array();
foreach ($events as $key => $event) {
$notifiable_phids = array();
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
$notifiable_phids[] = $invitee->getInviteePHID();
}
if (!$notifiable_phids) {
unset($events[$key]);
}
$attendee_map[$key] = array_fuse($notifiable_phids);
}
if (!$attendee_map) {
// None of the events have any notifiable attendees, so there is no
// one to notify of anything.
return;
}
$all_attendees = array();
foreach ($attendee_map as $key => $attendee_phids) {
foreach ($attendee_phids as $attendee_phid) {
$all_attendees[$attendee_phid] = $attendee_phid;
}
}
$user_map = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($all_attendees)
->withIsDisabled(false)
->needUserSettings(true)
->execute();
$user_map = mpull($user_map, null, 'getPHID');
if (!$user_map) {
// None of the attendees are valid users: they're all imported users
// or projects or invalid or some other kind of unnotifiable entity.
return;
}
$all_event_phids = array();
foreach ($events as $key => $event) {
foreach ($event->getNotificationPHIDs() as $phid) {
$all_event_phids[$phid] = $phid;
}
}
$table = new PhabricatorCalendarNotification();
$conn = $table->establishConnection('w');
$rows = queryfx_all(
$conn,
'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',
$table->getTableName(),
$all_event_phids,
$all_attendees);
$sent_map = array();
foreach ($rows as $row) {
$event_phid = $row['eventPHID'];
$target_phid = $row['targetPHID'];
$initial_epoch = $row['utcInitialEpoch'];
$sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
}
- $notify_min = $cursor;
- $notify_max = $cursor + $this->getNotifyWindow();
+ $now = PhabricatorTime::getNow();
+ $notify_min = $now;
+ $notify_max = $now + $this->getNotifyWindow();
$notify_map = array();
foreach ($events as $key => $event) {
$initial_epoch = $event->getUTCInitialEpoch();
$event_phids = $event->getNotificationPHIDs();
// Select attendees who actually exist, and who we have not sent any
// notifications to yet.
$attendee_phids = $attendee_map[$key];
$users = array_select_keys($user_map, $attendee_phids);
foreach ($users as $user_phid => $user) {
foreach ($event_phids as $event_phid) {
if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
unset($users[$user_phid]);
continue 2;
}
}
}
if (!$users) {
continue;
}
// Discard attendees for whom the event start time isn't soon. Events
// may start at different times for different users, so we need to
// check every user's start time.
foreach ($users as $user_phid => $user) {
$user_datetime = $event->newStartDateTime()
->setViewerTimezone($user->getTimezoneIdentifier());
$user_epoch = $user_datetime->getEpoch();
if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
unset($users[$user_phid]);
continue;
}
$view = id(new PhabricatorCalendarEventNotificationView())
->setViewer($user)
->setEvent($event)
->setDateTime($user_datetime)
->setEpoch($user_epoch);
$notify_map[$user_phid][] = $view;
}
}
$mail_list = array();
$mark_list = array();
$now = PhabricatorTime::getNow();
foreach ($notify_map as $user_phid => $events) {
$user = $user_map[$user_phid];
$locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
$caught = null;
try {
$mail_list[] = $this->newMailMessage($user, $events);
} catch (Exception $ex) {
$caught = $ex;
}
unset($locale);
if ($caught) {
throw $ex;
}
foreach ($events as $view) {
$event = $view->getEvent();
foreach ($event->getNotificationPHIDs() as $phid) {
$mark_list[] = qsprintf(
$conn,
'(%s, %s, %d, %d)',
$phid,
$user_phid,
$event->getUTCInitialEpoch(),
$now);
}
}
}
// Mark all the notifications we're about to send as delivered so we
// do not double-notify.
foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
queryfx(
$conn,
'INSERT IGNORE INTO %T
(eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
VALUES %Q',
$table->getTableName(),
$chunk);
}
foreach ($mail_list as $mail) {
$mail->saveAndSend();
}
}
private function newMailMessage(PhabricatorUser $viewer, array $events) {
$events = msort($events, 'getEpoch');
$next_event = head($events);
$body = new PhabricatorMetaMTAMailBody();
foreach ($events as $event) {
$body->addTextSection(
null,
pht(
'%s is starting in %s minute(s), at %s.',
$event->getEvent()->getName(),
$event->getDisplayMinutes(),
$event->getDisplayTime()));
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));
}
$next_event = head($events)->getEvent();
$subject = $next_event->getName();
if (count($events) > 1) {
$more = pht(
'(+%s more...)',
new PhutilNumber(count($events) - 1));
$subject = "{$subject} {$more}";
}
$calendar_phid = id(new PhabricatorCalendarApplication())
->getPHID();
return id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addTos(array($viewer->getPHID()))
->setSensitiveContent(false)
->setFrom($calendar_phid)
->setIsBulk(true)
->setSubjectPrefix(pht('[Calendar]'))
->setVarySubjectPrefix(pht('[Reminder]'))
->setThreadID($next_event->getPHID(), false)
->setRelatedPHID($next_event->getPHID())
->setBody($body->render())
->setHTMLBody($body->renderHTML());
}
}
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
index 3980c0635d..0c762ffa54 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -1,684 +1,684 @@
<?php
final class PhabricatorCalendarEventQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $rangeBegin;
private $rangeEnd;
private $inviteePHIDs;
private $hostPHIDs;
private $isCancelled;
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
private $parentEventPHIDs;
private $importSourcePHIDs;
private $importAuthorPHIDs;
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;
private $generateGhosts = false;
public function newResultObject() {
return new PhabricatorCalendarEvent();
}
public function setGenerateGhosts($generate_ghosts) {
$this->generateGhosts = $generate_ghosts;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDateRange($begin, $end) {
$this->rangeBegin = $begin;
$this->rangeEnd = $end;
return $this;
}
public function withUTCInitialEpochBetween($min, $max) {
$this->utcInitialEpochMin = $min;
$this->utcInitialEpochMax = $max;
return $this;
}
public function withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids;
return $this;
}
public function withHostPHIDs(array $phids) {
$this->hostPHIDs = $phids;
return $this;
}
public function withIsCancelled($is_cancelled) {
$this->isCancelled = $is_cancelled;
return $this;
}
public function withIsStub($is_stub) {
$this->isStub = $is_stub;
return $this;
}
public function withEventsWithNoParent($events_with_no_parent) {
$this->eventsWithNoParent = $events_with_no_parent;
return $this;
}
public function withInstanceSequencePairs(array $pairs) {
$this->instanceSequencePairs = $pairs;
return $this;
}
public function withParentEventPHIDs(array $parent_phids) {
$this->parentEventPHIDs = $parent_phids;
return $this;
}
public function withImportSourcePHIDs(array $import_phids) {
$this->importSourcePHIDs = $import_phids;
return $this;
}
public function withImportAuthorPHIDs(array $author_phids) {
$this->importAuthorPHIDs = $author_phids;
return $this;
}
public function withImportUIDs(array $uids) {
$this->importUIDs = $uids;
return $this;
}
public function withIsImported($is_imported) {
$this->isImported = $is_imported;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
public function getBuiltinOrders() {
return array(
'start' => array(
'vector' => array('start', 'id'),
'name' => pht('Event Start'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'start' => array(
'table' => $this->getPrimaryTableAlias(),
- 'column' => 'dateFrom',
+ 'column' => 'utcInitialEpoch',
'reverse' => true,
'type' => 'int',
'unique' => false,
),
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$event = $this->loadCursorObject($cursor);
return array(
'start' => $event->getStartDateTimeEpoch(),
'id' => $event->getID(),
);
}
protected function shouldLimitResults() {
// When generating ghosts, we can't rely on database ordering because
// MySQL can't predict the ghost start times. We'll just load all matching
// events, then generate results from there.
if ($this->generateGhosts) {
return false;
}
return true;
}
protected function loadPage() {
$events = $this->loadStandardPage($this->newResultObject());
$viewer = $this->getViewer();
foreach ($events as $event) {
$event->applyViewerTimezone($viewer);
}
if (!$this->generateGhosts) {
return $events;
}
$raw_limit = $this->getRawResultLimit();
if (!$raw_limit && !$this->rangeEnd) {
throw new Exception(
pht(
'Event queries which generate ghost events must include either a '.
'result limit or an end date, because they may otherwise generate '.
'an infinite number of results. This query has neither.'));
}
foreach ($events as $key => $event) {
$sequence_start = 0;
$sequence_end = null;
$end = null;
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of == null && $this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
// Pull out all of the parents first. We may discard them as we begin
// generating ghost events, but we still want to process all of them.
$parents = array();
foreach ($events as $key => $event) {
if ($event->isParentEvent()) {
$parents[$key] = $event;
}
}
// Now that we've picked out all the parent events, we can immediately
// discard anything outside of the time window.
$events = $this->getEventsInRange($events);
$generate_from = $this->rangeBegin;
$generate_until = $this->rangeEnd;
foreach ($parents as $key => $event) {
$duration = $event->getDuration();
$start_date = $this->getRecurrenceWindowStart(
$event,
$generate_from - $duration);
$end_date = $this->getRecurrenceWindowEnd(
$event,
$generate_until);
$limit = $this->getRecurrenceLimit($event, $raw_limit);
$set = $event->newRecurrenceSet();
$recurrences = $set->getEventsBetween(
null,
$end_date,
$limit + 1);
// We're generating events from the beginning and then filtering them
// here (instead of only generating events starting at the start date)
// because we need to know the proper sequence indexes to generate ghost
// events. This may change after RDATE support.
if ($start_date) {
$start_epoch = $start_date->getEpoch();
} else {
$start_epoch = null;
}
foreach ($recurrences as $sequence_index => $sequence_datetime) {
if (!$sequence_index) {
// This is the parent event, which we already have.
continue;
}
if ($start_epoch) {
if ($sequence_datetime->getEpoch() < $start_epoch) {
continue;
}
}
$events[] = $event->newGhost(
$viewer,
$sequence_index,
$sequence_datetime);
}
// NOTE: We're slicing results every time because this makes it cheaper
// to generate future ghosts. If we already have 100 events that occur
// before July 1, we know we never need to generate ghosts after that
// because they couldn't possibly ever appear in the result set.
if ($raw_limit) {
if (count($events) > $raw_limit) {
$events = msort($events, 'getStartDateTimeEpoch');
$events = array_slice($events, 0, $raw_limit, true);
$generate_until = last($events)->getEndDateTimeEpoch();
}
}
}
// Now that we're done generating ghost events, we're going to remove any
// ghosts that we have concrete events for (or which we can load the
// concrete events for). These concrete events are generated when users
// edit a ghost, and replace the ghost events.
// First, generate a map of all concrete <parentPHID, sequence> events we
// already loaded. We don't need to load these again.
$have_pairs = array();
foreach ($events as $event) {
if ($event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
$have_pairs[$parent_phid][$sequence] = true;
}
// Now, generate a map of all <parentPHID, sequence> events we generated
// ghosts for. We need to try to load these if we don't already have them.
$map = array();
$parent_pairs = array();
foreach ($events as $key => $event) {
if (!$event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
// We already loaded the concrete version of this event, so we can just
// throw out the ghost and move on.
if (isset($have_pairs[$parent_phid][$sequence])) {
unset($events[$key]);
continue;
}
// We didn't load the concrete version of this event, so we need to
// try to load it if it exists.
$parent_pairs[] = array($parent_phid, $sequence);
$map[$parent_phid][$sequence] = $key;
}
if ($parent_pairs) {
$instances = id(new self())
->setViewer($viewer)
->setParentQuery($this)
->withInstanceSequencePairs($parent_pairs)
->execute();
foreach ($instances as $instance) {
$parent_phid = $instance->getInstanceOfEventPHID();
$sequence = $instance->getSequenceIndex();
$indexes = idx($map, $parent_phid);
$key = idx($indexes, $sequence);
// Replace the ghost with the corresponding concrete event.
$events[$key] = $instance;
}
}
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
$parts = parent::buildJoinClauseParts($conn_r);
if ($this->inviteePHIDs !== null) {
$parts[] = qsprintf(
$conn_r,
'JOIN %T invitee ON invitee.eventPHID = event.phid
AND invitee.status != %s',
id(new PhabricatorCalendarEventInvitee())->getTableName(),
PhabricatorCalendarEventInvitee::STATUS_UNINVITED);
}
return $parts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'event.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'event.phid IN (%Ls)',
$this->phids);
}
// NOTE: The date ranges we query for are larger than the requested ranges
// because we need to catch all-day events. We'll refine this range later
// after adjusting the visible range of events we load.
if ($this->rangeBegin) {
$where[] = qsprintf(
$conn,
'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
$this->rangeBegin - phutil_units('16 hours in seconds'));
}
if ($this->rangeEnd) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
if ($this->utcInitialEpochMin !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch >= %d',
$this->utcInitialEpochMin);
}
if ($this->utcInitialEpochMax !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->utcInitialEpochMax);
}
if ($this->inviteePHIDs !== null) {
$where[] = qsprintf(
$conn,
'invitee.inviteePHID IN (%Ls)',
$this->inviteePHIDs);
}
if ($this->hostPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.hostPHID IN (%Ls)',
$this->hostPHIDs);
}
if ($this->isCancelled !== null) {
$where[] = qsprintf(
$conn,
'event.isCancelled = %d',
(int)$this->isCancelled);
}
if ($this->eventsWithNoParent == true) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IS NULL');
}
if ($this->instanceSequencePairs !== null) {
$sql = array();
foreach ($this->instanceSequencePairs as $pair) {
$sql[] = qsprintf(
$conn,
'(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',
$pair[0],
$pair[1]);
}
$where[] = qsprintf(
$conn,
'%Q',
implode(' OR ', $sql));
}
if ($this->isStub !== null) {
$where[] = qsprintf(
$conn,
'event.isStub = %d',
(int)$this->isStub);
}
if ($this->parentEventPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IN (%Ls)',
$this->parentEventPHIDs);
}
if ($this->importSourcePHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IN (%Ls)',
$this->importSourcePHIDs);
}
if ($this->importAuthorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importAuthorPHID IN (%Ls)',
$this->importAuthorPHIDs);
}
if ($this->importUIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importUID IN (%Ls)',
$this->importUIDs);
}
if ($this->isImported !== null) {
if ($this->isImported) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NULL');
}
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'event';
}
protected function shouldGroupQueryResultRows() {
if ($this->inviteePHIDs !== null) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function getApplicationSearchObjectPHIDColumn() {
return 'event.phid';
}
public function getQueryApplicationClass() {
return 'PhabricatorCalendarApplication';
}
protected function willFilterPage(array $events) {
$instance_of_event_phids = array();
$recurring_events = array();
$viewer = $this->getViewer();
$events = $this->getEventsInRange($events);
$import_phids = array();
foreach ($events as $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid !== null) {
$import_phids[$import_phid] = $import_phid;
}
}
if ($import_phids) {
$imports = id(new PhabricatorCalendarImportQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($import_phids)
->execute();
$imports = mpull($imports, null, 'getPHID');
} else {
$imports = array();
}
foreach ($events as $key => $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid === null) {
$event->attachImportSource(null);
continue;
}
$import = idx($imports, $import_phid);
if (!$import) {
unset($events[$key]);
$this->didRejectResult($event);
continue;
}
$event->attachImportSource($import);
}
$phids = array();
foreach ($events as $event) {
$phids[] = $event->getPHID();
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of) {
$instance_of_event_phids[] = $instance_of;
}
}
if (count($instance_of_event_phids) > 0) {
$recurring_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withPHIDs($instance_of_event_phids)
->withEventsWithNoParent(true)
->execute();
$recurring_events = mpull($recurring_events, null, 'getPHID');
}
if ($events) {
$invitees = id(new PhabricatorCalendarEventInviteeQuery())
->setViewer($viewer)
->withEventPHIDs($phids)
->execute();
$invitees = mgroup($invitees, 'getEventPHID');
} else {
$invitees = array();
}
foreach ($events as $key => $event) {
$event_invitees = idx($invitees, $event->getPHID(), array());
$event->attachInvitees($event_invitees);
$instance_of = $event->getInstanceOfEventPHID();
if (!$instance_of) {
continue;
}
$parent = idx($recurring_events, $instance_of);
// should never get here
if (!$parent) {
unset($events[$key]);
continue;
}
$event->attachParentEvent($parent);
if ($this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
private function getEventsInRange(array $events) {
$range_start = $this->rangeBegin;
$range_end = $this->rangeEnd;
foreach ($events as $key => $event) {
$event_start = $event->getStartDateTimeEpoch();
$event_end = $event->getEndDateTimeEpoch();
if ($range_start && $event_end < $range_start) {
unset($events[$key]);
}
if ($range_end && $event_start > $range_end) {
unset($events[$key]);
}
}
return $events;
}
private function getRecurrenceWindowStart(
PhabricatorCalendarEvent $event,
$generate_from) {
if (!$generate_from) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
}
private function getRecurrenceWindowEnd(
PhabricatorCalendarEvent $event,
$generate_until) {
$end_epochs = array();
if ($generate_until) {
$end_epochs[] = $generate_until;
}
$until_epoch = $event->getUntilDateTimeEpoch();
if ($until_epoch) {
$end_epochs[] = $until_epoch;
}
if (!$end_epochs) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
}
private function getRecurrenceLimit(
PhabricatorCalendarEvent $event,
$raw_limit) {
$count = $event->getRecurrenceCount();
if ($count && ($count <= $raw_limit)) {
return ($count - 1);
}
return $raw_limit;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 28, 3:01 AM (13 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
107697
Default Alt Text
(46 KB)

Event Timeline