Ejemplo n.º 1
0
 function GetListItemChangesSinceToken($listName, $viewFields = '', $query = '', $rowLimit = 0, $changeToken = '')
 {
     global $APPLICATION, $USER;
     if (!$this->__Init()) {
         return $this->error;
     }
     if (!($listName_original = CIntranetUtils::checkGUID($listName))) {
         return new CSoapFault('Data error', 'Wrong GUID - ' . $listName);
     }
     $listName = ToUpper(CIntranetUtils::makeGUID($listName_original));
     $arFilter = array();
     if ($changeToken) {
         $bitrixTimestamp = $changeToken + CTasksTools::getTimeZoneOffset();
         $arFilter['>CHANGED_DATE'] = ConvertTimeStamp($bitrixTimestamp, 'FULL');
         if (!$arFilter['>CHANGED_DATE']) {
             return new CSoapFault('Params error', 'Wrong changeToken: ' . $changeToken);
         }
     }
     $last_change = 0;
     $data = new CXMLCreator('listitems');
     $data->setAttribute('MinTimeBetweenSyncs', 0);
     $data->setAttribute('RecommendedTimeBetweenSyncs', 180);
     $data->setAttribute('TimeStamp', $this->__makeDateTime());
     $data->setAttribute('EffectivePermMask', 'FullMask');
     $data->setAttribute('IncludeAttachmentUrls', 'False');
     $data->addChild($obChanges = new CXMLCreator('Changes'));
     if (!$changeToken) {
         $obChanges->addChild($this->__getFieldsDefinition());
     }
     $data->addChild($obData = new CXMLCreator('rs:data'));
     $counter = 0;
     $arFilter['MEMBER'] = $USER->GetID();
     $dbRes = CTasks::GetList(array("ID" => "ASC"), $arFilter, array(), array('bGetZombie' => true));
     while ($arRes = $dbRes->Fetch()) {
         if ($arRes['ZOMBIE'] === 'Y') {
             $obId = new CXMLCreator('Id');
             $obId->setAttribute('ChangeType', 'Delete');
             $obId->setData($arRes['ID']);
             $obChanges->addChild($obId);
         } else {
             $rsFiles = CTaskFiles::GetList(array(), array("TASK_ID" => $arRes["ID"]));
             $arRes["FILES"] = array();
             while ($arFiles = $rsFiles->fetch()) {
                 $arRes["FILES"][] = $arFiles["FILE_ID"];
             }
             $obData->addChild($this->__getRow($arRes, $listName, $last_change));
             $counter++;
         }
     }
     $obData->setAttribute('ItemCount', $counter);
     $data->setAttribute('xmlns:rs', 'urn:schemas-microsoft-com:rowset');
     $data->setAttribute('xmlns:z', '#RowsetSchema');
     if ($last_change > 0) {
         $obChanges->setAttribute('LastChangeToken', $last_change);
     }
     return array('GetListItemChangesSinceTokenResult' => $data);
 }
Ejemplo n.º 2
0
 private static function getUserTimeZoneOffset($userId = 'current')
 {
     if (!isset(self::$cache['TIMEZONE'][$userId]) || !self::$cacheData) {
         self::$cache['TIMEZONE'][$userId] = CTasksTools::getTimeZoneOffset($userId == 'current' ? false : $userId);
     }
     return self::$cache['TIMEZONE'][$userId];
 }
Ejemplo n.º 3
0
 private function getCounts($arGroupsIds)
 {
     $arCounters = array();
     if (empty($arGroupsIds)) {
         return;
     }
     foreach ($arGroupsIds as $groupId) {
         $arCounters[$groupId] = array('ALL' => 0, 'IN_WORK' => 0, 'COMPLETE' => 0);
     }
     $oFilter = CTaskFilterCtrl::getInstance($this->arParams['USER_ID'], true);
     $arFilterAll = $oFilter->getFilterPresetConditionById(CTaskFilterCtrl::STD_PRESET_ALL_MY_TASKS);
     $arFilterInWork = $oFilter->getFilterPresetConditionById(CTaskFilterCtrl::STD_PRESET_ACTIVE_MY_TASKS);
     $arFilterComplete = $oFilter->getFilterPresetConditionById(CTaskFilterCtrl::STD_PRESET_COMPLETED_MY_TASKS);
     $arFilterInWorkExpired = $arFilterInWork;
     $arFilterInWorkExpired['<DEADLINE'] = ConvertTimeStamp(time() + CTasksTools::getTimeZoneOffset(), 'FULL');
     $arFilterAll['GROUP_ID'] = $arGroupsIds;
     $arFilterInWork['GROUP_ID'] = $arGroupsIds;
     $arFilterComplete['GROUP_ID'] = $arGroupsIds;
     $arFilterInWorkExpired['GROUP_ID'] = $arGroupsIds;
     $arMap = array('ALL' => &$arFilterAll, 'IN_WORK' => &$arFilterInWork, 'COMPLETE' => &$arFilterComplete, 'EXPIRED' => &$arFilterInWorkExpired);
     foreach ($arMap as $key => &$arFilter) {
         $rs = CTasks::GetCount($arFilter, array('bSkipUserFields' => true, 'bSkipExtraTables' => true, 'bSkipJoinTblViewed' => false), array('GROUP_ID'));
         while ($ar = $rs->fetch()) {
             $groupId = (int) $ar['GROUP_ID'];
             if ($groupId) {
                 $arCounters[$groupId][$key] = (int) $ar['CNT'];
             }
         }
     }
     unset($arFilter);
     return $arCounters;
 }
