1C-Bitrix 25.700.0
Загрузка...
Поиск...
Не найдено
outgoingeventmanager.php
См. документацию.
1<?php
2
3namespace Bitrix\Calendar\Sync\Google;
4
5use Bitrix\Calendar\Core\Base\BaseException;
6use Bitrix\Calendar\Core\Base\Date;
7use Bitrix\Calendar\Sync\Connection\EventConnection;
8use Bitrix\Calendar\Sync\Dictionary;
9use Bitrix\Calendar\Sync\Entities\InstanceMap;
10use Bitrix\Calendar\Sync\Entities\SyncEvent;
11use Bitrix\Calendar\Sync\Entities\SyncEventMap;
12use Bitrix\Calendar\Sync\Entities\SyncSection;
13use Bitrix\Calendar\Sync\Entities\SyncSectionMap;
14use Bitrix\Calendar\Sync\Google\Builders\BuilderEventConnectionFromExternalEvent;
15use Bitrix\Calendar\Sync\Managers\OutgoingEventManagerInterface;
16use Bitrix\Calendar\Sync\Util\EventContext;
17use Bitrix\Calendar\Sync\Util\Result;
18use Bitrix\Calendar\Util;
19use Bitrix\Main\ArgumentException;
20use Bitrix\Main\LoaderException;
21use Bitrix\Main\ObjectException;
22use Bitrix\Main\Web\HttpClient;
23use Bitrix\Main\Web\Json;
24use DateTimeInterface;
25use LogicException;
26
28{
29 public const LINE_SEPARATOR = "\r\n";
30 public const BATCH_PATH = 'https://www.googleapis.com/batch/calendar/v3/';
31 public const CHUNK_LENGTH = 50;
32
43 public function export(SyncEventMap $syncEventMap, SyncSectionMap $syncSectionMap): Result
44 {
45 $result = new Result();
46 $result->setData([
47 'syncEventMap' => $syncEventMap,
48 ]);
49
50 $syncEventListForExport = [];
51 $delayExportSyncEventList = [];
52
54 foreach ($syncEventMap as $syncEvent)
55 {
56 if (
57 $syncEvent->getEventConnection()
58 && ($syncEvent->getEvent()->getVersion() === $syncEvent->getEventConnection()->getVersion())
59 )
60 {
61 continue;
62 }
63
64 if (
65 $syncEvent->isRecurrence()
66 && $instanceMap = $syncEvent->getInstanceMap()
67 )
68 {
69 if (empty($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()]))
70 {
71 $delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()] = [];
72 }
73
74 array_push($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()], ...$instanceMap);
75 }
76
77 $syncEventListForExport[$syncEvent->getEvent()->getSection()->getId()][$syncEvent->getEvent()->getUid()] = $syncEvent;
78 }
79
81 foreach ($syncSectionMap as $syncSection)
82 {
83 if ($syncEventList = ($syncEventListForExport[$syncSection->getSection()->getId()] ?? null))
84 {
85 $this->exportBatch(
86 $syncEventList,
87 $syncSection,
88 $delayExportSyncEventList[$syncSection->getSection()->getId()] ?? null
89 );
90 }
91 }
92
93 return new Result();
94 }
95
102 private function exportBatch(array $syncEventList, SyncSection $syncSection, ?array $syncEventInstanceList = null): void
103 {
104
105 // single or recurrence
106 foreach (array_chunk($syncEventList, self::CHUNK_LENGTH) as $batch)
107 {
108 $body = $this->prepareMultipartMixed($batch, $syncSection);
109 // TODO: Remake it: move this logic to parent::request().
110 // Or, better, in separate class.
111 $this->httpClient->post(self::BATCH_PATH, $body);
112
113 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
114 }
115
116 // instances
117 if ($syncEventInstanceList !== null)
118 {
119 foreach (array_chunk($syncEventInstanceList, self::CHUNK_LENGTH) as $batch)
120 {
121 $body = $this->prepareMultipartMixed($batch, $syncSection, $syncEventList);
122 // TODO: Remake it: move this logic to parent::request().
123 // Or, better, in separate class.
124 $this->httpClient->post(self::BATCH_PATH, $body);
125
126 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
127 }
128 }
129 }
130
136 private function prepareMultipartMixed(
137 array $eventCollection,
138 SyncSection $syncSection,
139 array $syncEventList = []
140 ): string
141 {
142 $boundary = $this->generateBoundary();
143 $this->setContentTypeHeader($boundary);
144 $data = implode('', $this->getBatchItemList(
145 $eventCollection,
146 $syncSection,
147 $syncEventList,
148 $boundary,
149 ));
150 $data .= "--{$boundary}--" . self::LINE_SEPARATOR;
151
152 return $data;
153 }
154
159 private function calculateHttpMethod(SyncEvent $syncEvent): string
160 {
161 if (
162 $syncEvent->isInstance()
163 || ($syncEvent->getEventConnection() && $syncEvent->getEventConnection()->getVendorEventId())
164 )
165 {
166 return HttpClient::HTTP_PUT;
167 }
168
169 if ($syncEvent->getAction() === Dictionary::SYNC_EVENT_ACTION['delete'])
170 {
171 return HttpClient::HTTP_DELETE;
172 }
173
174 return HttpClient::HTTP_POST;
175 }
176
182 private function multipartDecode($response, array $syncEventList): void
183 {
184 $boundary = $this->httpClient->getClient()->getHeaders()->getBoundary();
185
186 $response = str_replace("--$boundary--", "--$boundary", $response);
187 $parts = explode("--$boundary" . self::LINE_SEPARATOR, $response);
188
189 foreach ($parts as $part)
190 {
191 $part = trim($part);
192 if (!empty($part))
193 {
194 $partEvent = explode(self::LINE_SEPARATOR . self::LINE_SEPARATOR, $part);
195 $data = $this->getMetaInfo($partEvent[1]);
196
197
198 $eventId = $this->getId($partEvent[0]);
199 if ($eventId === null)
200 {
201 continue;
202 }
203
204 try
205 {
206 $parsedData = Json::decode($partEvent[2]);
207 }
208 catch (ArgumentException $e)
209 {
210 continue;
211 }
212
213 if ($data['status'] === 200)
214 {
215
217 $syncEvent = $syncEventList[$parsedData['iCalUID']];
218
219 if ($syncEvent === null)
220 {
221 continue;
222 }
223
224 if ($syncEvent->hasInstances() && isset($parsedData['originalStartTime']))
225 {
226 $syncEvent = $this->getInstanceByOriginalDate($syncEvent, $parsedData);
227
228 // TODO: it's workaround to skip errors
229 if ($syncEvent === null)
230 {
231 continue;
232 }
233 }
234
235 $eventConnection = (new BuilderEventConnectionFromExternalEvent(
236 $parsedData,
237 $syncEvent,
238 $this->connection
239 ))->build();
240
241 $syncEvent
242 ->setEventConnection($eventConnection)
243 ->setAction(Dictionary::SYNC_EVENT_ACTION['success'])
244 ;
245 }
246 elseif (isset($parsedData['error']['code'], $parsedData['error']['message']))
247 {
248 return;
249 }
250 }
251 }
252 }
253
258 private function getMetaInfo($headers): array
259 {
260
261 $data = [];
262 foreach (explode("\n", $headers) as $k => $header)
263 {
264 if($k === 0 && preg_match('#HTTP\S+ (\d+)#', $header, $find))
265 {
266 $data['status'] = (int)$find[1];
267 continue;
268 }
269
270 if(mb_strpos($header, ':') !== false)
271 {
272 [$headerName, $headerValue] = explode(':', $header, 2);
273 if(mb_strtolower($headerName) === 'etag')
274 {
275 $data['etag'] = trim($headerValue);
276 }
277 }
278 }
279
280 return $data;
281 }
282
287 private function getId($headers): ?int
288 {
289 foreach (explode("\n", $headers) as $header)
290 {
291 if(mb_strpos($header, ':') !== false)
292 {
293 [$headerName, $headerValue] = explode(':', $header, 2);
294 if(mb_strtolower($headerName) === 'content-id')
295 {
296 $part = explode(':', $headerValue);
297 return (int)rtrim($part[1], '>');
298 }
299 }
300 }
301
302 return null;
303 }
304
312 private function prepareEventContextForInstance(
313 ?SyncEvent $masterEvent,
314 SyncEvent $syncEvent,
315 EventContext $eventContext
316 ): void
317 {
319 if ($masterEvent && $masterEvent->isSuccessAction())
320 {
321 $masterVendorEventId = $masterEvent->getVendorEventId();
322 }
323 else
324 {
325 //todo handle instance. possible write to log
326 return;
327 }
328
329 $prefix = $syncEvent->getEvent()->isFullDayEvent()
330 ? $syncEvent->getEvent()->getOriginalDateFrom()->format('Ymd')
331 : $syncEvent->getEvent()->getOriginalDateFrom()->setTimeZone(Util::prepareTimezone())->format('Ymd\THis\Z')
332 ;
333 $eventContext->setEventConnection(
334 (new EventConnection())
335 ->setVendorEventId(
336 $masterVendorEventId
337 . '_'
338 . $prefix
339 )
340 ->setRecurrenceId($masterVendorEventId)
341 );
342 }
343
348 private function getEventConverter(SyncEvent $syncEvent): EventConverter
349 {
350 return new EventConverter(
351 $syncEvent->getEvent(),
352 $syncEvent->getEventConnection(),
353 $syncEvent->getInstanceMap()
354 );
355 }
356
362 private function getInstanceByOriginalDate(SyncEvent $masterEvent, $event): ?SyncEvent
363 {
364 if (isset($event['originalStartTime']['dateTime']))
365 {
366 $eventOriginalStart = Date::createDateTimeFromFormat(
367 $event['originalStartTime']['dateTime'],
368 DateTimeInterface::ATOM
369 );
370 }
371 elseif (isset($event['originalStartTime']['date']))
372 {
373 $eventOriginalStart = Date::createDateFromFormat(
374 $event['originalStartTime']['date'],
375 Helper::DATE_FORMAT
376 );
377 }
378
379 return $masterEvent
380 ->getInstanceMap()
381 ->getItem(InstanceMap::getKeyByDate($eventOriginalStart));
382 }
383
389 private function prepareEventForInstance(SyncEvent $masterEvent, SyncEvent $syncEvent): void
390 {
391 if ($syncEvent->getEvent()->getVersion() < $masterEvent->getEvent()->getVersion())
392 {
393 $syncEvent->getEvent()->setVersion($masterEvent->getEvent()->getVersion());
394 }
395 }
396
406 private function prepareContextForHttpQuery(
407 SyncEvent $syncEvent,
408 SyncSection $syncSection,
409 array $syncEventList,
410 EventManager $eventManager
411 ): array
412 {
413 $method = $this->calculateHttpMethod($syncEvent);
414
415 $eventContext = (new EventContext())->setSectionConnection($syncSection->getSectionConnection());
416 if ($syncEvent->isInstance())
417 {
418 if ($eventConnection = $syncEvent->getEventConnection())
419 {
420 $eventContext->setEventConnection($eventConnection);
421 }
422 else
423 {
424 $this->prepareEventContextForInstance($syncEventList[$syncEvent->getUid()], $syncEvent, $eventContext);
425 $this->prepareEventForInstance($syncEventList[$syncEvent->getUid()], $syncEvent);
426 $syncEvent->setEventConnection($eventContext->getEventConnection());
427 }
428
429 if (
430 ($eventContext->getSectionConnection() === null)
431 || ($eventContext->getEventConnection() === null)
432 )
433 {
434 throw new LogicException('you should set event or section info');
435 }
436
437 $methodHeader = $method . ' ' . $eventManager->prepareUpdateUrl($eventContext) . self::LINE_SEPARATOR;
438 $converter = $this->getEventConverter($syncEvent);
439 $vendorEvent = $converter->convertForUpdate();
440 }
441 elseif ($syncEvent->getEventConnection() !== null)
442 {
443 $eventContext->setEventConnection($syncEvent->getEventConnection());
444 $methodHeader = $method . ' ' . $eventManager->prepareUpdateUrl($eventContext) . self::LINE_SEPARATOR;
445 $converter = $this->getEventConverter($syncEvent);
446 $vendorEvent = $converter->convertForUpdate();
447 }
449 {
450 $methodHeader = $method . ' ' . $eventManager->prepareCreateUrl($eventContext) . self::LINE_SEPARATOR;
451 $converter = $this->getEventConverter($syncEvent);
452 $vendorEvent = $converter->convertForCreate();
453 }
454 else
455 {
456 throw new LogicException('do not detect action');
457 }
458
459 return [$methodHeader, $vendorEvent];
460 }
461
470 private function prepareBatchItem(
471 string $boundary,
472 SyncEvent $syncEvent,
473 array $vendorEvent,
474 string $methodHeader
475 ): string
476 {
477 $data = '--' . $boundary . self::LINE_SEPARATOR;
478
479 $data .= 'Content-Type: application/http' . self::LINE_SEPARATOR;
480
481 $id = $syncEvent->getEvent()->getId();
482 $data .= "Content-ID: item{$id}:{$id}" . self::LINE_SEPARATOR . self::LINE_SEPARATOR;
483
484 $content = Json::encode($vendorEvent, JSON_UNESCAPED_SLASHES);
485
486 $data .= $methodHeader;
487 $data .= 'Content-type: application/json' . self::LINE_SEPARATOR;
488 $data .= 'Content-Length: ' . mb_strlen($content) . self::LINE_SEPARATOR . self::LINE_SEPARATOR;
489
490 $data .= $content;
491 $data .= self::LINE_SEPARATOR . self::LINE_SEPARATOR;
492
493 return $data;
494 }
495
508 private function getBatchItemList(
509 array $eventCollection,
510 SyncSection $syncSection,
511 array $syncEventList,
512 string $boundary
513 ): array
514 {
515 $batchItems = [];
516 /*** @var SyncEvent $syncEvent */
517 foreach ($eventCollection as $syncEvent)
518 {
519 try
520 {
521 $eventManager = new EventManager($this->connection, $this->userId);
522 [$methodHeader, $vendorEvent] = $this->prepareContextForHttpQuery(
523 $syncEvent,
524 $syncSection,
525 $syncEventList,
527 );
528
529 $batchItems[] = $this->prepareBatchItem($boundary, $syncEvent, $vendorEvent, $methodHeader);
530 }
531 catch (LogicException $e)
532 {
533 // $syncEvent->setAction($this->calculateAction($syncEvent));
534 continue;
535 }
536 }
537
538 return $batchItems;
539 }
540
544 private function generateBoundary(): string
545 {
546 return 'BXC' . md5(mt_rand() . time());
547 }
548
553 private function setContentTypeHeader(string $boundary): void
554 {
555 $this->httpClient->getClient()->setHeader('Content-type', 'multipart/mixed; boundary=' . $boundary);
556 }
557
563 private function findSyncEvent(array $syncEventList, int $eventId): array
564 {
565 return array_filter($syncEventList, function (SyncEvent $syncEvent) use ($eventId) {
566 if ($syncEvent->getEventId() === $eventId)
567 {
568 return true;
569 }
570
571 if ($syncEvent->hasInstances())
572 {
574 foreach ($syncEvent->getInstanceMap() as $instance)
575 {
576 if ($syncEvent->getEventId() === $eventId)
577 {
578 return true;
579 }
580 }
581 }
582
583 return false;
584 });
585 }
586
587 private function calculateLastSyncStatusForFailedSyncEvent(SyncEvent $syncEvent, array $error)
588 {
589 if ($error['code'] === 404)
590 {
591 if (
592 ($error['message'] === 'Not Found')
593 && $syncEvent->getAction() === Dictionary::SYNC_EVENT_ACTION['update']
594 )
595 {
596 $syncEvent->getEventConnection()->setLastSyncStatus(Dictionary::SYNC_STATUS['create']);
597 }
598 }
599 }
600
601 // /**
602 // * @param SyncEvent $syncEvent
603 // * @return void
604 // */
605 // private function calculateAction(SyncEvent $syncEvent)
606 // {
607 // $syncEvent->setAction(Dictionary::SYNC_EVENT_ACTION['success']);
608 // }
609}
const SYNC_EVENT_ACTION
Определения dictionary.php:47
const SYNC_STATUS
Определения dictionary.php:16
static prepareTimezone(?string $tz=null)
Определения util.php:80
$content
Определения commerceml.php:144
$data['IS_AVAILABLE']
Определения .description.php:13
</td ></tr ></table ></td ></tr >< tr >< td class="bx-popup-label bx-width30"><?=GetMessage("PAGE_NEW_TAGS")?> array( $site)
Определения file_new.php:804
$result
Определения get_property_values.php:14
export(Sync\Entities\SyncEventMap $syncEventMap, Sync\Entities\SyncSectionMap $syncSectionMap)
$event
Определения prolog_after.php:141
if( $daysToExpire >=0 &&$daysToExpire< 60 elseif)( $daysToExpire< 0)
Определения prolog_main_admin.php:393
$instance
Определения ps_b24_final.php:14
$response
Определения result.php:21
$method
Определения index.php:27
$eventManager
Определения include.php:412
$k
Определения template_pdf.php:567