Calendar::updateComponent()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 19
ccs 8
cts 9
cp 0.8889
rs 9.7333
cc 3
nc 4
nop 0
crap 3.0123
1
<?php
2
/**
3
 * CalDav calendar file.
4
 *
5
 * @package Integration
6
 *
7
 * @see   https://tools.ietf.org/html/rfc5545
8
 *
9
 * @package Integration
10
 *
11
 * @copyright YetiForce S.A.
12
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 */
15
16
namespace App\Integrations\Dav;
17
18
use Sabre\VObject;
19
20
/**
21
 *  CalDav calendar class.
22
 */
23
class Calendar
24
{
25
	/**
26
	 * Record model instance.
27
	 *
28
	 * @var \Vtiger_Record_Model[]
29
	 */
30
	private $records = [];
31
	/**
32
	 * Record data.
33
	 *
34
	 * @var \Vtiger_Record_Model
35
	 */
36
	private $record = [];
37
	/**
38
	 * VCalendar object.
39
	 *
40
	 * @var \Sabre\VObject\Component\VCalendar
41
	 */
42
	private $vcalendar;
43
	/**
44
	 * @var \Sabre\VObject\Component
45
	 */
46
	private $vcomponent;
47
	/**
48
	 * Optimization for creating a time zone.
49
	 *
50
	 * @var bool
51
	 */
52
	private $createdTimeZone = false;
53
	/**
54
	 * Custom values.
55
	 *
56
	 * @var string[]
57
	 */
58
	protected static $customValues = [
59
		'X-GOOGLE-CONFERENCE' => 'meeting_url',
60
		'X-MS-OLK-MWSURL' => 'meeting_url',
61
		'X-MICROSOFT-SKYPETEAMSMEETINGURL' => 'meeting_url',
62
		'X-MICROSOFT-ONLINEMEETINGCONFLINK' => 'meeting_url',
63
		'X-MICROSOFT-ONLINEMEETINGEXTERNALLINK' => 'meeting_url',
64
	];
65
	/**
66
	 * Max date.
67
	 *
68
	 * @var string
69
	 */
70
	const MAX_DATE = '2038-01-01';
71
72
	/**
73
	 * Delete calendar event by crm id.
74
	 *
75
	 * @param int $id
76
	 *
77
	 * @throws \yii\db\Exception
78
	 */
79
	public static function deleteByCrmId(int $id)
80
	{
81
		$dbCommand = \App\Db::getInstance()->createCommand();
82
		$dataReader = (new \App\Db\Query())->select(['calendarid'])->from('dav_calendarobjects')->where(['crmid' => $id])->createCommand()->query();
83
		$dbCommand->delete('dav_calendarobjects', ['crmid' => $id])->execute();
84
		while ($calendarId = $dataReader->readColumn(0)) {
85
			static::addChange($calendarId, $id . '.vcf', 3);
86
		}
87
		$dataReader->close();
88
	}
89
90 1
	/**
91
	 * Dav delete.
92 1
	 *
93 1
	 * @param array $calendar
94 1
	 *
95 1
	 * @throws \yii\db\Exception
96 1
	 */
97 1
	public static function delete(array $calendar)
98 1
	{
99 1
		static::addChange($calendar['calendarid'], $calendar['uri'], 3);
100 1
		\App\Db::getInstance()->createCommand()->delete('dav_calendarobjects', ['id' => $calendar['id']])->execute();
101 1
	}
102 1
103 1
	/**
104 1
	 * Add change to calendar.
105
	 *
106
	 * @param int    $calendarId
107
	 * @param string $uri
108
	 * @param int    $operation
109
	 *
110
	 * @throws \yii\db\Exception
111
	 */
112
	public static function addChange(int $calendarId, string $uri, int $operation)
113 1
	{
114
		$dbCommand = \App\Db::getInstance()->createCommand();
115 1
		$calendar = static::getCalendar($calendarId);
116
		$dbCommand->insert('dav_calendarchanges', [
117
			'uri' => $uri,
118
			'synctoken' => (int) $calendar['synctoken'],
119
			'calendarid' => $calendarId,
120
			'operation' => $operation,
121
		])->execute();
122
		$dbCommand->update('dav_calendars', [
123
			'synctoken' => ((int) $calendar['synctoken']) + 1,
124
		], ['id' => $calendarId])->execute();
125
	}
126
127
	/**
128
	 * Get calendar.
129
	 *
130
	 * @param int $id
131
	 *
132
	 * @return array
133
	 */
134
	public static function getCalendar(int $id)
135
	{
136
		return (new \App\Db\Query())->from('dav_calendars')->where(['id' => $id])->one();
0 ignored issues
show
Bug Best Practice introduced by
The expression return new App\Db\Query(...ay('id' => $id))->one() could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
137
	}
138
139 1
	/**
140
	 * Create instance from dav data.
141 1
	 *
142 1
	 * @param string $calendar
143 1
	 *
144 1
	 * @return \App\Integrations\Dav\Calendar
145 1
	 */
146
	public static function loadFromDav(string $calendar)
147
	{
148
		$instance = new self();
149
		$instance->record = \Vtiger_Record_Model::getCleanInstance('Calendar');
150
		$instance->vcalendar = VObject\Reader::read($calendar, \Sabre\VObject\Reader::OPTION_FORGIVING);
151
		foreach ($instance->vcalendar->children() as $child) {
152
			if (!$child instanceof VObject\Component) {
153 1
				continue;
154
			}
155 1
			if ('VTIMEZONE' === $child->name) {
156 1
				continue;
157
			}
158
			if (empty($instance->vcomponent)) {
159
				$instance->vcomponent = $child;
160
			}
161
		}
162
		return $instance;
163
	}
164
165
	/**
166
	 * Create empty instance.
167
	 *
168
	 * @return \App\Integrations\Dav\Calendar
169
	 */
170
	public static function createEmptyInstance()
171
	{
172
		$instance = new self();
173
		$instance->record = \Vtiger_Record_Model::getCleanInstance('Calendar');
174
		$instance->vcalendar = new VObject\Component\VCalendar();
175
		$instance->vcalendar->PRODID = '-//YetiForce//YetiForceCRM V' . \App\Version::get() . '//';
176
		return $instance;
177
	}
178
179
	/**
180
	 * Load record data.
181
	 *
182
	 * @param array $data
183
	 */
184
	public function loadFromArray(array $data)
185
	{
186
		$this->record->setData($data);
187
	}
188
189
	/**
190
	 * Create a class instance by crm id.
191
	 *
192
	 * @param int    $record
193
	 * @param string $uid
194
	 *
195
	 * @return bool
196
	 */
197
	public function getByRecordId(int $record, string $uid)
198
	{
199 1
		\App\Log::trace($record, __METHOD__);
200
		if ($record) {
201 1
			$this->records[$uid] = \Vtiger_Record_Model::getInstanceById($record, 'Calendar');
202
		}
203
		return $this->getByRecordInstance()[$uid] ?? false;
0 ignored issues
show
Bug introduced by
The method getByRecordInstance() does not exist on App\Integrations\Dav\Calendar. Did you maybe mean getRecordInstance()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

203
		return $this->/** @scrutinizer ignore-call */ getByRecordInstance()[$uid] ?? false;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
204
	}
205
206
	/**
207
	 * Create a class instance from vcalendar content.
208
	 *
209
	 * @param string                    $content
210
	 * @param \Vtiger_Record_Model|null $recordModel
211
	 * @param ?string                   $uid
212
	 *
213
	 * @return \App\Integrations\Dav\Calendar
214
	 */
215
	public static function loadFromContent(string $content, ?\Vtiger_Record_Model $recordModel = null, ?string $uid = null)
216
	{
217
		$instance = new self();
218
		$instance->vcalendar = VObject\Reader::read($content, \Sabre\VObject\Reader::OPTION_FORGIVING);
219
		if ($recordModel && $uid) {
220
			$instance->records[$uid] = $recordModel;
221
		}
222
		return $instance;
223
	}
224
225
	/**
226
	 * Get VCalendar instance.
227
	 *
228
	 * @return \Sabre\VObject\Component\VCalendar
229
	 */
230
	public function getVCalendar()
231
	{
232
		return $this->vcalendar;
233
	}
234
235
	/**
236
	 * Get calendar component instance.
237
	 *
238
	 * @return \Sabre\VObject\Component
239
	 */
240
	public function getComponent()
241
	{
242
		return $this->vcomponent;
243
	}
244
245
	/**
246
	 * Get record instance.
247
	 *
248
	 * @return \Vtiger_Record_Model[]
249
	 */
250
	public function getRecordInstance()
251
	{
252
		foreach ($this->vcalendar->getBaseComponents() ?: $this->vcalendar->getComponents() as $component) {
253
			$type = (string) $component->name;
254
			if ('VTODO' === $type || 'VEVENT' === $type) {
255
				$this->vcomponent = $component;
256
				$this->parseComponent();
257
			}
258
		}
259
		return $this->records;
260
	}
261
262
	/**
263
	 * Parse component.
264
	 *
265
	 * @return void
266
	 */
267
	private function parseComponent(): void
268
	{
269
		$uid = (string) $this->vcomponent->UID;
270
		if (isset($this->records[$uid])) {
271
			$this->record = $this->records[$uid];
272
		} else {
273
			$this->record = $this->records[$uid] = \Vtiger_Record_Model::getCleanInstance('Calendar');
274
		}
275
		$this->parseText('subject', 'SUMMARY');
276
		$this->parseText('location', 'LOCATION');
277
		$this->parseText('description', 'DESCRIPTION');
278
		$this->parseStatus();
279
		$this->parsePriority();
280
		$this->parseVisibility();
281
		$this->parseState();
282
		$this->parseType();
283
		$this->parseDateTime();
284
		$this->parseCustomValues();
285
	}
286
287
	/**
288
	 * Parse simple text.
289
	 *
290
	 * @param string               $fieldName
291
	 * @param string               $davName
292
	 * @param \Vtiger_Record_Model $recordModel
293
	 *
294
	 * @return void
295
	 */
296
	private function parseText(string $fieldName, string $davName): void
297
	{
298
		$separator = '-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::-';
299
		$value = (string) $this->vcomponent->{$davName};
300
		if (false !== strpos($value, $separator)) {
301
			[$html,$text] = explode($separator, $value, 2);
302
			$value = trim(strip_tags($html)) . "\n" . \trim(\str_replace($separator, '', $text));
303
		} else {
304
			$value = trim(\str_replace('\n', PHP_EOL, $value));
305
		}
306
		$value = \App\Purifier::decodeHtml(\App\Purifier::purify($value));
307
		if ($length = $this->record->getField($fieldName)->getMaxValue()) {
308
			$value = \App\TextUtils::textTruncate($value, $length, false);
309
		}
310
		$this->record->set($fieldName, \trim($value));
311
	}
312
313
	/**
314
	 * Parse status.
315
	 *
316
	 * @return void
317
	 */
318
	private function parseStatus(): void
319
	{
320
		$davValue = null;
321
		if (isset($this->vcomponent->STATUS)) {
322
			$davValue = strtoupper($this->vcomponent->STATUS->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

322
			$davValue = strtoupper($this->vcomponent->STATUS->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
323
		}
324
		if ('VEVENT' === (string) $this->vcomponent->name) {
325
			$values = [
326
				'TENTATIVE' => 'PLL_PLANNED',
327
				'CANCELLED' => 'PLL_CANCELLED',
328
				'CONFIRMED' => 'PLL_PLANNED',
329
			];
330
		} else {
331
			$values = [
332
				'NEEDS-ACTION' => 'PLL_PLANNED',
333
				'IN-PROCESS' => 'PLL_IN_REALIZATION',
334
				'CANCELLED' => 'PLL_CANCELLED',
335
				'COMPLETED' => 'PLL_COMPLETED',
336
			];
337
		}
338
		$value = reset($values);
339
		if ($davValue && isset($values[$davValue])) {
340
			$value = $values[$davValue];
341
		}
342
		$this->record->set('activitystatus', $value);
343
	}
344
345
	/**
346
	 * Parse visibility.
347
	 *
348
	 * @return void
349
	 */
350
	private function parseVisibility(): void
351
	{
352
		$davValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $davValue is dead and can be removed.
Loading history...
353
		$value = 'Private';
354
		if (isset($this->vcomponent->CLASS)) {
355
			$davValue = strtoupper($this->vcomponent->CLASS->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

355
			$davValue = strtoupper($this->vcomponent->CLASS->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
356
			$values = [
357
				'PUBLIC' => 'Public',
358
				'PRIVATE' => 'Private',
359
			];
360
			if ($davValue && isset($values[$davValue])) {
361
				$value = $values[$davValue];
362
			}
363
		}
364
		$this->record->set('visibility', $value);
365
	}
366
367
	/**
368
	 * Parse state.
369
	 *
370
	 * @return void
371
	 */
372
	private function parseState(): void
373
	{
374
		$davValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $davValue is dead and can be removed.
Loading history...
375
		$value = '';
376
		if (isset($this->vcomponent->TRANSP)) {
377
			$davValue = strtoupper($this->vcomponent->TRANSP->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

377
			$davValue = strtoupper($this->vcomponent->TRANSP->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
378
			$values = [
379
				'OPAQUE' => 'PLL_OPAQUE',
380
				'TRANSPARENT' => 'PLL_TRANSPARENT',
381
			];
382
			if ($davValue && isset($values[$davValue])) {
383
				$value = $values[$davValue];
384
			}
385
		}
386
		$this->record->set('state', $value);
387
	}
388
389
	/**
390
	 * Parse priority.
391
	 *
392
	 * @return void
393
	 */
394
	private function parsePriority(): void
395
	{
396
		$davValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $davValue is dead and can be removed.
Loading history...
397
		$value = 'Medium';
398
		if (isset($this->vcomponent->PRIORITY)) {
399
			$davValue = strtoupper($this->vcomponent->PRIORITY->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

399
			$davValue = strtoupper($this->vcomponent->PRIORITY->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
400
			$values = [
401
				1 => 'High',
402
				5 => 'Medium',
403
				9 => 'Low',
404
			];
405
			if ($davValue && isset($values[$davValue])) {
406
				$value = $values[$davValue];
407
			}
408
		}
409
		$this->record->set('taskpriority', $value);
410
	}
411
412
	/**
413
	 * Parse type.
414
	 *
415
	 * @return void
416
	 */
417
	private function parseType(): void
418
	{
419
		if ($this->record->isEmpty('activitytype')) {
420
			$this->record->set('activitytype', 'VTODO' === (string) $this->vcomponent->name ? 'Task' : 'Meeting');
421
		}
422
	}
423 1
424
	/**
425 1
	 * Parse date time.
426 1
	 *
427 1
	 * @return void
428 1
	 */
429 1
	private function parseDateTime(): void
430 1
	{
431
		$allDay = 0;
432
		$endHasTime = $startHasTime = false;
433
		$endField = 'VEVENT' === ((string) $this->vcomponent->name) ? 'DTEND' : 'DUE';
434
		if (isset($this->vcomponent->DTSTART)) {
435
			$timeStamp = $this->vcomponent->DTSTART->getDateTime()->getTimeStamp();
0 ignored issues
show
Bug introduced by
The method getDateTime() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

435
			$timeStamp = $this->vcomponent->DTSTART->/** @scrutinizer ignore-call */ getDateTime()->getTimeStamp();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getDateTime() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\DateTime or Sabre\VObject\Property\VCard\DateAndOrTime. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

435
			$timeStamp = $this->vcomponent->DTSTART->/** @scrutinizer ignore-call */ getDateTime()->getTimeStamp();
Loading history...
436
			$dateStart = date('Y-m-d', $timeStamp);
437
			$timeStart = date('H:i:s', $timeStamp);
438 1
			$startHasTime = $this->vcomponent->DTSTART->hasTime();
0 ignored issues
show
Bug introduced by
The method hasTime() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\DateTime. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

438
			/** @scrutinizer ignore-call */ 
439
   $startHasTime = $this->vcomponent->DTSTART->hasTime();
Loading history...
439
		} else {
440 1
			$timeStamp = $this->vcomponent->DTSTAMP->getDateTime()->getTimeStamp();
0 ignored issues
show
Bug introduced by
The method getDateTime() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

440
			$timeStamp = $this->vcomponent->DTSTAMP->/** @scrutinizer ignore-call */ getDateTime()->getTimeStamp();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
441 1
			$dateStart = date('Y-m-d', $timeStamp);
442 1
			$timeStart = date('H:i:s', $timeStamp);
443 1
		}
444 1
		if (isset($this->vcomponent->{$endField})) {
445 1
			$timeStamp = $this->vcomponent->{$endField}->getDateTime()->getTimeStamp();
446 1
			$endHasTime = $this->vcomponent->{$endField}->hasTime();
447 1
			$dueDate = date('Y-m-d', $timeStamp);
448 1
			$timeEnd = date('H:i:s', $timeStamp);
449 1
			if (!$endHasTime) {
450 1
				$endTime = strtotime('-1 day', strtotime("$dueDate $timeEnd"));
451 1
				$dueDate = date('Y-m-d', $endTime);
452
				$timeEnd = date('H:i:s', $endTime);
453 1
			}
454 1
		} else {
455
			$endTime = strtotime('+1 day', strtotime("$dateStart $timeStart"));
456
			$dueDate = date('Y-m-d', $endTime);
457
			$timeEnd = date('H:i:s', $endTime);
458 1
		}
459
		if (!$startHasTime && !$endHasTime && \App\User::getCurrentUserId()) {
460
			$allDay = 1;
461
			$currentUser = \App\User::getCurrentUserModel();
462
			$userTimeZone = new \DateTimeZone($currentUser->getDetail('time_zone'));
463
			$sysTimeZone = new \DateTimeZone(\App\Fields\DateTime::getTimeZone());
464
			[$hour , $minute] = explode(':', $currentUser->getDetail('start_hour'));
465
			$date = new \DateTime('now', $userTimeZone);
466
			$date->setTime($hour, $minute);
0 ignored issues
show
Bug introduced by
$minute of type string is incompatible with the type integer expected by parameter $minute of DateTime::setTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

466
			$date->setTime($hour, /** @scrutinizer ignore-type */ $minute);
Loading history...
Bug introduced by
$hour of type string is incompatible with the type integer expected by parameter $hour of DateTime::setTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

466
			$date->setTime(/** @scrutinizer ignore-type */ $hour, $minute);
Loading history...
467
			$date->setTimezone($sysTimeZone);
468 1
			$timeStart = $date->format('H:i:s');
469
470 1
			$date->setTimezone($userTimeZone);
471 1
			[$hour , $minute] = explode(':', $currentUser->getDetail('end_hour'));
472
			$date->setTime($hour, $minute);
473
			$date->setTimezone($sysTimeZone);
474
			$timeEnd = $date->format('H:i:s');
475
		}
476
		$this->record->set('allday', $allDay);
477 1
		$this->record->set('date_start', $dateStart);
478 1
		$this->record->set('due_date', $dueDate);
479
		$this->record->set('time_start', $timeStart);
480 1
		$this->record->set('time_end', $timeEnd);
481
	}
482
483
	/**
484
	 * Parse parse custom values.
485 1
	 *
486
	 * @return void
487 1
	 */
488 1
	private function parseCustomValues(): void
489
	{
490 1
		foreach (self::$customValues as $key => $fieldName) {
491
			if (isset($this->vcomponent->{$key})) {
492
				$this->record->set($fieldName, (string) $this->vcomponent->{$key});
493
			}
494
		}
495
	}
496
497
	/**
498 1
	 * Create calendar entry component.
499
	 *
500
	 * @return \Sabre\VObject\Component
501
	 */
502
	public function createComponent()
503
	{
504
		$componentType = 'Task' === $this->record->get('activitytype') ? 'VTODO' : 'VEVENT';
505
		$this->vcomponent = $this->vcalendar->createComponent($componentType);
506 1
		$this->vcomponent->UID = \str_replace('sabre-vobject', 'YetiForceCRM', (string) $this->vcomponent->UID);
507 1
		$this->updateComponent();
508
		$this->vcalendar->add($this->vcomponent);
509
		return $this->vcomponent;
510
	}
511 1
512
	/**
513
	 * Update calendar entry component.
514 1
	 *
515
	 * @throws \Sabre\VObject\InvalidDataException
516 1
	 */
517
	public function updateComponent()
518
	{
519
		$this->createDateTime();
520
		$this->createText('subject', 'SUMMARY');
521 1
		$this->createText('location', 'LOCATION');
522
		$this->createText('description', 'DESCRIPTION');
523 1
		$this->createStatus();
524
		$this->createVisibility();
525 1
		$this->createState();
526
		$this->createPriority();
527
		if (empty($this->vcomponent->CREATED)) {
528 1
			$createdTime = new \DateTime();
529 1
			$createdTime->setTimezone(new \DateTimeZone('UTC'));
530 1
			$this->vcomponent->add($this->vcalendar->createProperty('CREATED', $createdTime));
531
		}
532 1
		if (empty($this->vcomponent->SEQUENCE)) {
533
			$this->vcomponent->add($this->vcalendar->createProperty('SEQUENCE', 1));
534
		} else {
535 1
			$this->vcomponent->SEQUENCE = $this->vcomponent->SEQUENCE->getValue() + 1;
536
		}
537
	}
538 1
539
	/**
540 1
	 * Create a text value for dav.
541
	 *
542
	 * @param string $fieldName
543
	 * @param string $davName
544
	 *
545 1
	 * @throws \Sabre\VObject\InvalidDataException
546
	 */
547 1
	private function createText(string $fieldName, string $davName)
548
	{
549 1
		$empty = $this->record->isEmpty($fieldName);
550
		if (isset($this->vcomponent->{$davName})) {
551
			if ($empty) {
552 1
				$this->vcomponent->remove($davName);
553 1
			} else {
554 1
				$this->vcomponent->{$davName} = $this->record->get($fieldName);
555
			}
556
		} elseif (!$empty) {
557 1
			$this->vcomponent->add($this->vcalendar->createProperty($davName, $this->record->get($fieldName)));
558
		}
559
	}
560
561
	/**
562 1
	 * Create status value for dav.
563
	 */
564
	private function createStatus()
565
	{
566
		$status = $this->record->get('activitystatus');
567 1
		if ('VEVENT' === (string) $this->vcomponent->name) {
568
			$values = [
569 1
				'PLL_PLANNED' => 'TENTATIVE',
570
				'PLL_OVERDUE' => 'TENTATIVE',
571 1
				'PLL_POSTPONED' => 'CANCELLED',
572
				'PLL_CANCELLED' => 'CANCELLED',
573
				'PLL_COMPLETED' => 'CONFIRMED',
574
			];
575 1
		} else {
576 1
			$values = [
577 1
				'PLL_PLANNED' => 'NEEDS-ACTION',
578
				'PLL_IN_REALIZATION' => 'IN-PROCESS',
579 1
				'PLL_OVERDUE' => 'NEEDS-ACTION',
580
				'PLL_POSTPONED' => 'CANCELLED',
581
				'PLL_CANCELLED' => 'CANCELLED',
582 1
				'PLL_COMPLETED' => 'COMPLETED',
583
			];
584 1
		}
585
		if ($status && isset($values[$status])) {
586
			$value = $values[$status];
587
		} else {
588
			$value = reset($values);
589 1
		}
590
		if (isset($this->vcomponent->STATUS)) {
591 1
			$this->vcomponent->STATUS = $value;
592 1
		} else {
593 1
			$this->vcomponent->add($this->vcalendar->createProperty('STATUS', $value));
594 1
		}
595 1
	}
596 1
597 1
	/**
598 1
	 * Create visibility value for dav.
599 1
	 */
600 1
	private function createVisibility()
601
	{
602 1
		$visibility = $this->record->get('visibility');
603 1
		$values = [
604 1
			'Public' => 'PUBLIC',
605 1
			'Private' => 'PRIVATE',
606 1
		];
607 1
		$value = 'Private';
608
		if ($visibility && isset($values[$visibility])) {
609
			$value = $values[$visibility];
610 1
		}
611 1
		if (false !== \App\Config::component('Dav', 'CALDAV_DEFAULT_VISIBILITY_FROM_DAV')) {
612 1
			$value = \App\Config::component('Dav', 'CALDAV_DEFAULT_VISIBILITY_FROM_DAV');
613
		}
614
		if (isset($this->vcomponent->CLASS)) {
615
			$this->vcomponent->CLASS = $value;
616
		} else {
617
			$this->vcomponent->add($this->vcalendar->createProperty('CLASS', $value));
618
		}
619
	}
620
621
	/**
622
	 * Create visibility value for dav.
623
	 */
624
	private function createState()
625 1
	{
626
		$state = $this->record->get('state');
627 1
		$values = [
628
			'PLL_OPAQUE' => 'OPAQUE',
629
			'PLL_TRANSPARENT' => 'TRANSPARENT',
630 1
		];
631
		if ($state && isset($values[$state])) {
632
			$value = $values[$state];
633
			if (isset($this->vcomponent->TRANSP)) {
634 1
				$this->vcomponent->TRANSP = $value;
635
			} else {
636
				$this->vcomponent->add($this->vcalendar->createProperty('TRANSP', $value));
637
			}
638
		} elseif (isset($this->vcomponent->TRANSP)) {
639 1
			$this->vcomponent->remove('TRANSP');
640 1
		}
641 1
	}
642 1
643 1
	/**
644 1
	 * Create priority value for dav.
645 1
	 */
646 1
	private function createPriority()
647 1
	{
648
		$priority = $this->record->get('taskpriority');
649 1
		$values = [
650 1
			'High' => 1,
651 1
			'Medium' => 5,
652
			'Low' => 9,
653
		];
654 1
		$value = 5;
655 1
		if ($priority && isset($values[$priority])) {
656 1
			$value = $values[$priority];
657 1
		}
658 1
		if (isset($this->vcomponent->PRIORITY)) {
659
			$this->vcomponent->PRIORITY = $value;
660 1
		} else {
661 1
			$this->vcomponent->add($this->vcalendar->createProperty('PRIORITY', $value));
662 1
		}
663 1
	}
664
665 1
	/**
666 1
	 * Create date and time values for dav.
667 1
	 */
668 1
	private function createDateTime()
669
	{
670 1
		$end = $this->record->get('due_date') . ' ' . $this->record->get('time_end');
671 1
		$endField = 'VEVENT' == (string) $this->vcomponent->name ? 'DTEND' : 'DUE';
672
		$start = new \DateTime($this->record->get('date_start') . ' ' . $this->record->get('time_start'));
673 1
		$startProperty = $this->vcalendar->createProperty('DTSTART', $start);
674 1
		if ($this->record->get('allday')) {
675 1
			$end = new \DateTime($end);
676 1
			$end->modify('+1 day');
677
			$endProperty = $this->vcalendar->createProperty($endField, $end);
678
			$endProperty['VALUE'] = 'DATE';
679 1
			$startProperty['VALUE'] = 'DATE';
680 1
		} else {
681
			$end = new \DateTime($end);
682
			$endProperty = $this->vcalendar->createProperty($endField, $end);
683
			if (!$this->createdTimeZone) {
684 1
				unset($this->vcalendar->VTIMEZONE);
685 1
				$this->vcalendar->add($this->createTimeZone(date_default_timezone_get(), $start->getTimestamp(), $end->getTimestamp()));
686
				$this->createdTimeZone = true;
687
			}
688 1
		}
689
		$this->vcomponent->DTSTART = $startProperty;
690
		$this->vcomponent->{$endField} = $endProperty;
691
	}
692
693
	/**
694
	 * Create time zone.
695
	 *
696
	 * @param string $tzid
697
	 * @param int    $from
698 1
	 * @param int    $to
699
	 *
700 1
	 * @throws \Exception
701 1
	 *
702 1
	 * @return \Sabre\VObject\Component
703
	 */
704
	public function createTimeZone($tzid, $from = 0, $to = 0)
705
	{
706
		if (!$from) {
707 1
			$from = time();
708
		}
709
		if (!$to) {
710
			$to = $from;
711
		}
712
		try {
713
			$tz = new \DateTimeZone($tzid);
714
		} catch (\Exception $e) {
715
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Sabre\VObject\Component.
Loading history...
716
		}
717
		// get all transitions for one year back/ahead
718
		$year = 86400 * 360;
719
		$transitions = $tz->getTransitions($from - $year, $to + $year);
720
		$vt = $this->vcalendar->createComponent('VTIMEZONE');
721
		$vt->TZID = $tz->getName();
722
		$vt->TZURL = 'http://tzurl.org/zoneinfo/' . $tz->getName();
723
		$vt->add('X-LIC-LOCATION', $tz->getName());
724
		$dst = $std = null;
725
		foreach ($transitions as $i => $trans) {
726
			$cmp = null;
727
			// skip the first entry...
728
			if (0 == $i) { // ... but remember the offset for the next TZOFFSETFROM value
729
				$tzfrom = $trans['offset'] / 3600;
730
				continue;
731
			}
732
			// daylight saving time definition
733
			if ($trans['isdst']) {
734
				$t_dst = $trans['ts'];
735
				$dst = $this->vcalendar->createComponent('DAYLIGHT');
736
				$cmp = $dst;
737
				$cmpName = 'DAYLIGHT';
738
			} else { // standard time definition
739
				$t_std = $trans['ts'];
740
				$std = $this->vcalendar->createComponent('STANDARD');
741
				$cmp = $std;
742
				$cmpName = 'STANDARD';
743
			}
744
			if ($cmp && empty($vt->select($cmpName))) {
745
				$offset = $trans['offset'] / 3600;
746
				$cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tzfrom seems to be defined later in this foreach loop on line 729. Are you sure it is defined here?
Loading history...
747
				$cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', floor($offset), ($offset - floor($offset)) * 60);
748
				// add abbreviated timezone name if available
749
				if (!empty($trans['abbr'])) {
750
					$cmp->TZNAME = $trans['abbr'];
751
				}
752
				$dt = new \DateTime($trans['time']);
753
				$cmp->DTSTART = $dt->format('Ymd\THis');
754
				$tzfrom = $offset;
755
				$vt->add($cmp);
756
			}
757
			// we covered the entire date range
758
			if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $t_dst does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $t_std does not seem to be defined for all execution paths leading up to this point.
Loading history...
759
				break;
760
			}
761
		}
762
		// add X-MICROSOFT-CDO-TZID if available
763
		$microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
764
		if (\array_key_exists($tz->getName(), $microsoftExchangeMap)) {
765
			$vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
766
		}
767
		return $vt;
768
	}
769
770
	/**
771
	 * Get invitations for record id.
772
	 *
773
	 * @param int $recordId
774
	 *
775
	 * @return array
776
	 */
777
	public function getInvitations(int $recordId): array
778
	{
779
		$invities = [];
780
		$dataReader = (new \App\Db\Query())->from('u_#__activity_invitation')->where(['activityid' => $recordId])->createCommand()->query();
781 1
		while ($row = $dataReader->read()) {
782
			if (!empty($row['email'])) {
783 1
				$invities[$row['email']] = $row;
784 1
			}
785 1
		}
786 1
		return $invities;
787 1
	}
788
789
	/**
790
	 * Record save attendee.
791
	 *
792
	 * @param Vtiger_Record_Model $record
0 ignored issues
show
Bug introduced by
The type App\Integrations\Dav\Vtiger_Record_Model was not found. Did you mean Vtiger_Record_Model? If so, make sure to prefix the type with \.
Loading history...
793
	 */
794
	public function recordSaveAttendee(\Vtiger_Record_Model $record)
795
	{
796
		if ('VEVENT' === (string) $this->vcomponent->name) {
797
			$invities = $this->getInvitations($record->getId());
798
			$time = VObject\DateTimeParser::parse($this->vcomponent->DTSTAMP);
799
			$timeFormated = $time->format('Y-m-d H:i:s');
800
			$dbCommand = \App\Db::getInstance()->createCommand();
801
			$attendees = $this->vcomponent->select('ATTENDEE');
802
			foreach ($attendees as &$attendee) {
803
				$nameAttendee = isset($attendee->parameters['CN']) ? $attendee->parameters['CN']->getValue() : null;
804
				$value = $attendee->getValue();
805
				if (0 === stripos($value, 'mailto:')) {
806
					$value = substr($value, 7, \strlen($value) - 7);
807
				}
808
				if ($value && \App\TextUtils::getTextLength($value) > 100 || !\App\Validator::email($value)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($value && App\TextUtils...alidator::email($value), Probably Intended Meaning: $value && (App\TextUtils...lidator::email($value))
Loading history...
809
					throw new \Sabre\DAV\Exception\BadRequest('Invalid email: ' . $value);
810 1
				}
811
				if (isset($attendee['ROLE']) && 'CHAIR' === $attendee['ROLE']->getValue()) {
812
					$users = $this->findRecordByEmail($value, ['Users']);
813
					if (!empty($users)) {
814
						continue;
815
					}
816
				}
817
				$crmid = 0;
818 1
				$records = $this->findRecordByEmail($value, array_keys(array_merge(\App\ModuleHierarchy::getModulesByLevel(0), \App\ModuleHierarchy::getModulesByLevel(4))));
819
				if (!empty($records)) {
820
					$recordCrm = current($records);
821
					$crmid = $recordCrm['crmid'];
822
				}
823
				$status = $this->getAttendeeStatus(isset($attendee['PARTSTAT']) ? $attendee['PARTSTAT']->getValue() : '');
824
				if (isset($invities[$value])) {
825
					$row = $invities[$value];
826
					if ($row['status'] !== $status || $row['name'] !== $nameAttendee) {
827
						$dbCommand->update('u_#__activity_invitation', [
828
							'status' => $status,
829
							'time' => $timeFormated,
830
							'name' => \App\TextUtils::textTruncate($nameAttendee, 500, false),
831
						], ['activityid' => $record->getId(), 'email' => $value]
832
					)->execute();
833
					}
834
					unset($invities[$value]);
835
				} else {
836
					$params = [
837
						'email' => $value,
838
						'crmid' => $crmid,
839
						'status' => $status,
840
						'name' => \App\TextUtils::textTruncate($nameAttendee, 500, false),
841
						'activityid' => $record->getId(),
842
					];
843
					if ($status) {
844
						$params['time'] = $timeFormated;
845
					}
846
					$dbCommand->insert('u_#__activity_invitation', $params)->execute();
847
				}
848
			}
849
			foreach ($invities as &$invitation) {
850
				$dbCommand->delete('u_#__activity_invitation', ['inviteesid' => $invitation['inviteesid']])->execute();
851
			}
852
		}
853
	}
854
855
	/**
856
	 * Dav save attendee.
857
	 *
858
	 * @param array $record
859 1
	 */
860
	public function davSaveAttendee(array $record)
861 1
	{
862 1
		$owner = \Users_Privileges_Model::getInstanceById($record['assigned_user_id']);
863 1
		$invities = $this->getInvitations($record['id']);
864 1
		$attendees = $this->vcomponent->select('ATTENDEE');
865 1
		if (empty($attendees)) {
866 1
			if (!empty($invities)) {
867 1
				$organizer = $this->vcalendar->createProperty('ORGANIZER', 'mailto:' . $owner->get('email1'));
868
				$organizer->add('CN', $owner->getName());
869
				$this->vcomponent->add($organizer);
870 1
				$attendee = $this->vcalendar->createProperty('ATTENDEE', 'mailto:' . $owner->get('email1'));
871
				$attendee->add('CN', $owner->getName());
872
				$attendee->add('ROLE', 'CHAIR');
873 1
				$attendee->add('PARTSTAT', 'ACCEPTED');
874 1
				$attendee->add('RSVP', 'false');
875
				$this->vcomponent->add($attendee);
876 1
			}
877 1
		} else {
878 1
			foreach ($attendees as &$attendee) {
879
				$value = ltrim($attendee->getValue(), 'mailto:');
880
				if (isset($invities[$value])) {
881
					$row = $invities[$value];
882
					if (isset($attendee['PARTSTAT'])) {
883
						$attendee['PARTSTAT']->setValue($this->getAttendeeStatus($row['status'], false));
884
					} else {
885
						$attendee->add('PARTSTAT', $this->getAttendeeStatus($row['status']));
886
					}
887
					unset($invities[$value]);
888
				} else {
889
					$this->vcomponent->remove($attendee);
890
				}
891
			}
892
		}
893
		foreach ($invities as &$row) {
894
			$attendee = $this->vcalendar->createProperty('ATTENDEE', 'mailto:' . $row['email']);
895
			$attendee->add('CN', empty($row['crmid']) ? $row['name'] : \App\Record::getLabel($row['crmid']));
896
			$attendee->add('ROLE', 'REQ-PARTICIPANT');
897
			$attendee->add('PARTSTAT', $this->getAttendeeStatus($row['status'], false));
898
			$attendee->add('RSVP', '0' == $row['status'] ? 'true' : 'false');
899
			$this->vcomponent->add($attendee);
900
		}
901
	}
902
903
	/**
904
	 * Get attendee status.
905 1
	 *
906
	 * @param string $value
907
	 * @param bool   $toCrm
908 1
	 *
909
	 * @return false|string
910
	 */
911
	public function getAttendeeStatus(string $value, bool $toCrm = true)
912
	{
913 1
		$statuses = ['NEEDS-ACTION', 'ACCEPTED', 'DECLINED'];
914
		if ($toCrm) {
915 1
			$status = false;
916 1
			$statuses = array_flip($statuses);
917 1
		} else {
918 1
			$status = 'NEEDS-ACTION';
919 1
		}
920 1
		if (isset($statuses[$value])) {
921
			$status = $statuses[$value];
922
		}
923
		return $status;
924
	}
925
926
	/**
927
	 * Parses some information from calendar objects, used for optimized
928
	 * calendar-queries.
929
	 *
930
	 * Returns an array with the following keys:
931
	 *   * etag - An md5 checksum of the object without the quotes.
932
	 *   * size - Size of the object in bytes
933
	 *   * componentType - VEVENT, VTODO or VJOURNAL
934
	 *   * firstOccurence
935
	 *   * lastOccurence
936
	 *   * uid - value of the UID property
937
	 *
938
	 * @param string $calendarData
939
	 *
940
	 * @return array
941
	 *
942
	 * @see Sabre\CalDAV\Backend\PDO::getDenormalizedData
943
	 */
944
	public function getDenormalizedData($calendarData)
945
	{
946
		$vObject = VObject\Reader::read($calendarData);
947
		$uid = $lastOccurence = $firstOccurence = $component = $componentType = null;
948
		foreach ($vObject->getComponents() as $component) {
949
			if ('VTIMEZONE' !== $component->name) {
950
				$componentType = $component->name;
951
				$uid = (string) $component->UID;
952
				break;
953
			}
954
		}
955
		if (!$componentType) {
956
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
957
		}
958
		if ('VEVENT' === $componentType) {
959
			$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
960
			// Finding the last occurence is a bit harder
961
			if (!isset($component->RRULE)) {
962
				if (isset($component->DTEND)) {
963
					$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
964
				} elseif (isset($component->DURATION)) {
965
					$endDate = clone $component->DTSTART->getDateTime();
966
					$endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
967
					$lastOccurence = $endDate->getTimeStamp();
968
				} elseif (!$component->DTSTART->hasTime()) {
969
					$endDate = clone $component->DTSTART->getDateTime();
970
					$endDate = $endDate->modify('+1 day');
971
					$lastOccurence = $endDate->getTimeStamp();
972
				} else {
973
					$lastOccurence = $firstOccurence;
974
				}
975
			} else {
976
				$it = new VObject\Recur\EventIterator($vObject, (string) $component->UID);
977
				$maxDate = new \DateTime(self::MAX_DATE);
978
				if ($it->isInfinite()) {
979
					$lastOccurence = $maxDate->getTimeStamp();
980
				} else {
981
					$end = $it->getDtEnd();
982
					while ($it->valid() && $end < $maxDate) {
983
						$end = $it->getDtEnd();
984
						$it->next();
985
					}
986
					$lastOccurence = $end->getTimeStamp();
987
				}
988
			}
989
			// Ensure Occurence values are positive
990
			if ($firstOccurence < 0) {
991
				$firstOccurence = 0;
992
			}
993
			if ($lastOccurence < 0) {
994
				$lastOccurence = 0;
995
			}
996
		}
997
		// Destroy circular references to PHP will GC the object.
998
		$vObject->destroy();
999
		return [
1000
			'etag' => md5($calendarData),
1001
			'size' => \strlen($calendarData),
1002
			'componentType' => $componentType,
1003
			'firstOccurence' => $firstOccurence,
1004
			'lastOccurence' => $lastOccurence,
1005
			'uid' => $uid,
1006
		];
1007
	}
1008
1009
	/**
1010
	 * Find crm id by email.
1011
	 *
1012
	 * @param int|string $value
1013
	 * @param array      $allowedModules
1014
	 * @param array      $skipModules
1015
	 *
1016
	 * @return array
1017
	 */
1018
	public function findRecordByEmail($value, array $allowedModules = [], array $skipModules = [])
1019
	{
1020
		$db = \App\Db::getInstance();
1021
		$rows = $fields = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $rows is dead and can be removed.
Loading history...
1022
		$dataReader = (new \App\Db\Query())->select(['vtiger_field.columnname', 'vtiger_field.tablename', 'vtiger_field.fieldlabel', 'vtiger_field.tabid', 'vtiger_tab.name'])
1023
			->from('vtiger_field')->innerJoin('vtiger_tab', 'vtiger_field.tabid = vtiger_tab.tabid')
1024
			->where(['vtiger_tab.presence' => 0])
1025
			->andWhere(['<>', 'vtiger_field.presence', 1])
1026
			->andWhere(['or', ['uitype' => 13], ['uitype' => 104]])->createCommand()->query();
1027
		while ($row = $dataReader->read()) {
1028
			$fields[$row['name']][$row['tablename']][$row['columnname']] = $row;
1029
		}
1030
		$queryUnion = null;
1031
		foreach ($fields as $moduleName => $moduleFields) {
1032
			if (($allowedModules && !\in_array($moduleName, $allowedModules)) || \in_array($moduleName, $skipModules)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedModules of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1033
				continue;
1034
			}
1035
			$instance = \CRMEntity::getInstance($moduleName);
1036
			$isEntityType = isset($instance->tab_name_index['vtiger_crmentity']);
1037
			foreach ($moduleFields as $tablename => $columns) {
1038
				$tableIndex = $instance->tab_name_index[$tablename];
1039
				$query = (new \App\Db\Query())->select(['crmid' => $tableIndex, 'modules' => new \yii\db\Expression($db->quoteValue($moduleName))])
1040
					->from($tablename);
1041
				if ($isEntityType) {
1042
					$query->innerJoin('vtiger_crmentity', "vtiger_crmentity.crmid = {$tablename}.{$tableIndex}")->where(['vtiger_crmentity.deleted' => 0]);
1043
				}
1044
				$orWhere = ['or'];
1045
				foreach ($columns as $row) {
1046
					$orWhere[] = ["{$row['tablename']}.{$row['columnname']}" => $value];
1047
				}
1048
				$query->andWhere($orWhere);
1049
				if ($queryUnion) {
1050
					$queryUnion->union($query);
1051
				} else {
1052
					$queryUnion = $query;
1053
				}
1054
			}
1055
		}
1056
		$rows = $queryUnion->all();
0 ignored issues
show
Bug introduced by
The method all() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1056
		/** @scrutinizer ignore-call */ 
1057
  $rows = $queryUnion->all();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1057
		$labels = \App\Record::getLabel(array_column($rows, 'crmid'));
1058
		foreach ($rows as &$row) {
1059
			$row['label'] = $labels[$row['crmid']];
1060
		}
1061
		return $rows;
1062
	}
1063
}
1064