Ejemplo n.º 4
0
 protected static function getTimemanCloseDayData($arParams)
 {
     if (CModule::IncludeModule('timeman')) {
         global $USER;
         $arTasks = array();
         $userId = $USER->getId();
         $runningTaskId = null;
         $taskRunTime = null;
         // key features of that info:
         // [REPORT_REQ] => 'A' means that day will be closed right now. other variants - just form show.
         // [INFO][DATE_START] => 1385459336 - unix timestamp of day start
         // [INFO][TIME_START] => 46136 - short timestamp of day start
         // [DURATION]
         // [TIME_LEAKS]
         $arTimemanInfo = CTimeMan::GetRunTimeInfo(true);
         if (!($userId > 0)) {
             foreach ($arTimemanInfo['PLANNER']['DATA']['TASKS'] as $arTask) {
                 $arTask['TIME'] = 0;
                 $arTasks[] = $arTask;
             }
             return array('TASKS' => $arTasks);
         }
         $unixTsDateStart = (int) $arTimemanInfo['INFO']['DATE_START'];
         $oTimer = CTaskTimerManager::getInstance($userId);
         $arTimer = $oTimer->getLastTimer();
         if ($arTimer && $arTimer['TIMER_STARTED_AT'] > 0) {
             $runningTaskId = $arTimer['TASK_ID'];
             if ($arTimer['TIMER_STARTED_AT'] >= $unixTsDateStart) {
                 $taskRunTime = max(0, time() - (int) $arTimer['TIMER_STARTED_AT']);
             } else {
                 $taskRunTime = max(0, time() - $unixTsDateStart);
             }
         }
         $bitrixTimestampDateStart = $unixTsDateStart + CTasksTools::getTimeZoneOffset();
         $dateStartAsString = ConvertTimeStamp($bitrixTimestampDateStart, 'FULL');
         foreach ($arTimemanInfo['PLANNER']['DATA']['TASKS'] as $arTask) {
             $rsElapsedTime = CTaskElapsedTime::getList(array('ID' => 'ASC'), array('TASK_ID' => $arTask['ID'], 'USER_ID' => $userId, '>=CREATED_DATE' => $dateStartAsString), array('skipJoinUsers' => true));
             $arTask['TIME'] = 0;
             while ($arElapsedTime = $rsElapsedTime->fetch()) {
                 $arTask['TIME'] += max(0, $arElapsedTime['SECONDS']);
             }
             if ($runningTaskId && $arTask['ID'] == $runningTaskId) {
                 $arTask['TIME'] += $taskRunTime;
             }
             $arTasks[] = $arTask;
         }
         return array('TASKS' => $arTasks);
     }
 }
