Path: blob/master/src/applications/calendar/import/PhabricatorCalendarImportEngine.php
12256 views
<?php12abstract class PhabricatorCalendarImportEngine3extends Phobject {45const QUEUE_BYTE_LIMIT = 524288;67final public function getImportEngineType() {8return $this->getPhobjectClassConstant('ENGINETYPE', 64);9}1011abstract public function getImportEngineName();12abstract public function getImportEngineTypeName();13abstract public function getImportEngineHint();1415public function appendImportProperties(16PhabricatorUser $viewer,17PhabricatorCalendarImport $import,18PHUIPropertyListView $properties) {19return;20}2122abstract public function newEditEngineFields(23PhabricatorEditEngine $engine,24PhabricatorCalendarImport $import);2526abstract public function getDisplayName(PhabricatorCalendarImport $import);2728abstract public function importEventsFromSource(29PhabricatorUser $viewer,30PhabricatorCalendarImport $import,31$should_queue);3233abstract public function canDisable(34PhabricatorUser $viewer,35PhabricatorCalendarImport $import);3637public function explainCanDisable(38PhabricatorUser $viewer,39PhabricatorCalendarImport $import) {40throw new PhutilMethodNotImplementedException();41}4243abstract public function supportsTriggers(44PhabricatorCalendarImport $import);4546final public static function getAllImportEngines() {47return id(new PhutilClassMapQuery())48->setAncestorClass(__CLASS__)49->setUniqueMethod('getImportEngineType')50->setSortMethod('getImportEngineName')51->execute();52}5354final protected function importEventDocument(55PhabricatorUser $viewer,56PhabricatorCalendarImport $import,57PhutilCalendarRootNode $root = null) {5859$event_type = PhutilCalendarEventNode::NODETYPE;6061$nodes = array();62if ($root) {63foreach ($root->getChildren() as $document) {64foreach ($document->getChildren() as $node) {65$node_type = $node->getNodeType();66if ($node_type != $event_type) {67$import->newLogMessage(68PhabricatorCalendarImportIgnoredNodeLogType::LOGTYPE,69array(70'node.type' => $node_type,71));72continue;73}7475$nodes[] = $node;76}77}78}7980// Reject events which have dates outside of the range of a signed81// 32-bit integer. We'll need to accommodate a wider range of events82// eventually, but have about 20 years until it's an issue and we'll83// all be dead by then.84foreach ($nodes as $key => $node) {85$dates = array();86$dates[] = $node->getStartDateTime();87$dates[] = $node->getEndDateTime();88$dates[] = $node->getCreatedDateTime();89$dates[] = $node->getModifiedDateTime();90$rrule = $node->getRecurrenceRule();91if ($rrule) {92$dates[] = $rrule->getUntil();93}9495$bad_date = false;96foreach ($dates as $date) {97if ($date === null) {98continue;99}100101$year = $date->getYear();102if ($year < 1970 || $year > 2037) {103$bad_date = true;104break;105}106}107108if ($bad_date) {109$import->newLogMessage(110PhabricatorCalendarImportEpochLogType::LOGTYPE,111array());112unset($nodes[$key]);113}114}115116// Reject events which occur too frequently. Users do not normally define117// these events and the UI and application make many assumptions which are118// incompatible with events recurring once per second.119foreach ($nodes as $key => $node) {120$rrule = $node->getRecurrenceRule();121if (!$rrule) {122// This is not a recurring event, so we don't need to check the123// frequency.124continue;125}126$scale = $rrule->getFrequencyScale();127if ($scale >= PhutilCalendarRecurrenceRule::SCALE_DAILY) {128// This is a daily, weekly, monthly, or yearly event. These are129// supported.130} else {131// This is an hourly, minutely, or secondly event.132$import->newLogMessage(133PhabricatorCalendarImportFrequencyLogType::LOGTYPE,134array(135'frequency' => $rrule->getFrequency(),136));137unset($nodes[$key]);138}139}140141$node_map = array();142foreach ($nodes as $node) {143$full_uid = $this->getFullNodeUID($node);144if (isset($node_map[$full_uid])) {145$import->newLogMessage(146PhabricatorCalendarImportDuplicateLogType::LOGTYPE,147array(148'uid.full' => $full_uid,149));150continue;151}152$node_map[$full_uid] = $node;153}154155// If we already know about some of these events and they were created156// here, we're not going to import it again. This can happen if a user157// exports an event and then tries to import it again. This is probably158// not what they meant to do and this pathway generally leads to madness.159$likely_phids = array();160foreach ($node_map as $full_uid => $node) {161$uid = $node->getUID();162$matches = null;163if (preg_match('/^(PHID-.*)@(.*)\z/', $uid, $matches)) {164$likely_phids[$full_uid] = $matches[1];165}166}167168if ($likely_phids) {169// NOTE: We're using the omnipotent viewer here because we don't want170// to collide with events that already exist, even if you can't see171// them.172$events = id(new PhabricatorCalendarEventQuery())173->setViewer(PhabricatorUser::getOmnipotentUser())174->withPHIDs($likely_phids)175->execute();176$events = mpull($events, null, 'getPHID');177foreach ($node_map as $full_uid => $node) {178$phid = idx($likely_phids, $full_uid);179if (!$phid) {180continue;181}182183$event = idx($events, $phid);184if (!$event) {185continue;186}187188$import->newLogMessage(189PhabricatorCalendarImportOriginalLogType::LOGTYPE,190array(191'phid' => $event->getPHID(),192));193194unset($node_map[$full_uid]);195}196}197198if ($node_map) {199$events = id(new PhabricatorCalendarEventQuery())200->setViewer($viewer)201->withImportAuthorPHIDs(array($import->getAuthorPHID()))202->withImportUIDs(array_keys($node_map))203->execute();204$events = mpull($events, null, 'getImportUID');205} else {206$events = null;207}208209$xactions = array();210$update_map = array();211$invitee_map = array();212$attendee_map = array();213foreach ($node_map as $full_uid => $node) {214$event = idx($events, $full_uid);215if (!$event) {216$event = PhabricatorCalendarEvent::initializeNewCalendarEvent($viewer);217}218219$event220->setImportAuthorPHID($import->getAuthorPHID())221->setImportSourcePHID($import->getPHID())222->setImportUID($full_uid)223->attachImportSource($import);224225$this->updateEventFromNode($viewer, $event, $node);226$xactions[$full_uid] = $this->newUpdateTransactions($event, $node);227$update_map[$full_uid] = $event;228229$attendee_map[$full_uid] = array();230$attendees = $node->getAttendees();231$private_index = 1;232foreach ($attendees as $attendee) {233// Generate a "name" for this attendee which is not an email address.234// We avoid disclosing email addresses to be consistent with the rest235// of the product.236$name = $attendee->getName();237if (preg_match('/@/', $name)) {238$name = new PhutilEmailAddress($name);239$name = $name->getDisplayName();240}241242// If we don't have a name or the name still looks like it's an243// email address, give them a dummy placeholder name.244if (!strlen($name) || preg_match('/@/', $name)) {245$name = pht('Private User %d', $private_index);246$private_index++;247}248249$attendee_map[$full_uid][$name] = $attendee;250}251}252253$attendee_names = array();254foreach ($attendee_map as $full_uid => $event_attendees) {255foreach ($event_attendees as $name => $attendee) {256$attendee_names[$name] = $attendee;257}258}259260if ($attendee_names) {261$external_invitees = id(new PhabricatorCalendarExternalInviteeQuery())262->setViewer($viewer)263->withNames(array_keys($attendee_names))264->execute();265$external_invitees = mpull($external_invitees, null, 'getName');266267foreach ($attendee_names as $name => $attendee) {268if (isset($external_invitees[$name])) {269continue;270}271272$external_invitee = id(new PhabricatorCalendarExternalInvitee())273->setName($name)274->setURI($attendee->getURI())275->setSourcePHID($import->getPHID());276277try {278$external_invitee->save();279} catch (AphrontDuplicateKeyQueryException $ex) {280$external_invitee =281id(new PhabricatorCalendarExternalInviteeQuery())282->setViewer($viewer)283->withNames(array($name))284->executeOne();285}286287$external_invitees[$name] = $external_invitee;288}289}290291// Reorder events so we create parents first. This allows us to populate292// "instanceOfEventPHID" correctly.293$insert_order = array();294foreach ($update_map as $full_uid => $event) {295$parent_uid = $this->getParentNodeUID($node_map[$full_uid]);296if ($parent_uid === null) {297$insert_order[$full_uid] = $full_uid;298continue;299}300301if (empty($update_map[$parent_uid])) {302// The parent was not present in this import, which means it either303// does not exist or we're going to delete it anyway. We just drop304// this node.305306$import->newLogMessage(307PhabricatorCalendarImportOrphanLogType::LOGTYPE,308array(309'uid.full' => $full_uid,310'uid.parent' => $parent_uid,311));312313continue;314}315316// Otherwise, we're going to insert the parent first, then insert317// the child.318$insert_order[$parent_uid] = $parent_uid;319$insert_order[$full_uid] = $full_uid;320}321322// TODO: Define per-engine content sources so this can say "via Upload" or323// whatever.324$content_source = PhabricatorContentSource::newForSource(325PhabricatorWebContentSource::SOURCECONST);326327// NOTE: We're using the omnipotent user here because imported events are328// otherwise immutable.329$edit_actor = PhabricatorUser::getOmnipotentUser();330331$update_map = array_select_keys($update_map, $insert_order);332foreach ($update_map as $full_uid => $event) {333$parent_uid = $this->getParentNodeUID($node_map[$full_uid]);334if ($parent_uid) {335$parent_phid = $update_map[$parent_uid]->getPHID();336} else {337$parent_phid = null;338}339340$event->setInstanceOfEventPHID($parent_phid);341342$event_xactions = $xactions[$full_uid];343344$editor = id(new PhabricatorCalendarEventEditor())345->setActor($edit_actor)346->setActingAsPHID($import->getPHID())347->setContentSource($content_source)348->setContinueOnNoEffect(true)349->setContinueOnMissingFields(true);350351$is_new = !$event->getID();352353$editor->applyTransactions($event, $event_xactions);354355// We're just forcing attendees to the correct values here because356// transactions intentionally don't let you RSVP for other users. This357// might need to be turned into a special type of transaction eventually.358$attendees = $attendee_map[$full_uid];359$old_map = $event->getInvitees();360$old_map = mpull($old_map, null, 'getInviteePHID');361362$new_map = array();363foreach ($attendees as $name => $attendee) {364$phid = $external_invitees[$name]->getPHID();365366$invitee = idx($old_map, $phid);367if (!$invitee) {368$invitee = id(new PhabricatorCalendarEventInvitee())369->setEventPHID($event->getPHID())370->setInviteePHID($phid)371->setInviterPHID($import->getPHID());372}373374switch ($attendee->getStatus()) {375case PhutilCalendarUserNode::STATUS_ACCEPTED:376$status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;377break;378case PhutilCalendarUserNode::STATUS_DECLINED:379$status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;380break;381case PhutilCalendarUserNode::STATUS_INVITED:382default:383$status = PhabricatorCalendarEventInvitee::STATUS_INVITED;384break;385}386$invitee->setStatus($status);387$invitee->save();388389$new_map[$phid] = $invitee;390}391392foreach ($old_map as $phid => $invitee) {393if (empty($new_map[$phid])) {394$invitee->delete();395}396}397398$event->attachInvitees($new_map);399400$import->newLogMessage(401PhabricatorCalendarImportUpdateLogType::LOGTYPE,402array(403'new' => $is_new,404'phid' => $event->getPHID(),405));406}407408if (!$update_map) {409$import->newLogMessage(410PhabricatorCalendarImportEmptyLogType::LOGTYPE,411array());412}413414// Delete any events which are no longer present in the source.415$updated_events = mpull($update_map, null, 'getPHID');416$source_events = id(new PhabricatorCalendarEventQuery())417->setViewer($viewer)418->withImportSourcePHIDs(array($import->getPHID()))419->execute();420421$engine = new PhabricatorDestructionEngine();422foreach ($source_events as $source_event) {423if (isset($updated_events[$source_event->getPHID()])) {424// We imported and updated this event, so keep it around.425continue;426}427428$import->newLogMessage(429PhabricatorCalendarImportDeleteLogType::LOGTYPE,430array(431'name' => $source_event->getName(),432));433434$engine->destroyObject($source_event);435}436}437438private function getFullNodeUID(PhutilCalendarEventNode $node) {439$uid = $node->getUID();440$instance_epoch = $this->getNodeInstanceEpoch($node);441$full_uid = $uid.'/'.$instance_epoch;442443return $full_uid;444}445446private function getParentNodeUID(PhutilCalendarEventNode $node) {447$recurrence_id = $node->getRecurrenceID();448449if (!strlen($recurrence_id)) {450return null;451}452453return $node->getUID().'/';454}455456private function getNodeInstanceEpoch(PhutilCalendarEventNode $node) {457$instance_iso = $node->getRecurrenceID();458if (strlen($instance_iso)) {459$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601(460$instance_iso);461$instance_epoch = $instance_datetime->getEpoch();462} else {463$instance_epoch = null;464}465466return $instance_epoch;467}468469private function newUpdateTransactions(470PhabricatorCalendarEvent $event,471PhutilCalendarEventNode $node) {472473$xactions = array();474$uid = $node->getUID();475476if (!$event->getID()) {477$xactions[] = id(new PhabricatorCalendarEventTransaction())478->setTransactionType(PhabricatorTransactions::TYPE_CREATE)479->setNewValue(true);480}481482$name = $node->getName();483if (!strlen($name)) {484if (strlen($uid)) {485$name = pht('Unnamed Event "%s"', $uid);486} else {487$name = pht('Unnamed Imported Event');488}489}490$xactions[] = id(new PhabricatorCalendarEventTransaction())491->setTransactionType(492PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE)493->setNewValue($name);494495$description = $node->getDescription();496$xactions[] = id(new PhabricatorCalendarEventTransaction())497->setTransactionType(498PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE)499->setNewValue((string)$description);500501$is_recurring = (bool)$node->getRecurrenceRule();502$xactions[] = id(new PhabricatorCalendarEventTransaction())503->setTransactionType(504PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE)505->setNewValue($is_recurring);506507return $xactions;508}509510private function updateEventFromNode(511PhabricatorUser $actor,512PhabricatorCalendarEvent $event,513PhutilCalendarEventNode $node) {514515$instance_epoch = $this->getNodeInstanceEpoch($node);516$event->setUTCInstanceEpoch($instance_epoch);517518$timezone = $actor->getTimezoneIdentifier();519520// TODO: These should be transactional, but the transaction only accepts521// epoch timestamps right now.522$start_datetime = $node->getStartDateTime()523->setViewerTimezone($timezone);524$end_datetime = $node->getEndDateTime()525->setViewerTimezone($timezone);526527$event528->setStartDateTime($start_datetime)529->setEndDateTime($end_datetime);530531$event->setIsAllDay((int)$start_datetime->getIsAllDay());532533// TODO: This should be transactional, but the transaction only accepts534// simple frequency rules right now.535$rrule = $node->getRecurrenceRule();536if ($rrule) {537$event->setRecurrenceRule($rrule);538539$until_datetime = $rrule->getUntil();540if ($until_datetime) {541$until_datetime->setViewerTimezone($timezone);542$event->setUntilDateTime($until_datetime);543}544545$count = $rrule->getCount();546$event->setParameter('recurrenceCount', $count);547}548549return $event;550}551552public function canDeleteAnyEvents(553PhabricatorUser $viewer,554PhabricatorCalendarImport $import) {555556$table = new PhabricatorCalendarEvent();557$conn = $table->establishConnection('r');558559// Using a CalendarEventQuery here was failing oddly in a way that was560// difficult to reproduce locally (see T11808). Just check the table561// directly; this is significantly more efficient anyway.562563$any_event = queryfx_all(564$conn,565'SELECT phid FROM %T WHERE importSourcePHID = %s LIMIT 1',566$table->getTableName(),567$import->getPHID());568569return (bool)$any_event;570}571572final protected function shouldQueueDataImport($data) {573return (strlen($data) > self::QUEUE_BYTE_LIMIT);574}575576final protected function queueDataImport(577PhabricatorCalendarImport $import,578$data) {579580$import->newLogMessage(581PhabricatorCalendarImportQueueLogType::LOGTYPE,582array(583'data.size' => strlen($data),584'data.limit' => self::QUEUE_BYTE_LIMIT,585));586587// When we queue on this pathway, we're queueing in response to an explicit588// user action (like uploading a big `.ics` file), so we queue at normal589// priority instead of bulk/import priority.590591PhabricatorWorker::scheduleTask(592'PhabricatorCalendarImportReloadWorker',593array(594'importPHID' => $import->getPHID(),595'via' => PhabricatorCalendarImportReloadWorker::VIA_BACKGROUND,596),597array(598'objectPHID' => $import->getPHID(),599));600}601602603}604605606