Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F23020866
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
94 KB
Subscribers
None
View Options
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
index 3eac22301..e735e8220 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -1,479 +1,528 @@
<?php
final class PhabricatorCalendarEventQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $rangeBegin;
private $rangeEnd;
private $inviteePHIDs;
private $creatorPHIDs;
private $isCancelled;
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
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 withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids;
return $this;
}
public function withCreatorPHIDs(array $phids) {
$this->creatorPHIDs = $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;
}
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',
'reverse' => true,
'type' => 'int',
'unique' => false,
),
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$event = $this->loadCursorObject($cursor);
return array(
'start' => $event->getViewerDateFrom(),
'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;
}
- $enforced_end = null;
$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;
- $duration = $event->getDuration();
$end = null;
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of == null && $this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
+ }
- if ($event->getIsRecurring() && $instance_of == null) {
- $frequency = $event->getFrequencyUnit();
- $modify_key = '+1 '.$frequency;
+ // 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;
+ }
+ }
- if (($this->rangeBegin !== null) &&
- ($this->rangeBegin > $event->getViewerDateFrom())) {
- $max_date = $this->rangeBegin - $duration;
- $date = $event->getViewerDateFrom();
- $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
+ // Now that we've picked out all the parent events, we can immediately
+ // discard anything outside of the time window.
+ $events = $this->getEventsInRange($events);
- while ($date < $max_date) {
- // TODO: optimize this to not loop through all off-screen events
- $sequence_start++;
- $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
- $date = $datetime->modify($modify_key)->format('U');
- }
+ $enforced_end = null;
+ foreach ($parents as $key => $event) {
+ $sequence_start = 0;
+ $sequence_end = null;
+ $start = null;
- $start = $this->rangeBegin;
- } else {
- $start = $event->getViewerDateFrom() - $duration;
- }
+ $duration = $event->getDuration();
- $date = $start;
+ $frequency = $event->getFrequencyUnit();
+ $modify_key = '+1 '.$frequency;
+
+ if (($this->rangeBegin !== null) &&
+ ($this->rangeBegin > $event->getViewerDateFrom())) {
+ $max_date = $this->rangeBegin - $duration;
+ $date = $event->getViewerDateFrom();
$datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
- if (($this->rangeEnd && $event->getRecurrenceEndDate()) &&
- $this->rangeEnd < $event->getRecurrenceEndDate()) {
- $end = $this->rangeEnd;
- } else if ($event->getRecurrenceEndDate()) {
- $end = $event->getRecurrenceEndDate();
- } else if ($this->rangeEnd) {
- $end = $this->rangeEnd;
- } else if ($enforced_end) {
- if ($end) {
- $end = min($end, $enforced_end);
- } else {
- $end = $enforced_end;
- }
+ while ($date < $max_date) {
+ // TODO: optimize this to not loop through all off-screen events
+ $sequence_start++;
+ $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
+ $date = $datetime->modify($modify_key)->format('U');
}
- if ($end) {
- $sequence_end = $sequence_start;
- while ($date < $end) {
- $sequence_end++;
- $datetime->modify($modify_key);
- $date = $datetime->format('U');
- if ($sequence_end > $raw_limit + $sequence_start) {
- break;
- }
- }
- } else {
- $sequence_end = $raw_limit + $sequence_start;
- }
+ $start = $this->rangeBegin;
+ } else {
+ $start = $event->getViewerDateFrom() - $duration;
+ }
- $sequence_start = max(1, $sequence_start);
+ $date = $start;
+ $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
- for ($index = $sequence_start; $index < $sequence_end; $index++) {
- $events[] = $event->newGhost($viewer, $index);
- }
+ // Select the minimum end time we need to generate events until.
+ $end_times = array();
+ if ($this->rangeEnd) {
+ $end_times[] = $this->rangeEnd;
+ }
- // 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 ($event->getRecurrenceEndDate()) {
+ $end_times[] = $event->getRecurrenceEndDate();
+ }
- if ($raw_limit) {
- if (count($events) >= $raw_limit) {
- $events = msort($events, 'getViewerDateFrom');
- $events = array_slice($events, 0, $raw_limit, true);
- $enforced_end = last($events)->getViewerDateFrom();
+ if ($enforced_end) {
+ $end_times[] = $enforced_end;
+ }
+
+ if ($end_times) {
+ $end = min($end_times);
+ $sequence_end = $sequence_start;
+ while ($date < $end) {
+ $sequence_end++;
+ $datetime->modify($modify_key);
+ $date = $datetime->format('U');
+ if ($sequence_end > $raw_limit + $sequence_start) {
+ break;
}
}
+ } else {
+ $sequence_end = $raw_limit + $sequence_start;
+ }
+
+ $sequence_start = max(1, $sequence_start);
+ for ($index = $sequence_start; $index < $sequence_end; $index++) {
+ $events[] = $event->newGhost($viewer, $index);
+ }
+
+ // 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, 'getViewerDateFrom');
+ $events = array_slice($events, 0, $raw_limit, true);
+ $enforced_end = last($events)->getViewerDateFrom();
+ }
}
}
// 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, 'getViewerDateFrom');
+
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) {
$where[] = qsprintf(
$conn,
'event.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$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.dateTo >= %d OR event.isRecurring = 1',
$this->rangeBegin - phutil_units('16 hours in seconds'));
}
if ($this->rangeEnd) {
$where[] = qsprintf(
$conn,
'event.dateFrom <= %d',
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
if ($this->inviteePHIDs !== null) {
$where[] = qsprintf(
$conn,
'invitee.inviteePHID IN (%Ls)',
$this->inviteePHIDs);
}
if ($this->creatorPHIDs) {
$where[] = qsprintf(
$conn,
'event.userPHID IN (%Ls)',
$this->creatorPHIDs);
}
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);
}
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) {
- $range_start = $this->rangeBegin;
- $range_end = $this->rangeEnd;
$instance_of_event_phids = array();
$recurring_events = array();
$viewer = $this->getViewer();
- foreach ($events as $key => $event) {
- $event_start = $event->getViewerDateFrom();
- $event_end = $event->getViewerDateTo();
-
- if ($range_start && $event_end < $range_start) {
- unset($events[$key]);
- }
- if ($range_end && $event_start > $range_end) {
- unset($events[$key]);
- }
- }
+ $events = $this->getEventsInRange($events);
$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, 'getViewerDateFrom');
return $events;
}
+ private function getEventsInRange(array $events) {
+ $range_start = $this->rangeBegin;
+ $range_end = $this->rangeEnd;
+
+ foreach ($events as $key => $event) {
+ $event_start = $event->getViewerDateFrom();
+ $event_end = $event->getViewerDateTo();
+
+ if ($range_start && $event_end < $range_start) {
+ unset($events[$key]);
+ }
+
+ if ($range_end && $event_start > $range_end) {
+ unset($events[$key]);
+ }
+ }
+
+ return $events;
+ }
+
}
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
index 3465d3ee7..bdff84fc1 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
@@ -1,591 +1,592 @@
<?php
final class PhabricatorCalendarEventSearchEngine
extends PhabricatorApplicationSearchEngine {
private $calendarYear;
private $calendarMonth;
private $calendarDay;
public function getResultTypeDescription() {
return pht('Calendar Events');
}
public function getApplicationClassName() {
return 'PhabricatorCalendarApplication';
}
public function newQuery() {
return new PhabricatorCalendarEventQuery();
}
protected function shouldShowOrderField() {
return false;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Created By'))
->setKey('creatorPHIDs')
->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Invited'))
->setKey('invitedPHIDs')
->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
id(new PhabricatorSearchDateControlField())
->setLabel(pht('Occurs After'))
->setKey('rangeStart'),
id(new PhabricatorSearchDateControlField())
->setLabel(pht('Occurs Before'))
->setKey('rangeEnd')
->setAliases(array('rangeEnd')),
id(new PhabricatorSearchCheckboxesField())
->setKey('upcoming')
->setOptions(array(
'upcoming' => pht('Show only upcoming events.'),
)),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Cancelled Events'))
->setKey('isCancelled')
->setOptions($this->getCancelledOptions())
->setDefault('active'),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Display Options'))
->setKey('display')
->setOptions($this->getViewOptions())
->setDefault('month'),
);
}
private function getCancelledOptions() {
return array(
'active' => pht('Active Events Only'),
'cancelled' => pht('Cancelled Events Only'),
'both' => pht('Both Cancelled and Active Events'),
);
}
private function getViewOptions() {
return array(
'month' => pht('Month View'),
'day' => pht('Day View'),
'list' => pht('List View'),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
$viewer = $this->requireViewer();
if ($map['creatorPHIDs']) {
$query->withCreatorPHIDs($map['creatorPHIDs']);
}
if ($map['invitedPHIDs']) {
$query->withInvitedPHIDs($map['invitedPHIDs']);
}
$range_start = $map['rangeStart'];
$range_end = $map['rangeEnd'];
$display = $map['display'];
if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') {
$upcoming = true;
} else {
$upcoming = false;
}
list($range_start, $range_end) = $this->getQueryDateRange(
$range_start,
$range_end,
$display,
$upcoming);
$query->withDateRange($range_start, $range_end);
switch ($map['isCancelled']) {
case 'active':
$query->withIsCancelled(false);
break;
case 'cancelled':
$query->withIsCancelled(true);
break;
}
// Generate ghosts (and ignore stub events) if we aren't querying for
// specific events.
if (!$map['ids'] && !$map['phids']) {
$query
->withIsStub(false)
->setGenerateGhosts(true);
}
return $query;
}
private function getQueryDateRange(
$start_date_wild,
$end_date_wild,
$display,
$upcoming) {
$start_date_value = $this->getSafeDate($start_date_wild);
$end_date_value = $this->getSafeDate($end_date_wild);
$viewer = $this->requireViewer();
$timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
$min_range = null;
$max_range = null;
$min_range = $start_date_value->getEpoch();
$max_range = $end_date_value->getEpoch();
if ($display == 'month' || $display == 'day') {
list($start_year, $start_month, $start_day) =
$this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display);
$start_day = new DateTime(
"{$start_year}-{$start_month}-{$start_day}",
$timezone);
$next = clone $start_day;
if ($display == 'month') {
$next->modify('+1 month');
} else if ($display == 'day') {
$next->modify('+7 day');
}
$display_start = $start_day->format('U');
$display_end = $next->format('U');
$start_of_week = $viewer->getUserSetting(
PhabricatorWeekStartDaySetting::SETTINGKEY);
$end_of_week = ($start_of_week + 6) % 7;
$first_of_month = $start_day->format('w');
$last_of_month = id(clone $next)->modify('-1 day')->format('w');
if (!$min_range || ($min_range < $display_start)) {
$min_range = $display_start;
if ($display == 'month' &&
$first_of_month !== $start_of_week) {
$interim_day_num = ($first_of_month + 7 - $start_of_week) % 7;
$min_range = id(clone $start_day)
->modify('-'.$interim_day_num.' days')
->format('U');
}
}
if (!$max_range || ($max_range > $display_end)) {
$max_range = $display_end;
if ($display == 'month' &&
$last_of_month !== $end_of_week) {
$interim_day_num = ($end_of_week + 7 - $last_of_month) % 7;
$max_range = id(clone $next)
->modify('+'.$interim_day_num.' days')
->format('U');
}
}
}
if ($upcoming) {
+ $now = PhabricatorTime::getNow();
if ($min_range) {
- $min_range = max(time(), $min_range);
+ $min_range = max($now, $min_range);
} else {
- $min_range = time();
+ $min_range = $now;
}
}
return array($min_range, $max_range);
}
protected function getURI($path) {
return '/calendar/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'month' => pht('Month View'),
'day' => pht('Day View'),
'upcoming' => pht('Upcoming Events'),
'all' => pht('All Events'),
);
return $names;
}
public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
$this->calendarYear = $year;
$this->calendarMonth = $month;
$this->calendarDay = $day;
return $this;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'month':
return $query->setParameter('display', 'month');
case 'day':
return $query->setParameter('display', 'day');
case 'upcoming':
return $query
->setParameter('display', 'list')
->setParameter('upcoming', array(
0 => 'upcoming',
));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($objects as $event) {
$phids[$event->getUserPHID()] = 1;
}
return array_keys($phids);
}
protected function renderResultList(
array $events,
PhabricatorSavedQuery $query,
array $handles) {
if ($this->isMonthView($query)) {
return $this->buildCalendarView($events, $query, $handles);
} else if ($this->isDayView($query)) {
return $this->buildCalendarDayView($events, $query, $handles);
}
assert_instances_of($events, 'PhabricatorCalendarEvent');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
foreach ($events as $event) {
$event_date_info = $this->getEventDateLabel($event);
$creator_handle = $handles[$event->getUserPHID()];
$attendees = array();
foreach ($event->getInvitees() as $invitee) {
$status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
if ($invitee->getStatus() === $status_attending) {
$attendees[] = $invitee->getInviteePHID();
}
}
if ($event->getIsGhostEvent()) {
$title_text = $event->getMonogram()
.' ('
.$event->getSequenceIndex()
.'): '
.$event->getName();
} else {
$title_text = $event->getMonogram().': '.$event->getName();
}
$item = id(new PHUIObjectItemView())
->setUser($viewer)
->setObject($event)
->setHeader($title_text)
->setHref($event->getURI())
->addAttribute($event_date_info);
if ($attendees) {
$attending = pht(
'Attending: %s',
$viewer->renderHandleList($attendees)
->setAsInline(1)
->render());
$item->addAttribute($attending);
}
if ($event->getDuration()) {
$duration = pht(
'Duration: %s',
$event->getDisplayDuration());
$item->addIcon('none', $duration);
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No events found.'));
return $result;
}
private function buildCalendarView(
array $statuses,
PhabricatorSavedQuery $query,
array $handles) {
$viewer = $this->requireViewer();
$now = time();
list($start_year, $start_month) =
$this->getDisplayYearAndMonthAndDay(
$this->getQueryDateFrom($query)->getEpoch(),
$this->getQueryDateTo($query)->getEpoch(),
$query->getParameter('display'));
$now_year = phabricator_format_local_time($now, $viewer, 'Y');
$now_month = phabricator_format_local_time($now, $viewer, 'm');
$now_day = phabricator_format_local_time($now, $viewer, 'j');
if ($start_month == $now_month && $start_year == $now_year) {
$month_view = new PHUICalendarMonthView(
$this->getQueryDateFrom($query),
$this->getQueryDateTo($query),
$start_month,
$start_year,
$now_day);
} else {
$month_view = new PHUICalendarMonthView(
$this->getQueryDateFrom($query),
$this->getQueryDateTo($query),
$start_month,
$start_year);
}
$month_view->setUser($viewer);
$phids = mpull($statuses, 'getUserPHID');
foreach ($statuses as $status) {
$viewer_is_invited = $status->getIsUserInvited($viewer->getPHID());
$event = new AphrontCalendarEventView();
$event->setEpochRange(
$status->getViewerDateFrom(),
$status->getViewerDateTo());
$event->setIsAllDay($status->getIsAllDay());
$event->setIcon($status->getIcon());
$name_text = $handles[$status->getUserPHID()]->getName();
$status_text = $status->getName();
$event->setUserPHID($status->getUserPHID());
$event->setDescription(pht('%s (%s)', $name_text, $status_text));
$event->setName($status_text);
$event->setURI($status->getURI());
$event->setViewerIsInvited($viewer_is_invited);
$month_view->addEvent($event);
}
$month_view->setBrowseURI(
$this->getURI('query/'.$query->getQueryKey().'/'));
// TODO redesign-2015 : Move buttons out of PHUICalendarView?
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($month_view);
return $result;
}
private function buildCalendarDayView(
array $statuses,
PhabricatorSavedQuery $query,
array $handles) {
$viewer = $this->requireViewer();
list($start_year, $start_month, $start_day) =
$this->getDisplayYearAndMonthAndDay(
$this->getQueryDateFrom($query)->getEpoch(),
$this->getQueryDateTo($query)->getEpoch(),
$query->getParameter('display'));
$day_view = id(new PHUICalendarDayView(
$this->getQueryDateFrom($query)->getEpoch(),
$this->getQueryDateTo($query)->getEpoch(),
$start_year,
$start_month,
$start_day))
->setQuery($query->getQueryKey());
$day_view->setUser($viewer);
$phids = mpull($statuses, 'getUserPHID');
foreach ($statuses as $status) {
if ($status->getIsCancelled()) {
continue;
}
$viewer_is_invited = $status->getIsUserInvited($viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$status,
PhabricatorPolicyCapability::CAN_EDIT);
$event = new AphrontCalendarEventView();
$event->setCanEdit($can_edit);
$event->setEventID($status->getID());
$event->setEpochRange(
$status->getViewerDateFrom(),
$status->getViewerDateTo());
$event->setIsAllDay($status->getIsAllDay());
$event->setIcon($status->getIcon());
$event->setViewerIsInvited($viewer_is_invited);
$event->setName($status->getName());
$event->setURI($status->getURI());
$day_view->addEvent($event);
}
$day_view->setBrowseURI(
$this->getURI('query/'.$query->getQueryKey().'/'));
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($day_view);
return $result;
}
private function getDisplayYearAndMonthAndDay(
$range_start,
$range_end,
$display) {
$viewer = $this->requireViewer();
$epoch = null;
if ($this->calendarYear && $this->calendarMonth) {
$start_year = $this->calendarYear;
$start_month = $this->calendarMonth;
$start_day = $this->calendarDay ? $this->calendarDay : 1;
} else {
if ($range_start) {
$epoch = $range_start;
} else if ($range_end) {
$epoch = $range_end;
} else {
$epoch = time();
}
if ($display == 'month') {
$day = 1;
} else {
$day = phabricator_format_local_time($epoch, $viewer, 'd');
}
$start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
$start_month = phabricator_format_local_time($epoch, $viewer, 'm');
$start_day = $day;
}
return array($start_year, $start_month, $start_day);
}
public function getPageSize(PhabricatorSavedQuery $saved) {
if ($this->isMonthView($saved) || $this->isDayView($saved)) {
return $saved->getParameter('limit', 1000);
} else {
return $saved->getParameter('limit', 100);
}
}
private function getQueryDateFrom(PhabricatorSavedQuery $saved) {
return $this->getQueryDate($saved, 'rangeStart');
}
private function getQueryDateTo(PhabricatorSavedQuery $saved) {
return $this->getQueryDate($saved, 'rangeEnd');
}
private function getQueryDate(PhabricatorSavedQuery $saved, $key) {
$viewer = $this->requireViewer();
$wild = $saved->getParameter($key);
return $this->getSafeDate($wild);
}
private function getSafeDate($value) {
$viewer = $this->requireViewer();
if ($value) {
// ideally this would be consistent and always pass in the same type
if ($value instanceof AphrontFormDateControlValue) {
return $value;
} else {
$value = AphrontFormDateControlValue::newFromWild($viewer, $value);
}
} else {
$value = AphrontFormDateControlValue::newFromEpoch(
$viewer,
PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U'));
$value->setEnabled(false);
}
$value->setOptional(true);
return $value;
}
private function isMonthView(PhabricatorSavedQuery $query) {
if ($this->isDayView($query)) {
return false;
}
if ($query->getParameter('display') == 'month') {
return true;
}
}
private function isDayView(PhabricatorSavedQuery $query) {
if ($query->getParameter('display') == 'day') {
return true;
}
if ($this->calendarDay) {
return true;
}
return false;
}
private function getEventDateLabel($event) {
$viewer = $this->requireViewer();
$from_datetime = PhabricatorTime::getDateTimeFromEpoch(
$event->getViewerDateFrom(),
$viewer);
$to_datetime = PhabricatorTime::getDateTimeFromEpoch(
$event->getViewerDateTo(),
$viewer);
$from_date_formatted = $from_datetime->format('Y m d');
$to_date_formatted = $to_datetime->format('Y m d');
if ($event->getIsAllDay()) {
if ($from_date_formatted == $to_date_formatted) {
return pht(
'%s, All Day',
phabricator_date($event->getViewerDateFrom(), $viewer));
} else {
return pht(
'%s - %s, All Day',
phabricator_date($event->getViewerDateFrom(), $viewer),
phabricator_date($event->getViewerDateTo(), $viewer));
}
} else if ($from_date_formatted == $to_date_formatted) {
return pht(
'%s - %s',
phabricator_datetime($event->getViewerDateFrom(), $viewer),
phabricator_time($event->getViewerDateTo(), $viewer));
} else {
return pht(
'%s - %s',
phabricator_datetime($event->getViewerDateFrom(), $viewer),
phabricator_datetime($event->getViewerDateTo(), $viewer));
}
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 0efca33b6..8d4398012 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,2106 +1,2113 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
private $spaceIsArchived;
private $ngrams = array();
protected function getPageCursors(array $page) {
return array(
$this->getResultCursor(head($page)),
$this->getResultCursor(last($page)),
);
}
protected function getResultCursor($object) {
if (!is_object($object)) {
throw new Exception(
pht(
'Expected object, got "%s".',
gettype($object)));
}
return $object->getID();
}
protected function nextPage(array $page) {
// See getPagingViewer() for a description of this flag.
$this->internalPaging = true;
if ($this->beforeID !== null) {
$page = array_reverse($page, $preserve_keys = true);
list($before, $after) = $this->getPageCursors($page);
$this->beforeID = $before;
} else {
list($before, $after) = $this->getPageCursors($page);
$this->afterID = $after;
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final protected function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function getBeforeID() {
return $this->beforeID;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table->getTableName(),
(string)$this->getPrimaryTableAlias(),
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $rows;
}
/**
* Get the viewer for making cursor paging queries.
*
* NOTE: You should ONLY use this viewer to load cursor objects while
* building paging queries.
*
* Cursor paging can happen in two ways. First, the user can request a page
* like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
* can fall back to implicit paging if we filter some results out of a
* result list because the user can't see them and need to go fetch some more
* results to generate a large enough result list.
*
* In the first case, want to use the viewer's policies to load the object.
* This prevents an attacker from figuring out information about an object
* they can't see by executing queries like `/stuff/?after=33&order=name`,
* which would otherwise give them a hint about the name of the object.
* Generally, if a user can't see an object, they can't use it to page.
*
* In the second case, we need to load the object whether the user can see
* it or not, because we need to examine new results. For example, if a user
* loads `/stuff/` and we run a query for the first 100 items that they can
* see, but the first 100 rows in the database aren't visible, we need to
* be able to issue a query for the next 100 results. If we can't load the
* cursor object, we'll fail or issue the same query over and over again.
* So, generally, internal paging must bypass policy controls.
*
* This method returns the appropriate viewer, based on the context in which
* the paging is occuring.
*
* @return PhabricatorUser Viewer for executing paging queries.
*/
final protected function getPagingViewer() {
if ($this->internalPaging) {
return PhabricatorUser::getOmnipotentUser();
} else {
return $this->getViewer();
}
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
- if ($this->getRawResultLimit()) {
- return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
- } else {
- return '';
+ if ($this->shouldLimitResults()) {
+ $limit = $this->getRawResultLimit();
+ if ($limit) {
+ return qsprintf($conn_r, 'LIMIT %d', $limit);
+ }
}
+
+ return '';
+ }
+
+ protected function shouldLimitResults() {
+ return true;
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
list($before, $after) = $this->getPageCursors($sliced_results);
if ($pager->getBeforeID() || ($count > $limit)) {
$pager->setNextPageID($after);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$pager->setPrevPageID($before);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = '*';
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
$joins[] = $this->buildNgramsJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
$where[] = $this->buildNgramsWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
return $this->formatHavingClause($having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return '';
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
if ($this->shouldGroupNgramResultRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
if ($this->beforeID !== null) {
$cursor = $this->beforeID;
$reversed = true;
} else if ($this->afterID !== null) {
$cursor = $this->afterID;
$reversed = false;
} else {
// No paging is being applied to this query so we do not need to
// construct a paging clause.
return '';
}
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
$value_map = $this->getPagingValueMap($cursor, $keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (!array_key_exists($key, $value_map)) {
throw new Exception(
pht(
'Query "%s" failed to return a value from getPagingValueMap() '.
'for column "%s".',
get_class($this),
$key));
}
$column = $orderable[$key];
$column['value'] = $value_map[$key];
// If the vector component is reversed, we need to reverse whatever the
// order of the column is.
if ($order->getIsReversed()) {
$column['reverse'] = !idx($column, 'reverse', false);
}
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
/**
* @task paging
*/
protected function getPagingValueMap($cursor, array $keys) {
return array(
'id' => $cursor,
);
}
/**
* @task paging
*/
protected function loadCursorObject($cursor) {
$query = newv(get_class($this), array())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$cursor));
$this->willExecuteCursorQuery($query);
$object = $query->executeOne();
if (!$object) {
throw new Exception(
pht(
'Cursor "%s" does not identify a valid object in query "%s".',
$cursor,
get_class($this)));
}
return $object;
}
/**
* @task paging
*/
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return;
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional constuction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? '>' : '<',
$value);
}
if ($parts) {
if (count($parts) > 1) {
$clause[] = '('.implode(') OR (', $parts).')';
} else {
$clause[] = head($parts);
}
}
if ($clause) {
if (count($clause) > 1) {
$clauses[] = '('.implode(') AND (', $clause).')';
} else {
$clauses[] = head($clause);
}
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
return '('.implode(') OR (', $clauses).')';
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
$aliases = $this->getBuiltinOrderAliasMap();
if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
implode(', ', array_keys($aliases))));
}
$this->builtinOrder = $aliases[$order];
$this->orderVector = null;
return $this;
}
/**
* Set a grouping order to apply before primary result ordering.
*
* This allows you to preface the query order vector with additional orders,
* so you can effect "group by" queries while still respecting "order by".
*
* This is a high-level method which works alongside @{method:setOrder}. For
* lower-level control over order vectors, use @{method:setOrderVector}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setGroupVector($vector) {
$this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$legacy_key = 'custom:'.$field->getFieldKey();
$modern_key = $field->getModernFieldKey();
$orders[$modern_key] = array(
'vector' => array($modern_key, 'id'),
'name' => $field->getFieldName(),
'aliases' => array($legacy_key),
);
$orders['-'.$modern_key] = array(
'vector' => array('-'.$modern_key, '-id'),
'name' => pht('%s (Reversed)', $field->getFieldName()),
);
}
}
return $orders;
}
public function getBuiltinOrderAliasMap() {
$orders = $this->getBuiltinOrders();
$map = array();
foreach ($orders as $key => $order) {
$keys = array();
$keys[] = $key;
foreach (idx($order, 'aliases', array()) as $alias) {
$keys[] = $alias;
}
foreach ($keys as $alias) {
if (isset($map[$alias])) {
throw new Exception(
pht(
'Two builtin orders ("%s" and "%s") define the same key or '.
'alias ("%s"). Each order alias and key must be unique and '.
'identify a single order.',
$key,
$map[$alias],
$alias));
}
$map[$alias] = $key;
}
}
return $map;
}
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
if ($this->groupVector) {
$group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
$group->appendVector($vector);
$vector = $group;
}
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$digest = $field->getFieldIndex();
$key = $field->getModernFieldKey();
$columns[$key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
$cache->setKey($cache_key, $columns);
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts);
}
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts) {
$is_query_reversed = false;
if ($this->getBeforeID()) {
$is_query_reversed = !$is_query_reversed;
}
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql));
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'value' => $value,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'value' => array($min, $max),
);
return $this;
}
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @return string Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn() {
if ($this->getPrimaryTableAlias()) {
$prefix = $this->getPrimaryTableAlias().'.';
} else {
$prefix = '';
}
return $prefix.'phid';
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count((array)$value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn_r) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn_r,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
} else {
return '';
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn_r) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = 'appsearch_'.$key;
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
switch ($cond) {
case '=':
$type = $constraint['type'];
switch ($type) {
case 'string':
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue IN (%Ls)',
$alias,
(array)$constraint['value']);
break;
case 'int':
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue IN (%Ld)',
$alias,
(array)$constraint['value']);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
$joins[] = qsprintf(
$conn_r,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn_r,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
return implode(' ', $joins);
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Ngrams )------------------------------------------------------------- */
protected function withNgramsConstraint(
PhabricatorSearchNgrams $index,
$value) {
if (strlen($value)) {
$this->ngrams[] = array(
'index' => $index,
'value' => $value,
'length' => count(phutil_utf8v($value)),
);
}
return $this;
}
protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) {
$flat = array();
foreach ($this->ngrams as $spec) {
$index = $spec['index'];
$value = $spec['value'];
$length = $spec['length'];
if ($length >= 3) {
$ngrams = $index->getNgramsFromString($value, 'query');
$prefix = false;
} else if ($length == 2) {
$ngrams = $index->getNgramsFromString($value, 'prefix');
$prefix = false;
} else {
$ngrams = array(' '.$value);
$prefix = true;
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $index->getTableName(),
'ngram' => $ngram,
'prefix' => $prefix,
);
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$id_column = qsprintf($conn, '%T.%T', $alias, 'id');
} else {
$id_column = qsprintf($conn, '%T', 'id');
}
$idx = 1;
$joins = array();
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$prefix = $spec['prefix'];
$alias = 'ngm'.$idx++;
if ($prefix) {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
} else {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
}
}
return $joins;
}
protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->ngrams as $ngram) {
$index = $ngram['index'];
$value = $ngram['value'];
$column = $index->getColumnName();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$column = qsprintf($conn, '%T.%T', $alias, $column);
} else {
$column = qsprintf($conn, '%T', $column);
}
$tokens = $index->tokenizeString($value);
foreach ($tokens as $token) {
$where[] = qsprintf(
$conn,
'%Q LIKE %~',
$column,
$token);
}
}
return $where;
}
protected function shouldGroupNgramResultRows() {
return (bool)$this->ngrams;
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$this->edgeLogicConstraints[$edge_type][$operator][] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// This is tricky. We have a query which specifies multiple
// projects, each of which may have an arbitrarily large number
// of descendants.
// Suppose the projects are "Engineering" and "Operations", and
// "Engineering" has subprojects X, Y and Z.
// We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row
// is not part of Engineering at all, or some number other than
// 0 if it is.
// Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and
// any other value to an index (say, 1) for the ancestor.
// We build these up for every ancestor, then use `COALESCE(...)`
// to select the non-null one, giving us an ancestor which this
// row is a member of.
// From there, we use `COUNT(DISTINCT(...))` to make sure that
// each result row is a member of all ancestors.
if (count($list) > 1) {
$idx = 1;
$parts = array();
foreach ($list as $constraint) {
$parts[] = qsprintf(
$conn,
'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)',
$alias,
(array)$constraint->getValue(),
$idx++);
}
$parts = implode(', ', $parts);
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(COALESCE(%Q))) %T',
$parts,
$this->buildEdgeLogicTableAliasAncestor($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
$phids = array();
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$phids[$v] = $v;
}
}
$phids = array_keys($phids);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = 'LEFT';
} else {
$join_type = '';
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$full = $this->formatWhereSubclause($full);
$null = $this->formatWhereSubclause($null);
$where[] = qsprintf($conn, '(%Q OR %Q)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasAncestor($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasAncestor($alias) {
return $alias.'_ancestor';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$values[] = $v;
}
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_EMPTY:
throw new PhabricatorEmptyQueryException(
pht('This query specifies an empty constraint.'));
}
}
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
public function withSpaceIsArchived($archived) {
$this->spaceIsArchived = $archived;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
// If we have an omnipotent viewer and no formal space constraints, don't
// emit a clause. This primarily enables older migrations to run cleanly,
// without fataling because they try to match a `spacePHID` column which
// does not exist yet. See T8743, T8746.
if ($viewer->isOmnipotent()) {
if ($this->spaceIsArchived === null && $this->spacePHIDs === null) {
return null;
}
}
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
if ($this->spaceIsArchived !== null) {
if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
continue;
}
}
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = 'spacePHID';
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, May 12, 6:46 PM (13 h, 21 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
64/73/aa42cc406f220ae493977c6bc27f
Attached To
rPHAB Phabricator
Event Timeline
Log In to Comment