Ejemplo n.º 5
0
 /**
  * This method is deprecated. Use CTaskItem::delete() instead.
  * @deprecated
  */
 public static function Delete($ID, $arParams = array())
 {
     global $DB, $CACHE_MANAGER, $USER;
     if (isset($USER) && is_object($USER)) {
         $actorUserId = (int) $USER->getId();
     } else {
         $actorUserId = 1;
     }
     if (isset($arParams['META::EVENT_GUID'])) {
         $eventGUID = $arParams['META::EVENT_GUID'];
         unset($arParams['META::EVENT_GUID']);
     } else {
         $eventGUID = sha1(uniqid('AUTOGUID', true));
     }
     $paramSkipExchangeSync = false;
     if (is_array($arParams)) {
         if (isset($arParams['skipExchangeSync']) && ($arParams['skipExchangeSync'] === 'Y' || $arParams['skipExchangeSync'] === true)) {
             $paramSkipExchangeSync = true;
         }
     }
     $ID = intval($ID);
     if ($ID < 1) {
         return false;
     }
     $rsTask = CTasks::GetByID($ID, false);
     if ($arTask = $rsTask->Fetch()) {
         foreach (GetModuleEvents('tasks', 'OnBeforeTaskDelete', true) as $arEvent) {
             if (ExecuteModuleEventEx($arEvent, array($ID, $arTask)) === false) {
                 return false;
             }
         }
         CTaskCountersProcessor::onBeforeTaskDelete($ID, $arTask);
         CTaskMembers::DeleteByTaskID($ID);
         CTaskFiles::DeleteByTaskID($ID);
         CTaskDependence::DeleteByTaskID($ID);
         CTaskDependence::DeleteByDependsOnID($ID);
         CTaskTags::DeleteByTaskID($ID);
         $strSql = "DELETE FROM b_tasks_viewed WHERE TASK_ID = " . $ID;
         $DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__);
         $strSql = "DELETE FROM b_tasks_reminder WHERE TASK_ID = " . $ID;
         $DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__);
         // clear cache
         if ($arTask["GROUP_ID"]) {
             $CACHE_MANAGER->ClearByTag("tasks_group_" . $arTask["GROUP_ID"]);
         }
         $arParticipants = array_unique(array_merge(array($arTask["CREATED_BY"], $arTask["RESPONSIBLE_ID"]), $arTask["ACCOMPLICES"], $arTask["AUDITORS"]));
         foreach ($arParticipants as $userId) {
             $CACHE_MANAGER->ClearByTag("tasks_user_" . $userId);
         }
         $strSql = "UPDATE b_tasks_template SET TASK_ID = NULL WHERE TASK_ID = " . $ID;
         $DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__);
         $strSql = "UPDATE b_tasks_template SET PARENT_ID = " . ($arTask["PARENT_ID"] ? $arTask["PARENT_ID"] : "NULL") . " WHERE PARENT_ID = " . $ID;
         $DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__);
         $strSql = "UPDATE b_tasks SET PARENT_ID = " . ($arTask["PARENT_ID"] ? $arTask["PARENT_ID"] : "NULL") . " WHERE PARENT_ID = " . $ID;
         $DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__);
         $strUpdate = $DB->PrepareUpdate("b_tasks", array('ZOMBIE' => 'Y', 'CHANGED_BY' => $actorUserId, 'CHANGED_DATE' => date($DB->DateFormatToPHP(CSite::GetDateFormat('FULL')), time() + CTasksTools::getTimeZoneOffset())), "tasks");
         $strSql = "UPDATE b_tasks SET " . $strUpdate . " WHERE ID = " . (int) $ID;
         if ($DB->Query($strSql, false, "File: " . __FILE__ . "<br>Line: " . __LINE__)) {
             CTaskNotifications::SendDeleteMessage($arTask);
             if ($arTask["FORUM_TOPIC_ID"] && CModule::IncludeModule("forum")) {
                 CForumTopic::Delete($arTask["FORUM_TOPIC_ID"]);
             }
             if (!$paramSkipExchangeSync) {
                 CTaskSync::DeleteItem($arTask);
             }
             // MS Exchange
             // Emit pull event
             try {
                 $arPullRecipients = array();
                 foreach ($arParticipants as $userId) {
                     $arPullRecipients[] = (int) $userId;
                 }
                 $taskGroupId = 0;
                 // no group
                 if (isset($arTask['GROUP_ID']) && $arTask['GROUP_ID'] > 0) {
                     $taskGroupId = (int) $arTask['GROUP_ID'];
                 }
                 $arPullData = array('TASK_ID' => (int) $ID, 'BEFORE' => array('GROUP_ID' => $taskGroupId), 'TS' => time(), 'event_GUID' => $eventGUID);
                 self::EmitPullWithTagPrefix($arPullRecipients, 'TASKS_GENERAL_', 'task_remove', $arPullData);
                 self::EmitPullWithTag($arPullRecipients, 'TASKS_TASK_' . (int) $ID, 'task_remove', $arPullData);
             } catch (Exception $e) {
             }
             foreach (GetModuleEvents('tasks', 'OnTaskDelete', true) as $arEvent) {
                 ExecuteModuleEventEx($arEvent, array($ID));
             }
             if (CModule::IncludeModule("search")) {
                 CSearch::DeleteIndex("tasks", $ID);
             }
         }
         return true;
     }
     return false;
 }
 /**
  * This function resets all counters for all users and recounts them.
  * 
  * This function do work which IS NOT multi-thread safe.
  */
 public static function setup($step = self::STEP_BEGIN, $extraParam = null)
 {
     $nextStep = $nextStepDelay = $nextStepExtraParam = null;
     //$s = 'CTaskCountersProcessorInstaller::setup()';
     //soundex($s);
     $timeLimit = microtime(true) + 5;
     // give at least 5 seconds for work
     while (microtime(true) <= $timeLimit) {
         /** @noinspection PhpUnusedLocalVariableInspection */
         $nextStep = $nextStepDelay = $nextStepExtraParam = null;
         //soundex($step);
         if ($step === self::STEP_BEGIN) {
             self::setStage(self::STAGE_INSTALL_IN_PROGRESS);
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessor::agent();', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup();', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_DROP_COUNTERS . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_NEW_FOR_RESPONSIBLES . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_NEW_FOR_ACCOMPLICES . '");', 'tasks');
             //CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_NEW_FOR_AUDITORS . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_WAIT_CTRL_FOR_ORIGINATORS . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_EXPIRED . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_WITHOUT_DEADLINES_MY . '");', 'tasks');
             /** @noinspection PhpDynamicAsStaticMethodCallInspection */
             CAgent::RemoveAgent('CTaskCountersProcessorInstaller::setup("' . self::STEP_COUNT_WITHOUT_DEADLINES_FOR_ORIGINATORS . '");', 'tasks');
             // defer for at min 130 seconds the next step,
             // because we must give time for CTaskCountersProcessor::agent()
             // to complete its work, if it was already started
             $nextStep = self::STEP_DROP_COUNTERS;
             $nextStepDelay = 130;
         } else {
             switch ($step) {
                 case self::STEP_DROP_COUNTERS:
                     // reset DEADLINE_COUNTED flags and all tasks counters for all users
                     self::reset();
                     $nextStep = self::STEP_COUNT_NEW_FOR_RESPONSIBLES;
                     break;
                 case self::STEP_COUNT_NEW_FOR_RESPONSIBLES:
                     self::recountCounters_MY_NEW($userId = '*');
                     // recount for all users
                     $nextStep = self::STEP_COUNT_NEW_FOR_ACCOMPLICES;
                     break;
                 case self::STEP_COUNT_NEW_FOR_ACCOMPLICES:
                     self::recountCounters_ACCOMPLICE_NEW($userId = '*');
                     // recount for all users
                     //$nextStep = self::STEP_COUNT_NEW_FOR_AUDITORS;
                     $nextStep = self::STEP_COUNT_WAIT_CTRL_FOR_ORIGINATORS;
                     break;
                 case self::STEP_COUNT_WAIT_CTRL_FOR_ORIGINATORS:
                     self::recountCounters_ORIGINATORS_WAIT_CTRL($userId = '*');
                     // recount for all users
                     $nextStep = self::STEP_COUNT_WITHOUT_DEADLINES_MY;
                     break;
                 case self::STEP_COUNT_WITHOUT_DEADLINES_MY:
                     self::recountCounters_MY_WITHOUT_DEADLINES($userId = '*');
                     // recount for all users
                     $nextStep = self::STEP_COUNT_WITHOUT_DEADLINES_FOR_ORIGINATORS;
                     break;
                 case self::STEP_COUNT_WITHOUT_DEADLINES_FOR_ORIGINATORS:
                     self::recountCounters_ORIGINATORS_WITHOUT_DEADLINES($userId = '*');
                     // recount for all users
                     $nextStep = self::STEP_COUNT_EXPIRED;
                     break;
                 case self::STEP_COUNT_EXPIRED:
                     $executionTimeLimit = mt_rand(1, 6);
                     // time limit in seconds
                     $itemsProcessed = CTaskCountersProcessor::countExpiredAndExpiredSoonTasks($executionTimeLimit);
                     // Some items processed?
                     if ($itemsProcessed > 0) {
                         // try again
                         $nextStep = self::STEP_COUNT_EXPIRED;
                         $nextStepDelay = 5;
                     } else {
                         /** @noinspection PhpDynamicAsStaticMethodCallInspection */
                         CAgent::AddAgent('CTaskCountersProcessor::agent();', 'tasks', 'N', 900);
                         // every 15 minutes
                         self::setStage(self::STAGE_INSTALL_COMPLETE);
                         CTaskCountersProcessorHomeostasis::onCalculationComplete();
                         $nextStep = null;
                         // the end
                     }
                     break;
                 default:
                     CTaskAssert::logError('[0xd7b90d6d] ');
                     $nextStep = null;
                     // the end
                     break;
             }
         }
         if ($nextStep === null) {
             break;
         }
         if ($nextStepDelay > 0) {
             break;
         }
         $step = $nextStep;
     }
     if ($nextStep !== null) {
         /** @noinspection PhpDeprecationInspection */
         $nextExecTimestamp = time() + CTasksTools::getTimeZoneOffset();
         if ($nextStepDelay !== null) {
             $nextExecTimestamp += $nextStepDelay;
         } else {
             $nextExecTimestamp += 5;
         }
         // delay for ~5 seconds due to probably DB and PHP server time desynchronization
         $delayedTime = ConvertTimeStamp($nextExecTimestamp, 'FULL');
         /** @noinspection PhpDynamicAsStaticMethodCallInspection */
         CAgent::AddAgent('CTaskCountersProcessorInstaller::setup("' . $nextStep . '");', 'tasks', 'N', 86400000, $delayedTime, "Y", $delayedTime, 13);
     }
     return "";
     // remove self from agents table
 }
