@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 754 lines 20 kB view raw
1<?php 2 3/** 4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorCalendarEvent> 5 */ 6final class PhabricatorCalendarEventQuery 7 extends PhabricatorCursorPagedPolicyAwareQuery { 8 9 private $ids; 10 private $phids; 11 private $rangeBegin; 12 private $rangeEnd; 13 private $inviteePHIDs; 14 private $hostPHIDs; 15 private $isCancelled; 16 private $eventsWithNoParent; 17 private $instanceSequencePairs; 18 private $isStub; 19 private $parentEventPHIDs; 20 private $importSourcePHIDs; 21 private $importAuthorPHIDs; 22 private $importUIDs; 23 private $utcInitialEpochMin; 24 private $utcInitialEpochMax; 25 private $isImported; 26 private $needRSVPs; 27 28 private $generateGhosts = false; 29 30 public function newResultObject() { 31 return new PhabricatorCalendarEvent(); 32 } 33 34 public function setGenerateGhosts($generate_ghosts) { 35 $this->generateGhosts = $generate_ghosts; 36 return $this; 37 } 38 39 public function withIDs(array $ids) { 40 $this->ids = $ids; 41 return $this; 42 } 43 44 public function withPHIDs(array $phids) { 45 $this->phids = $phids; 46 return $this; 47 } 48 49 public function withDateRange($begin, $end) { 50 $this->rangeBegin = $begin; 51 $this->rangeEnd = $end; 52 return $this; 53 } 54 55 public function withUTCInitialEpochBetween($min, $max) { 56 $this->utcInitialEpochMin = $min; 57 $this->utcInitialEpochMax = $max; 58 return $this; 59 } 60 61 public function withInvitedPHIDs(array $phids) { 62 $this->inviteePHIDs = $phids; 63 return $this; 64 } 65 66 public function withHostPHIDs(array $phids) { 67 $this->hostPHIDs = $phids; 68 return $this; 69 } 70 71 public function withIsCancelled($is_cancelled) { 72 $this->isCancelled = $is_cancelled; 73 return $this; 74 } 75 76 public function withIsStub($is_stub) { 77 $this->isStub = $is_stub; 78 return $this; 79 } 80 81 public function withEventsWithNoParent($events_with_no_parent) { 82 $this->eventsWithNoParent = $events_with_no_parent; 83 return $this; 84 } 85 86 public function withInstanceSequencePairs(array $pairs) { 87 $this->instanceSequencePairs = $pairs; 88 return $this; 89 } 90 91 public function withParentEventPHIDs(array $parent_phids) { 92 $this->parentEventPHIDs = $parent_phids; 93 return $this; 94 } 95 96 public function withImportSourcePHIDs(array $import_phids) { 97 $this->importSourcePHIDs = $import_phids; 98 return $this; 99 } 100 101 public function withImportAuthorPHIDs(array $author_phids) { 102 $this->importAuthorPHIDs = $author_phids; 103 return $this; 104 } 105 106 public function withImportUIDs(array $uids) { 107 $this->importUIDs = $uids; 108 return $this; 109 } 110 111 public function withIsImported($is_imported) { 112 $this->isImported = $is_imported; 113 return $this; 114 } 115 116 public function needRSVPs(array $phids) { 117 $this->needRSVPs = $phids; 118 return $this; 119 } 120 121 protected function getDefaultOrderVector() { 122 return array('start', 'id'); 123 } 124 125 public function getBuiltinOrders() { 126 return array( 127 'start' => array( 128 'vector' => array('start', 'id'), 129 'name' => pht('Event Start'), 130 ), 131 ) + parent::getBuiltinOrders(); 132 } 133 134 public function getOrderableColumns() { 135 return array( 136 'start' => array( 137 'table' => $this->getPrimaryTableAlias(), 138 'column' => 'utcInitialEpoch', 139 'reverse' => true, 140 'type' => 'int', 141 'unique' => false, 142 ), 143 ) + parent::getOrderableColumns(); 144 } 145 146 protected function newPagingMapFromPartialObject($object) { 147 return array( 148 'id' => (int)$object->getID(), 149 'start' => (int)$object->getStartDateTimeEpoch(), 150 ); 151 } 152 153 protected function shouldLimitResults() { 154 // When generating ghosts, we can't rely on database ordering because 155 // MySQL can't predict the ghost start times. We'll just load all matching 156 // events, then generate results from there. 157 if ($this->generateGhosts) { 158 return false; 159 } 160 161 return true; 162 } 163 164 protected function loadPage() { 165 $events = $this->loadStandardPage($this->newResultObject()); 166 167 $viewer = $this->getViewer(); 168 foreach ($events as $event) { 169 $event->applyViewerTimezone($viewer); 170 } 171 172 if (!$this->generateGhosts) { 173 return $events; 174 } 175 176 $raw_limit = $this->getRawResultLimit(); 177 if (!$raw_limit && !$this->rangeEnd) { 178 throw new Exception( 179 pht( 180 'Event queries which generate ghost events must include either a '. 181 'result limit or an end date, because they may otherwise generate '. 182 'an infinite number of results. This query has neither.')); 183 } 184 185 foreach ($events as $key => $event) { 186 $sequence_start = 0; 187 $sequence_end = null; 188 $end = null; 189 190 $instance_of = $event->getInstanceOfEventPHID(); 191 192 if ($instance_of == null && $this->isCancelled !== null) { 193 if ($event->getIsCancelled() != $this->isCancelled) { 194 unset($events[$key]); 195 continue; 196 } 197 } 198 } 199 200 // Pull out all of the parents first. We may discard them as we begin 201 // generating ghost events, but we still want to process all of them. 202 $parents = array(); 203 foreach ($events as $key => $event) { 204 if ($event->isParentEvent()) { 205 $parents[$key] = $event; 206 } 207 } 208 209 // Now that we've picked out all the parent events, we can immediately 210 // discard anything outside of the time window. 211 $events = $this->getEventsInRange($events); 212 213 $generate_from = $this->rangeBegin; 214 $generate_until = $this->rangeEnd; 215 foreach ($parents as $key => $event) { 216 $duration = $event->getDuration(); 217 218 $start_date = $this->getRecurrenceWindowStart( 219 $event, 220 $generate_from - $duration); 221 222 $end_date = $this->getRecurrenceWindowEnd( 223 $event, 224 $generate_until); 225 226 $limit = $this->getRecurrenceLimit($event, $raw_limit); 227 228 // note that this can be NULL for some imported events 229 $set = $event->newRecurrenceSet(); 230 231 $recurrences = array(); 232 if ($set) { 233 $recurrences = $set->getEventsBetween( 234 $start_date, 235 $end_date, 236 $limit + 1); 237 } 238 239 // We're generating events from the beginning and then filtering them 240 // here (instead of only generating events starting at the start date) 241 // because we need to know the proper sequence indexes to generate ghost 242 // events. This may change after RDATE support. 243 if ($start_date) { 244 $start_epoch = $start_date->getEpoch(); 245 } else { 246 $start_epoch = null; 247 } 248 249 foreach ($recurrences as $sequence_index => $sequence_datetime) { 250 if (!$sequence_index) { 251 // This is the parent event, which we already have. 252 continue; 253 } 254 255 if ($start_epoch) { 256 if ($sequence_datetime->getEpoch() < $start_epoch) { 257 continue; 258 } 259 } 260 261 $events[] = $event->newGhost( 262 $viewer, 263 $sequence_index, 264 $sequence_datetime); 265 } 266 267 // NOTE: We're slicing results every time because this makes it cheaper 268 // to generate future ghosts. If we already have 100 events that occur 269 // before July 1, we know we never need to generate ghosts after that 270 // because they couldn't possibly ever appear in the result set. 271 272 if ($raw_limit) { 273 if (count($events) > $raw_limit) { 274 $events = msort($events, 'getStartDateTimeEpoch'); 275 $events = array_slice($events, 0, $raw_limit, true); 276 $generate_until = last($events)->getEndDateTimeEpoch(); 277 } 278 } 279 } 280 281 // Now that we're done generating ghost events, we're going to remove any 282 // ghosts that we have concrete events for (or which we can load the 283 // concrete events for). These concrete events are generated when users 284 // edit a ghost, and replace the ghost events. 285 286 // First, generate a map of all concrete <parentPHID, sequence> events we 287 // already loaded. We don't need to load these again. 288 $have_pairs = array(); 289 foreach ($events as $event) { 290 if ($event->getIsGhostEvent()) { 291 continue; 292 } 293 294 $parent_phid = $event->getInstanceOfEventPHID(); 295 $sequence = $event->getSequenceIndex(); 296 297 $have_pairs[$parent_phid][$sequence] = true; 298 } 299 300 // Now, generate a map of all <parentPHID, sequence> events we generated 301 // ghosts for. We need to try to load these if we don't already have them. 302 $map = array(); 303 $parent_pairs = array(); 304 foreach ($events as $key => $event) { 305 if (!$event->getIsGhostEvent()) { 306 continue; 307 } 308 309 $parent_phid = $event->getInstanceOfEventPHID(); 310 $sequence = $event->getSequenceIndex(); 311 312 // We already loaded the concrete version of this event, so we can just 313 // throw out the ghost and move on. 314 if (isset($have_pairs[$parent_phid][$sequence])) { 315 unset($events[$key]); 316 continue; 317 } 318 319 // We didn't load the concrete version of this event, so we need to 320 // try to load it if it exists. 321 $parent_pairs[] = array($parent_phid, $sequence); 322 $map[$parent_phid][$sequence] = $key; 323 } 324 325 if ($parent_pairs) { 326 $instances = id(new self()) 327 ->setViewer($viewer) 328 ->setParentQuery($this) 329 ->withInstanceSequencePairs($parent_pairs) 330 ->execute(); 331 332 foreach ($instances as $instance) { 333 $parent_phid = $instance->getInstanceOfEventPHID(); 334 $sequence = $instance->getSequenceIndex(); 335 336 $indexes = idx($map, $parent_phid); 337 $key = idx($indexes, $sequence); 338 339 // Replace the ghost with the corresponding concrete event. 340 $events[$key] = $instance; 341 } 342 } 343 344 $events = msort($events, 'getStartDateTimeEpoch'); 345 346 return $events; 347 } 348 349 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { 350 $parts = parent::buildJoinClauseParts($conn_r); 351 352 if ($this->inviteePHIDs !== null) { 353 $parts[] = qsprintf( 354 $conn_r, 355 'JOIN %T invitee ON invitee.eventPHID = event.phid 356 AND invitee.status != %s', 357 id(new PhabricatorCalendarEventInvitee())->getTableName(), 358 PhabricatorCalendarEventInvitee::STATUS_UNINVITED); 359 } 360 361 return $parts; 362 } 363 364 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 365 $where = parent::buildWhereClauseParts($conn); 366 367 if ($this->ids !== null) { 368 $where[] = qsprintf( 369 $conn, 370 'event.id IN (%Ld)', 371 $this->ids); 372 } 373 374 if ($this->phids !== null) { 375 $where[] = qsprintf( 376 $conn, 377 'event.phid IN (%Ls)', 378 $this->phids); 379 } 380 381 // NOTE: The date ranges we query for are larger than the requested ranges 382 // because we need to catch all-day events. We'll refine this range later 383 // after adjusting the visible range of events we load. 384 385 if ($this->rangeBegin) { 386 $where[] = qsprintf( 387 $conn, 388 '(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)', 389 $this->rangeBegin - phutil_units('16 hours in seconds')); 390 } 391 392 if ($this->rangeEnd) { 393 $where[] = qsprintf( 394 $conn, 395 'event.utcInitialEpoch <= %d', 396 $this->rangeEnd + phutil_units('16 hours in seconds')); 397 } 398 399 if ($this->utcInitialEpochMin !== null) { 400 $where[] = qsprintf( 401 $conn, 402 'event.utcInitialEpoch >= %d', 403 $this->utcInitialEpochMin); 404 } 405 406 if ($this->utcInitialEpochMax !== null) { 407 $where[] = qsprintf( 408 $conn, 409 'event.utcInitialEpoch <= %d', 410 $this->utcInitialEpochMax); 411 } 412 413 if ($this->inviteePHIDs !== null) { 414 $where[] = qsprintf( 415 $conn, 416 'invitee.inviteePHID IN (%Ls)', 417 $this->inviteePHIDs); 418 } 419 420 if ($this->hostPHIDs !== null) { 421 $where[] = qsprintf( 422 $conn, 423 'event.hostPHID IN (%Ls)', 424 $this->hostPHIDs); 425 } 426 427 if ($this->isCancelled !== null) { 428 $where[] = qsprintf( 429 $conn, 430 'event.isCancelled = %d', 431 (int)$this->isCancelled); 432 } 433 434 if ($this->eventsWithNoParent == true) { 435 $where[] = qsprintf( 436 $conn, 437 'event.instanceOfEventPHID IS NULL'); 438 } 439 440 if ($this->instanceSequencePairs !== null) { 441 $sql = array(); 442 443 foreach ($this->instanceSequencePairs as $pair) { 444 $sql[] = qsprintf( 445 $conn, 446 '(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)', 447 $pair[0], 448 $pair[1]); 449 } 450 451 $where[] = qsprintf( 452 $conn, 453 '%LO', 454 $sql); 455 } 456 457 if ($this->isStub !== null) { 458 $where[] = qsprintf( 459 $conn, 460 'event.isStub = %d', 461 (int)$this->isStub); 462 } 463 464 if ($this->parentEventPHIDs !== null) { 465 $where[] = qsprintf( 466 $conn, 467 'event.instanceOfEventPHID IN (%Ls)', 468 $this->parentEventPHIDs); 469 } 470 471 if ($this->importSourcePHIDs !== null) { 472 $where[] = qsprintf( 473 $conn, 474 'event.importSourcePHID IN (%Ls)', 475 $this->importSourcePHIDs); 476 } 477 478 if ($this->importAuthorPHIDs !== null) { 479 $where[] = qsprintf( 480 $conn, 481 'event.importAuthorPHID IN (%Ls)', 482 $this->importAuthorPHIDs); 483 } 484 485 if ($this->importUIDs !== null) { 486 $where[] = qsprintf( 487 $conn, 488 'event.importUID IN (%Ls)', 489 $this->importUIDs); 490 } 491 492 if ($this->isImported !== null) { 493 if ($this->isImported) { 494 $where[] = qsprintf( 495 $conn, 496 'event.importSourcePHID IS NOT NULL'); 497 } else { 498 $where[] = qsprintf( 499 $conn, 500 'event.importSourcePHID IS NULL'); 501 } 502 } 503 504 return $where; 505 } 506 507 protected function getPrimaryTableAlias() { 508 return 'event'; 509 } 510 511 protected function shouldGroupQueryResultRows() { 512 if ($this->inviteePHIDs !== null) { 513 return true; 514 } 515 return parent::shouldGroupQueryResultRows(); 516 } 517 518 public function getQueryApplicationClass() { 519 return PhabricatorCalendarApplication::class; 520 } 521 522 protected function willFilterPage(array $events) { 523 $instance_of_event_phids = array(); 524 $recurring_events = array(); 525 $viewer = $this->getViewer(); 526 527 $events = $this->getEventsInRange($events); 528 529 $import_phids = array(); 530 foreach ($events as $event) { 531 $import_phid = $event->getImportSourcePHID(); 532 if ($import_phid !== null) { 533 $import_phids[$import_phid] = $import_phid; 534 } 535 } 536 537 if ($import_phids) { 538 $imports = id(new PhabricatorCalendarImportQuery()) 539 ->setParentQuery($this) 540 ->setViewer($viewer) 541 ->withPHIDs($import_phids) 542 ->execute(); 543 $imports = mpull($imports, null, 'getPHID'); 544 } else { 545 $imports = array(); 546 } 547 548 foreach ($events as $key => $event) { 549 $import_phid = $event->getImportSourcePHID(); 550 if ($import_phid === null) { 551 $event->attachImportSource(null); 552 continue; 553 } 554 555 $import = idx($imports, $import_phid); 556 if (!$import) { 557 unset($events[$key]); 558 $this->didRejectResult($event); 559 continue; 560 } 561 562 $event->attachImportSource($import); 563 } 564 565 $phids = array(); 566 567 foreach ($events as $event) { 568 $phids[] = $event->getPHID(); 569 $instance_of = $event->getInstanceOfEventPHID(); 570 571 if ($instance_of) { 572 $instance_of_event_phids[] = $instance_of; 573 } 574 } 575 576 if (count($instance_of_event_phids) > 0) { 577 $recurring_events = id(new PhabricatorCalendarEventQuery()) 578 ->setViewer($viewer) 579 ->withPHIDs($instance_of_event_phids) 580 ->withEventsWithNoParent(true) 581 ->execute(); 582 583 $recurring_events = mpull($recurring_events, null, 'getPHID'); 584 } 585 586 if ($events) { 587 $invitees = id(new PhabricatorCalendarEventInviteeQuery()) 588 ->setViewer($viewer) 589 ->withEventPHIDs($phids) 590 ->execute(); 591 $invitees = mgroup($invitees, 'getEventPHID'); 592 } else { 593 $invitees = array(); 594 } 595 596 foreach ($events as $key => $event) { 597 $event_invitees = idx($invitees, $event->getPHID(), array()); 598 $event->attachInvitees($event_invitees); 599 600 $instance_of = $event->getInstanceOfEventPHID(); 601 if (!$instance_of) { 602 continue; 603 } 604 $parent = idx($recurring_events, $instance_of); 605 606 // should never get here 607 if (!$parent) { 608 unset($events[$key]); 609 continue; 610 } 611 $event->attachParentEvent($parent); 612 613 if ($this->isCancelled !== null) { 614 if ($event->getIsCancelled() != $this->isCancelled) { 615 unset($events[$key]); 616 continue; 617 } 618 } 619 } 620 621 $events = msort($events, 'getStartDateTimeEpoch'); 622 623 if ($this->needRSVPs) { 624 $rsvp_phids = $this->needRSVPs; 625 $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; 626 627 $project_phids = array(); 628 foreach ($events as $event) { 629 foreach ($event->getInvitees() as $invitee) { 630 $invitee_phid = $invitee->getInviteePHID(); 631 if (phid_get_type($invitee_phid) == $project_type) { 632 $project_phids[] = $invitee_phid; 633 } 634 } 635 } 636 637 if ($project_phids) { 638 $member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; 639 640 $query = id(new PhabricatorEdgeQuery()) 641 ->withSourcePHIDs($project_phids) 642 ->withEdgeTypes(array($member_type)) 643 ->withDestinationPHIDs($rsvp_phids); 644 645 $edges = $query->execute(); 646 647 $project_map = array(); 648 foreach ($edges as $src => $types) { 649 foreach ($types as $type => $dsts) { 650 foreach ($dsts as $dst => $edge) { 651 $project_map[$dst][] = $src; 652 } 653 } 654 } 655 } else { 656 $project_map = array(); 657 } 658 659 $membership_map = array(); 660 foreach ($rsvp_phids as $rsvp_phid) { 661 $membership_map[$rsvp_phid] = array(); 662 $membership_map[$rsvp_phid][] = $rsvp_phid; 663 664 $project_phids = idx($project_map, $rsvp_phid); 665 if ($project_phids) { 666 foreach ($project_phids as $project_phid) { 667 $membership_map[$rsvp_phid][] = $project_phid; 668 } 669 } 670 } 671 672 foreach ($events as $event) { 673 $invitees = $event->getInvitees(); 674 $invitees = mpull($invitees, null, 'getInviteePHID'); 675 676 $rsvp_map = array(); 677 foreach ($rsvp_phids as $rsvp_phid) { 678 $membership_phids = $membership_map[$rsvp_phid]; 679 $rsvps = array_select_keys($invitees, $membership_phids); 680 $rsvp_map[$rsvp_phid] = $rsvps; 681 } 682 683 $event->attachRSVPs($rsvp_map); 684 } 685 } 686 687 return $events; 688 } 689 690 private function getEventsInRange(array $events) { 691 $range_start = $this->rangeBegin; 692 $range_end = $this->rangeEnd; 693 694 foreach ($events as $key => $event) { 695 $event_start = $event->getStartDateTimeEpoch(); 696 $event_end = $event->getEndDateTimeEpoch(); 697 698 if ($range_start && $event_end < $range_start) { 699 unset($events[$key]); 700 } 701 702 if ($range_end && $event_start > $range_end) { 703 unset($events[$key]); 704 } 705 } 706 707 return $events; 708 } 709 710 private function getRecurrenceWindowStart( 711 PhabricatorCalendarEvent $event, 712 $generate_from) { 713 714 if (!$generate_from) { 715 return null; 716 } 717 718 return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from); 719 } 720 721 private function getRecurrenceWindowEnd( 722 PhabricatorCalendarEvent $event, 723 $generate_until) { 724 725 $end_epochs = array(); 726 if ($generate_until) { 727 $end_epochs[] = $generate_until; 728 } 729 730 $until_epoch = $event->getUntilDateTimeEpoch(); 731 if ($until_epoch) { 732 $end_epochs[] = $until_epoch; 733 } 734 735 if (!$end_epochs) { 736 return null; 737 } 738 739 return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs)); 740 } 741 742 private function getRecurrenceLimit( 743 PhabricatorCalendarEvent $event, 744 $raw_limit) { 745 746 $count = $event->getRecurrenceCount(); 747 if ($count && ($count <= $raw_limit)) { 748 return ($count - 1); 749 } 750 751 return $raw_limit; 752 } 753 754}