Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
googleapisync.php
1<?
3
13use \Bitrix\Main\Web;
16use CDavConnection;
17
23final class GoogleApiSync
24{
26 const ONE_DAY = 86400; //60*60*24;
27 const CHANNEL_EXPIRATION = 604800; //60*60**24*7
28 const CONNECTION_CHANNEL_TYPE = 'BX_CONNECTION';
29 const SECTION_CHANNEL_TYPE = 'BX_SECTION';
30 const SECTION_CONNECTION_CHANNEL_TYPE = 'SECTION_CONNECTION';
32 const SYNC_EVENTS_DATE_INTERVAL = '-1 months';
33 const DEFAULT_TIMEZONE = 'UTC';
34 const DATE_TIME_FORMAT = 'Y-m-d\TH:i:sP';
35 public const END_OF_DATE = "01.01.2038";
36 public const EXTERNAL_LINK = 'https://www.bitrix24.com/controller/google_calendar_push.php?target_host=';
37
41 private $syncTransport;
42 private $nextSyncToken = '',
43 $defaultTimezone = self::DEFAULT_TIMEZONE,
44 $userId = 0,
45 $calendarList = array(),
46 $defaultReminderData = array(),
47 $calendarColors = false,
48 $eventColors = false,
49 $eventMapping = array(
50 'DAV_XML_ID' => 'iCalUID',
51 'NAME' => 'summary',
52// 'DESCRIPTION' => 'description',
53 'CAL_DAV_LABEL' => 'etag'
54 );
58 private $connectionId;
62 private $nextPageToken = '';
63
70 public function __construct($userId = 0, $connectionId = 0)
71 {
72 if (!$userId)
73 {
74 $userId = \CCalendar::GetUserId();
75 }
76 $this->userId = $userId;
77 $this->connectionId = $connectionId;
78 $this->syncTransport = new GoogleApiTransport((int)$userId);
79 }
80
87 public function stopChannel($channelId, $resourceId)
88 {
89 $this->syncTransport->stopChannel($channelId, $resourceId);
90
91 $error = $this->getTransportConnectionError();
92 if (is_string($error))
93 {
94 $this->updateLastResultConnection($error);
95 return false;
96 }
97
98 return true;
99 }
100
107 public function startWatchCalendarList($name)
108 {
109 $channel = $this->syncTransport->openCalendarListChannel($this->makeChannelParams($name, self::CONNECTION_CHANNEL_TYPE));
110 if (!$this->syncTransport->getErrors())
111 {
112 $channel['expiration'] = Type\DateTime::createFromTimestamp($channel['expiration']/1000);
113 return $channel;
114 }
115
116 $error = $this->getTransportConnectionError();
117 if (is_string($error))
118 {
119 $this->updateLastResultConnection($error);
120 }
121
122 return [];
123 }
124
125 private function makeChannelParams($inputSecretWord, $type)
126 {
127 if (defined('BX24_HOST_NAME') && BX24_HOST_NAME)
128 {
129 $externalUrl = self::EXTERNAL_LINK . BX24_HOST_NAME;
130 }
131 else
132 {
133 $request = Context::getCurrent()->getRequest();
134 if (defined('SITE_SERVER_NAME') && SITE_SERVER_NAME)
135 {
136 $host = SITE_SERVER_NAME;
137 }
138 else
139 {
140 $host = Option::get('main', 'server_name', $request->getHttpHost());
141 }
142
143 $externalUrl = 'https://' . $host . '/bitrix/tools/calendar/push.php';
144 }
145
146 return [
147 'id' => $type.'_'.$this->userId.'_'.md5($inputSecretWord. time()),
148 'type' => 'web_hook',
149 'address' => $externalUrl,
150 'expiration' => (time() + self::CHANNEL_EXPIRATION) * 1000,
151 ];
152 }
153
160 public function startWatchEventsChannel($calendarId = 'primary')
161 {
162 $channel = $this->syncTransport->openEventsWatchChannel(
163 $calendarId,
164 $this->makeChannelParams($calendarId, self::SECTION_CHANNEL_TYPE)
165 );
166
167 if (!$this->syncTransport->getErrors())
168 {
169 $channel['expiration'] = Type\DateTime::createFromTimestamp($channel['expiration']/1000);
170 return $channel;
171 }
172
173 if (($error = $this->getTransportConnectionError()) && is_string($error))
174 {
175 $this->updateLastResultConnection($error);
176 }
177
178 return false;
179 }
180
185 public function testConnection()
186 {
187 $this->setColors();
188 if ($this->getTransportErrors())
189 {
190 return false;
191 }
192 return true;
193 }
194
199 public function getTransportErrors()
200 {
201 return $this->syncTransport->getErrors();
202 }
203
204 private function setColors()
205 {
206 if ($this->calendarColors === false || $this->eventColors === false)
207 {
208 $cacheTime = 86400 * 7;
209 $colorData = null;
210
211 if ($cacheTime)
212 {
213 $cache = \Bitrix\Main\Data\Cache::createInstance();
214 $cacheId = "google_colors";
215 $cachePath = 'googlecolors';
216
217 if ($cache->initCache($cacheTime, $cacheId, $cachePath))
218 {
219 $res = $cache->getVars();
220 $colorData = $res["colorData"];
221 }
222 }
223
224 if (!$cacheTime || empty($colorData))
225 {
226 $colorData = $this->syncTransport->getColors();
227 if ($cacheTime && isset($cache, $cacheId, $cachePath))
228 {
229 $cache->startDataCache($cacheTime, $cacheId, $cachePath);
230 $cache->endDataCache(array(
231 "colorData" => $colorData
232 ));
233 }
234 }
235
236 $this->calendarColors = array();
237 $this->eventColors = array();
238 if (is_array($colorData) && !empty($colorData['calendar']) && !empty($colorData['event']))
239 {
240 foreach ($colorData['calendar'] as $key => $color)
241 {
242 $this->calendarColors[$key] = $color;
243 }
244
245 foreach ($colorData['event'] as $key => $color)
246 {
247 $this->eventColors[$key] = $color;
248 }
249 }
250 }
251 }
252
258 private function getCalendarColor($colorId, $background = true)
259 {
260 $calendarColors = is_array($this->calendarColors) ? $this->calendarColors : array();
261 return $calendarColors[$colorId][($background ? 'background' : 'foreground')];
262 }
263
269 private function getEventColor($colorId, $background = true)
270 {
271 $eventColors = is_array($this->eventColors) ? $this->eventColors : array();
272 return $eventColors[$colorId][($background ? 'background' : 'foreground')];
273 }
274
281 {
282 $connectionError = $this->syncTransport->getErrorByCode('CONNECTION');
283 if ($connectionError)
284 {
285 return $connectionError['message'];
286 }
287
288 return [];
289 }
290
297 public function getCalendarItems(string $syncToken = null): array
298 {
299 $this->setColors();
300 $response = [];
301
302 if (empty($this->calendarList))
303 {
304 $response = $this->syncTransport->getCalendarList($this->getCalendarListParams($syncToken));
305 }
306
307 if ($response && !empty($response['items']))
308 {
309 foreach($response['items'] as $calendarItem)
310 {
311 $calendarItem['backgroundColor'] = $this->getCalendarColor($calendarItem['colorId']);
312 $calendarItem['textColor'] = $this->getCalendarColor($calendarItem['colorId'], true);
313 $this->calendarList[] = $calendarItem;
314 }
315
316 $this->nextSyncToken = $response['nextSyncToken'];
317 }
318
319
320 return $this->calendarList;
321 }
322
326 public function getNextSyncToken()
327 {
328 return $this->nextSyncToken;
329 }
330
337 public function getEvents(array $calendarData): array
338 {
339 $this->setColors();
340 $this->nextSyncToken = $calendarData['SYNC_TOKEN'] ?? '';
341 $this->nextPageToken = $calendarData['PAGE_TOKEN'] ?? '';
342
343 if (!empty($response = $this->runSyncEvents($calendarData['GAPI_CALENDAR_ID'])))
344 {
345 return $this->processResponseReceivingEvents($response);
346 }
347
348 return [];
349 }
350
356 public function getPrimaryId()
357 {
358 $calendar = $this->getCalendarById('primary');
359 return !empty($calendar) ? $calendar['id'] : '';
360 }
361
368 private function getCalendarById($calendarId)
369 {
370 $this->getCalendarItems();
371
372 foreach ($this->calendarList as $calendar)
373 {
374 if (($calendar['id'] == $calendarId) || (isset($calendar['primary']) && $calendarId == 'primary'))
375 {
376 return $calendar;
377 }
378 }
379 return array();
380 }
381
389 public function deleteEvent($eventId, $calendarId)
390 {
391 return $this->syncTransport->deleteEvent($eventId, urlencode($calendarId));
392 }
393
401 public function saveEvent($eventData, $calendarId, $parameters = []): ?array
402 {
403 $params['editInstance'] = $parameters['editInstance'] ?? false;
404 $params['originalDavXmlId'] = $parameters['originalDavXmlId'] ?? null;
405 $params['editParentEvents'] = $parameters['editParentEvents'] ?? false;
406 $params['editNextEvents'] = $parameters['editNextEvents'] ?? false;
407 $params['calendarId'] = $calendarId;
408 $params['instanceTz'] = $parameters['instanceTz'] ?? null;
409 $params['originalDateFrom'] = $eventData['ORIGINAL_DATE_FROM'] ?? null;
410 $params['gEventId'] = $eventData['G_EVENT_ID'] ?: str_replace('@google.com', '', $eventData['DAV_XML_ID']);
411 $params['syncCaldav'] = $parameters['syncCaldav'] ?? false;
412
413 $newEvent = $this->prepareToSaveEvent($eventData, $params);
414
415 $externalEvent = $this->sendToSaveEvent($newEvent, $params);
416
417 if ($externalEvent)
418 {
419 return [
420 'DAV_XML_ID' => $externalEvent['iCalUID'],
421 'CAL_DAV_LABEL' => $externalEvent['etag'],
422 'ORIGINAL_DATE_FROM' => $externalEvent['originalStartTime'] ? $eventData['ORIGINAL_DATE_FROM'] : null,
423 'G_EVENT_ID' => $externalEvent['id'],
424 ];
425 }
426
427 return null;
428 }
429
437 public function saveBatchEvents(array $events, string $gApiCalendarId, array $params): array
438 {
439 $responseFields = [];
440 $prepareEvents = [];
441
442 foreach ($events as $event)
443 {
444 $localEvent['gEventId'] = $event['gEventId'];
445 $partBody = $this->prepareToSaveEvent($event);
446 $localEvent['partBody'] = Web\Json::encode($partBody, JSON_UNESCAPED_SLASHES);
447 $prepareEvents[$event['ID']] = $localEvent;
448 }
449
450 $externalEvents = $this->syncTransport->sendBatchEvents($prepareEvents, $gApiCalendarId, $params);
451
452 if ($externalEvents)
453 {
454 foreach ($externalEvents as $key => $externalEvent)
455 {
456 $responseFields[$key]['DAV_XML_ID'] = $externalEvent['iCalUID'];
457 $responseFields[$key]['CAL_DAV_LABEL'] = $externalEvent['etag'];
458 $responseFields[$key]['G_EVENT_ID'] = $externalEvent['id'];
459 $responseFields[$key]['ORIGINAL_DATE_FROM'] = $externalEvent['originalStartTime'] ? $events[$key]['ORIGINAL_DATE_FROM'] : null;
460 }
461 }
462
463 return $responseFields;
464 }
465
466 public function updateLastResultConnection(string $lastResult): void
467 {
468 if (Loader::includeModule('dav') && !empty($this->connectionId))
469 {
470 CDavConnection::Update($this->connectionId, [
471 "LAST_RESULT" => $lastResult,
472 "SYNCHRONIZED" => ConvertTimeStamp(time(), "FULL"),
473 ]);
474 }
475
476 if (GoogleApiPush::isConnectionError($lastResult))
477 {
478 AddMessage2Log("Bad interaction with Google calendar for connectionId: " . $this->connectionId . " " .$lastResult, "calendar");
479 }
480 }
481
482 public function updateSuccessLastResultConnection(): void
483 {
484 $this->updateLastResultConnection("[200] OK");
485 }
486
492 private function prepareEvent($event): array
493 {
494 $returnData = [
495 'TZ_FROM' => $this->defaultTimezone,
496 'TZ_TO' => $this->defaultTimezone
497 ];
498
499 foreach ($this->eventMapping as $internalKey => $externalKey)
500 {
501 $returnData[$internalKey] = (isset($event[$externalKey]) ? $event[$externalKey] : '');
502 }
503
504 $returnData['iCalUID'] = $event['iCalUID'];
505 $returnData['DAV_XML_ID'] = $event['iCalUID'];
506 $returnData['G_EVENT_ID'] = $event['id'];
507
508 if (!empty($event['description']))
509 {
510 $description = str_replace("<br>", "\r\n", $event['description']);
511 $returnData["DESCRIPTION"] = trim(\CTextParser::clearAllTags($description));
512 }
513
514 if (empty($event['summary']))
515 {
516 $returnData['NAME'] = GetMessage('EC_T_NEW_EVENT');
517 }
518
519 if (!empty($event['transparency']) && $event['transparency'] == 'transparent')
520 {
521 $returnData['ACCESSIBILITY'] = 'free';
522 }
523 else
524 {
525 $returnData['ACCESSIBILITY'] = 'busy';
526 }
527
528 if (!empty($event['visibility']) && $event['visibility'] === 'private')
529 {
530 $returnData['PRIVATE_EVENT'] = true;
531 }
532 else
533 {
534 $returnData['PRIVATE_EVENT'] = false;
535 }
536
537 $returnData['OWNER_ID'] = $this->userId;
538 $returnData['CREATED_BY'] = $this->userId;
539 $returnData['CAL_TYPE'] = 'user';
540
541 if (!empty($event['start']['dateTime']) && !empty($event['end']['dateTime']))
542 {
543 $returnData['TZ_FROM'] = Util::isTimezoneValid($event['start']['timeZone']) ? $event['start']['timeZone'] : $this->defaultTimezone;
544 $returnData['TZ_TO'] = Util::isTimezoneValid($event['end']['timeZone']) ? $event['end']['timeZone'] : $this->defaultTimezone;
545
546 $eventStartDateTime = new Type\DateTime($event['start']['dateTime'], self::DATE_TIME_FORMAT, new \DateTimeZone($this->defaultTimezone));
547 $returnData['DATE_FROM'] = \CCalendar::Date(\CCalendar::Timestamp($eventStartDateTime->setTimeZone(new \DateTimeZone($returnData['TZ_FROM']))->format(Type\Date::convertFormatToPhp(FORMAT_DATETIME))));
548
549 $eventStartDateTime = new Type\DateTime($event['end']['dateTime'], self::DATE_TIME_FORMAT, new \DateTimeZone($this->defaultTimezone));
550 $returnData['DATE_TO'] = \CCalendar::Date(\CCalendar::Timestamp($eventStartDateTime->setTimeZone(new \DateTimeZone($returnData['TZ_TO']))->format(Type\Date::convertFormatToPhp(FORMAT_DATETIME))));
551 }
552
553 if (!empty($event['start']['date']))
554 {
555 $returnData['DATE_FROM'] = \CCalendar::Date(strtotime($event['start']['date']), false);
556 }
557
558 if (!empty($event['end']['date']))
559 {
560 if ($event['end']['date'] === $event['start']['date'])
561 {
562 $dateStr = strtotime($event['end']['date']);
563 }
564 else
565 {
566 $dateStr = strtotime($event['end']['date']) - self::ONE_DAY;
567 }
568
569 $returnData['DATE_TO'] = \CCalendar::Date($dateStr, false);
570 $returnData['DT_SKIP_TIME'] = 'Y';
571 }
572 else
573 {
574 $returnData['DT_SKIP_TIME'] = 'N';
575 }
576
577 $returnData['DATE_CREATE'] = \CCalendar::Date(strtotime($event['created']));
578
579 if (!empty($event['colorId']))
580 {
581 $returnData['COLOR'] = $this->getEventColor($event['colorId']);
582 $returnData['TEXT_COLOR'] = $this->getEventColor($event['colorId'], false);
583 }
584
585 $returnData['DATE_CREATE'] = \CCalendar::Date(time());
586 $returnData['status'] = $event['status'];
587 $returnData['hasMoved'] = "N";
588 $returnData['isRecurring'] = "N";
589
590 $exDates = [];
591
592 if ($event['recurrence'])
593 {
594 foreach ($event['recurrence'] as $recurrence)
595 {
596 if (preg_match('/(RRULE)/', $recurrence))
597 {
598 $rRuleData = preg_replace('/(RRULE\:)/', '', $recurrence);
599 $rRuleList = explode(';', $rRuleData);
600 $rRuleSet = [];
601 foreach ($rRuleList as $rRuleElement)
602 {
603 [$rRuleProp, $rRuleVal] = explode('=', $rRuleElement);
604 switch ($rRuleProp)
605 {
606 case 'FREQ':
607 if (in_array($rRuleVal, ['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']))
608 {
609 $rRuleSet['FREQ'] = $rRuleVal;
610 }
611 break;
612 case 'COUNT':
613 $rRuleSet['COUNT'] = $rRuleVal;
614 break;
615 case 'INTERVAL':
616 $rRuleSet['INTERVAL'] = $rRuleVal;
617 break;
618 case 'BYDAY':
619 $rRuleByDay = array();
620 foreach(explode(',', $rRuleVal) as $day)
621 {
622 $matches = array();
623 if (preg_match('/((\-|\+)?\d+)?(MO|TU|WE|TH|FR|SA|SU)/', $day, $matches))
624 {
625 $rRuleByDay[$matches[1] === ''
626 ? $matches[3]
627 : $matches[1]] =
628 $matches[1] === ''
629 ? $matches[3]
630 : $matches[1];
631 }
632 }
633 if (!empty($rRuleByDay))
634 {
635 $rRuleSet['BYDAY'] = $rRuleByDay;
636 }
637 break;
638 case 'UNTIL':
639 try
640 {
641 $rRuleValDateTime = new Type\DateTime($rRuleVal, 'Ymd\THis\Z', new \DateTimeZone('UTC'));
642 $rRuleValDateTime->setTimeZone(new \DateTimeZone($returnData['TZ_TO']));
643 $untilDateTime = explode("T", $rRuleVal);
644
645 if ($untilDateTime[1] === "000000Z")
646 {
647 $rRuleValDateTime = $rRuleValDateTime->add("-1 day");
648 }
649
650 $rRuleSet['UNTIL'] = \CCalendar::Date(\CCalendar::Timestamp($rRuleValDateTime->format(Type\Date::convertFormatToPhp(FORMAT_DATETIME))) - 60, false, false);
651 }
652 catch(\Exception $e)
653 {
654 $rRuleSet['UNTIL'] = \CCalendar::Date(strtotime($rRuleVal), false, false);
655 }
656
657 break;
658 }
659 }
660 $returnData["RRULE"] = \CCalendarEvent::CheckRRULE($rRuleSet);
661 }
662 elseif (preg_match('/(\d{4}-?\d{2}-?\d{2})(Z)?/', $recurrence, $date))
663 {
664 if (!empty($date[1]))
665 {
666 $exDates[] = \CCalendar::Date(strtotime($date[1]), false);
667 }
668 }
669 }
670 }
671
672 if (!empty($event['recurringEventId']))
673 {
674 $returnData['isRecurring'] = "Y";
675 if ($event['status'] === 'cancelled')
676 {
677 $exDates[] = date(Date::convertFormatToPhp(FORMAT_DATE), strtotime(
678 !empty($event['originalStartTime']['dateTime'])
679 ? $event['originalStartTime']['dateTime']
680 : $event['originalStartTime']['date']
681 ));
682 }
683 elseif ($event['status'] === 'confirmed' && !empty($event['originalStartTime']))
684 {
685 $returnData['hasMoved'] = "Y";
686 $exDates[] = date(Date::convertFormatToPhp(FORMAT_DATE), strtotime(!empty($event['originalStartTime']['dateTime']) ? $event['originalStartTime']['dateTime'] : $event['originalStartTime']['date']));
687
688 if (!empty($event['originalStartTime']['dateTime']))
689 {
690 $originalTimeZone = Util::isTimezoneValid($event['originalStartTime']['timeZone']) ? $event['originalStartTime']['timeZone'] : $returnData['TZ_FROM'];
691 $eventOriginalDateFrom = new Type\DateTime($event['originalStartTime']['dateTime'], self::DATE_TIME_FORMAT, new \DateTimeZone($this->defaultTimezone));
692 $returnData['ORIGINAL_DATE_FROM'] = \CCalendar::Date(\CCalendar::Timestamp($eventOriginalDateFrom->setTimeZone(new \DateTimeZone($originalTimeZone))->format(Type\Date::convertFormatToPhp(FORMAT_DATETIME))));
693 }
694
695 if (!empty($event['originalStartTime']['date']))
696 {
697 $returnData['ORIGINAL_DATE_FROM'] = \CCalendar::Date(strtotime($event['originalStartTime']['date']), false);
698 }
699 }
700
701 $returnData['recurringEventId'] = $event['recurringEventId'];
702 }
703 if (!empty($exDates))
704 {
705 $returnData['EXDATE'] = implode(';', $exDates);
706 }
707
708 $returnData['REMIND'] = [];
709 if (!empty($event['reminders']['overrides']))
710 {
711 foreach ($event['reminders']['overrides'] as $remindData)
712 {
713 $remindTimeout = $remindData['minutes'];
714 $returnData['REMIND'][] = [
715 'type' => 'min',
716 'count' => $remindTimeout
717 ];
718 }
719 }
720 if (!empty($event['reminders']['useDefault']) && !empty($this->defaultReminderData) && $event['reminders']['useDefault'] === 1)
721 {
722 foreach ($this->defaultReminderData as $remindData)
723 {
724 $remindTimeout = $remindData['minutes'];
725 $returnData['REMIND'][] = [
726 'type' => 'min',
727 'count' => $remindTimeout
728 ];
729 }
730 }
731 if (!empty($event['location']))
732 {
733 $returnData['LOCATION'] = Rooms\Util::unParseTextLocation($event['location']);
734 }
735
736 if (!empty($event['sequence']))
737 {
738 $returnData['VERSION'] = (int)$event['sequence'] + Util::VERSION_DIFFERENCE;
739 }
740
741 return $returnData;
742 }
743
753 private function prepareToSaveEvent($eventData, $params = null): array
754 {
755 $newEvent = [];
756 $newEvent['summary'] = $eventData['NAME'];
757
758 if (!empty($eventData['ATTENDEES_CODES']) && is_string($eventData['ATTENDEES_CODES']))
759 {
760 $eventData['ATTENDEES_CODES'] = explode(",", $eventData['ATTENDEES_CODES']);
761 }
762
763 if (is_string($eventData['MEETING']))
764 {
765 $eventData['MEETING'] = unserialize($eventData['MEETING'], ['allowed_classes' => false]);
766 }
767 if (empty($eventData['MEETING']['LANGUAGE_ID']))
768 {
769 $eventData['MEETING']['LANGUAGE_ID'] = \CCalendar::getUserLanguageId((int)$eventData['OWNER_ID']);
770 }
771
772 if (isset($eventData['ATTENDEES_CODES']) && is_countable($eventData['ATTENDEES_CODES']) && count($eventData['ATTENDEES_CODES']) > 1)
773 {
774 $users = Util::getAttendees($eventData['ATTENDEES_CODES']);
775 $newEvent['description'] = Loc::getMessage('ATTENDEES_EVENT', null, $eventData['MEETING']['LANGUAGE_ID']).': '
776 . stripcslashes(implode(', ', $users))
777 . "\r\n"
778 . $eventData["DESCRIPTION"];
779 }
780 elseif (!empty($eventData['DESCRIPTION']) && is_string($eventData['DESCRIPTION']))
781 {
782 $newEvent['description'] = $eventData['DESCRIPTION'];
783 }
784
785 if (!empty($eventData['ACCESSIBILITY']) && $eventData['ACCESSIBILITY'] === 'busy')
786 {
787 $newEvent['transparency'] = 'opaque';
788 }
789 else
790 {
791 $newEvent['transparency'] = 'transparent';
792 }
793
794 if (!empty($eventData['LOCATION']['NEW']) && is_string($eventData['LOCATION']['NEW']))
795 {
796 $newEvent['location'] = \CCalendar::GetTextLocation($eventData['LOCATION']['NEW']);
797 }
798 elseif (!empty($eventData['LOCATION']) && is_string($eventData['LOCATION']))
799 {
800 $newEvent['location'] = \CCalendar::GetTextLocation($eventData['LOCATION']);
801 }
802
803 if (!empty($eventData['REMIND']))
804 {
805 $newEvent['reminders'] = $this->prepareRemind($eventData);
806 }
807
808 if ($eventData['DT_SKIP_TIME'] === "Y")
809 {
810 $newEvent['start']['date'] = Util::getDateObject($eventData['DATE_FROM'])
811 ->format('Y-m-d');
812 $newEvent['end']['date'] = Util::getDateObject($eventData['DATE_TO'])
813 ->add('+1 day')
814 ->format('Y-m-d');
815
816 if (!empty($eventData['G_EVENT_ID']))
817 {
818 $newEvent['start']['dateTime'] = null;
819 $newEvent['end']['dateTime'] = null;
820 $newEvent['start']['timeZone'] = null;
821 $newEvent['end']['timeZone'] = null;
822 }
823 }
824 else
825 {
826 $newEvent['start']['dateTime'] = Util::getDateObject($eventData['DATE_FROM'], false, $eventData['TZ_FROM'])
827 ->format(self::DATE_TIME_FORMAT);
828 $newEvent['start']['timeZone'] = Util::prepareTimezone($eventData['TZ_FROM'])->getName();
829
830 $newEvent['end']['dateTime'] = Util::getDateObject($eventData['DATE_TO'], false, $eventData['TZ_TO'])
831 ->format(self::DATE_TIME_FORMAT);
832 $newEvent['end']['timeZone'] = Util::prepareTimezone($eventData['TZ_TO'])->getName();
833
834 if (!empty($eventData['G_EVENT_ID']))
835 {
836 $newEvent['start']['date'] = null;
837 $newEvent['end']['date'] = null;
838 }
839 }
840
841 if (
842 !empty($eventData['RRULE'])
843 && is_array($eventData['RRULE'])
844 && isset($eventData['RRULE']['FREQ'])
845 && $eventData['RRULE']['FREQ'] !== 'NONE'
846 )
847 {
848 $newEvent['recurrence'] = $this->prepareRRule($eventData, $params['editNextEvents']);
849 }
850
851 if (isset($eventData['ORIGINAL_DATE_FROM']))
852 {
853 $newEvent['originalStartTime'] = Util::getDateObject($eventData['ORIGINAL_DATE_FROM'], false, $eventData['TZ_FROM'])
854 ->format(self::DATE_TIME_FORMAT);
855 }
856
857 if (isset($eventData['G_EVENT_ID']) && isset($eventData['RECURRENCE_ID']))
858 {
859 $newEvent['recurringEventId'] = $eventData['G_EVENT_ID'];
860 }
861
862 if ($params['syncCaldav'] || isset($eventData['DAV_XML_ID']))
863 {
864 $newEvent['iCalUID'] = $eventData['DAV_XML_ID'];
865 }
866
867 if (isset($eventData['G_EVENT_ID']))
868 {
869 $newEvent['id'] = $eventData['G_EVENT_ID'];
870 }
871
872 if (!empty($eventData['PRIVATE_EVENT']))
873 {
874 $newEvent['visibility'] = "private";
875 }
876 else
877 {
878 $newEvent['visibility'] = "public";
879 }
880
881 if (isset($eventData['VERSION']))
882 {
883 $newEvent['sequence'] = $eventData['VERSION'] - Util::VERSION_DIFFERENCE;
884 }
885
886 return $newEvent;
887 }
888
895 private function sendToSaveEvent($newEvent, $params)
896 {
897 if ($params['editInstance'] === true)
898 {
899 $eventOriginalStart = Util::getDateObject($params['originalDateFrom'], false, $params['instanceTz']);
900 $originalStart = $eventOriginalStart->format(self::DATE_TIME_FORMAT);
901 $externalId = $params['gEventId'] ?: $params['originalDavXmlId'];
902 $instance = $this->syncTransport->getInstanceRecurringEvent($params['calendarId'], $externalId, $originalStart);
903
904 $newEvent['originalStartTime'] = $originalStart;
905 $newEvent['recurringEventId'] = $params['originalDavXmlId'];
906
907 if (is_array($instance['items']))
908 {
909 return $this->syncTransport->updateEvent($newEvent, urlencode($params['calendarId']), $instance['items'][0]['id']);
910 }
911 }
912 elseif ($params['editParentEvents'] === true)
913 {
914 return $this->syncTransport->updateEvent($newEvent, urldecode($params['calendarId']), $params['gEventId']);
915 }
916 elseif ($params['syncCaldav'])
917 {
918 return $this->syncTransport->importEvent($newEvent, urlencode($params['calendarId']));
919 }
920 elseif (($params['gEventId']))
921 {
922 return $this->syncTransport->updateEvent($newEvent, urlencode($params['calendarId']), $params['gEventId']);
923 }
924 else
925 {
926 return $this->syncTransport->insertEvent($newEvent, urlencode($params['calendarId']));
927 }
928
929 return [];
930 }
931
936 public function createCalendar($calendar): ?array
937 {
938 $externalData = $this->syncTransport->insertCalendar($this->prepareCalendar($calendar));
939
940 return $externalData
941 ? ['GAPI_CALENDAR_ID' => $externalData['id']]
942 : null
943 ;
944 }
945
950 private function prepareCalendar($calendar): array
951 {
952 $returnData['summary'] = Emoji::decode($calendar['NAME']);
953 if (isset($calendar['EXTERNAL_TYPE']) && $calendar['EXTERNAL_TYPE'] === \CCalendarSect::EXTERNAL_TYPE_LOCAL)
954 {
955 IncludeModuleLangFile($_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/modules/calendar/classes/general/calendar.php');
956 $returnData['summary'] = Loc::getMessage('EC_CALENDAR_BITRIX24_NAME') . " " . $returnData['summary'];
957 }
958
959 return $returnData;
960 }
961
966 public static function getChannelOwner(string $channelId = null): ?int
967 {
968 if (empty($channelId))
969 {
970 return null;
971 }
972
973 $matches = [];
974 preg_match('/(' . self::CONNECTION_CHANNEL_TYPE . '|' . self::SECTION_CHANNEL_TYPE . ')_(\d+)_.+/', $channelId, $matches);
975
976 return !empty($matches) && (int)$matches[2] > 0
977 ? (int)$matches[2]
978 : null
979 ;
980 }
981
982 public function hasMoreEvents()
983 {
984 return !empty($this->nextPageToken);
985 }
986
990 private function hasExpiredSyncTokenError(): bool
991 {
992 return !empty($this->getExpiredSyncTokenError());
993 }
994
998 private function getExpiredSyncTokenError(): array
999 {
1000 return array_filter($this->syncTransport->getErrors(), function ($error) {
1001 return preg_match("/^\[(410)\][a-z0-9 _]*/i", $error['message']);
1002 });
1003 }
1004
1009 private function processResponseReceivingEvents(array $response): array
1010 {
1011 $this->setSyncSettings($response);
1012
1013 return $this->getEventsList($response['items']);
1014 }
1015
1019 private function getRequestParamsWithSyncToken(): array
1020 {
1021 return [
1022 'pageToken' => $this->nextPageToken,
1023 'syncToken' => $this->nextSyncToken,
1024 'showDeleted' => 'true',
1025 ];
1026 }
1027
1031 private function getRequestParamsForFirstSync(): array
1032 {
1033 return [
1034 'pageToken' => $this->nextPageToken,
1035 'showDeleted' => 'true',
1036 'maxResults' => self::SYNC_EVENTS_LIMIT,
1037 'timeMin' => (new Type\DateTime())->add(self::SYNC_EVENTS_DATE_INTERVAL)->format(self::DATE_TIME_FORMAT),
1038 ];
1039 }
1040
1045 private function runSyncEvents($gApiCalendarId)
1046 {
1047 $response = !empty($this->nextSyncToken)
1048 ? $this->syncTransport->getEvents($gApiCalendarId, $this->getRequestParamsWithSyncToken())
1049 : $this->syncTransport->getEvents($gApiCalendarId, $this->getRequestParamsForFirstSync());
1050
1051 if (!$response && $this->hasExpiredSyncTokenError())
1052 {
1053 return $this->syncTransport->getEvents($gApiCalendarId, $this->getRequestParamsForFirstSync());
1054 }
1055
1056 return $response;
1057 }
1058
1063 private function getEventsList(iterable $events = null): array
1064 {
1065 if (empty($events))
1066 return [];
1067
1068 $eventsList = [];
1069 foreach ($events as $event)
1070 {
1071 $preparedEvent = $this->prepareEvent($event);
1072 $eventsList[$preparedEvent['G_EVENT_ID']] = $preparedEvent;
1073 }
1074
1075 return $eventsList;
1076 }
1077
1081 private function setSyncSettings(array $response = null): void
1082 {
1083 $this->nextPageToken = $response['nextPageToken'] ?? '';
1084 $this->nextSyncToken = $response['nextSyncToken'] ?? '';
1085 $this->defaultReminderData = $response['defaultReminders'] ?? $this->defaultReminderData;
1086 $this->defaultTimezone = Util::isTimezoneValid($response['timeZone']) ? $response['timeZone'] : $this->defaultTimezone;
1087 }
1088
1094 private function prepareRemind($eventData): array
1095 {
1096 $reminders = [];
1097 $reminders['useDefault'] = false;
1098 $reminders['overrides'] = [];
1099
1100 if (!is_iterable($eventData['REMIND']))
1101 {
1102 return [];
1103 }
1104
1105 foreach ($eventData['REMIND'] as $remindRule)
1106 {
1107 $minutes = $remindRule['count'];
1108 if ($remindRule['type'] === 'hour')
1109 {
1110 $minutes = 60 * $remindRule['count'];
1111 }
1112 elseif ($remindRule['type'] === 'day')
1113 {
1114 $minutes = 24 * 60 * $remindRule['count'];
1115 }
1116 elseif ($remindRule['type'] === 'daybefore')
1117 {
1118 $dateFrom = Util::getDateObject(
1119 $eventData['DATE_FROM'],
1120 $eventData['DT_SKIP_TIME'] === 'Y',
1121 $eventData['TZ_FROM']
1122 );
1123
1124 $remind = clone $dateFrom;
1125 if (method_exists($remind, 'setTime'))
1126 {
1127 $remind->setTime(0, 0, 0);
1128 }
1129 $remind->add("-{$remindRule['before']} days")->add("{$remindRule['time']} minutes");
1130
1131 if ($dateFrom->getTimestamp() < $remind->getTimestamp())
1132 {
1133 continue;
1134 }
1135
1136 $minutes = $this->calculateMinutes($dateFrom, $remind);
1137 }
1138 elseif ($remindRule['type'] === 'date')
1139 {
1140 $dateFrom = Util::getDateObject(
1141 $eventData['DATE_FROM'],
1142 $eventData['DT_SKIP_TIME'] === 'Y',
1143 $eventData['TZ_FROM']
1144 );
1145 $remind = Util::getDateObject(
1146 $remindRule['value'],
1147 $eventData['DT_SKIP_TIME'] === 'Y',
1148 $eventData['TZ_FROM']
1149 );
1150
1151 if ($dateFrom->getTimestamp() < $remind->getTimestamp())
1152 {
1153 continue;
1154 }
1155
1156 $minutes = $this->calculateMinutes($dateFrom, $remind);
1157 }
1158
1159 $reminders['overrides'][] = [
1160 'minutes' => $minutes,
1161 'method' => 'popup',
1162 ];
1163 }
1164
1165 return $reminders;
1166 }
1167
1173 private function calculateMinutes(Type\Date $dateFrom, Type\Date $remind): int
1174 {
1175 $diff = $dateFrom->getDiff($remind);
1176 $days = $diff->format('%d');
1177 $hours = $diff->format('%h');
1178 $minutes = $diff->format('%i');
1179
1180 return ((int)$days * 24 * 60) + ((int)$hours * 60) + (int)$minutes;
1181 }
1182
1189 private function prepareRRule($event, $editNextEvents): array
1190 {
1191 $rule = [];
1192 $parsedRule = \CCalendarEvent::ParseRRULE($event['RRULE']);
1193 $rRule = 'RRULE:';
1194 $rRule .= 'FREQ=' .$parsedRule['FREQ'];
1195 $rRule .= !empty($parsedRule['INTERVAL']) ? ';INTERVAL=' . $parsedRule['INTERVAL'] : '';
1196 if (!empty($parsedRule['BYDAY']))
1197 {
1198 if (is_string($parsedRule['BYDAY']))
1199 {
1200 $rRule .= ';BYDAY=' . $parsedRule['BYDAY'];
1201 }
1202 elseif (is_array($parsedRule['BYDAY']))
1203 {
1204 $rRule .= ';BYDAY=' . implode(",", $parsedRule['BYDAY']);
1205 }
1206 else
1207 {
1208 $rRule = '';
1209 }
1210 }
1211
1212 if (!empty($parsedRule['COUNT']))
1213 {
1214 $rRule .= ';COUNT=' . $parsedRule['COUNT'];
1215 }
1216 elseif (!empty($parsedRule['UNTIL']))
1217 {
1218 $tsTo = Util::getDateObject($parsedRule['UNTIL']);
1219 if ($event['DT_SKIP_TIME'] === "N" && $tsTo->getTimestamp() < (new Type\Date(self::END_OF_DATE, "d.m.Y"))->getTimestamp())
1220 {
1221 $tsTo->add('+1 day');
1222 }
1223 $rRule .= ';UNTIL='.$tsTo->format('Ymd\THis\Z');
1224 }
1225
1226 $rule[] = $rRule;
1227
1228 if (!empty($event['EXDATE']) && $editNextEvents !== true)
1229 {
1230 $exDates = explode(';', $event['EXDATE']);
1231 foreach ($exDates as $exDate)
1232 {
1233 if ($event['DT_SKIP_TIME'] === "Y")
1234 {
1235 $rule[] = 'EXDATE;VALUE=DATE:' . date('Ymd', strtotime($exDate));
1236 }
1237 else
1238 {
1239 $rule[] = 'EXDATE;TZID=UTC:'
1240 . date("Ymd", strtotime($exDate))
1241 . Util::getDateObject($event['DATE_FROM'], false, $event['TZ_FROM'])
1242 ->setTimeZone(new \DateTimeZone('UTC'))->format('\\THis\\Z');
1243 }
1244 }
1245 }
1246
1247 return $rule;
1248 }
1249
1254 public function deleteCalendar(string $gApiCalendarId): void
1255 {
1256 $this->syncTransport->deleteCalendar($gApiCalendarId);
1257 }
1258
1262 private function getCalendarListParams(string $syncToken = null): array
1263 {
1264 if ($syncToken === null)
1265 {
1266 return [];
1267 }
1268
1269 return [
1270 'showDeleted' => 'true',
1271 'showHidden' => 'true',
1272 'syncToken' => $syncToken,
1273 ];
1274 }
1275
1281 public function updateCalendar(string $gApiCalendarId, array $calendarData): void
1282 {
1283 $this->syncTransport->updateCalendar($gApiCalendarId, $this->prepareCalendar($calendarData));
1284 }
1285
1292 public function updateCalendarList(string $gApiCalendarId, array $section): array
1293 {
1294 return $this->syncTransport->updateCalendarList($gApiCalendarId, $this->prepareCalendarList($section));
1295 }
1296
1297
1302 private function prepareCalendarList(array $calendar): array
1303 {
1304 $parameters = [];
1305
1306 if (isset($calendar['COLOR']))
1307 {
1308 $parameters['backgroundColor'] = $calendar['COLOR'];
1309 $parameters['foregroundColor'] = '#ffffff';
1310 }
1311
1312 $parameters['selected'] = 'true';
1313
1314 return $parameters;
1315 }
1316
1320 public function getNextPageToken(): string
1321 {
1322 return $this->nextPageToken;
1323 }
1324}
static isConnectionError(string $lastResult=null)
updateLastResultConnection(string $lastResult)
saveBatchEvents(array $events, string $gApiCalendarId, array $params)
__construct($userId=0, $connectionId=0)
stopChannel($channelId, $resourceId)
getCalendarItems(string $syncToken=null)
deleteEvent($eventId, $calendarId)
static getChannelOwner(string $channelId=null)
deleteCalendar(string $gApiCalendarId)
startWatchEventsChannel($calendarId='primary')
saveEvent($eventData, $calendarId, $parameters=[])
updateCalendarList(string $gApiCalendarId, array $section)
updateCalendar(string $gApiCalendarId, array $calendarData)
static getAttendees(array $codeAttendees=null, string $stringWrapper='')
Definition util.php:267
static prepareTimezone(?string $tz=null)
Definition util.php:75
static isTimezoneValid(?string $timeZone)
Definition util.php:66
static getDateObject(string $date=null, ?bool $fullDay=true, ?string $tz='UTC')
Definition util.php:102
static getCurrent()
Definition context.php:241
static getMessage($code, $replace=null, $language=null)
Definition loc.php:29