Ejemplo n.º 7
0
 public static function countExpiredAndExpiredSoonTasks($executionTimeLimit = 0.5)
 {
     $unixTs = time();
     /** @noinspection PhpDeprecationInspection */
     $bts = $unixTs + CTasksTools::getTimeZoneOffset();
     $expiredEdgeDateTime = ConvertTimeStamp($bts, 'FULL');
     // Calc current time + 24hours (86400 seconds)
     $expiredSoonEdgeDateTime = ConvertTimeStamp($bts + 86400, 'FULL');
     // Two different filters for expired tasks and to be expired tasks
     // This is cause to DB->CurrentTimeFunction() != current time in general case
     // TODO: this should be optimized to one getList() request
     $arFilterExpiredSoon = array('::LOGIC' => 'AND', '!REAL_STATUS' => array(CTasks::STATE_SUPPOSEDLY_COMPLETED, CTasks::STATE_COMPLETED, CTasks::STATE_DECLINED), '>=DEADLINE' => $expiredEdgeDateTime, '<DEADLINE' => $expiredSoonEdgeDateTime, '!=DEADLINE_COUNTED' => self::DEADLINE_COUNTED_AS_EXPIRED_CANDIDATE);
     $arFilterExpired = array('::LOGIC' => 'AND', '!REAL_STATUS' => array(CTasks::STATE_SUPPOSEDLY_COMPLETED, CTasks::STATE_COMPLETED, CTasks::STATE_DECLINED), '<DEADLINE' => $expiredEdgeDateTime, '!=DEADLINE_COUNTED' => self::DEADLINE_COUNTED_AS_EXPIRED);
     $arSelect = array('ID', 'CREATED_BY', 'RESPONSIBLE_ID', 'REAL_STATUS', 'DEADLINE', 'DEADLINE_COUNTED');
     $arParams = array('nPageTop' => 50, 'bIgnoreErrors' => true, 'USER_ID' => self::getAdminId());
     $itemsTotalProcessed = $itemsLastProcessed = 0;
     try {
         $timeLimit = microtime(true) + $executionTimeLimit;
         $minIterations = 2;
         // do not less than 2 iterations (in case if there was processed items)
         $iterationsDone = 0;
         do {
             $itemsLastProcessed = self::iterateAgentWork($arFilterExpiredSoon, $arFilterExpired, $arSelect, $arParams);
             $itemsTotalProcessed += $itemsLastProcessed;
             $iterationsDone++;
             if ($itemsLastProcessed == 0) {
                 break;
             }
         } while ($iterationsDone < $minIterations || microtime(true) <= $timeLimit);
     } catch (Exception $e) {
         CTaskAssert::logError('[0x87d58c6c] ');
     }
     // flush counters
     if ($itemsTotalProcessed != 0) {
         CTaskCountersQueue::execute();
     }
     return $itemsTotalProcessed;
 }
