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