Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / calendar / query / PhabricatorCalendarEventSearchEngine.php
blob79d43215457612b2e52dc06b706dc11a6b1066bd
1 <?php
3 final class PhabricatorCalendarEventSearchEngine
4 extends PhabricatorApplicationSearchEngine {
6 private $calendarYear;
7 private $calendarMonth;
8 private $calendarDay;
10 public function getResultTypeDescription() {
11 return pht('Calendar Events');
14 public function getApplicationClassName() {
15 return 'PhabricatorCalendarApplication';
18 public function newQuery() {
19 $viewer = $this->requireViewer();
21 return id(new PhabricatorCalendarEventQuery())
22 ->needRSVPs(array($viewer->getPHID()));
25 protected function shouldShowOrderField() {
26 return false;
29 protected function buildCustomSearchFields() {
30 return array(
31 id(new PhabricatorSearchDatasourceField())
32 ->setLabel(pht('Hosts'))
33 ->setKey('hostPHIDs')
34 ->setAliases(array('host', 'hostPHID', 'hosts'))
35 ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
36 id(new PhabricatorSearchDatasourceField())
37 ->setLabel(pht('Invited'))
38 ->setKey('invitedPHIDs')
39 ->setDatasource(new PhabricatorCalendarInviteeDatasource()),
40 id(new PhabricatorSearchDateControlField())
41 ->setLabel(pht('Occurs After'))
42 ->setKey('rangeStart'),
43 id(new PhabricatorSearchDateControlField())
44 ->setLabel(pht('Occurs Before'))
45 ->setKey('rangeEnd')
46 ->setAliases(array('rangeEnd')),
47 id(new PhabricatorSearchCheckboxesField())
48 ->setKey('upcoming')
49 ->setOptions(array(
50 'upcoming' => pht('Show only upcoming events.'),
51 )),
52 id(new PhabricatorSearchSelectField())
53 ->setLabel(pht('Cancelled Events'))
54 ->setKey('isCancelled')
55 ->setOptions($this->getCancelledOptions())
56 ->setDefault('active'),
57 id(new PhabricatorPHIDsSearchField())
58 ->setLabel(pht('Import Sources'))
59 ->setKey('importSourcePHIDs')
60 ->setAliases(array('importSourcePHID')),
61 id(new PhabricatorSearchSelectField())
62 ->setLabel(pht('Display Options'))
63 ->setKey('display')
64 ->setOptions($this->getViewOptions())
65 ->setDefault('month'),
69 private function getCancelledOptions() {
70 return array(
71 'active' => pht('Active Events Only'),
72 'cancelled' => pht('Cancelled Events Only'),
73 'both' => pht('Both Cancelled and Active Events'),
77 private function getViewOptions() {
78 return array(
79 'month' => pht('Month View'),
80 'day' => pht('Day View'),
81 'list' => pht('List View'),
85 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
86 $query = parent::buildQueryFromSavedQuery($saved);
88 // If this is an export query for generating an ".ics" file, don't
89 // build ghost events.
90 if ($saved->getParameter('export')) {
91 $query->setGenerateGhosts(false);
94 return $query;
97 protected function buildQueryFromParameters(array $map) {
98 $query = $this->newQuery();
99 $viewer = $this->requireViewer();
101 if ($map['hostPHIDs']) {
102 $query->withHostPHIDs($map['hostPHIDs']);
105 if ($map['invitedPHIDs']) {
106 $query->withInvitedPHIDs($map['invitedPHIDs']);
109 $range_start = $map['rangeStart'];
110 $range_end = $map['rangeEnd'];
111 $display = $map['display'];
113 if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') {
114 $upcoming = true;
115 } else {
116 $upcoming = false;
119 list($range_start, $range_end) = $this->getQueryDateRange(
120 $range_start,
121 $range_end,
122 $display,
123 $upcoming);
125 $query->withDateRange($range_start, $range_end);
127 switch ($map['isCancelled']) {
128 case 'active':
129 $query->withIsCancelled(false);
130 break;
131 case 'cancelled':
132 $query->withIsCancelled(true);
133 break;
136 if ($map['importSourcePHIDs']) {
137 $query->withImportSourcePHIDs($map['importSourcePHIDs']);
140 if (!$map['ids'] && !$map['phids']) {
141 $query
142 ->withIsStub(false)
143 ->setGenerateGhosts(true);
146 return $query;
149 private function getQueryDateRange(
150 $start_date_wild,
151 $end_date_wild,
152 $display,
153 $upcoming) {
155 $start_date_value = $this->getSafeDate($start_date_wild);
156 $end_date_value = $this->getSafeDate($end_date_wild);
158 $viewer = $this->requireViewer();
159 $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
160 $min_range = null;
161 $max_range = null;
163 $min_range = $start_date_value->getEpoch();
164 $max_range = $end_date_value->getEpoch();
166 if ($display == 'month' || $display == 'day') {
167 list($start_year, $start_month, $start_day) =
168 $this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display);
170 $start_day = new DateTime(
171 "{$start_year}-{$start_month}-{$start_day}",
172 $timezone);
173 $next = clone $start_day;
175 if ($display == 'month') {
176 $next->modify('+1 month');
177 } else if ($display == 'day') {
178 $next->modify('+7 day');
181 $display_start = $start_day->format('U');
182 $display_end = $next->format('U');
184 $start_of_week = $viewer->getUserSetting(
185 PhabricatorWeekStartDaySetting::SETTINGKEY);
187 $end_of_week = ($start_of_week + 6) % 7;
189 $first_of_month = $start_day->format('w');
190 $last_of_month = id(clone $next)->modify('-1 day')->format('w');
192 if (!$min_range || ($min_range < $display_start)) {
193 $min_range = $display_start;
195 if ($display == 'month' &&
196 $first_of_month !== $start_of_week) {
197 $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7;
198 $min_range = id(clone $start_day)
199 ->modify('-'.$interim_day_num.' days')
200 ->format('U');
203 if (!$max_range || ($max_range > $display_end)) {
204 $max_range = $display_end;
206 if ($display == 'month' &&
207 $last_of_month !== $end_of_week) {
208 $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7;
209 $max_range = id(clone $next)
210 ->modify('+'.$interim_day_num.' days')
211 ->format('U');
216 if ($upcoming) {
217 $now = PhabricatorTime::getNow();
218 if ($min_range) {
219 $min_range = max($now, $min_range);
220 } else {
221 $min_range = $now;
225 return array($min_range, $max_range);
228 protected function getURI($path) {
229 return '/calendar/'.$path;
232 protected function getBuiltinQueryNames() {
233 $names = array(
234 'month' => pht('Month View'),
235 'day' => pht('Day View'),
236 'upcoming' => pht('Upcoming Events'),
237 'all' => pht('All Events'),
240 return $names;
243 public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
244 $this->calendarYear = $year;
245 $this->calendarMonth = $month;
246 $this->calendarDay = $day;
248 return $this;
251 public function buildSavedQueryFromBuiltin($query_key) {
252 $query = $this->newSavedQuery();
253 $query->setQueryKey($query_key);
255 switch ($query_key) {
256 case 'month':
257 return $query->setParameter('display', 'month');
258 case 'day':
259 return $query->setParameter('display', 'day');
260 case 'upcoming':
261 return $query
262 ->setParameter('display', 'list')
263 ->setParameter('upcoming', array(
264 0 => 'upcoming',
266 case 'all':
267 return $query;
270 return parent::buildSavedQueryFromBuiltin($query_key);
273 protected function renderResultList(
274 array $events,
275 PhabricatorSavedQuery $query,
276 array $handles) {
278 if ($this->isMonthView($query)) {
279 $result = $this->buildCalendarMonthView($events, $query);
280 } else if ($this->isDayView($query)) {
281 $result = $this->buildCalendarDayView($events, $query);
282 } else {
283 $result = $this->buildCalendarListView($events, $query);
286 return $result;
289 private function buildCalendarListView(
290 array $events,
291 PhabricatorSavedQuery $query) {
293 assert_instances_of($events, 'PhabricatorCalendarEvent');
294 $viewer = $this->requireViewer();
295 $list = new PHUIObjectItemListView();
297 foreach ($events as $event) {
298 if ($event->getIsGhostEvent()) {
299 $monogram = $event->getParentEvent()->getMonogram();
300 $index = $event->getSequenceIndex();
301 $monogram = "{$monogram}/{$index}";
302 } else {
303 $monogram = $event->getMonogram();
306 $item = id(new PHUIObjectItemView())
307 ->setUser($viewer)
308 ->setObject($event)
309 ->setObjectName($monogram)
310 ->setHeader($event->getName())
311 ->setHref($event->getURI());
313 $item->addAttribute($event->renderEventDate($viewer, false));
315 if ($event->getIsCancelled()) {
316 $item->setDisabled(true);
319 $status_icon = $event->getDisplayIcon($viewer);
320 $status_color = $event->getDisplayIconColor($viewer);
321 $status_label = $event->getDisplayIconLabel($viewer);
323 $item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
325 $host = pht(
326 'Hosted by %s',
327 $viewer->renderHandle($event->getHostPHID()));
328 $item->addByline($host);
330 $list->addItem($item);
333 return $this->newResultView()
334 ->setObjectList($list)
335 ->setNoDataString(pht('No events found.'));
338 private function buildCalendarMonthView(
339 array $events,
340 PhabricatorSavedQuery $query) {
341 assert_instances_of($events, 'PhabricatorCalendarEvent');
343 $viewer = $this->requireViewer();
344 $now = PhabricatorTime::getNow();
346 list($start_year, $start_month) =
347 $this->getDisplayYearAndMonthAndDay(
348 $this->getQueryDateFrom($query)->getEpoch(),
349 $this->getQueryDateTo($query)->getEpoch(),
350 $query->getParameter('display'));
352 $now_year = phabricator_format_local_time($now, $viewer, 'Y');
353 $now_month = phabricator_format_local_time($now, $viewer, 'm');
354 $now_day = phabricator_format_local_time($now, $viewer, 'j');
356 if ($start_month == $now_month && $start_year == $now_year) {
357 $month_view = new PHUICalendarMonthView(
358 $this->getQueryDateFrom($query),
359 $this->getQueryDateTo($query),
360 $start_month,
361 $start_year,
362 $now_day);
363 } else {
364 $month_view = new PHUICalendarMonthView(
365 $this->getQueryDateFrom($query),
366 $this->getQueryDateTo($query),
367 $start_month,
368 $start_year);
371 $month_view->setUser($viewer);
373 $viewer_phid = $viewer->getPHID();
374 foreach ($events as $event) {
375 $epoch_min = $event->getStartDateTimeEpoch();
376 $epoch_max = $event->getEndDateTimeEpoch();
378 $is_invited = $event->isRSVPInvited($viewer_phid);
379 $is_attending = $event->getIsUserAttending($viewer_phid);
381 $event_view = id(new AphrontCalendarEventView())
382 ->setHostPHID($event->getHostPHID())
383 ->setEpochRange($epoch_min, $epoch_max)
384 ->setIsCancelled($event->getIsCancelled())
385 ->setName($event->getName())
386 ->setURI($event->getURI())
387 ->setIsAllDay($event->getIsAllDay())
388 ->setIcon($event->getDisplayIcon($viewer))
389 ->setViewerIsInvited($is_invited || $is_attending)
390 ->setDatetimeSummary($event->renderEventDate($viewer, true))
391 ->setIconColor($event->getDisplayIconColor($viewer));
393 $month_view->addEvent($event_view);
396 $month_view->setBrowseURI(
397 $this->getURI('query/'.$query->getQueryKey().'/'));
399 $from = $this->getQueryDateFrom($query)->getDateTime();
401 $crumbs = array();
402 $crumbs[] = id(new PHUICrumbView())
403 ->setName($from->format('F Y'));
405 $header = id(new PHUIHeaderView())
406 ->setProfileHeader(true)
407 ->setHeader($from->format('F Y'));
409 return $this->newResultView($month_view)
410 ->setCrumbs($crumbs)
411 ->setHeader($header);
414 private function buildCalendarDayView(
415 array $events,
416 PhabricatorSavedQuery $query) {
418 $viewer = $this->requireViewer();
420 list($start_year, $start_month, $start_day) =
421 $this->getDisplayYearAndMonthAndDay(
422 $this->getQueryDateFrom($query)->getEpoch(),
423 $this->getQueryDateTo($query)->getEpoch(),
424 $query->getParameter('display'));
426 $day_view = id(new PHUICalendarDayView(
427 $this->getQueryDateFrom($query),
428 $this->getQueryDateTo($query),
429 $start_year,
430 $start_month,
431 $start_day))
432 ->setQuery($query->getQueryKey());
434 $day_view->setUser($viewer);
436 $phids = mpull($events, 'getHostPHID');
438 foreach ($events as $event) {
439 $can_edit = PhabricatorPolicyFilter::hasCapability(
440 $viewer,
441 $event,
442 PhabricatorPolicyCapability::CAN_EDIT);
444 $epoch_min = $event->getStartDateTimeEpoch();
445 $epoch_max = $event->getEndDateTimeEpoch();
447 $status_icon = $event->getDisplayIcon($viewer);
448 $status_color = $event->getDisplayIconColor($viewer);
450 $event_view = id(new AphrontCalendarEventView())
451 ->setCanEdit($can_edit)
452 ->setEventID($event->getID())
453 ->setEpochRange($epoch_min, $epoch_max)
454 ->setIsAllDay($event->getIsAllDay())
455 ->setIcon($status_icon)
456 ->setIconColor($status_color)
457 ->setName($event->getName())
458 ->setURI($event->getURI())
459 ->setDatetimeSummary($event->renderEventDate($viewer, true))
460 ->setIsCancelled($event->getIsCancelled());
462 $day_view->addEvent($event_view);
465 $browse_uri = $this->getURI('query/'.$query->getQueryKey().'/');
466 $day_view->setBrowseURI($browse_uri);
468 $from = $this->getQueryDateFrom($query)->getDateTime();
469 $month_uri = $browse_uri.$from->format('Y/m/');
471 $crumbs = array(
472 id(new PHUICrumbView())
473 ->setName($from->format('F Y'))
474 ->setHref($month_uri),
475 id(new PHUICrumbView())
476 ->setName($from->format('D jS')),
479 $header = id(new PHUIHeaderView())
480 ->setProfileHeader(true)
481 ->setHeader($from->format('D, F jS'));
483 return $this->newResultView($day_view)
484 ->setCrumbs($crumbs)
485 ->setHeader($header);
488 private function getDisplayYearAndMonthAndDay(
489 $range_start,
490 $range_end,
491 $display) {
493 $viewer = $this->requireViewer();
494 $epoch = null;
496 if ($this->calendarYear && $this->calendarMonth) {
497 $start_year = $this->calendarYear;
498 $start_month = $this->calendarMonth;
499 $start_day = $this->calendarDay ? $this->calendarDay : 1;
500 } else {
501 if ($range_start) {
502 $epoch = $range_start;
503 } else if ($range_end) {
504 $epoch = $range_end;
505 } else {
506 $epoch = time();
508 if ($display == 'month') {
509 $day = 1;
510 } else {
511 $day = phabricator_format_local_time($epoch, $viewer, 'd');
513 $start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
514 $start_month = phabricator_format_local_time($epoch, $viewer, 'm');
515 $start_day = $day;
517 return array($start_year, $start_month, $start_day);
520 public function getPageSize(PhabricatorSavedQuery $saved) {
521 if ($this->isMonthView($saved) || $this->isDayView($saved)) {
522 return $saved->getParameter('limit', 1000);
523 } else {
524 return $saved->getParameter('limit', 100);
528 private function getQueryDateFrom(PhabricatorSavedQuery $saved) {
529 if ($this->calendarYear && $this->calendarMonth) {
530 $viewer = $this->requireViewer();
532 $start_year = $this->calendarYear;
533 $start_month = $this->calendarMonth;
534 $start_day = $this->calendarDay ? $this->calendarDay : 1;
536 return AphrontFormDateControlValue::newFromDictionary(
537 $viewer,
538 array(
539 'd' => "{$start_year}-{$start_month}-{$start_day}",
543 return $this->getQueryDate($saved, 'rangeStart');
546 private function getQueryDateTo(PhabricatorSavedQuery $saved) {
547 return $this->getQueryDate($saved, 'rangeEnd');
550 private function getQueryDate(PhabricatorSavedQuery $saved, $key) {
551 $viewer = $this->requireViewer();
553 $wild = $saved->getParameter($key);
554 return $this->getSafeDate($wild);
557 private function getSafeDate($value) {
558 $viewer = $this->requireViewer();
559 if ($value) {
560 // ideally this would be consistent and always pass in the same type
561 if ($value instanceof AphrontFormDateControlValue) {
562 return $value;
563 } else {
564 $value = AphrontFormDateControlValue::newFromWild($viewer, $value);
566 } else {
567 $value = AphrontFormDateControlValue::newFromEpoch(
568 $viewer,
569 PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U'));
570 $value->setEnabled(false);
573 $value->setOptional(true);
575 return $value;
578 private function isMonthView(PhabricatorSavedQuery $query) {
579 if ($this->isDayView($query)) {
580 return false;
582 if ($query->getParameter('display') == 'month') {
583 return true;
587 private function isDayView(PhabricatorSavedQuery $query) {
588 if ($query->getParameter('display') == 'day') {
589 return true;
591 if ($this->calendarDay) {
592 return true;
595 return false;
598 public function newUseResultsActions(PhabricatorSavedQuery $saved) {
599 $viewer = $this->requireViewer();
600 $can_export = $viewer->isLoggedIn();
602 return array(
603 id(new PhabricatorActionView())
604 ->setIcon('fa-download')
605 ->setName(pht('Export Query as .ics'))
606 ->setDisabled(!$can_export)
607 ->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()),
612 private function newResultView($content = null) {
613 // If we aren't rendering a dashboard panel, activate global drag-and-drop
614 // so you can import ".ics" files by dropping them directly onto the
615 // calendar.
616 if (!$this->isPanelContext()) {
617 $drop_upload = id(new PhabricatorGlobalUploadTargetView())
618 ->setViewer($this->requireViewer())
619 ->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import'))
620 ->setSubmitURI('/calendar/import/drop/')
621 ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
623 $content = array(
624 $drop_upload,
625 $content,
629 return id(new PhabricatorApplicationSearchResultView())
630 ->setContent($content);