app/Integrations/Dav/Calendar.php
<?php
/**
* CalDav calendar file.
*
* @package Integration
*
* @see https://tools.ietf.org/html/rfc5545
*
* @package Integration
*
* @copyright YetiForce S.A.
* @license YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
* @author Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
*/
namespace App\Integrations\Dav;
use Sabre\VObject;
/**
* CalDav calendar class.
*/
class Calendar
{
/**
* Record model instance.
*
* @var \Vtiger_Record_Model[]
*/
private $records = [];
/**
* Record data.
*
* @var \Vtiger_Record_Model
*/
private $record = [];
/**
* VCalendar object.
*
* @var \Sabre\VObject\Component\VCalendar
*/
private $vcalendar;
/**
* @var \Sabre\VObject\Component
*/
private $vcomponent;
/**
* Optimization for creating a time zone.
*
* @var bool
*/
private $createdTimeZone = false;
/**
* Custom values.
*
* @var string[]
*/
protected static $customValues = [
'X-GOOGLE-CONFERENCE' => 'meeting_url',
'X-MS-OLK-MWSURL' => 'meeting_url',
'X-MICROSOFT-SKYPETEAMSMEETINGURL' => 'meeting_url',
'X-MICROSOFT-ONLINEMEETINGCONFLINK' => 'meeting_url',
'X-MICROSOFT-ONLINEMEETINGEXTERNALLINK' => 'meeting_url',
];
/**
* Max date.
*
* @var string
*/
const MAX_DATE = '2038-01-01';
/**
* Delete calendar event by crm id.
*
* @param int $id
*
* @throws \yii\db\Exception
*/
public static function deleteByCrmId(int $id)
{
$dbCommand = \App\Db::getInstance()->createCommand();
$dataReader = (new \App\Db\Query())->select(['calendarid'])->from('dav_calendarobjects')->where(['crmid' => $id])->createCommand()->query();
$dbCommand->delete('dav_calendarobjects', ['crmid' => $id])->execute();
while ($calendarId = $dataReader->readColumn(0)) {
static::addChange($calendarId, $id . '.vcf', 3);
}
$dataReader->close();
}
/**
* Dav delete.
*
* @param array $calendar
*
* @throws \yii\db\Exception
*/
public static function delete(array $calendar)
{
static::addChange($calendar['calendarid'], $calendar['uri'], 3);
\App\Db::getInstance()->createCommand()->delete('dav_calendarobjects', ['id' => $calendar['id']])->execute();
}
/**
* Add change to calendar.
*
* @param int $calendarId
* @param string $uri
* @param int $operation
*
* @throws \yii\db\Exception
*/
public static function addChange(int $calendarId, string $uri, int $operation)
{
$dbCommand = \App\Db::getInstance()->createCommand();
$calendar = static::getCalendar($calendarId);
$dbCommand->insert('dav_calendarchanges', [
'uri' => $uri,
'synctoken' => (int) $calendar['synctoken'],
'calendarid' => $calendarId,
'operation' => $operation,
])->execute();
$dbCommand->update('dav_calendars', [
'synctoken' => ((int) $calendar['synctoken']) + 1,
], ['id' => $calendarId])->execute();
}
/**
* Get calendar.
*
* @param int $id
*
* @return array
*/
public static function getCalendar(int $id)
{
return (new \App\Db\Query())->from('dav_calendars')->where(['id' => $id])->one();
}
/**
* Create instance from dav data.
*
* @param string $calendar
*
* @return \App\Integrations\Dav\Calendar
*/
public static function loadFromDav(string $calendar)
{
$instance = new self();
$instance->record = \Vtiger_Record_Model::getCleanInstance('Calendar');
$instance->vcalendar = VObject\Reader::read($calendar, \Sabre\VObject\Reader::OPTION_FORGIVING);
foreach ($instance->vcalendar->children() as $child) {
if (!$child instanceof VObject\Component) {
continue;
}
if ('VTIMEZONE' === $child->name) {
continue;
}
if (empty($instance->vcomponent)) {
$instance->vcomponent = $child;
}
}
return $instance;
}
/**
* Create empty instance.
*
* @return \App\Integrations\Dav\Calendar
*/
public static function createEmptyInstance()
{
$instance = new self();
$instance->record = \Vtiger_Record_Model::getCleanInstance('Calendar');
$instance->vcalendar = new VObject\Component\VCalendar();
$instance->vcalendar->PRODID = '-//YetiForce//YetiForceCRM V' . \App\Version::get() . '//';
return $instance;
}
/**
* Load record data.
*
* @param array $data
*/
public function loadFromArray(array $data)
{
$this->record->setData($data);
}
/**
* Create a class instance by crm id.
*
* @param int $record
* @param string $uid
*
* @return bool
*/
public function getByRecordId(int $record, string $uid)
{
\App\Log::trace($record, __METHOD__);
if ($record) {
$this->records[$uid] = \Vtiger_Record_Model::getInstanceById($record, 'Calendar');
}
return $this->getByRecordInstance()[$uid] ?? false;
}
/**
* Create a class instance from vcalendar content.
*
* @param string $content
* @param \Vtiger_Record_Model|null $recordModel
* @param ?string $uid
*
* @return \App\Integrations\Dav\Calendar
*/
public static function loadFromContent(string $content, ?\Vtiger_Record_Model $recordModel = null, ?string $uid = null)
{
$instance = new self();
$instance->vcalendar = VObject\Reader::read($content, \Sabre\VObject\Reader::OPTION_FORGIVING);
if ($recordModel && $uid) {
$instance->records[$uid] = $recordModel;
}
return $instance;
}
/**
* Get VCalendar instance.
*
* @return \Sabre\VObject\Component\VCalendar
*/
public function getVCalendar()
{
return $this->vcalendar;
}
/**
* Get calendar component instance.
*
* @return \Sabre\VObject\Component
*/
public function getComponent()
{
return $this->vcomponent;
}
/**
* Get record instance.
*
* @return \Vtiger_Record_Model[]
*/
public function getRecordInstance()
{
foreach ($this->vcalendar->getBaseComponents() ?: $this->vcalendar->getComponents() as $component) {
$type = (string) $component->name;
if ('VTODO' === $type || 'VEVENT' === $type) {
$this->vcomponent = $component;
$this->parseComponent();
}
}
return $this->records;
}
/**
* Parse component.
*
* @return void
*/
private function parseComponent(): void
{
$uid = (string) $this->vcomponent->UID;
if (isset($this->records[$uid])) {
$this->record = $this->records[$uid];
} else {
$this->record = $this->records[$uid] = \Vtiger_Record_Model::getCleanInstance('Calendar');
}
$this->parseText('subject', 'SUMMARY');
$this->parseText('location', 'LOCATION');
$this->parseText('description', 'DESCRIPTION');
$this->parseStatus();
$this->parsePriority();
$this->parseVisibility();
$this->parseState();
$this->parseType();
$this->parseDateTime();
$this->parseCustomValues();
}
/**
* Parse simple text.
*
* @param string $fieldName
* @param string $davName
* @param \Vtiger_Record_Model $recordModel
*
* @return void
*/
private function parseText(string $fieldName, string $davName): void
{
$separator = '-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::-';
$value = (string) $this->vcomponent->{$davName};
if (false !== strpos($value, $separator)) {
[$html,$text] = explode($separator, $value, 2);
$value = trim(strip_tags($html)) . "\n" . \trim(\str_replace($separator, '', $text));
} else {
$value = trim(\str_replace('\n', PHP_EOL, $value));
}
$value = \App\Purifier::decodeHtml(\App\Purifier::purify($value));
if ($length = $this->record->getField($fieldName)->getMaxValue()) {
$value = \App\TextUtils::textTruncate($value, $length, false);
}
$this->record->set($fieldName, \trim($value));
}
/**
* Parse status.
*
* @return void
*/
private function parseStatus(): void
{
$davValue = null;
if (isset($this->vcomponent->STATUS)) {
$davValue = strtoupper($this->vcomponent->STATUS->getValue());
}
if ('VEVENT' === (string) $this->vcomponent->name) {
$values = [
'TENTATIVE' => 'PLL_PLANNED',
'CANCELLED' => 'PLL_CANCELLED',
'CONFIRMED' => 'PLL_PLANNED',
];
} else {
$values = [
'NEEDS-ACTION' => 'PLL_PLANNED',
'IN-PROCESS' => 'PLL_IN_REALIZATION',
'CANCELLED' => 'PLL_CANCELLED',
'COMPLETED' => 'PLL_COMPLETED',
];
}
$value = reset($values);
if ($davValue && isset($values[$davValue])) {
$value = $values[$davValue];
}
$this->record->set('activitystatus', $value);
}
/**
* Parse visibility.
*
* @return void
*/
private function parseVisibility(): void
{
$davValue = null;
$value = 'Private';
if (isset($this->vcomponent->CLASS)) {
$davValue = strtoupper($this->vcomponent->CLASS->getValue());
$values = [
'PUBLIC' => 'Public',
'PRIVATE' => 'Private',
];
if ($davValue && isset($values[$davValue])) {
$value = $values[$davValue];
}
}
$this->record->set('visibility', $value);
}
/**
* Parse state.
*
* @return void
*/
private function parseState(): void
{
$davValue = null;
$value = '';
if (isset($this->vcomponent->TRANSP)) {
$davValue = strtoupper($this->vcomponent->TRANSP->getValue());
$values = [
'OPAQUE' => 'PLL_OPAQUE',
'TRANSPARENT' => 'PLL_TRANSPARENT',
];
if ($davValue && isset($values[$davValue])) {
$value = $values[$davValue];
}
}
$this->record->set('state', $value);
}
/**
* Parse priority.
*
* @return void
*/
private function parsePriority(): void
{
$davValue = null;
$value = 'Medium';
if (isset($this->vcomponent->PRIORITY)) {
$davValue = strtoupper($this->vcomponent->PRIORITY->getValue());
$values = [
1 => 'High',
5 => 'Medium',
9 => 'Low',
];
if ($davValue && isset($values[$davValue])) {
$value = $values[$davValue];
}
}
$this->record->set('taskpriority', $value);
}
/**
* Parse type.
*
* @return void
*/
private function parseType(): void
{
if ($this->record->isEmpty('activitytype')) {
$this->record->set('activitytype', 'VTODO' === (string) $this->vcomponent->name ? 'Task' : 'Meeting');
}
}
/**
* Parse date time.
*
* @return void
*/
private function parseDateTime(): void
{
$allDay = 0;
$endHasTime = $startHasTime = false;
$endField = 'VEVENT' === ((string) $this->vcomponent->name) ? 'DTEND' : 'DUE';
if (isset($this->vcomponent->DTSTART)) {
$timeStamp = $this->vcomponent->DTSTART->getDateTime()->getTimeStamp();
$dateStart = date('Y-m-d', $timeStamp);
$timeStart = date('H:i:s', $timeStamp);
$startHasTime = $this->vcomponent->DTSTART->hasTime();
} else {
$timeStamp = $this->vcomponent->DTSTAMP->getDateTime()->getTimeStamp();
$dateStart = date('Y-m-d', $timeStamp);
$timeStart = date('H:i:s', $timeStamp);
}
if (isset($this->vcomponent->{$endField})) {
$timeStamp = $this->vcomponent->{$endField}->getDateTime()->getTimeStamp();
$endHasTime = $this->vcomponent->{$endField}->hasTime();
$dueDate = date('Y-m-d', $timeStamp);
$timeEnd = date('H:i:s', $timeStamp);
if (!$endHasTime) {
$endTime = strtotime('-1 day', strtotime("$dueDate $timeEnd"));
$dueDate = date('Y-m-d', $endTime);
$timeEnd = date('H:i:s', $endTime);
}
} else {
$endTime = strtotime('+1 day', strtotime("$dateStart $timeStart"));
$dueDate = date('Y-m-d', $endTime);
$timeEnd = date('H:i:s', $endTime);
}
if (!$startHasTime && !$endHasTime && \App\User::getCurrentUserId()) {
$allDay = 1;
$currentUser = \App\User::getCurrentUserModel();
$userTimeZone = new \DateTimeZone($currentUser->getDetail('time_zone'));
$sysTimeZone = new \DateTimeZone(\App\Fields\DateTime::getTimeZone());
[$hour , $minute] = explode(':', $currentUser->getDetail('start_hour'));
$date = new \DateTime('now', $userTimeZone);
$date->setTime($hour, $minute);
$date->setTimezone($sysTimeZone);
$timeStart = $date->format('H:i:s');
$date->setTimezone($userTimeZone);
[$hour , $minute] = explode(':', $currentUser->getDetail('end_hour'));
$date->setTime($hour, $minute);
$date->setTimezone($sysTimeZone);
$timeEnd = $date->format('H:i:s');
}
$this->record->set('allday', $allDay);
$this->record->set('date_start', $dateStart);
$this->record->set('due_date', $dueDate);
$this->record->set('time_start', $timeStart);
$this->record->set('time_end', $timeEnd);
}
/**
* Parse parse custom values.
*
* @return void
*/
private function parseCustomValues(): void
{
foreach (self::$customValues as $key => $fieldName) {
if (isset($this->vcomponent->{$key})) {
$this->record->set($fieldName, (string) $this->vcomponent->{$key});
}
}
}
/**
* Create calendar entry component.
*
* @return \Sabre\VObject\Component
*/
public function createComponent()
{
$componentType = 'Task' === $this->record->get('activitytype') ? 'VTODO' : 'VEVENT';
$this->vcomponent = $this->vcalendar->createComponent($componentType);
$this->vcomponent->UID = \str_replace('sabre-vobject', 'YetiForceCRM', (string) $this->vcomponent->UID);
$this->updateComponent();
$this->vcalendar->add($this->vcomponent);
return $this->vcomponent;
}
/**
* Update calendar entry component.
*
* @throws \Sabre\VObject\InvalidDataException
*/
public function updateComponent()
{
$this->createDateTime();
$this->createText('subject', 'SUMMARY');
$this->createText('location', 'LOCATION');
$this->createText('description', 'DESCRIPTION');
$this->createStatus();
$this->createVisibility();
$this->createState();
$this->createPriority();
if (empty($this->vcomponent->CREATED)) {
$createdTime = new \DateTime();
$createdTime->setTimezone(new \DateTimeZone('UTC'));
$this->vcomponent->add($this->vcalendar->createProperty('CREATED', $createdTime));
}
if (empty($this->vcomponent->SEQUENCE)) {
$this->vcomponent->add($this->vcalendar->createProperty('SEQUENCE', 1));
} else {
$this->vcomponent->SEQUENCE = $this->vcomponent->SEQUENCE->getValue() + 1;
}
}
/**
* Create a text value for dav.
*
* @param string $fieldName
* @param string $davName
*
* @throws \Sabre\VObject\InvalidDataException
*/
private function createText(string $fieldName, string $davName)
{
$empty = $this->record->isEmpty($fieldName);
if (isset($this->vcomponent->{$davName})) {
if ($empty) {
$this->vcomponent->remove($davName);
} else {
$this->vcomponent->{$davName} = $this->record->get($fieldName);
}
} elseif (!$empty) {
$this->vcomponent->add($this->vcalendar->createProperty($davName, $this->record->get($fieldName)));
}
}
/**
* Create status value for dav.
*/
private function createStatus()
{
$status = $this->record->get('activitystatus');
if ('VEVENT' === (string) $this->vcomponent->name) {
$values = [
'PLL_PLANNED' => 'TENTATIVE',
'PLL_OVERDUE' => 'TENTATIVE',
'PLL_POSTPONED' => 'CANCELLED',
'PLL_CANCELLED' => 'CANCELLED',
'PLL_COMPLETED' => 'CONFIRMED',
];
} else {
$values = [
'PLL_PLANNED' => 'NEEDS-ACTION',
'PLL_IN_REALIZATION' => 'IN-PROCESS',
'PLL_OVERDUE' => 'NEEDS-ACTION',
'PLL_POSTPONED' => 'CANCELLED',
'PLL_CANCELLED' => 'CANCELLED',
'PLL_COMPLETED' => 'COMPLETED',
];
}
if ($status && isset($values[$status])) {
$value = $values[$status];
} else {
$value = reset($values);
}
if (isset($this->vcomponent->STATUS)) {
$this->vcomponent->STATUS = $value;
} else {
$this->vcomponent->add($this->vcalendar->createProperty('STATUS', $value));
}
}
/**
* Create visibility value for dav.
*/
private function createVisibility()
{
$visibility = $this->record->get('visibility');
$values = [
'Public' => 'PUBLIC',
'Private' => 'PRIVATE',
];
$value = 'Private';
if ($visibility && isset($values[$visibility])) {
$value = $values[$visibility];
}
if (false !== \App\Config::component('Dav', 'CALDAV_DEFAULT_VISIBILITY_FROM_DAV')) {
$value = \App\Config::component('Dav', 'CALDAV_DEFAULT_VISIBILITY_FROM_DAV');
}
if (isset($this->vcomponent->CLASS)) {
$this->vcomponent->CLASS = $value;
} else {
$this->vcomponent->add($this->vcalendar->createProperty('CLASS', $value));
}
}
/**
* Create visibility value for dav.
*/
private function createState()
{
$state = $this->record->get('state');
$values = [
'PLL_OPAQUE' => 'OPAQUE',
'PLL_TRANSPARENT' => 'TRANSPARENT',
];
if ($state && isset($values[$state])) {
$value = $values[$state];
if (isset($this->vcomponent->TRANSP)) {
$this->vcomponent->TRANSP = $value;
} else {
$this->vcomponent->add($this->vcalendar->createProperty('TRANSP', $value));
}
} elseif (isset($this->vcomponent->TRANSP)) {
$this->vcomponent->remove('TRANSP');
}
}
/**
* Create priority value for dav.
*/
private function createPriority()
{
$priority = $this->record->get('taskpriority');
$values = [
'High' => 1,
'Medium' => 5,
'Low' => 9,
];
$value = 5;
if ($priority && isset($values[$priority])) {
$value = $values[$priority];
}
if (isset($this->vcomponent->PRIORITY)) {
$this->vcomponent->PRIORITY = $value;
} else {
$this->vcomponent->add($this->vcalendar->createProperty('PRIORITY', $value));
}
}
/**
* Create date and time values for dav.
*/
private function createDateTime()
{
$end = $this->record->get('due_date') . ' ' . $this->record->get('time_end');
$endField = 'VEVENT' == (string) $this->vcomponent->name ? 'DTEND' : 'DUE';
$start = new \DateTime($this->record->get('date_start') . ' ' . $this->record->get('time_start'));
$startProperty = $this->vcalendar->createProperty('DTSTART', $start);
if ($this->record->get('allday')) {
$end = new \DateTime($end);
$end->modify('+1 day');
$endProperty = $this->vcalendar->createProperty($endField, $end);
$endProperty['VALUE'] = 'DATE';
$startProperty['VALUE'] = 'DATE';
} else {
$end = new \DateTime($end);
$endProperty = $this->vcalendar->createProperty($endField, $end);
if (!$this->createdTimeZone) {
unset($this->vcalendar->VTIMEZONE);
$this->vcalendar->add($this->createTimeZone(date_default_timezone_get(), $start->getTimestamp(), $end->getTimestamp()));
$this->createdTimeZone = true;
}
}
$this->vcomponent->DTSTART = $startProperty;
$this->vcomponent->{$endField} = $endProperty;
}
/**
* Create time zone.
*
* @param string $tzid
* @param int $from
* @param int $to
*
* @throws \Exception
*
* @return \Sabre\VObject\Component
*/
public function createTimeZone($tzid, $from = 0, $to = 0)
{
if (!$from) {
$from = time();
}
if (!$to) {
$to = $from;
}
try {
$tz = new \DateTimeZone($tzid);
} catch (\Exception $e) {
return false;
}
// get all transitions for one year back/ahead
$year = 86400 * 360;
$transitions = $tz->getTransitions($from - $year, $to + $year);
$vt = $this->vcalendar->createComponent('VTIMEZONE');
$vt->TZID = $tz->getName();
$vt->TZURL = 'http://tzurl.org/zoneinfo/' . $tz->getName();
$vt->add('X-LIC-LOCATION', $tz->getName());
$dst = $std = null;
foreach ($transitions as $i => $trans) {
$cmp = null;
// skip the first entry...
if (0 == $i) { // ... but remember the offset for the next TZOFFSETFROM value
$tzfrom = $trans['offset'] / 3600;
continue;
}
// daylight saving time definition
if ($trans['isdst']) {
$t_dst = $trans['ts'];
$dst = $this->vcalendar->createComponent('DAYLIGHT');
$cmp = $dst;
$cmpName = 'DAYLIGHT';
} else { // standard time definition
$t_std = $trans['ts'];
$std = $this->vcalendar->createComponent('STANDARD');
$cmp = $std;
$cmpName = 'STANDARD';
}
if ($cmp && empty($vt->select($cmpName))) {
$offset = $trans['offset'] / 3600;
$cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
$cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', floor($offset), ($offset - floor($offset)) * 60);
// add abbreviated timezone name if available
if (!empty($trans['abbr'])) {
$cmp->TZNAME = $trans['abbr'];
}
$dt = new \DateTime($trans['time']);
$cmp->DTSTART = $dt->format('Ymd\THis');
$tzfrom = $offset;
$vt->add($cmp);
}
// we covered the entire date range
if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
break;
}
}
// add X-MICROSOFT-CDO-TZID if available
$microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
if (\array_key_exists($tz->getName(), $microsoftExchangeMap)) {
$vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
}
return $vt;
}
/**
* Get invitations for record id.
*
* @param int $recordId
*
* @return array
*/
public function getInvitations(int $recordId): array
{
$invities = [];
$dataReader = (new \App\Db\Query())->from('u_#__activity_invitation')->where(['activityid' => $recordId])->createCommand()->query();
while ($row = $dataReader->read()) {
if (!empty($row['email'])) {
$invities[$row['email']] = $row;
}
}
return $invities;
}
/**
* Record save attendee.
*
* @param Vtiger_Record_Model $record
*/
public function recordSaveAttendee(\Vtiger_Record_Model $record)
{
if ('VEVENT' === (string) $this->vcomponent->name) {
$invities = $this->getInvitations($record->getId());
$time = VObject\DateTimeParser::parse($this->vcomponent->DTSTAMP);
$timeFormated = $time->format('Y-m-d H:i:s');
$dbCommand = \App\Db::getInstance()->createCommand();
$attendees = $this->vcomponent->select('ATTENDEE');
foreach ($attendees as &$attendee) {
$nameAttendee = isset($attendee->parameters['CN']) ? $attendee->parameters['CN']->getValue() : null;
$value = $attendee->getValue();
if (0 === stripos($value, 'mailto:')) {
$value = substr($value, 7, \strlen($value) - 7);
}
if ($value && \App\TextUtils::getTextLength($value) > 100 || !\App\Validator::email($value)) {
throw new \Sabre\DAV\Exception\BadRequest('Invalid email: ' . $value);
}
if (isset($attendee['ROLE']) && 'CHAIR' === $attendee['ROLE']->getValue()) {
$users = $this->findRecordByEmail($value, ['Users']);
if (!empty($users)) {
continue;
}
}
$crmid = 0;
$records = $this->findRecordByEmail($value, array_keys(array_merge(\App\ModuleHierarchy::getModulesByLevel(0), \App\ModuleHierarchy::getModulesByLevel(4))));
if (!empty($records)) {
$recordCrm = current($records);
$crmid = $recordCrm['crmid'];
}
$status = $this->getAttendeeStatus(isset($attendee['PARTSTAT']) ? $attendee['PARTSTAT']->getValue() : '');
if (isset($invities[$value])) {
$row = $invities[$value];
if ($row['status'] !== $status || $row['name'] !== $nameAttendee) {
$dbCommand->update('u_#__activity_invitation', [
'status' => $status,
'time' => $timeFormated,
'name' => \App\TextUtils::textTruncate($nameAttendee, 500, false),
], ['activityid' => $record->getId(), 'email' => $value]
)->execute();
}
unset($invities[$value]);
} else {
$params = [
'email' => $value,
'crmid' => $crmid,
'status' => $status,
'name' => \App\TextUtils::textTruncate($nameAttendee, 500, false),
'activityid' => $record->getId(),
];
if ($status) {
$params['time'] = $timeFormated;
}
$dbCommand->insert('u_#__activity_invitation', $params)->execute();
}
}
foreach ($invities as &$invitation) {
$dbCommand->delete('u_#__activity_invitation', ['inviteesid' => $invitation['inviteesid']])->execute();
}
}
}
/**
* Dav save attendee.
*
* @param array $record
*/
public function davSaveAttendee(array $record)
{
$owner = \Users_Privileges_Model::getInstanceById($record['assigned_user_id']);
$invities = $this->getInvitations($record['id']);
$attendees = $this->vcomponent->select('ATTENDEE');
if (empty($attendees)) {
if (!empty($invities)) {
$organizer = $this->vcalendar->createProperty('ORGANIZER', 'mailto:' . $owner->get('email1'));
$organizer->add('CN', $owner->getName());
$this->vcomponent->add($organizer);
$attendee = $this->vcalendar->createProperty('ATTENDEE', 'mailto:' . $owner->get('email1'));
$attendee->add('CN', $owner->getName());
$attendee->add('ROLE', 'CHAIR');
$attendee->add('PARTSTAT', 'ACCEPTED');
$attendee->add('RSVP', 'false');
$this->vcomponent->add($attendee);
}
} else {
foreach ($attendees as &$attendee) {
$value = ltrim($attendee->getValue(), 'mailto:');
if (isset($invities[$value])) {
$row = $invities[$value];
if (isset($attendee['PARTSTAT'])) {
$attendee['PARTSTAT']->setValue($this->getAttendeeStatus($row['status'], false));
} else {
$attendee->add('PARTSTAT', $this->getAttendeeStatus($row['status']));
}
unset($invities[$value]);
} else {
$this->vcomponent->remove($attendee);
}
}
}
foreach ($invities as &$row) {
$attendee = $this->vcalendar->createProperty('ATTENDEE', 'mailto:' . $row['email']);
$attendee->add('CN', empty($row['crmid']) ? $row['name'] : \App\Record::getLabel($row['crmid']));
$attendee->add('ROLE', 'REQ-PARTICIPANT');
$attendee->add('PARTSTAT', $this->getAttendeeStatus($row['status'], false));
$attendee->add('RSVP', '0' == $row['status'] ? 'true' : 'false');
$this->vcomponent->add($attendee);
}
}
/**
* Get attendee status.
*
* @param string $value
* @param bool $toCrm
*
* @return false|string
*/
public function getAttendeeStatus(string $value, bool $toCrm = true)
{
$statuses = ['NEEDS-ACTION', 'ACCEPTED', 'DECLINED'];
if ($toCrm) {
$status = false;
$statuses = array_flip($statuses);
} else {
$status = 'NEEDS-ACTION';
}
if (isset($statuses[$value])) {
$status = $statuses[$value];
}
return $status;
}
/**
* Parses some information from calendar objects, used for optimized
* calendar-queries.
*
* Returns an array with the following keys:
* * etag - An md5 checksum of the object without the quotes.
* * size - Size of the object in bytes
* * componentType - VEVENT, VTODO or VJOURNAL
* * firstOccurence
* * lastOccurence
* * uid - value of the UID property
*
* @param string $calendarData
*
* @return array
*
* @see Sabre\CalDAV\Backend\PDO::getDenormalizedData
*/
public function getDenormalizedData($calendarData)
{
$vObject = VObject\Reader::read($calendarData);
$uid = $lastOccurence = $firstOccurence = $component = $componentType = null;
foreach ($vObject->getComponents() as $component) {
if ('VTIMEZONE' !== $component->name) {
$componentType = $component->name;
$uid = (string) $component->UID;
break;
}
}
if (!$componentType) {
throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
}
if ('VEVENT' === $componentType) {
$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
// Finding the last occurence is a bit harder
if (!isset($component->RRULE)) {
if (isset($component->DTEND)) {
$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
} elseif (isset($component->DURATION)) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
$lastOccurence = $endDate->getTimeStamp();
} elseif (!$component->DTSTART->hasTime()) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate = $endDate->modify('+1 day');
$lastOccurence = $endDate->getTimeStamp();
} else {
$lastOccurence = $firstOccurence;
}
} else {
$it = new VObject\Recur\EventIterator($vObject, (string) $component->UID);
$maxDate = new \DateTime(self::MAX_DATE);
if ($it->isInfinite()) {
$lastOccurence = $maxDate->getTimeStamp();
} else {
$end = $it->getDtEnd();
while ($it->valid() && $end < $maxDate) {
$end = $it->getDtEnd();
$it->next();
}
$lastOccurence = $end->getTimeStamp();
}
}
// Ensure Occurence values are positive
if ($firstOccurence < 0) {
$firstOccurence = 0;
}
if ($lastOccurence < 0) {
$lastOccurence = 0;
}
}
// Destroy circular references to PHP will GC the object.
$vObject->destroy();
return [
'etag' => md5($calendarData),
'size' => \strlen($calendarData),
'componentType' => $componentType,
'firstOccurence' => $firstOccurence,
'lastOccurence' => $lastOccurence,
'uid' => $uid,
];
}
/**
* Find crm id by email.
*
* @param int|string $value
* @param array $allowedModules
* @param array $skipModules
*
* @return array
*/
public function findRecordByEmail($value, array $allowedModules = [], array $skipModules = [])
{
$db = \App\Db::getInstance();
$rows = $fields = [];
$dataReader = (new \App\Db\Query())->select(['vtiger_field.columnname', 'vtiger_field.tablename', 'vtiger_field.fieldlabel', 'vtiger_field.tabid', 'vtiger_tab.name'])
->from('vtiger_field')->innerJoin('vtiger_tab', 'vtiger_field.tabid = vtiger_tab.tabid')
->where(['vtiger_tab.presence' => 0])
->andWhere(['<>', 'vtiger_field.presence', 1])
->andWhere(['or', ['uitype' => 13], ['uitype' => 104]])->createCommand()->query();
while ($row = $dataReader->read()) {
$fields[$row['name']][$row['tablename']][$row['columnname']] = $row;
}
$queryUnion = null;
foreach ($fields as $moduleName => $moduleFields) {
if (($allowedModules && !\in_array($moduleName, $allowedModules)) || \in_array($moduleName, $skipModules)) {
continue;
}
$instance = \CRMEntity::getInstance($moduleName);
$isEntityType = isset($instance->tab_name_index['vtiger_crmentity']);
foreach ($moduleFields as $tablename => $columns) {
$tableIndex = $instance->tab_name_index[$tablename];
$query = (new \App\Db\Query())->select(['crmid' => $tableIndex, 'modules' => new \yii\db\Expression($db->quoteValue($moduleName))])
->from($tablename);
if ($isEntityType) {
$query->innerJoin('vtiger_crmentity', "vtiger_crmentity.crmid = {$tablename}.{$tableIndex}")->where(['vtiger_crmentity.deleted' => 0]);
}
$orWhere = ['or'];
foreach ($columns as $row) {
$orWhere[] = ["{$row['tablename']}.{$row['columnname']}" => $value];
}
$query->andWhere($orWhere);
if ($queryUnion) {
$queryUnion->union($query);
} else {
$queryUnion = $query;
}
}
}
$rows = $queryUnion->all();
$labels = \App\Record::getLabel(array_column($rows, 'crmid'));
foreach ($rows as &$row) {
$row['label'] = $labels[$row['crmid']];
}
return $rows;
}
}