Ejemplo n.º 8
0
 public static function SendUpdateMessage($arFields, $arTask, $bSpawnedByAgent = false)
 {
     global $USER;
     $isBbCodeDescription = true;
     if (isset($arFields['DESCRIPTION_IN_BBCODE'])) {
         if ($arFields['DESCRIPTION_IN_BBCODE'] === 'N') {
             $isBbCodeDescription = false;
         }
     } elseif (isset($arTask['DESCRIPTION_IN_BBCODE'])) {
         if ($arTask['DESCRIPTION_IN_BBCODE'] === 'N') {
             $isBbCodeDescription = false;
         }
     }
     $taskReassignedTo = null;
     if (isset($arFields['RESPONSIBLE_ID']) && $arFields['RESPONSIBLE_ID'] > 0 && $arFields['RESPONSIBLE_ID'] != $arTask['RESPONSIBLE_ID']) {
         $taskReassignedTo = $arFields['RESPONSIBLE_ID'];
     }
     foreach (array('CREATED_BY', 'RESPONSIBLE_ID', 'ACCOMPLICES', 'AUDITORS', 'TITLE') as $field) {
         if (!isset($arFields[$field]) && isset($arTask[$field])) {
             $arFields[$field] = $arTask[$field];
         }
     }
     $arChanges = CTaskLog::GetChanges($arTask, $arFields);
     $arMerged = array('ADDITIONAL_RECIPIENTS' => array());
     if (isset($arTask['CREATED_BY'])) {
         $arMerged['ADDITIONAL_RECIPIENTS'][] = $arTask['CREATED_BY'];
     }
     if (isset($arTask['RESPONSIBLE_ID'])) {
         $arMerged['ADDITIONAL_RECIPIENTS'][] = $arTask['RESPONSIBLE_ID'];
     }
     if (isset($arTask['ACCOMPLICES']) && is_array($arTask['ACCOMPLICES'])) {
         foreach ($arTask['ACCOMPLICES'] as $userId) {
             $arMerged['ADDITIONAL_RECIPIENTS'][] = $userId;
         }
     }
     if (isset($arTask['AUDITORS']) && is_array($arTask['AUDITORS'])) {
         foreach ($arTask['AUDITORS'] as $userId) {
             $arMerged['ADDITIONAL_RECIPIENTS'][] = $userId;
         }
     }
     if (isset($arFields['ADDITIONAL_RECIPIENTS'])) {
         $arFields['ADDITIONAL_RECIPIENTS'] = array_merge($arFields['ADDITIONAL_RECIPIENTS'], $arMerged['ADDITIONAL_RECIPIENTS']);
     } else {
         $arFields['ADDITIONAL_RECIPIENTS'] = $arMerged['ADDITIONAL_RECIPIENTS'];
     }
     $arUsers = CTaskNotifications::__GetUsers($arFields);
     $arRecipientsIDs = array_unique(CTaskNotifications::GetRecipientsIDs($arFields));
     if (!empty($arRecipientsIDs) && (is_object($USER) && $USER->GetID() || $arFields["CREATED_BY"])) {
         $curUserTzOffset = (int) CTasksTools::getTimeZoneOffset();
         $arInvariantChangesStrs = array();
         $arVolatileDescriptions = array();
         $arRecipientsIDsByTimezone = array();
         $i = 0;
         foreach ($arChanges as $key => $value) {
             ++$i;
             $actionMessage = GetMessage("TASKS_MESSAGE_" . $key);
             if (strlen($actionMessage)) {
                 $tmpStr = $actionMessage . ": [COLOR=#000]";
                 switch ($key) {
                     case 'TIME_ESTIMATE':
                         $tmpStr .= self::formatTimeHHMM($value["FROM_VALUE"], true) . " -> " . self::formatTimeHHMM($value["TO_VALUE"], true);
                         break;
                     case "TITLE":
                         $tmpStr .= $value["FROM_VALUE"] . " -> " . $value["TO_VALUE"];
                         break;
                     case "RESPONSIBLE_ID":
                         $tmpStr .= CTaskNotifications::__Users2String($value["FROM_VALUE"], $arUsers, $arFields["NAME_TEMPLATE"]) . ' -> ' . CTaskNotifications::__Users2String($value["TO_VALUE"], $arUsers, $arFields["NAME_TEMPLATE"]);
                         break;
                     case "ACCOMPLICES":
                     case "AUDITORS":
                         $tmpStr .= CTaskNotifications::__Users2String(explode(",", $value["FROM_VALUE"]), $arUsers, $arFields["NAME_TEMPLATE"]) . ' -> ' . CTaskNotifications::__Users2String(explode(",", $value["TO_VALUE"]), $arUsers, $arFields["NAME_TEMPLATE"]);
                         break;
                     case "DEADLINE":
                     case "START_DATE_PLAN":
                     case "END_DATE_PLAN":
                         // CTasks::Log() returns bitrix timestamps for dates, so adjust them to correct unix timestamps.
                         $utsFromValue = $value['FROM_VALUE'] - $curUserTzOffset;
                         $utsToValue = $value['TO_VALUE'] - $curUserTzOffset;
                         // It will be replaced below to formatted string with correct dates for different timezones
                         $placeholder = '###PLACEHOLDER###' . $i . '###';
                         $tmpStr .= $placeholder;
                         // Collect recipients' timezones
                         foreach ($arRecipientsIDs as $userId) {
                             $tzOffset = (int) CTasksTools::getTimeZoneOffset($userId);
                             if (!isset($arVolatileDescriptions[$tzOffset])) {
                                 $arVolatileDescriptions[$tzOffset] = array();
                             }
                             if (!isset($arVolatileDescriptions[$tzOffset][$placeholder])) {
                                 // Make bitrix timestamps for given user
                                 $bitrixTsFromValue = $utsFromValue + $tzOffset;
                                 $bitrixTsToValue = $utsToValue + $tzOffset;
                                 $description = '';
                                 if ($utsFromValue > 360000) {
                                     $fromValueAsString = FormatDate('^' . CDatabase::DateFormatToPHP(FORMAT_DATETIME), $bitrixTsFromValue);
                                     $description .= $fromValueAsString;
                                 }
                                 $description .= ' --> ';
                                 if ($utsToValue > 360000) {
                                     $toValueAsString = FormatDate('^' . CDatabase::DateFormatToPHP(FORMAT_DATETIME), $bitrixTsToValue);
                                     $description .= $toValueAsString;
                                 }
                                 $arVolatileDescriptions[$tzOffset][$placeholder] = $description;
                             }
                             $arRecipientsIDsByTimezone[$tzOffset][] = $userId;
                         }
                         break;
                     case "DESCRIPTION":
                         $tmpStr .= HTMLToTxt($arFields["DESCRIPTION"]);
                         break;
                     case "TAGS":
                         $tmpStr .= ($value["FROM_VALUE"] ? str_replace(",", ", ", $value["FROM_VALUE"]) . " -> " : "") . ($value["TO_VALUE"] ? str_replace(",", ", ", $value["TO_VALUE"]) : GetMessage("TASKS_MESSAGE_NO_VALUE"));
                         break;
                     case "PRIORITY":
                         $tmpStr .= GetMessage("TASKS_PRIORITY_" . $value["FROM_VALUE"]) . " -> " . GetMessage("TASKS_PRIORITY_" . $value["TO_VALUE"]);
                         break;
                     case "GROUP_ID":
                         if ($value["FROM_VALUE"] && CSocNetGroup::CanUserViewGroup($USER->GetID(), $value["FROM_VALUE"])) {
                             $arGroupFrom = CSocNetGroup::GetByID($value["FROM_VALUE"]);
                             if ($arGroupFrom) {
                                 $tmpStr .= $arGroupFrom["NAME"] . " -> ";
                             }
                         }
                         if ($value["TO_VALUE"] && CSocNetGroup::CanUserViewGroup($USER->GetID(), $value["TO_VALUE"])) {
                             $arGroupTo = CSocNetGroup::GetByID($value["TO_VALUE"]);
                             if ($arGroupTo) {
                                 $tmpStr .= $arGroupTo["NAME"];
                             }
                         } else {
                             $tmpStr .= GetMessage("TASKS_MESSAGE_NO_VALUE");
                         }
                         break;
                     case "PARENT_ID":
                         if ($value["FROM_VALUE"]) {
                             $rsTaskFrom = CTasks::GetList(array(), array("ID" => $value["FROM_VALUE"]), array('ID', 'TITLE'));
                             if ($arTaskFrom = $rsTaskFrom->GetNext()) {
                                 $tmpStr .= $arTaskFrom["TITLE"] . " -> ";
                             }
                         }
                         if ($value["TO_VALUE"]) {
                             $rsTaskTo = CTasks::GetList(array(), array("ID" => $value["TO_VALUE"]), array('ID', 'TITLE'));
                             if ($arTaskTo = $rsTaskTo->GetNext()) {
                                 $tmpStr .= $arTaskTo["TITLE"];
                             }
                         } else {
                             $tmpStr .= GetMessage("TASKS_MESSAGE_NO_VALUE");
                         }
                         break;
                     case "DEPENDS_ON":
                         $arTasksFromStr = array();
                         if ($value["FROM_VALUE"]) {
                             $rsTasksFrom = CTasks::GetList(array(), array("ID" => explode(",", $value["FROM_VALUE"])), array('ID', 'TITLE'));
                             while ($arTaskFrom = $rsTasksFrom->GetNext()) {
                                 $arTasksFromStr[] = $arTaskFrom["TITLE"];
                             }
                         }
                         $arTasksToStr = array();
                         if ($value["TO_VALUE"]) {
                             $rsTasksTo = CTasks::GetList(array(), array("ID" => explode(",", $value["TO_VALUE"])), array('ID', 'TITLE'));
                             while ($arTaskTo = $rsTasksTo->GetNext()) {
                                 $arTasksToStr[] = $arTaskTo["TITLE"];
                             }
                         }
                         $tmpStr .= ($arTasksFromStr ? implode(", ", $arTasksFromStr) . " -> " : "") . ($arTasksToStr ? implode(", ", $arTasksToStr) : GetMessage("TASKS_MESSAGE_NO_VALUE"));
                         break;
                     case "MARK":
                         $tmpStr .= (!$value["FROM_VALUE"] ? GetMessage("TASKS_MARK_NONE") : GetMessage("TASKS_MARK_" . $value["FROM_VALUE"])) . " -> " . (!$value["TO_VALUE"] ? GetMessage("TASKS_MARK_NONE") : GetMessage("TASKS_MARK_" . $value["TO_VALUE"]));
                         break;
                     case "ADD_IN_REPORT":
                         $tmpStr .= ($value["FROM_VALUE"] == "Y" ? GetMessage("TASKS_MESSAGE_IN_REPORT_YES") : GetMessage("TASKS_MESSAGE_IN_REPORT_NO")) . " -> " . ($value["TO_VALUE"] == "Y" ? GetMessage("TASKS_MESSAGE_IN_REPORT_YES") : GetMessage("TASKS_MESSAGE_IN_REPORT_NO"));
                         break;
                     case "DELETED_FILES":
                         $tmpStr .= $value["FROM_VALUE"];
                         $tmpStr .= $value["TO_VALUE"];
                         break;
                     case "NEW_FILES":
                         $tmpStr .= $value["TO_VALUE"];
                         break;
                 }
                 $tmpStr .= "[/COLOR]";
                 $arInvariantChangesStrs[] = $tmpStr;
             }
         }
         $occurAsUserId = CTasksTools::getOccurAsUserId();
         if (!$occurAsUserId) {
             $occurAsUserId = is_object($USER) && $USER->GetID() ? $USER->GetID() : $arFields["CREATED_BY"];
         }
         $invariantDescription = null;
         if (!empty($arInvariantChangesStrs)) {
             $invariantDescription = implode("\r\n", $arInvariantChangesStrs);
         }
         if ($invariantDescription !== null && !empty($arRecipientsIDs)) {
             // If there is no volatile part of descriptions, send to all recipients at once
             if (empty($arVolatileDescriptions)) {
                 $arVolatileDescriptions['some_timezone'] = array();
                 $arRecipientsIDsByTimezone['some_timezone'] = $arRecipientsIDs;
             }
             foreach ($arVolatileDescriptions as $tzOffset => $arVolatileDescriptionsData) {
                 $strDescription = $invariantDescription;
                 foreach ($arVolatileDescriptionsData as $placeholder => $strReplaceTo) {
                     $strDescription = str_replace($placeholder, $strReplaceTo, $strDescription);
                 }
                 $message = str_replace(array("#TASK_TITLE#", "#TASK_EXTRA#"), array(self::formatTaskName($arTask['ID'], $arTask['TITLE'], $arTask['GROUP_ID'], true), $strDescription), GetMessage("TASKS_TASK_CHANGED_MESSAGE"));
                 if ($isBbCodeDescription) {
                     $parser = new CTextParser();
                     $htmlDescription = str_replace("\t", ' &nbsp; &nbsp;', $parser->convertText($strDescription));
                 } else {
                     $htmlDescription = $strDescription;
                 }
                 $message_email = str_replace(array("#TASK_TITLE#", "#TASK_EXTRA#"), array(self::formatTaskName($arTask['ID'], $arTask['TITLE'], $arTask['GROUP_ID']), $htmlDescription . "\r\n" . GetMessage('TASKS_MESSAGE_LINK') . ': #PATH_TO_TASK#'), GetMessage("TASKS_TASK_CHANGED_MESSAGE"));
                 CTaskNotifications::SendMessage($occurAsUserId, $arRecipientsIDsByTimezone[$tzOffset], $message, $arTask["ID"], $message_email, array('ACTION' => 'TASK_UPDATE', 'arFields' => $arFields, 'arChanges' => $arChanges), $taskReassignedTo);
             }
         }
     }
     // sonet log
     self::SendMessageToSocNet($arFields, $bSpawnedByAgent, $arChanges, $arTask);
 }