Current Path : /var/www/axolotl/data/www/arhangelsk.axolotls.ru/bitrix/modules/tasks/lib/provider/ |
Current File : /var/www/axolotl/data/www/arhangelsk.axolotls.ru/bitrix/modules/tasks/lib/provider/ctaskslegacy.php |
<?php /** * Bitrix Framework * @package bitrix * @subpackage tasks * @copyright 2001-2013 Bitrix * * @global $USER_FIELD_MANAGER CUserTypeManager * @global $APPLICATION CMain * * @deprecated */ namespace Bitrix\Tasks\Provider; global $USER_FIELD_MANAGER; use Bitrix\Main\Application; use Bitrix\Main\DB\MssqlConnection; use Bitrix\Main\DB\MysqlCommonConnection; use Bitrix\Main\DB\OracleConnection; use Bitrix\Main\DB\SqlExpression; use Bitrix\Main\Entity; use Bitrix\Main\Entity\Query; use Bitrix\Main\Entity\Query\Join; use Bitrix\Main\Localization\Loc; use Bitrix\Main\UserTable; use Bitrix\Tasks\Integration; use Bitrix\Tasks\Internals\Counter; use Bitrix\Tasks\Internals\Helper\Task\Dependence; use Bitrix\Tasks\Internals\SearchIndex; use Bitrix\Tasks\Internals\Task\CheckListTable; use Bitrix\Tasks\Internals\Task\FavoriteTable; use Bitrix\Tasks\Internals\Task\MemberTable; use Bitrix\Tasks\Internals\Task\ParameterTable; use Bitrix\Tasks\Internals\Task\ProjectDependenceTable; use Bitrix\Tasks\Internals\Task\SearchIndexTable; use Bitrix\Tasks\Internals\Task\SortingTable; use Bitrix\Tasks\Internals\Task\ViewedTable; use Bitrix\Tasks\Kanban\TaskStageTable; use Bitrix\Tasks\Util\Calendar; use Bitrix\Tasks\Util\Replicator; use Bitrix\Tasks\Util\Type; use Bitrix\Tasks\Util\Type\DateTime; use Bitrix\Tasks\Util\User; use Bitrix\Tasks\Util\UserField; class CTasksLegacy { //Task statuses: 1 - New, 2 - Pending, 3 - In Progress, 4 - Supposedly completed, 5 - Completed, 6 - Deferred, 7 - Declined // todo: using statuses in the way "-2, -1" is a bad idea. its better to have separate (probably runtime) fields called "viewed" and "expired" // todo: and then, if you want to know if the task is "virgin new", just apply filter array('=VIEWED' => false, '=STATUS' => 2/*or 1*/) const METASTATE_VIRGIN_NEW = -2; // unseen const METASTATE_EXPIRED = -1; const METASTATE_EXPIRED_SOON = -3; const STATE_NEW = 1; const STATE_PENDING = 2; // Pending === Accepted const STATE_IN_PROGRESS = 3; const STATE_SUPPOSEDLY_COMPLETED = 4; const STATE_COMPLETED = 5; const STATE_DEFERRED = 6; const STATE_DECLINED = 7; const PRIORITY_LOW = 0; const PRIORITY_AVERAGE = 1; const PRIORITY_HIGH = 2; const MARK_POSITIVE = 'P'; const MARK_NEGATIVE = 'N'; const TIME_UNIT_TYPE_SECOND = 'secs'; const TIME_UNIT_TYPE_MINUTE = 'mins'; const TIME_UNIT_TYPE_HOUR = 'hours'; const TIME_UNIT_TYPE_DAY = 'days'; const TIME_UNIT_TYPE_WEEK = 'weeks'; const TIME_UNIT_TYPE_MONTH = 'monts'; // 5 chars max :) const TIME_UNIT_TYPE_YEAR = 'years'; const PARAMETER_PROJECT_PLAN_FROM_SUBTASKS = 0x01; const PARAMETER_COMPLETE_TASK_FROM_SUBTASKS = 0x02; const MAX_INT = 2147483647; private $_errors = array(); private $lastOperationResultData = array(); private $previousData = array(); private static $cacheIds = array(); private static $cacheClearEnabled = true; function GetErrors() { return $this->_errors; } public function getLastOperationResultData() { return $this->lastOperationResultData; } public function getPreviousData() { return $this->previousData; } function CheckFields(&$arFields, $ID = false, $effectiveUserId = null) { global $APPLICATION; if ($effectiveUserId === null) { $effectiveUserId = User::getId(); if (!$effectiveUserId) { $effectiveUserId = User::getAdminId(); } } if ((is_set($arFields, "TITLE") || $ID === false)) { $arFields["TITLE"] = trim((string)$arFields["TITLE"]); if ($arFields["TITLE"] == '') { $this->_errors[] = array("text" => GetMessage("TASKS_BAD_TITLE"), "id" => "ERROR_BAD_TASKS_TITLE"); } elseif (strlen($arFields['TITLE']) > 250) { $arFields['TITLE'] = substr($arFields['TITLE'], 0, 250); } } if (is_set($arFields, 'STATUS') && $arFields['STATUS'] == 1) { $arFields['STATUS'] = 2; // status self::STATE_NEW (=1) deprecated } // you are not allowed to clear up END_DATE_PLAN while the task is linked if ($ID && ((isset($arFields['END_DATE_PLAN']) && (string)$arFields['END_DATE_PLAN'] == ''))) { if (ProjectDependenceTable::checkItemLinked($ID)) { $this->_errors[] = array( "text" => GetMessage("TASKS_IS_LINKED_END_DATE_PLAN_REMOVE"), "id" => "ERROR_TASKS_IS_LINKED" ); } } if (array_key_exists('GROUP_ID', $arFields) && (int)$arFields['GROUP_ID'] > 0) { if (\Bitrix\Main\Loader::IncludeModule('socialnetwork')) { $group = \CSocNetGroup::getById($arFields['GROUP_ID']); if ($group && $group['PROJECT'] == 'Y' && ($group['PROJECT_DATE_START'] || $group['PROJECT_DATE_FINISH'])) { $projectStartDate = DateTime::createFrom($group['PROJECT_DATE_START']); $projectFinishDate = DateTime::createFrom($group['PROJECT_DATE_FINISH']); if ($projectFinishDate) { $projectFinishDate->addSecond(86399); // + 23:59:59 } $deadline = null; $endDatePlan = null; $startDatePlan = null; if (isset($arFields['DEADLINE']) && $arFields['DEADLINE']) { $deadline = DateTime::createFrom($arFields['DEADLINE']); } if (isset($arFields['END_DATE_PLAN']) && $arFields['END_DATE_PLAN']) { $endDatePlan = DateTime::createFrom($arFields['END_DATE_PLAN']); } if (isset($arFields['START_DATE_PLAN']) && $arFields['START_DATE_PLAN']) { $startDatePlan = DateTime::createFrom($arFields['START_DATE_PLAN']); } if ($deadline && !$deadline->checkInRange($projectStartDate, $projectFinishDate)) { $this->_errors[] = ["text" => GetMessage("TASKS_DEADLINE_OUT_OF_PROJECT_RANGE"), "id" => "ERROR_TASKS_OUT_OF_PROJECT_DATE"]; } if ($endDatePlan && !$endDatePlan->checkInRange($projectStartDate, $projectFinishDate)) { $this->_errors[] = ["text" => GetMessage("TASKS_PLAN_DATE_END_OUT_OF_PROJECT_RANGE"), "id" => "ERROR_TASKS_OUT_OF_PROJECT_DATE"]; } if ($startDatePlan && !$startDatePlan->checkInRange($projectStartDate, $projectFinishDate)) { $this->_errors[] = ["text" => GetMessage("TASKS_PLAN_DATE_START_OUT_OF_PROJECT_RANGE"), "id" => "ERROR_TASKS_OUT_OF_PROJECT_DATE"]; } } } } if ($ID && (isset($arFields['PARENT_ID']) && intval($arFields['PARENT_ID']) > 0)) { if (ProjectDependenceTable::checkLinkExists($ID, $arFields['PARENT_ID'], array('BIDIRECTIONAL' => true))) { $this->_errors[] = array( "text" => GetMessage("TASKS_IS_LINKED_SET_PARENT"), "id" => "ERROR_TASKS_IS_LINKED" ); } } // If plan dates were set if (isset($arFields['START_DATE_PLAN']) && ($arFields['START_DATE_PLAN'] != '') && isset($arFields['END_DATE_PLAN']) && ($arFields['END_DATE_PLAN'] != '')) { $startDate = MakeTimeStamp($arFields['START_DATE_PLAN']); $endDate = MakeTimeStamp($arFields['END_DATE_PLAN']); // and they were really set if ($startDate > 0 && $endDate > 0) { // and end date is before start date => then emit error if ($endDate < $startDate) { $this->_errors[] = array( 'text' => GetMessage('TASKS_BAD_PLAN_DATES'), 'id' => 'ERROR_BAD_TASKS_PLAN_DATES' ); } $duration = $endDate - $startDate; if ($duration > self::MAX_INT) { $this->_errors[] = array( 'text' => GetMessage('TASKS_BAD_DURATION'), 'id' => 'ERROR_TASKS_BAD_DURATION' ); } } } if ($ID === false && !is_set($arFields, "RESPONSIBLE_ID")) { $this->_errors[] = array( "text" => GetMessage("TASKS_BAD_RESPONSIBLE_ID"), "id" => "ERROR_TASKS_BAD_RESPONSIBLE_ID" ); } if ($ID === false && !is_set($arFields, "CREATED_BY")) $this->_errors[] = array( "text" => GetMessage("TASKS_BAD_CREATED_BY"), "id" => "ERROR_TASKS_BAD_CREATED_BY" ); if (is_set($arFields, "CREATED_BY")) { if (!($arFields['CREATED_BY'] >= 1)) $this->_errors[] = array( "text" => GetMessage("TASKS_BAD_CREATED_BY"), "id" => "ERROR_TASKS_BAD_CREATED_BY" ); } if (is_set($arFields, "RESPONSIBLE_ID")) { $r = \CUser::GetByID($arFields["RESPONSIBLE_ID"]); if ($arUser = $r->Fetch()) { if ($ID) { $rsTask = self::GetList( array(), array("ID" => $ID), array("RESPONSIBLE_ID"), array('USER_ID' => $effectiveUserId) ); if ($arTask = $rsTask->Fetch()) { $currentResponsible = $arTask["RESPONSIBLE_ID"]; } } // new task or responsible changed if (!$ID || (isset($currentResponsible) && $currentResponsible != $arFields["RESPONSIBLE_ID"])) { // check if $createdBy is director for responsible $createdBy = $arFields["CREATED_BY"]; $arSubDeps = self::GetSubordinateDeps($createdBy); if (!is_array($arUser["UF_DEPARTMENT"])) $bSubordinate = (sizeof(array_intersect($arSubDeps, array($arUser["UF_DEPARTMENT"]))) > 0); else $bSubordinate = (sizeof(array_intersect($arSubDeps, $arUser["UF_DEPARTMENT"])) > 0); if (!$arFields["STATUS"]) { $arFields["STATUS"] = self::STATE_PENDING; } if (!$bSubordinate) { $arFields["ADD_IN_REPORT"] = "N"; } $arFields["DECLINE_REASON"] = false; } } else { $this->_errors[] = array( "text" => GetMessage("TASKS_BAD_RESPONSIBLE_ID_EX"), "id" => "ERROR_TASKS_BAD_RESPONSIBLE_ID_EX" ); } } // move 0 to null in PARENT_ID to avoid constraint and query problems // todo: move PARENT_ID, GROUP_ID and other "foreign keys" to the unique way of keeping absense of relation: null, 0 or '' if (array_key_exists('PARENT_ID', $arFields)) { $parentId = intval($arFields['PARENT_ID']); if (!intval($parentId)) { $arFields['PARENT_ID'] = false; } } if (is_set($arFields, "PARENT_ID") && intval($arFields["PARENT_ID"]) > 0) { $r = self::GetByID($arFields["PARENT_ID"], true, array('USER_ID' => $effectiveUserId)); if (!$r->Fetch()) { $this->_errors[] = array( "text" => GetMessage("TASKS_BAD_PARENT_ID"), "id" => "ERROR_TASKS_BAD_PARENT_ID" ); } } if ($ID !== false && intval($arFields["PARENT_ID"])) { $result = \Bitrix\Tasks\Internals\Helper\Task\Dependence::canAttach($ID, $arFields["PARENT_ID"]); if (!$result->isSuccess()) { foreach ($result->getErrors()->getMessages() as $message) { $this->_errors[] = array("text" => $message, "id" => "ERROR_TASKS_PARENT_SELF"); } } } if ($ID !== false && is_array($arFields["DEPENDS_ON"]) && in_array($ID, $arFields["DEPENDS_ON"])) { $this->_errors[] = array( "text" => GetMessage("TASKS_DEPENDS_ON_SELF"), "id" => "ERROR_TASKS_DEPENDS_ON_SELF" ); } /* if(!$ID) { // since this time we dont allow to create tasks with a non-bbcode description if($arFields['DESCRIPTION_IN_BBCODE'] == 'N') { $this->_errors[] = array("text" => GetMessage("TASKS_DESCRIPTION_IN_BBCODE_NO_NOT_ALLOWED"), "id" => "ERROR_TASKS_DESCRIPTION_IN_BBCODE_NO_NOT_ALLOWED"); } else { $arFields['DESCRIPTION_IN_BBCODE'] = 'Y'; } } */ // accomplices & auditors Type::checkArrayOfUPIntegerKey($arFields, 'ACCOMPLICES'); Type::checkArrayOfUPIntegerKey($arFields, 'AUDITORS'); if (!Type::checkEnumKey( $arFields, 'STATUS', array( self::STATE_NEW, self::STATE_PENDING, self::STATE_IN_PROGRESS, self::STATE_SUPPOSEDLY_COMPLETED, self::STATE_COMPLETED, self::STATE_DEFERRED, self::STATE_DECLINED, ) )) { $this->_errors[] = array( "text" => GetMessage("TASKS_INCORRECT_STATUS"), "id" => "ERROR_TASKS_INCORRECT_STATUS" ); } Type::checkEnumKey( $arFields, 'PRIORITY', array(self::PRIORITY_LOW, self::PRIORITY_AVERAGE, self::PRIORITY_HIGH), self::PRIORITY_AVERAGE ); Type::checkEnumKey($arFields, 'MARK', array(self::MARK_NEGATIVE, self::MARK_POSITIVE, '')); // flags Type::checkYNKey($arFields, 'ALLOW_CHANGE_DEADLINE'); Type::checkYNKey($arFields, 'TASK_CONTROL'); Type::checkYNKey($arFields, 'ADD_IN_REPORT'); Type::checkYNKey($arFields, 'MATCH_WORK_TIME'); Type::checkYNKey($arFields, 'REPLICATE'); if (!empty($this->_errors)) { $e = new CAdminException($this->_errors); $APPLICATION->ThrowException($e); return false; } return true; } /** * This method is deprecated. Use CTaskItem::add() instead. * @deprecated */ public function Add($arFields, $arParams = array()) { global $DB, $USER_FIELD_MANAGER, $CACHE_MANAGER, $APPLICATION; if (isset($arFields['META::EVENT_GUID'])) { $eventGUID = $arFields['META::EVENT_GUID']; unset($arFields['META::EVENT_GUID']); } else $eventGUID = sha1(uniqid('AUTOGUID', true)); if (!array_key_exists('GUID', $arFields)) $arFields['GUID'] = CTasksTools::genUuid(); if (!isset($arFields['SITE_ID'])) $arFields['SITE_ID'] = SITE_ID; if (!isset($arParams['CORRECT_DATE_PLAN'])) { $arParams['CORRECT_DATE_PLAN'] = true; } if (isset($arFields['ALLOW_CHANGE_DEADLINE_COUNT'])) { $availableValues = array_column(\Bitrix\Tasks\UI\Controls\Fields\Deadline::getCountTimesItems(), 'VALUE'); if(!in_array($arFields['ALLOW_CHANGE_DEADLINE_COUNT'], $availableValues)) { $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = '*;'; } $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = $arFields['ALLOW_CHANGE_DEADLINE_COUNT']=='*' ? null: (int)$arFields['ALLOW_CHANGE_DEADLINE_COUNT']; } if (isset($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'])) { $availableValues = array_column(\Bitrix\Tasks\UI\Controls\Fields\Deadline::getTimesItems(), 'VALUE'); if(!in_array($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'], $availableValues)) { $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] = '*;'; } if($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] != '*') { $maxDate = Datetime::createFromTimestamp(strtotime('+'.$arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'])); } $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE'] = $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']=='*' ? null: $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']; $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] = $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']=='*' ? null: $maxDate; } // force GROUP_ID to 0 if not set (prevent occur as NULL in database) $arFields['GROUP_ID'] = intval($arFields['GROUP_ID']); $bWasFatalError = false; $spawnedByAgent = false; $effectiveUserId = null; $bCheckRightsOnFiles = false; // for backward compatibility if (is_array($arParams)) { if (isset($arParams['SPAWNED_BY_AGENT']) && (($arParams['SPAWNED_BY_AGENT'] === 'Y') || ($arParams['SPAWNED_BY_AGENT'] === true))) { $spawnedByAgent = true; } if (isset($arParams['USER_ID']) && ($arParams['USER_ID'] > 0)) $effectiveUserId = (int)$arParams['USER_ID']; if (isset($arParams['CHECK_RIGHTS_ON_FILES'])) { if (($arParams['CHECK_RIGHTS_ON_FILES'] === 'Y') || ($arParams['CHECK_RIGHTS_ON_FILES'] === true)) { $bCheckRightsOnFiles = true; } else $bCheckRightsOnFiles = false; } } self::processDurationPlanFields($arFields, $arFields['DURATION_TYPE']); if ($effectiveUserId === null) { $effectiveUserId = User::getId(); if (!$effectiveUserId) { $effectiveUserId = 1; // nasty, but for compatibility :( } } if ((!isset($arFields['CREATED_BY'])) || (!$arFields['CREATED_BY'])) { $arFields['CREATED_BY'] = $effectiveUserId; } if ($this->CheckFields($arFields, false, $effectiveUserId)) { // never, never step on this option. hot lava! if ($arParams['CLONE_DISK_FILE_ATTACHMENT'] === true || $arParams['CLONE_DISK_FILE_ATTACHMENT'] === 'Y') { // when you pass existing file attachments to add(), you must copy all the files and make new attachments // currently only for one field: UF_TASK_WEBDAV_FILES if (array_key_exists('UF_TASK_WEBDAV_FILES', $arFields) && is_array($arFields['UF_TASK_WEBDAV_FILES'])) { $arFields['UF_TASK_WEBDAV_FILES'] = Integration\Disk::cloneFileAttachment($arFields['UF_TASK_WEBDAV_FILES'], $effectiveUserId); } } if ($USER_FIELD_MANAGER->CheckFields("TASKS_TASK", 0, $arFields, $effectiveUserId)) { $nowDateTimeString = \Bitrix\Tasks\UI::formatDateTime(User::getTime()); if (!isset($arFields["CREATED_DATE"])) // created date was not set manually { $arFields["CREATED_DATE"] = $nowDateTimeString; } if (!isset($arFields["CHANGED_BY"])) { $arFields["STATUS_CHANGED_BY"] = $arFields["CHANGED_BY"] = $arFields["CREATED_BY"]; $arFields["STATUS_CHANGED_DATE"] = $arFields["CHANGED_DATE"] = $arFields["CREATED_DATE"] = $nowDateTimeString; } if (isset($arFields['DEADLINE']) && (string)$arFields['DEADLINE'] != '' && isset($arFields['MATCH_WORK_TIME']) && $arFields['MATCH_WORK_TIME'] == 'Y') { $arFields['DEADLINE'] = static::getDeadlineMatchWorkTime($arFields['DEADLINE']); } $shiftResult = null; if ($arParams['CORRECT_DATE_PLAN'] && ((string)$arFields['START_DATE_PLAN'] != '' || (string)$arFields['END_DATE_PLAN'] != '')) { $scheduler = \Bitrix\Tasks\Processor\Task\Scheduler::getInstance($effectiveUserId); $shiftResult = $scheduler->processEntity( 0, $arFields, array( 'MODE' => 'BEFORE_ATTACH', ) ); if ($shiftResult->isSuccess()) { $shiftData = $shiftResult->getImpactById(0); if ($shiftData) { // will be saved... $arFields['START_DATE_PLAN'] = $shiftData['START_DATE_PLAN']; $arFields['END_DATE_PLAN'] = $shiftData['END_DATE_PLAN']; $arFields['DURATION_PLAN_SECONDS'] = $shiftData['DURATION_PLAN_SECONDS']; } } } self::processDurationPlanFields($arFields, $arFields['DURATION_TYPE']); $arFields["OUTLOOK_VERSION"] = 1; foreach (GetModuleEvents('tasks', 'OnBeforeTaskAdd', true) as $arEvent) { if (ExecuteModuleEventEx($arEvent, array(&$arFields)) === false) { $e = $APPLICATION->GetException(); if ($e) { if ($e instanceof CAdminException) { if (is_array($e->messages)) { foreach ($e->messages as $msg) { $this->_errors[] = $msg; } } } else { $this->_errors[] = array('text' => $e->getString(), 'id' => 'unknown'); } } if (empty($this->_errors)) $this->_errors[] = array( "text" => GetMessage("TASKS_UNKNOWN_ADD_ERROR"), "id" => "ERROR_UNKNOWN_ADD_TASK_ERROR" ); return false; } } // Timezone hack http://jabber.bx/view.php?id=105626 $disabled = !\CTimeZone::enabled(); if ($disabled) { \CTimeZone::enable(); } $ID = $DB->Add("b_tasks", $arFields, array("DESCRIPTION"), "tasks"); if ($disabled) { \CTimeZone::disable(); } $arFields["ACCOMPLICES"] = (array)$arFields["ACCOMPLICES"]; $arFields["AUDITORS"] = (array)$arFields["AUDITORS"]; if ($ID) { $rsTask = self::GetByID($ID, false); if ($arTask = $rsTask->Fetch()) { // add to favorite, if needed if (intval($arFields['PARENT_ID']) && FavoriteTable::check( array('TASK_ID' => $arFields['PARENT_ID'], 'USER_ID' => $effectiveUserId) )) { FavoriteTable::add( array('TASK_ID' => $ID, 'USER_ID' => $effectiveUserId), array('CHECK_EXISTENCE' => false) ); } // drop, then re-add $res = MemberTable::getList(array('filter' => array('=TASK_ID' => $ID))); while ($item = $res->fetch()) { MemberTable::delete($item); } // add responsible and creator to the member "cache" table MemberTable::add( array( 'TASK_ID' => $ID, 'USER_ID' => $arTask['CREATED_BY'], 'TYPE' => 'O', ) ); MemberTable::add( array( 'TASK_ID' => $ID, 'USER_ID' => $arTask['RESPONSIBLE_ID'], 'TYPE' => 'R', ) ); self::AddAccomplices($ID, $arFields["ACCOMPLICES"]); self::AddAuditors($ID, $arFields["AUDITORS"]); self::AddFiles( $ID, $arFields["FILES"], array( 'USER_ID' => $effectiveUserId, 'CHECK_RIGHTS_ON_FILES' => $bCheckRightsOnFiles ) ); $newTags = static::detectTags($arFields); if (!empty($newTags)) { if (!isset($arFields['TAGS'])) { $arFields['TAGS'] = array(); } if (!is_array($arFields['TAGS'])) { $arFields['TAGS'] = array($arFields['TAGS']); } $arFields['TAGS'] = array_unique(array_merge($arFields['TAGS'], $newTags)); } self::AddTags($ID, $arTask["CREATED_BY"], $arFields["TAGS"], $effectiveUserId); self::AddPrevious($ID, $arFields["DEPENDS_ON"]); $arFields = self::processUserFields($arFields, $ID, $effectiveUserId); $USER_FIELD_MANAGER->Update("TASKS_TASK", $ID, $arFields, $effectiveUserId); // backward compatibility with PARENT_ID $parentId = intval($arFields["PARENT_ID"]); if ($parentId) { \Bitrix\Tasks\Internals\Helper\Task\Dependence::attachNew($ID, $parentId); } $arFields["ID"] = $ID; self::__updateViewed($ID, $effectiveUserId, $onTaskAdd = true); // CTaskCountersProcessor::onAfterTaskAdd($arFields); Counter::onAfterTaskAdd($arFields); CTaskComments::onAfterTaskAdd($ID, $arFields); $occurAsUserId = CTasksTools::getOccurAsUserId(); if (!$occurAsUserId) { $occurAsUserId = ($effectiveUserId ? $effectiveUserId : 1); } CTaskNotifications::SendAddMessage( array_merge($arFields, array('CHANGED_BY' => $occurAsUserId)), array('SPAWNED_BY_AGENT' => $spawnedByAgent) ); CTaskSync::AddItem($arFields); // MS Exchange // changes log $arLogFields = array( "TASK_ID" => $ID, "USER_ID" => $occurAsUserId, "CREATED_DATE" => $nowDateTimeString, "FIELD" => "NEW" ); $log = new CTaskLog(); $log->Add($arLogFields); try { $lastEventName = ''; foreach (GetModuleEvents('tasks', 'OnTaskAdd', true) as $arEvent) { $lastEventName = $arEvent['TO_CLASS'].'::'.$arEvent['TO_METHOD'].'()'; ExecuteModuleEventEx($arEvent, array($ID, &$arFields)); } } catch (Exception $e) { \CTaskAssert::logWarning( '[0x37eb64ae] exception in module event: '.$lastEventName ); \Bitrix\Tasks\Util::log($e); } $mergedFields = array_merge($arTask, $arFields); self::Index($mergedFields, $arFields["TAGS"]); // search index SearchIndex::setTaskSearchIndex($ID, $mergedFields); // clear cache if ($arFields["GROUP_ID"]) { $CACHE_MANAGER->ClearByTag("tasks_group_".$arFields["GROUP_ID"]); } $arParticipants = array_unique( array_merge( array($arFields["CREATED_BY"], $arFields["RESPONSIBLE_ID"]), $arFields["ACCOMPLICES"], $arFields["AUDITORS"] ) ); foreach ($arParticipants as $userId) { $CACHE_MANAGER->ClearByTag("tasks_user_".$userId); } // Emit pull event try { $arPullRecipients = array(); foreach ($arParticipants as $userId) { $arPullRecipients[] = (int)$userId; } $taskGroupId = 0; // no group if (isset($arFields['GROUP_ID']) && ($arFields['GROUP_ID'] > 0)) { $taskGroupId = (int)$arFields['GROUP_ID']; } $arPullData = [ 'TASK_ID' => (int)$ID, 'AFTER' => [ 'GROUP_ID' => $taskGroupId ], 'TS' => time(), 'event_GUID' => $eventGUID ]; // self::EmitPullWithTagPrefix( // $arPullRecipients, // 'TASKS_GENERAL_', // 'task_add', // $arPullData // ); self::EmitPullWithTag( $arPullRecipients, 'TASKS_TASK_'.(int)$ID, 'task_add', $arPullData ); \Bitrix\Tasks\Internals\Counter::sendPushCounters($arPullRecipients); } catch (Exception $e) { $bWasFatalError = true; $this->_errors[] = 'at line '.$e->GetLine().', '.$e->GetMessage(); } // tasks dependence if ($shiftResult !== null) { if ($parentId) { $childrenCountDbResult = self::getChildrenCount(array(), $parentId); $fetchedChildrenCount = $childrenCountDbResult->Fetch(); $childrenCount = $fetchedChildrenCount['CNT']; if ($childrenCount == 1) { $scheduler = \Bitrix\Tasks\Processor\Task\Scheduler::getInstance($effectiveUserId); $shiftResult = $scheduler->processEntity( 0, $arFields, array('MODE' => 'BEFORE_ATTACH') ); } } $shiftResult->save(array('!ID' => 0)); } if ($arFields['GROUP_ID'] && \CModule::IncludeModule("socialnetwork")) { CSocNetGroup::SetLastActivity($arFields['GROUP_ID']); } } } if ($bWasFatalError) soundex('push&pull: bWasFatalError === true'); return $ID; } else { $e = $APPLICATION->GetException(); foreach ($e->messages as $msg) { $this->_errors[] = $msg; } } } if (empty($this->_errors)) $this->_errors[] = array( "text" => GetMessage("TASKS_UNKNOWN_ADD_ERROR"), "id" => "ERROR_UNKNOWN_ADD_TASK_ERROR" ); return false; } private static function processDurationPlanFields(&$arFields, $type) { $durationPlan = false; if (isset($arFields['DURATION_PLAN_SECONDS'])) { $durationPlan = $arFields['DURATION_PLAN_SECONDS']; } elseif (isset($arFields['DURATION_PLAN'])) { $durationPlan = self::convertDurationToSeconds($arFields['DURATION_PLAN'], $type); } if ($durationPlan !== false) // smth were done { $arFields['DURATION_PLAN'] = $durationPlan; unset($arFields['DURATION_PLAN_SECONDS']); } } /** * Changes user fields if needed * * @param $fields * @param $taskId * @param $userId * * @return mixed */ private static function processUserFields($fields, $taskId, $userId) { global $USER_FIELD_MANAGER; $systemUserFields = array('UF_CRM_TASK', 'UF_TASK_WEBDAV_FILES'); $userFields = $USER_FIELD_MANAGER->GetUserFields('TASKS_TASK', $taskId, false, $userId); foreach ($fields as $key => $field) { if (array_key_exists($key, $userFields) && !array_key_exists($key, $systemUserFields) && $userFields[$key]['USER_TYPE_ID'] == 'boolean') { $fields[$key] = Type::convertBooleanUserFieldValue($field); } } return $fields; } /** * This method is deprecated. Use CTaskItem::update() instead. * @deprecated */ public function Update($ID, $arFields, $arParams = array( 'CORRECT_DATE_PLAN_DEPENDENT_TASKS' => true, 'CORRECT_DATE_PLAN' => true, 'THROTTLE_MESSAGES' => false )) { //$GLOBALS['LS'] = true; global $DB, $USER_FIELD_MANAGER, $APPLICATION; $updatePins = false; if (!isset($arParams['CORRECT_DATE_PLAN'])) { $arParams['CORRECT_DATE_PLAN'] = true; } if (!isset($arParams['CORRECT_DATE_PLAN_DEPENDENT_TASKS'])) { $arParams['CORRECT_DATE_PLAN_DEPENDENT_TASKS'] = true; } if (!isset($arParams['THROTTLE_MESSAGES'])) { $arParams['THROTTLE_MESSAGES'] = false; } $this->lastOperationResultData = array(); if (isset($arFields['META::EVENT_GUID'])) { $eventGUID = $arFields['META::EVENT_GUID']; unset($arFields['META::EVENT_GUID']); } else $eventGUID = sha1(uniqid('AUTOGUID', true)); $bWasFatalError = false; $ID = intval($ID); if ($ID < 1) return false; $userID = null; $bCheckRightsOnFiles = false; // for backward compatibility if (!is_array($arParams)) { $arParams = array(); } if (isset($arParams['USER_ID']) && ($arParams['USER_ID'] > 0)) { $userID = (int)$arParams['USER_ID']; } if (isset($arParams['CHECK_RIGHTS_ON_FILES'])) { if (($arParams['CHECK_RIGHTS_ON_FILES'] === 'Y') || ($arParams['CHECK_RIGHTS_ON_FILES'] === true)) { $bCheckRightsOnFiles = true; } else $bCheckRightsOnFiles = false; } if (!isset($arParams['CORRECT_DATE_PLAN_DEPENDENT_TASKS'])) { $arParams['CORRECT_DATE_PLAN_DEPENDENT_TASKS'] = true; } if (!isset($arParams['CORRECT_DATE_PLAN'])) { $arParams['CORRECT_DATE_PLAN'] = true; } if ($userID === null) { $userID = User::getId(); if (!$userID) { $userID = 1; // nasty, but for compatibility :( } } $rsTask = self::GetByID($ID, false, array('USER_ID' => $userID)); if ($arTask = $rsTask->Fetch()) { if ($this->CheckFields($arFields, $ID, $userID)) { $ufCheck = true; $hasUfs = UserField::checkContainsUFKeys($arFields); if ($hasUfs) { $ufCheck = $USER_FIELD_MANAGER->CheckFields("TASKS_TASK", $ID, $arFields, $userID); } /* //region allow change deadline count if(!array_key_exists('ALLOW_CHANGE_DEADLINE_COUNT_VALUE', $arFields)) // if change deadline only { if (array_key_exists('DEADLINE', $arFields)) { $canChangeDeadline = $arFields['CREATED_BY'] === User::getId() || User::isAdmin() || User::isSuper(); if ($canChangeDeadline) { if ($arFields['DEADLINE'] == '') { if (!is_null($arTask['ALLOW_CHANGE_DEADLINE_COUNT']) || !is_null($arTask['ALLOW_CHANGE_DEADLINE_MAXTIME'])) { $this->_errors[] = array( "text" => GetMessage("ERROR_TASKS_CHANGE_DEADLINE_NULL"), "id" => "ERROR_TASKS_CHANGE_DEADLINE_NULL" ); } } else { if($arTask['ALLOW_CHANGE_DEADLINE_COUNT_VALUE'] != '*') { if ($arTask['ALLOW_CHANGE_DEADLINE_COUNT'] <= 0) { $this->_errors[] = array( "text" => GetMessage("ERROR_TASKS_CHANGE_DEADLINE_COUNT_OVER"), "id" => "ERROR_TASKS_CHANGE_DEADLINE_COUNT_OVER" ); } else { $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = $arTask['ALLOW_CHANGE_DEADLINE_COUNT'] - 1; } } else { $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = null; } if($arTask['ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE'] != '*') { if (!is_null($arTask['ALLOW_CHANGE_DEADLINE_MAXTIME'])) { $maxDatetime = DateTime::createFromTimestamp( strtotime($arTask['ALLOW_CHANGE_DEADLINE_MAXTIME']) ); $deadline = DateTime::createFromTimestamp(strtotime($arFields['DEADLINE'])); if ($deadline->checkGT($maxDatetime)) { $this->_errors[] = array( "text" => GetMessage( "ERROR_TASKS_CHANGE_DEADLINE_MAXTIME_OVER", ['#DEADLINE#' => $maxDatetime->toString()] ), "id" => "ERROR_TASKS_CHANGE_DEADLINE_MAXTIME_OVER" ); } } } else { $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] = null; } } if (!empty($this->_errors)) { $e = new CAdminException($this->_errors); $APPLICATION->ThrowException($e); return false; } } if ($canChangeDeadline) { if (isset($arFields['ALLOW_CHANGE_DEADLINE_COUNT'])) { $availableValues = array_column( \Bitrix\Tasks\UI\Controls\Fields\Deadline::getCountTimesItems(), 'VALUE' ); if (!in_array($arFields['ALLOW_CHANGE_DEADLINE_COUNT'], $availableValues)) { $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = '*'; } $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] == '*' ? null : (int)$arFields['ALLOW_CHANGE_DEADLINE_COUNT']; } if (isset($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'])) { $availableValues = array_column( \Bitrix\Tasks\UI\Controls\Fields\Deadline::getTimesItems(), 'VALUE' ); if (!in_array($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'], $availableValues)) { $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] = '*'; } if ($arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] != '*') { $maxDate = Datetime::createFromTimestamp( strtotime('+'.$arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']) ); } $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE'] = $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] == '*' ? null : $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']; $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] = $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME'] == '*' ? null : $maxDate; } } } } else { // if update if(array_key_exists('DEADLINE', $arFields) && $arFields['DEADLINE']=='') { $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME']=''; $arFields['ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE']='*'; $arFields['ALLOW_CHANGE_DEADLINE_COUNT']=''; $arFields['ALLOW_CHANGE_DEADLINE_COUNT_VALUE']='*'; } else { if($arFields['ALLOW_CHANGE_DEADLINE_COUNT_VALUE'] != '*') { // check valid value!!! SEC $arFields['ALLOW_CHANGE_DEADLINE_COUNT'] = $arFields['ALLOW_CHANGE_DEADLINE_COUNT_VALUE']; } } } //endregion */ // detect hashtags in body if (isset($arFields['TITLE']) || isset($arFields['DESCRIPTION'])) { $oldTags = static::detectTags($arTask); $newTags = static::detectTags($arFields); if ($oldTags != $newTags) { $deleteTags = array_diff($oldTags, $newTags); $newNewTags = array_diff($newTags, $oldTags); // def vals if (!isset($arTask['TAGS'])) { $arTask['TAGS'] = array(); } if (!isset($arFields['TAGS'])) { $arFields['TAGS'] = $arTask['TAGS']; } if (!is_array($arFields['TAGS'])) { $arFields['TAGS'] = array($arFields['TAGS']); } // new tags deteced in body if (!empty($newNewTags)) { $arFields['TAGS'] = array_merge($arFields['TAGS'], $newNewTags); } // some tags was removed from body if (!empty($deleteTags)) { for ($t = 0, $c = count($arFields['TAGS']); $t < $c; $t++) { if (in_array($arFields['TAGS'][$t], $deleteTags)) { unset($arFields['TAGS'][$t]); } } } $arFields['TAGS'] = array_unique($arFields['TAGS']); } } if ($ufCheck) { unset($arFields["ID"]); $arBinds = array( "DESCRIPTION" => $arFields["DESCRIPTION"], "DECLINE_REASON" => $arFields["DECLINE_REASON"] ); $time = \Bitrix\Tasks\UI::formatDateTime(User::getTime()); $arFields["CHANGED_BY"] = $userID; $arFields["CHANGED_DATE"] = $time; $occurAsUserId = CTasksTools::getOccurAsUserId(); if (!$occurAsUserId) { $occurAsUserId = ($arFields["CHANGED_BY"] ? $arFields["CHANGED_BY"] : 1); } if (!$arFields["OUTLOOK_VERSION"]) { $arFields["OUTLOOK_VERSION"] = ($arTask["OUTLOOK_VERSION"] ? $arTask["OUTLOOK_VERSION"] : 1) + 1; } // If new status code given AND new status code != current status => than update if (isset($arFields["STATUS"]) && (int)$arTask['STATUS'] !== (int)$arFields['STATUS']) { $arFields["STATUS_CHANGED_BY"] = $userID; $arFields["STATUS_CHANGED_DATE"] = $time; if ($arFields["STATUS"] == 5 || $arFields["STATUS"] == 4) { $arFields["CLOSED_BY"] = $userID; $arFields["CLOSED_DATE"] = $time; } else { $arFields["CLOSED_BY"] = false; $arFields["CLOSED_DATE"] = false; if ($arFields["STATUS"] == 3 && !$arTask["DATE_START"]) { $arFields["DATE_START"] = $time; } } } if (isset($arFields['DEADLINE']) && (string)$arFields['DEADLINE'] != '' && $arTask['MATCH_WORK_TIME'] == 'Y') { $arFields['DEADLINE'] = static::getDeadlineMatchWorkTime($arFields['DEADLINE']); } $shiftResult = null; if ($arParams['CORRECT_DATE_PLAN']) { $parentChanged = static::parentChanged($arTask, $arFields, $userID); $datesChanged = static::datesChanged($arTask, $arFields); $followDatesChanged = static::followDatesSetTrue($arFields); if ($parentChanged) { // task was attached previously, and now it is being unattached or reattached to smth else // then we need to recalculate its previous parent... $scheduler = \Bitrix\Tasks\Processor\Task\Scheduler::getInstance($userID); $shiftResultPrev = $scheduler->processEntity( $ID, $arTask, array( 'MODE' => 'BEFORE_DETACH', ) ); if ($shiftResultPrev->isSuccess()) { $shiftResultPrev->save(array('!ID' => $ID)); } } else { if (array_key_exists('PARENT_ID', $arFields)) { unset($arFields['PARENT_ID']); } } // when updating end or start date plan, we need to be sure the time is correct if ($parentChanged || $datesChanged || $followDatesChanged) { $scheduler = \Bitrix\Tasks\Processor\Task\Scheduler::getInstance($userID); $shiftResult = $scheduler->processEntity( $ID, $arFields, array( 'MODE' => $parentChanged ? 'BEFORE_ATTACH' : '', ) ); if ($shiftResult->isSuccess()) { $shiftData = $shiftResult->getImpactById($ID); if ($shiftData) { // will be saved... $arFields['START_DATE_PLAN'] = ((isset($arFields['START_DATE_PLAN']) && $shiftData['START_DATE_PLAN'] == null) ? false : $shiftData['START_DATE_PLAN']); $arFields['END_DATE_PLAN'] = ((isset($arFields['END_DATE_PLAN']) && $shiftData['END_DATE_PLAN'] == null) ? false : $shiftData['END_DATE_PLAN']); $arFields['DURATION_PLAN_SECONDS'] = $shiftData['DURATION_PLAN_SECONDS']; $this->lastOperationResultData['SHIFT_RESULT'][$ID] = $shiftData; } } } } // END_DATE_PLAN will be dropped if (isset($arFields['END_DATE_PLAN']) && (string)$arFields['END_DATE_PLAN'] == '') { // duration is no longer adequate $arFields['DURATION_PLAN'] = 0; } self::processDurationPlanFields( $arFields, (string)$arFields['DURATION_TYPE'] != '' ? $arFields['DURATION_TYPE'] : $arTask['DURATION_TYPE'] ); $arTaskCopy = $arTask; // this will allow transfer data by pointer for speed-up foreach (GetModuleEvents('tasks', 'OnBeforeTaskUpdate', true) as $arEvent) { if (ExecuteModuleEventEx($arEvent, array($ID, &$arFields, &$arTaskCopy)) === false) { $errmsg = GetMessage("TASKS_UNKNOWN_UPDATE_ERROR"); $errno = 'ERROR_UNKNOWN_UPDATE_TASK_ERROR'; if ($ex = $APPLICATION->getException()) { $errmsg = $ex->getString(); $errno = $ex->getId(); } $this->_errors[] = array('text' => $errmsg, 'id' => $errno); return false; } } if ($arFields['GROUP_ID'] && $arFields['GROUP_ID'] != $arTask['GROUP_ID']) { $updatePins = true; $arFields['STAGE_ID'] = 0; } $strUpdate = $DB->PrepareUpdate("b_tasks", $arFields, "tasks"); $strSql = "UPDATE b_tasks SET ".$strUpdate." WHERE ID=".$ID; $result = $DB->QueryBind($strSql, $arBinds, false, "File: ".__FILE__."<br>Line: ".__LINE__); if ($result) { $arParticipants = array_merge( array( $arTask['CREATED_BY'], $arTask['RESPONSIBLE_ID'] ), (array)$arTask['ACCOMPLICES'], (array)$arTask['AUDITORS'] ); if (isset($arFields['CREATED_BY'])) $arParticipants[] = $arFields['CREATED_BY']; if (isset($arFields['RESPONSIBLE_ID'])) $arParticipants[] = $arFields['RESPONSIBLE_ID']; if (isset($arFields['ACCOMPLICES'])) { $arParticipants = array_merge( $arParticipants, (array)$arFields['ACCOMPLICES'] ); } if (isset($arFields['AUDITORS'])) { $arParticipants = array_merge( $arParticipants, (array)$arFields['AUDITORS'] ); } $arParticipants = array_unique($arParticipants); if (array_key_exists('STATUS', $arFields) && ($arFields['STATUS'] == static::STATE_SUPPOSEDLY_COMPLETED || $arFields['STATUS'] == static::STATE_COMPLETED)) { // stop timer for responsible and accomplices, if exists $responsibleTimer = CTaskTimerManager::getInstance($arTask['RESPONSIBLE_ID']); $responsibleTimer->stop($ID); $accomplices = $arTask['ACCOMPLICES']; if (isset($accomplices) && !empty($accomplices)) { foreach ($accomplices as $accompliceId) { $accompliceTimer = CTaskTimerManager::getInstance($accompliceId); $accompliceTimer->stop($ID); } } } // Emit pull event try { $arPullRecipients = array(); foreach ($arParticipants as $userId) { $arPullRecipients[] = (int)$userId; } $taskGroupId = 0; // no group $taskGroupIdBeforeUpdate = 0; // no group if (isset($arTask['GROUP_ID']) && ($arTask['GROUP_ID'] > 0)) $taskGroupId = (int)$arTask['GROUP_ID']; // if $arFields['GROUP_ID'] not given, than it means, // that group not changed during this update, so // we must take existing group_id (from $arTask) if (!array_key_exists('GROUP_ID', $arFields)) { if (isset($arTask['GROUP_ID']) && ($arTask['GROUP_ID'] > 0)) $taskGroupIdBeforeUpdate = (int)$arTask['GROUP_ID']; else $taskGroupIdBeforeUpdate = 0; // no group } else // Group given, use it { if ($arFields['GROUP_ID'] > 0) $taskGroupIdBeforeUpdate = (int)$arFields['GROUP_ID']; else $taskGroupIdBeforeUpdate = 0; // no group } $arPullData = array( 'TASK_ID' => (int)$ID, 'BEFORE' => array( 'GROUP_ID' => $taskGroupId ), 'AFTER' => array( 'GROUP_ID' => $taskGroupIdBeforeUpdate ), 'TS' => time(), 'event_GUID' => $eventGUID, 'params'=>[ 'HIDE'=>array_key_exists('HIDE', $arParams)?(bool)$arParams['HIDE']:true ] ); // self::EmitPullWithTagPrefix( // $arPullRecipients, // 'TASKS_GENERAL_', // 'task_update', // $arPullData // ); self::EmitPullWithTag( $arPullRecipients, 'TASKS_TASK_'.(int)$ID, 'task_update', $arPullData ); } catch (Exception $e) { $bWasFatalError = true; $this->_errors[] = 'at line '.$e->GetLine().', '.$e->GetMessage(); } // changes log $arTmp = array('arTask' => $arTask, 'arFields' => $arFields); if (isset($arTask['DURATION_PLAN']))// && isset($arTask['DURATION_TYPE'])) { $arTmp['arTask']['DURATION_PLAN_SECONDS'] = $arTask['DURATION_PLAN_SECONDS']; unset($arTmp['arTask']['DURATION_PLAN']); } if (isset($arFields['DURATION_PLAN']))// && isset($arFields['DURATION_TYPE'])) { // at this point, $arFields['DURATION_PLAN'] in seconds $arTmp['arFields']['DURATION_PLAN_SECONDS'] = $arFields['DURATION_PLAN']; unset($arTmp['arFields']['DURATION_PLAN']); } $arChanges = CTaskLog::GetChanges($arTmp['arTask'], $arTmp['arFields']); unset($arTmp); foreach ($arChanges as $key => $value) { $arLogFields = array( "TASK_ID" => $ID, "USER_ID" => $occurAsUserId, "CREATED_DATE" => $arFields["CHANGED_DATE"], "FIELD" => $key, "FROM_VALUE" => $value["FROM_VALUE"], "TO_VALUE" => $value["TO_VALUE"] ); $log = new CTaskLog(); $log->Add($arLogFields); } if (isset($arFields["RESPONSIBLE_ID"]) && isset($arChanges["RESPONSIBLE_ID"])) { CTaskMembers::updateForTask($ID, array($arFields['RESPONSIBLE_ID']), 'R'); } if (isset($arFields["CREATED_BY"]) && isset($arChanges["CREATED_BY"])) { CTaskMembers::updateForTask($ID, array($arFields['CREATED_BY']), 'O'); } if (isset($arFields["ACCOMPLICES"]) && isset($arChanges["ACCOMPLICES"])) { CTaskMembers::updateForTask($ID, $arFields["ACCOMPLICES"], 'A'); } if (isset($arFields["AUDITORS"]) && isset($arChanges["AUDITORS"])) { CTaskMembers::updateForTask($ID, $arFields["AUDITORS"], 'U'); } if (isset($arFields["FILES"]) && (isset($arChanges["NEW_FILES"]) || isset($arChanges["DELETED_FILES"]))) { $arNotDeleteFiles = $arFields["FILES"]; CTaskFiles::DeleteByTaskID($ID, $arNotDeleteFiles); self::AddFiles( $ID, $arFields["FILES"], array( 'USER_ID' => $userID, 'CHECK_RIGHTS_ON_FILES' => $bCheckRightsOnFiles ) ); } if (isset($arFields["TAGS"]) && isset($arChanges["TAGS"])) { CTaskTags::DeleteByTaskID($ID); self::AddTags($ID, $arTask["CREATED_BY"], $arFields["TAGS"], $userID); } if (isset($arFields["DEPENDS_ON"]) && isset($arChanges["DEPENDS_ON"])) { CTaskDependence::DeleteByTaskID($ID); self::AddPrevious($ID, $arFields["DEPENDS_ON"]); } if ($hasUfs) { $USER_FIELD_MANAGER->Update("TASKS_TASK", $ID, $arFields, $userID); } // backward compatibility with PARENT_ID if (array_key_exists('PARENT_ID', $arFields)) { // PARENT_ID changed, reattach subtree from previous location to new one \Bitrix\Tasks\Internals\Helper\Task\Dependence::attach($ID, intval($arFields['PARENT_ID'])); } // tasks dependence if ($shiftResult !== null && $arParams['CORRECT_DATE_PLAN_DEPENDENT_TASKS']) { $saveResult = $shiftResult->save(array('!ID' => $ID)); if ($saveResult->isSuccess()) { $this->lastOperationResultData['SHIFT_RESULT'] = $shiftResult->exportData(); } } if (array_key_exists('STATUS', $arFields) && $arFields['STATUS'] == 5) { if ($arParams['AUTO_CLOSE'] !== false) { $closer = \Bitrix\Tasks\Processor\Task\AutoCloser::getInstance($userID); $closeResult = $closer->processEntity($ID, $arFields); if ($closeResult->isSuccess()) { $closeResult->save(array('!ID' => $ID)); } } } $bSkipNotification = (isset($arParams['SKIP_NOTIFICATION']) && $arParams['SKIP_NOTIFICATION']); $notifArFields = array_merge($arFields, array('CHANGED_BY' => $occurAsUserId)); if (($status = intval($arFields["STATUS"])) && $status > 0 && $status < 8 && ((int)$arTask['STATUS'] !== (int)$arFields['STATUS']) // only if status changed ) { if ($status == 7) { $arTask["DECLINE_REASON"] = $arFields["DECLINE_REASON"]; } if (!$bSkipNotification) { CTaskNotifications::SendStatusMessage($arTask, $status, $notifArFields); } } if (!$bSkipNotification) { CTaskNotifications::SendUpdateMessage($notifArFields, $arTask, false, $arParams); } CTaskComments::onAfterTaskUpdate($ID, $arTask, $arFields); $arFields["ID"] = $ID; $arMergedFields = array_merge($arTask, $arFields); CTaskSync::UpdateItem($arFields, $arTask); // MS Exchange $arFields['META:PREV_FIELDS'] = $arTask; try { $lastEventName = ''; foreach (GetModuleEvents('tasks', 'OnTaskUpdate', true) as $arEvent) { $lastEventName = $arEvent['TO_CLASS'].'::'.$arEvent['TO_METHOD'].'()'; ExecuteModuleEventEx($arEvent, array($ID, &$arFields, &$arTaskCopy)); } } catch (Exception $e) { \CTaskAssert::logWarning( '[0xee8999a8] exception in module event: '. $lastEventName. '; at file: '. $e->getFile(). ':'. $e->getLine(). ";\n" ); \Bitrix\Tasks\Util::log($e); } unset($arFields['META:PREV_FIELDS']); self::Index($arMergedFields, $arFields["TAGS"]); // search index SearchIndex::setTaskSearchIndex($ID, $arMergedFields); // clear cache static::addCacheIdToClear("tasks_".$ID); if ($arTask["GROUP_ID"]) { static::addCacheIdToClear("tasks_group_".$arTask["GROUP_ID"]); } if ($arFields['GROUP_ID'] && ($arFields['GROUP_ID'] != $arTask['GROUP_ID'])) { static::addCacheIdToClear("tasks_group_".$arFields["GROUP_ID"]); } foreach ($arParticipants as $userId) { static::addCacheIdToClear("tasks_user_".$userId); } // CTaskCountersProcessor::onAfterTaskUpdate($arTask, $arFields); Counter::onAfterTaskUpdate($arTask, $arFields, $arParams); static::clearCache(); if ($bWasFatalError) { soundex('push&pull: bWasFatalError === true'); } //_dump_r($this->lastOperationResultData['SHIFT_RESULT']); $this->previousData = $arTask; if ($updatePins) { \Bitrix\Tasks\Kanban\StagesTable::pinInStage( $ID, array( 'CREATED_BY' => $arTask['CREATED_BY']// because is not new task ), true ); } if ($arTask['FORUM_TOPIC_ID'] && $arTask['TITLE'] !== $arFields['TITLE']) { Integration\Forum\Task\Topic::updateTopicTitle($arTask['FORUM_TOPIC_ID'], $arFields['TITLE']); } Integration\Bizproc\Listener::onTaskUpdate($ID, $arFields, $arTaskCopy); return true; } } else { $e = $APPLICATION->GetException(); foreach ($e->messages as $msg) { $this->_errors[] = $msg; } } } } if (sizeof($this->_errors) == 0) $this->_errors[] = array( "text" => GetMessage("TASKS_UNKNOWN_UPDATE_ERROR"), "id" => "ERROR_UNKNOWN_UPDATE_TASK_ERROR" ); return false; } /** * Check if deadline is matching work time. * Returns closest work time if not. * * @param $deadline * * @return DateTime|int|static */ private static function getDeadlineMatchWorkTime($deadline) { $resultDeadline = DateTime::createFromUserTimeGmt($deadline); $calendar = new Calendar(); if (!$calendar->isWorkTime($resultDeadline)) { $resultDeadline = $calendar->getClosestWorkTime($resultDeadline); } $resultDeadline = $resultDeadline->convertToLocalTime()->getTimestamp(); $resultDeadline = DateTime::createFromTimestamp($resultDeadline - User::getTimeZoneOffsetCurrentUser()); return $resultDeadline; } /** * Occurs when user does not know anything about main task but is trying to change its sub task. * This method returns true in that case, so we should not change parent. * * @param $oldParentId * @param $newParentId * @param $userId * * @return bool */ private static function checkFakeParentChange($oldParentId, $newParentId, $userId) { try { if (User::isSuper($userId)) { return false; } if ($newParentId == false && $oldParentId) { try { $parentTask = new \CTaskItem($oldParentId, $userId); $parentTask->getData(false, ['select' => ['ID'], 'bSkipExtraData' => true]); } /** @noinspection PhpDeprecationInspection */ catch (\TasksException $e) { /** @noinspection PhpDeprecationInspection */ if ($e->getCode() == \TasksException::TE_TASK_NOT_FOUND_OR_NOT_ACCESSIBLE) { return true; } } } return false; } catch (\Exception $exception) { return false; } } private static function parentChanged($oldData, $newData, $userId) { if (array_key_exists('PARENT_ID', $newData)) { $fakeParentChange = static::checkFakeParentChange($oldData['PARENT_ID'], $newData['PARENT_ID'], $userId); return !$fakeParentChange && ($newData['PARENT_ID'] != $oldData['PARENT_ID']); } return false; } private static function datesChanged($oldData, $newData) { if (!array_key_exists('START_DATE_PLAN', $newData) && !array_key_exists('END_DATE_PLAN', $newData)) { return false; } return ((string)$oldData['START_DATE_PLAN'] != (string)$newData['START_DATE_PLAN']) || ((string)$oldData['END_DATE_PLAN'] != (string)$newData['END_DATE_PLAN']); } private static function followDatesSetTrue($fields) { if (array_key_exists('SE_PARAMETER', $fields) && is_array($fields['SE_PARAMETER'])) { foreach ($fields['SE_PARAMETER'] as $parameter) { if ($parameter['CODE'] == 1 && $parameter['VALUE'] == 'Y') { return true; } } } return false; } public static function checkCacheAutoClearEnabled() { return static::$cacheClearEnabled; } public static function disableCacheAutoClear() { if (!static::$cacheClearEnabled) { return false; } static::$cacheClearEnabled = false; return true; } public static function enableCacheAutoClear($clearNow = true) { static::$cacheClearEnabled = true; if ($clearNow) { static::clearCache(); } } private static function addCacheIdToClear($cacheId) { if ((string)$cacheId === '') { return; } static::$cacheIds[$cacheId] = true; } private static function clearCache() { if (!static::$cacheClearEnabled) { return; } global $CACHE_MANAGER; if (!empty(static::$cacheIds)) { foreach (static::$cacheIds as $id => $void) { $CACHE_MANAGER->ClearByTag($id); } static::$cacheIds = array(); } } /** * This method is deprecated. Use CTaskItem::delete() instead. * * @param $taskId * @param array $parameters * @return bool * * @deprecated */ public static function Delete($taskId, $parameters = []) { global $DB, $CACHE_MANAGER, $USER_FIELD_MANAGER; $taskId = intval($taskId); if ($taskId < 1) { return false; } $actorUserId = User::getId(); if (!$actorUserId) { $actorUserId = User::getAdminId(); } if (isset($parameters['META::EVENT_GUID'])) { $eventGuid = $parameters['META::EVENT_GUID']; unset($parameters['META::EVENT_GUID']); } else { $eventGuid = sha1(uniqid('AUTOGUID', true)); } if (isset($parameters['skipExchangeSync']) && ($parameters['skipExchangeSync'] === 'Y' || $parameters['skipExchangeSync'] === true)) { $skipExchangeSync = true; } else { $skipExchangeSync = false; } /** @noinspection PhpDeprecationInspection */ $taskDbResult = self::GetByID($taskId, false); if ($taskData = $taskDbResult->Fetch()) { $safeDelete = false; try { if (\Bitrix\Main\Loader::includeModule('recyclebin')) { $result = Integration\Recyclebin\Task::OnBeforeTaskDelete($taskId, $taskData); $safeDelete = $result; } } catch (\Exception $e) { } foreach (GetModuleEvents('tasks', 'OnBeforeTaskDelete', true) as $arEvent) { if (ExecuteModuleEventEx($arEvent, [$taskId, $taskData]) === false) { return false; } } // stop timer, if exists $timer = CTaskTimerManager::getInstance($taskData['RESPONSIBLE_ID']); $timer->stop($taskId); CTaskMembers::DeleteAllByTaskID($taskId); CTaskDependence::DeleteByTaskID($taskId); CTaskDependence::DeleteByDependsOnID($taskId); CTaskReminders::DeleteByTaskID($taskId); $tableResult = ProjectDependenceTable::getList([ "select" => ['TASK_ID'], "filter" => [ "=TASK_ID" => $taskId, "DEPENDS_ON_ID" => $taskId ] ]); if (ProjectDependenceTable::checkItemLinked($taskId) || $tableResult->fetch()) { ProjectDependenceTable::deleteLink($taskId, $taskId); } if (!$safeDelete) { CTaskFiles::DeleteByTaskID($taskId); CTaskTags::DeleteByTaskID($taskId); FavoriteTable::deleteByTaskId($taskId, ['LOW_LEVEL' => true]); SortingTable::deleteByTaskId($taskId); TaskStageTable::clearTask($taskId); $tablesToClear = [ ViewedTable::class => ['TASK_ID', 'USER_ID'], ParameterTable::class => ['ID'], CheckListTable::class => ['ID'], SearchIndexTable::class => ['ID'] ]; foreach ($tablesToClear as $table => $select) { /** @var \Bitrix\Main\ORM\Query\Result $tableResult */ $tableResult = $table::getList([ "select" => $select, "filter" => [ "=TASK_ID" => $taskId, ], ]); while ($item = $tableResult->fetch()) { $table::delete($item); } } } SortingTable::fixSiblingsEx($taskId); // by default, self::Delete() should not delete the entire sub-tree, so we need to delete only node itself $children = Dependence::getSubTree($taskId)->find(['__PARENT_ID' => $taskId])->getData(); Dependence::delete($taskId); if ($taskData['PARENT_ID'] && !empty($children)) { foreach ($children as $child) { Dependence::attach($child['__ID'], $taskData['PARENT_ID']); } } if ($taskData['PARENT_ID'] && $taskData['START_DATE_PLAN'] && $taskData['END_DATE_PLAN']) { // we need to scan for parent bracket tasks change... $scheduler = \Bitrix\Tasks\Processor\Task\Scheduler::getInstance($actorUserId); // we could use MODE => DETACH here, but there we can act in more effective way by // re-calculating tree of PARENT_ID after removing link between ID and PARENT_ID // we also do not need to calculate detached tree // it is like DETACH_AFTER $shiftResult = $scheduler->processEntity($taskData['PARENT_ID']); if ($shiftResult->isSuccess()) { $shiftResult->save(); } } // todo: see \CTaskPlannerMaintance::reviseTaskLists(), move task list from option to a table, and then just do cleaning // todo: dayplan by TASK_ID here for each user, regardless to the role; the following solution works only for current user, creator and responsible //\CTaskPlannerMaintance::plannerActions(array('remove' => array($taskId))); //\CTaskPlannerMaintance::plannerActions(array('remove' => array($taskId)), SITE_ID, $taskData['CREATED_BY']); //\CTaskPlannerMaintance::plannerActions(array('remove' => array($taskId)), SITE_ID, $taskData['RESPONSIBLE_ID']); $CACHE_MANAGER->ClearByTag("tasks_" . $taskId); // clear cache if ($taskData["GROUP_ID"]) { $CACHE_MANAGER->ClearByTag("tasks_group_" . $taskData["GROUP_ID"]); } $arParticipants = array_unique( array_merge( [ $taskData["CREATED_BY"], $taskData["RESPONSIBLE_ID"] ], $taskData["ACCOMPLICES"], $taskData["AUDITORS"] ) ); foreach ($arParticipants as $userId) { $CACHE_MANAGER->ClearByTag("tasks_user_" . $userId); } $strSql = "UPDATE b_tasks_template SET TASK_ID = NULL WHERE TASK_ID = " . $taskId; $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); $strSql = "UPDATE b_tasks_template SET PARENT_ID = " . ($taskData["PARENT_ID"]? $taskData["PARENT_ID"] : "NULL") . " WHERE PARENT_ID = " . $taskId; $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); $strSql = "UPDATE b_tasks SET PARENT_ID = " . ($taskData["PARENT_ID"] ? $taskData["PARENT_ID"] : "NULL") . " WHERE PARENT_ID = " . $taskId; $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); $strUpdate = $DB->PrepareUpdate( "b_tasks", [ 'ZOMBIE' => 'Y', 'CHANGED_BY' => $actorUserId, 'CHANGED_DATE' => \Bitrix\Tasks\UI::formatDateTime(User::getTime()) ], "tasks" ); $strSql = "UPDATE b_tasks SET " . $strUpdate . " WHERE ID = " . $taskId; if ($DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__)) { CTaskNotifications::SendDeleteMessage($taskData); if (!$safeDelete) { Integration\Forum\Task\Topic::delete($taskData["FORUM_TOPIC_ID"]); $USER_FIELD_MANAGER->Delete('TASKS_TASK', $taskId); } if (!$skipExchangeSync) { CTaskSync::DeleteItem($taskData); // MS Exchange } // Emit pull event try { $arPullRecipients = []; foreach ($arParticipants as $userId) { $arPullRecipients[] = (int)$userId; } $taskGroupId = 0; // no group if (isset($taskData['GROUP_ID']) && $taskData['GROUP_ID'] > 0) { $taskGroupId = (int)$taskData['GROUP_ID']; } $arPullData = [ 'TASK_ID' => $taskId, 'TS' => time(), 'event_GUID' => $eventGuid, 'BEFORE' => [ 'GROUP_ID' => $taskGroupId ], ]; // self::EmitPullWithTagPrefix( // $arPullRecipients, // 'TASKS_GENERAL_', // 'task_remove', // $arPullData // ); self::EmitPullWithTag( $arPullRecipients, 'TASKS_TASK_' . $taskId, 'task_remove', $arPullData ); } catch (Exception $e) { } foreach (GetModuleEvents('tasks', 'OnTaskDelete', true) as $arEvent) { ExecuteModuleEventEx($arEvent, [$taskId]); } if (\CModule::IncludeModule("search")) { CSearch::DeleteIndex("tasks", $taskId); } Counter::onAfterTaskDelete($taskData); Integration\Bizproc\Listener::onTaskDelete($taskId); } return true; } return false; } /** * @param $ID * * This method MUST be called after sync with all Outlook clients. * We can't determine such moment, so we should terminate zombies * for some time after task been deleted. */ private static function terminateZombie($ID) { global $DB, $USER_FIELD_MANAGER; $res = self::GetList( array(), array('ID' => (int)$ID, 'ZOMBIE' => 'Y'), array('ID'), array('bGetZombie' => true) ); if ($res && ($task = $res->fetch())) { foreach (GetModuleEvents('tasks', 'OnBeforeTaskZombieDelete', true) as $arEvent) { ExecuteModuleEventEx($arEvent, array($ID)); } CTaskCheckListItem::deleteByTaskId($ID); CTaskLog::DeleteByTaskId($ID); $USER_FIELD_MANAGER->Delete("TASKS_TASK", $ID); $strSql = "DELETE FROM b_tasks WHERE ID = ".(int)$ID; $DB->query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); foreach (GetModuleEvents('tasks', 'OnTaskZombieDelete', true) as $arEvent) { ExecuteModuleEventEx($arEvent, array($ID)); } } } protected static function GetSqlByFilter($arFilter, $userID, $sAliasPrefix, $bGetZombie, $bMembersTableJoined = false, $params = []) { global $DB; $bFullJoin = null; if (!is_array($arFilter)) throw new \TasksException( 'GetSqlByFilter: expected array, but something other given: '.var_export($arFilter, true) ); $logicStr = ' AND '; if (isset($arFilter['::LOGIC'])) { switch ($arFilter['::LOGIC']) { case 'AND': $logicStr = ' AND '; break; case 'OR': $logicStr = ' OR '; break; default: throw new \TasksException('Unknown logic in filter'); break; } } $arSqlSearch = array(); foreach ($arFilter as $key => $val) { // Skip meta-key if ($key === '::LOGIC') continue; // Skip markers if ($key === '::MARKERS') continue; // Subfilter? if (static::isSubFilterKey($key)) { $arSqlSearch[] = self::GetSqlByFilter($val, $userID, $sAliasPrefix, $bGetZombie, $bMembersTableJoined, $params); continue; } $key = ltrim($key); // This type of operations should be processed in special way // Fields like "META:DEADLINE_TS" will be replaced to "DEADLINE" if (substr($key, -3) === '_TS') { $arSqlSearch = array_merge( $arSqlSearch, self::getSqlForTimestamps($key, $val, $userID, $sAliasPrefix, $bGetZombie) ); continue; } $res = self::MkOperationFilter($key); $key = $res["FIELD"]; $cOperationType = $res["OPERATION"]; $key = strtoupper($key); switch ($key) { case 'META::ID_OR_NAME': if (strtoupper($DB->type) == "ORACLE") $arSqlSearch[] = " (". $sAliasPrefix. "T.ID = '". intval($val). "' OR (UPPER(". $sAliasPrefix. "T.TITLE) UPPER('%". $DB->ForSqlLike($val). "%')) ) "; else $arSqlSearch[] = " (". $sAliasPrefix. "T.ID = '". intval($val). "' OR (UPPER(". $sAliasPrefix. "T.TITLE) LIKE UPPER('%". $DB->ForSqlLike($val). "%')) ) "; break; //case "DURATION_PLAN": // temporal case "PARENT_ID": case "GROUP_ID": case "STATUS_CHANGED_BY": case "FORUM_TOPIC_ID": $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "number", $bFullJoin, $cOperationType ); break; case "ID": case "PRIORITY": case "CREATED_BY": case "RESPONSIBLE_ID": case "STAGE_ID": case 'TIME_ESTIMATE': case 'FORKED_BY_TEMPLATE_ID': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "number_wo_nulls", $bFullJoin, $cOperationType ); break; case "REFERENCE:RESPONSIBLE_ID": $key = 'RESPONSIBLE_ID'; $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, 'reference', $bFullJoin, $cOperationType ); break; case "REFERENCE:START_DATE_PLAN": $key = 'START_DATE_PLAN'; $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, 'reference', $bFullJoin, $cOperationType ); break; case 'META:GROUP_ID_IS_NULL_OR_ZERO': $key = 'GROUP_ID'; $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "null_or_zero", $bFullJoin, $cOperationType, false ); break; case 'META:PARENT_ID_OR_NULL': if ((array)$val) { $arSqlSearch[] = '(T.PARENT_ID IN ('.join(', ', array_map('intval', (array)$val)).') OR T.PARENT_ID IS NULL)'; } break; case "CHANGED_BY": $arSqlSearch[] = self::FilterCreate( "CASE WHEN ". $sAliasPrefix. "T.". $key. " IS NULL THEN ". $sAliasPrefix. "T.CREATED_BY ELSE ". $sAliasPrefix. "T.". $key. " END", $val, "number", $bFullJoin, $cOperationType ); break; case 'GUID': case 'TITLE': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "string", $bFullJoin, $cOperationType ); break; case 'FULL_SEARCH_INDEX': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."TSIF.SEARCH_INDEX", $val, "fulltext", $bFullJoin, $cOperationType ); break; case 'COMMENT_SEARCH_INDEX': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."TSIC.SEARCH_INDEX", $val, "fulltext", $bFullJoin, $cOperationType ); break; case "TAG": if (!is_array($val)) { $val = array($val); } $arConds = array(); foreach ($val as $tag) { if ($tag) { $arConds[] = "(".$sAliasPrefix."TT.NAME = '".$DB->ForSql($tag)."')"; } } if (sizeof($arConds)) { $arSqlSearch[] = "EXISTS( SELECT 'x' FROM b_tasks_tag ".$sAliasPrefix."TT WHERE (".implode(" OR ", $arConds).") AND ".$sAliasPrefix."TT.TASK_ID = ".$sAliasPrefix."T.ID )"; } break; case 'REAL_STATUS': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.STATUS", $val, "number", $bFullJoin, $cOperationType ); break; case 'DEADLINE_COUNTED': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.DEADLINE_COUNTED", $val, "number_wo_nulls", $bFullJoin, $cOperationType ); break; case 'VIEWED': $arSqlSearch[] = self::FilterCreate( " CASE WHEN ".$sAliasPrefix."TV.USER_ID IS NULL AND (".$sAliasPrefix."T.STATUS = 1 OR ".$sAliasPrefix."T.STATUS = 2) THEN '0' ELSE '1' END ", $val, "number", $bFullJoin, $cOperationType ); break; case "STATUS_EXPIRED": // expired: deadline in past and $arSqlSearch[] = ($cOperationType == 'N' ? 'not' : ''). "(". $sAliasPrefix. "T.DEADLINE < ". $DB->CurrentTimeFunction(). " AND ". $sAliasPrefix. "T.STATUS != '4' AND ". $sAliasPrefix. "T.STATUS != '5' AND (". $sAliasPrefix. "T.STATUS != '7' OR ". $sAliasPrefix. "T.RESPONSIBLE_ID != ". $userID. "))"; break; case "STATUS_NEW": // viewed by a specified user + status is either new or pending $arSqlSearch[] = ($cOperationType == 'N' ? 'not' : '')."( ".$sAliasPrefix."TV.USER_ID IS NULL AND ".$sAliasPrefix."T.CREATED_BY != ".$userID." AND (".$sAliasPrefix."T.STATUS = 1 OR ".$sAliasPrefix."T.STATUS = 2) )"; $bFullJoin = true; // join TV break; case "STATUS": $arSqlSearch[] = self::FilterCreate( " CASE WHEN ". $sAliasPrefix. "T.DEADLINE < DATE_ADD(". $DB->CurrentTimeFunction(). ", INTERVAL ". Counter::getDeadlineTimeLimit(). " SECOND) AND ". $sAliasPrefix. "T.DEADLINE >= ". $DB->CurrentTimeFunction(). " AND ". $sAliasPrefix. "T.STATUS != '4' AND ". $sAliasPrefix. "T.STATUS != '5' AND ( ". $sAliasPrefix. "T.STATUS != '7' OR ". $sAliasPrefix. "T.RESPONSIBLE_ID != ". intval($userID). " ) THEN '-3' WHEN ". $sAliasPrefix. "T.DEADLINE < ". $DB->CurrentTimeFunction(). " AND ". $sAliasPrefix. "T.STATUS != '4' AND ". $sAliasPrefix. "T.STATUS != '5' AND (". $sAliasPrefix. "T.STATUS != '7' OR ". $sAliasPrefix. "T.RESPONSIBLE_ID != ". $userID. ") THEN '-1' WHEN ". $sAliasPrefix. "TV.USER_ID IS NULL AND ". $sAliasPrefix. "T.CREATED_BY != ". $userID. " AND (". $sAliasPrefix. "T.STATUS = 1 OR ". $sAliasPrefix. "T.STATUS = 2) THEN '-2' ELSE ". $sAliasPrefix. "T.STATUS END ", $val, "number", $bFullJoin, $cOperationType ); break; case 'MARK': case 'XML_ID': case 'SITE_ID': case 'ZOMBIE': case 'ADD_IN_REPORT': case 'ALLOW_TIME_TRACKING': case 'ALLOW_CHANGE_DEADLINE': case 'MATCH_WORK_TIME': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "string_equal", $bFullJoin, $cOperationType ); break; case "END_DATE_PLAN": case "START_DATE_PLAN": case "DATE_START": case "DEADLINE": case "CREATED_DATE": case "CLOSED_DATE": if (($val === false) || ($val === '')) $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $val, "date", $bFullJoin, $cOperationType, $bSkipEmpty = false ); else $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."T.".$key, $DB->CharToDateFunction($val), "date", $bFullJoin, $cOperationType ); break; case "CHANGED_DATE": $arSqlSearch[] = self::FilterCreate( "CASE WHEN ". $sAliasPrefix. "T.". $key. " IS NULL THEN ". $sAliasPrefix. "T.CREATED_DATE ELSE ". $sAliasPrefix. "T.". $key. " END", $DB->CharToDateFunction($val), "date", $bFullJoin, $cOperationType ); break; case "ACCOMPLICE": if (!is_array($val)) $val = array($val); $val = array_filter($val); $arConds = array(); if ($bMembersTableJoined) { if ($cOperationType !== 'N') { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID = '".intval($id)."')"; } if (!empty($arConds)) $arSqlSearch[] = '('.$sAliasPrefix."TM.TYPE = 'A' AND (".implode(" OR ", $arConds).'))'; } else { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID != '".intval($id)."')"; } if (!empty($arConds)) $arSqlSearch[] = '('. $sAliasPrefix. "TM.TYPE = 'A' AND (". implode(" AND ", $arConds). '))'; } } else { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID = '".intval($id)."')"; } if (!empty($arConds)) { $arSqlSearch[] = ($cOperationType !== 'N' ? 'EXISTS' : 'NOT EXISTS')."( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."TM WHERE (".implode(" OR ", $arConds).") AND ".$sAliasPrefix."TM.TASK_ID = ".$sAliasPrefix."T.ID AND ".$sAliasPrefix."TM.TYPE = 'A' )"; } } break; case "PERIOD": case "ACTIVE": if ($val["START"] || $val["END"]) { $strDateStart = $strDateEnd = false; if (MakeTimeStamp($val['START']) > 0) { $strDateStart = $DB->CharToDateFunction( $DB->ForSql( CDatabase::FormatDate( $val['START'], FORMAT_DATETIME ) ) ); } if (MakeTimeStamp($val['END'])) { $strDateEnd = $DB->CharToDateFunction( $DB->ForSql( CDatabase::FormatDate( $val['END'], FORMAT_DATETIME ) ) ); } if (($strDateStart !== false) && ($strDateEnd !== false)) { $arSqlSearch[] = "( (T.CREATED_DATE >= $strDateStart AND T.CLOSED_DATE <= $strDateEnd) OR (T.CHANGED_DATE >= $strDateStart AND T.CHANGED_DATE <= $strDateEnd) OR (T.CREATED_DATE <= $strDateStart AND T.CLOSED_DATE IS NULL) )"; } elseif (($strDateStart !== false) && ($strDateEnd === false)) { $arSqlSearch[] = "( (T.CREATED_DATE >= $strDateStart) OR (T.CHANGED_DATE >= $strDateStart) )"; } elseif (($strDateStart === false) && ($strDateEnd !== false)) { $arSqlSearch[] = "( (T.CLOSED_DATE <= $strDateEnd) (T.CHANGED_DATE <= $strDateEnd) )"; } } break; case "AUDITOR": if (!is_array($val)) $val = array($val); $val = array_filter($val); $arConds = array(); if ($bMembersTableJoined) { if ($cOperationType !== 'N') { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID = '".intval($id)."')"; } if (!empty($arConds)) $arSqlSearch[] = '('.$sAliasPrefix."TM.TYPE = 'U' AND (".implode(" OR ", $arConds).'))'; } else { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID != '".intval($id)."')"; } if (!empty($arConds)) $arSqlSearch[] = '('. $sAliasPrefix. "TM.TYPE = 'U' AND (". implode(" AND ", $arConds). '))'; } } else { foreach ($val as $id) { $arConds[] = "(".$sAliasPrefix."TM.USER_ID = '".intval($id)."')"; } if (!empty($arConds)) { $arSqlSearch[] = ($cOperationType !== 'N' ? 'EXISTS' : 'NOT EXISTS')."( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."TM WHERE (".implode(" OR ", $arConds).") AND ".$sAliasPrefix."TM.TASK_ID = ".$sAliasPrefix."T.ID AND ".$sAliasPrefix."TM.TYPE = 'U' )"; } } break; case "DOER": $val = intval($val); $arSqlSearch[] = "( ".$sAliasPrefix."T.RESPONSIBLE_ID = ".$val." OR EXISTS( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."TM WHERE ".$sAliasPrefix."TM.TASK_ID = ".$sAliasPrefix."T.ID AND ".$sAliasPrefix."TM.USER_ID = '".$val."' AND ".$sAliasPrefix."TM.TYPE = 'A' ) )"; break; case "MEMBER": $val = intval($val); $arSqlSearch[] = "( ".$sAliasPrefix."T.CREATED_BY = ".intval($val)." OR ".$sAliasPrefix."T.RESPONSIBLE_ID = ".intval($val)." OR EXISTS( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."TM WHERE ".$sAliasPrefix."TM.TASK_ID = ".$sAliasPrefix."T.ID AND ".$sAliasPrefix."TM.USER_ID = '".$val."' ) )"; break; case "DEPENDS_ON": if (!is_array($val)) { $val = array($val); } $arConds = array(); foreach ($val as $id) { if ($id) { $arConds[] = "(".$sAliasPrefix."TD.TASK_ID = '".intval($id)."')"; } } if (sizeof($arConds)) { $arSqlSearch[] = "EXISTS( SELECT 'x' FROM b_tasks_dependence ".$sAliasPrefix."TD WHERE (".implode(" OR ", $arConds).") AND ".$sAliasPrefix."TD.DEPENDS_ON_ID = ".$sAliasPrefix."T.ID )"; } break; case "ONLY_ROOT_TASKS": if ($val == "Y") { $arSqlSearch[] = "(". $sAliasPrefix. "T.PARENT_ID IS NULL OR ". $sAliasPrefix. "T.PARENT_ID = '0' OR NOT EXISTS (". self::GetRootSubQuery($arFilter, $bGetZombie, $sAliasPrefix, $params). "))"; } break; case "SUBORDINATE_TASKS": if ($val == "Y") { $arSubSqlSearch = array( $sAliasPrefix."T.CREATED_BY = ".$userID, $sAliasPrefix."T.RESPONSIBLE_ID = ".$userID, "EXISTS( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."TM WHERE ".$sAliasPrefix."TM.TASK_ID = ".$sAliasPrefix."T.ID AND ".$sAliasPrefix."TM.USER_ID = ".$userID." )" ); // subordinate check if ($strSql = self::GetSubordinateSql($sAliasPrefix, array('USER_ID' => $userID))) { $arSubSqlSearch[] = "EXISTS(".$strSql.")"; } $arSqlSearch[] = "(".implode(" OR ", $arSubSqlSearch).")"; } break; case "OVERDUED": if ($val == "Y") { $arSqlSearch[] = $sAliasPrefix. "T.CLOSED_DATE IS NOT NULL AND ". $sAliasPrefix. "T.DEADLINE IS NOT NULL AND ". $sAliasPrefix. "T.DEADLINE < CLOSED_DATE"; } break; case "SAME_GROUP_PARENT": if ($val == "Y" && !array_key_exists("ONLY_ROOT_TASKS", $arFilter)) { $arSqlSearch[] = "EXISTS( SELECT 'x' FROM b_tasks ".$sAliasPrefix."PT WHERE ".$sAliasPrefix."T.PARENT_ID = ".$sAliasPrefix."PT.ID AND (".$sAliasPrefix."PT.GROUP_ID = ".$sAliasPrefix."T.GROUP_ID OR (".$sAliasPrefix."PT.GROUP_ID IS NULL AND ".$sAliasPrefix."T.GROUP_ID IS NULL) OR (".$sAliasPrefix."PT.GROUP_ID = 0 AND ".$sAliasPrefix."T.GROUP_ID IS NULL) OR (".$sAliasPrefix."PT.GROUP_ID IS NULL AND ".$sAliasPrefix."T.GROUP_ID = 0) ) ".($bGetZombie ? "" : " AND ".$sAliasPrefix."PT.ZOMBIE = 'N' ")." )"; } break; case "DEPARTMENT_ID": if ($strSql = self::GetDeparmentSql($val, $sAliasPrefix)) { $arSqlSearch[] = "EXISTS(".$strSql.")"; } break; case 'CHECK_PERMISSIONS': break; case 'FAVORITE': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."FVT.TASK_ID", $val, "left_existence", $bFullJoin, $cOperationType, false ); break; case 'SORTING': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."SRT.TASK_ID", $val, "left_existence", $bFullJoin, $cOperationType, false ); break; case 'STAGES_ID': $arSqlSearch[] = self::FilterCreate( $sAliasPrefix."STG.STAGE_ID", $val, "number", $bFullJoin, $cOperationType, false ); break; default: if ((strlen($key) >= 3) && (substr($key, 0, 3) === 'UF_')) { ; // It's OK, this fields will be processed by UserFieldManager } else { $extraData = ''; if (isset($_POST['action']) && ($_POST['action'] === 'group_action')) { $extraData = '; Extra data: <data0>'. serialize(array($_POST['arFilter'], $_POST['action'], $arFilter)). '</data0>'; } else { $extraData = '; Extra data: <data1>'.serialize($arFilter).'</data1>'; } //\CTaskAssert::logError('[0x6024749e] unexpected field in filter: ' . $key . $extraData); //throw new \TasksException('Bad filter argument: '.$key, \TasksException::TE_WRONG_ARGUMENTS); } break; } } $sql = implode( $logicStr, array_filter( $arSqlSearch ) ); if ($sql == '') $sql = '1=1'; return ('('.$sql.')'); } private static function getSqlForTimestamps($key, $val, $userID, $sAliasPrefix, $bGetZombie) { static $ts = null; // some fixed timestamp of "now" (for consistency) if ($ts === null) $ts = CTasksPerHitOption::getHitTimestamp(); $bTzWasDisabled = !CTimeZone::enabled(); if ($bTzWasDisabled) CTimeZone::enable(); // Adjust UNIX TS to "Bitrix timestamp" $tzOffset = CTimeZone::getOffset(); $ts += $tzOffset; if ($bTzWasDisabled) CTimeZone::disable(); $arSqlSearch = array(); $arFilter = array( '::LOGIC' => 'AND' ); $key = ltrim($key); $res = self::MkOperationFilter($key); $fieldName = substr($res["FIELD"], 5, -3); // Cutoff prefix "META:" and suffix "_TS" $cOperationType = $res["OPERATION"]; $operationSymbol = substr($key, 0, -1 * strlen($res["FIELD"])); if (substr($cOperationType, 0, 1) !== '#') { switch ($operationSymbol) { case '<': $operationCode = \CTaskFilterCtrl::OP_STRICTLY_LESS; break; case '>': $operationCode = \CTaskFilterCtrl::OP_STRICTLY_GREATER; break; case '<=': $operationCode = \CTaskFilterCtrl::OP_LESS_OR_EQUAL; break; case '>=': $operationCode = \CTaskFilterCtrl::OP_GREATER_OR_EQUAL; break; case '!=': $operationCode = \CTaskFilterCtrl::OP_NOT_EQUAL; break; case '': case '=': $operationCode = \CTaskFilterCtrl::OP_EQUAL; break; default: \CTaskAssert::log( 'Unknown operation code: '. $operationSymbol. '; $key = '. $key. '; it will be silently ignored, incorrect results expected', \CTaskAssert::ELL_ERROR // errors, incorrect results expected ); return ($arSqlSearch); break; } } else $operationCode = (int)substr($cOperationType, 1); $date1 = $date2 = $cOperationType1 = $cOperationType2 = null; // sometimes we can have DAYS in $val, not TIMESTAMP if ($operationCode != \CTaskFilterCtrl::OP_DATE_NEXT_DAYS && $operationCode != \CTaskFilterCtrl::OP_DATE_LAST_DAYS) { $val += $tzOffset; } // Convert cOperationType to format accepted by self::FilterCreate switch ($operationCode) { case \CTaskFilterCtrl::OP_EQUAL: case \CTaskFilterCtrl::OP_DATE_TODAY: case \CTaskFilterCtrl::OP_DATE_YESTERDAY: case \CTaskFilterCtrl::OP_DATE_TOMORROW: case \CTaskFilterCtrl::OP_DATE_CUR_WEEK: case \CTaskFilterCtrl::OP_DATE_PREV_WEEK: case \CTaskFilterCtrl::OP_DATE_NEXT_WEEK: case \CTaskFilterCtrl::OP_DATE_CUR_MONTH: case \CTaskFilterCtrl::OP_DATE_PREV_MONTH: case \CTaskFilterCtrl::OP_DATE_NEXT_MONTH: case \CTaskFilterCtrl::OP_DATE_NEXT_DAYS: case \CTaskFilterCtrl::OP_DATE_LAST_DAYS: $cOperationType1 = '>='; $cOperationType2 = '<='; break; case \CTaskFilterCtrl::OP_LESS_OR_EQUAL: $cOperationType1 = '<='; break; case \CTaskFilterCtrl::OP_GREATER_OR_EQUAL: $cOperationType1 = '>='; break; case \CTaskFilterCtrl::OP_NOT_EQUAL: $cOperationType1 = '<'; $cOperationType2 = '>'; break; case \CTaskFilterCtrl::OP_STRICTLY_LESS: $cOperationType1 = '<'; break; case \CTaskFilterCtrl::OP_STRICTLY_GREATER: $cOperationType1 = '>'; break; default: \CTaskAssert::log( 'Unknown operation code: '. $operationCode. '; $key = '. $key. '; it will be silently ignored, incorrect results expected', \CTaskAssert::ELL_ERROR // errors, incorrect results expected ); return ($arSqlSearch); break; } // Convert/generate dates $ts1 = $ts2 = null; switch ($operationCode) { case \CTaskFilterCtrl::OP_DATE_TODAY: $ts1 = $ts2 = $ts; break; case \CTaskFilterCtrl::OP_DATE_YESTERDAY: $ts1 = $ts2 = $ts - 86400; break; case \CTaskFilterCtrl::OP_DATE_TOMORROW: $ts1 = $ts2 = $ts + 86400; break; case \CTaskFilterCtrl::OP_DATE_CUR_WEEK: $weekDay = date('N'); // numeric representation of the day of the week (1 to 7) $ts1 = $ts - ($weekDay - 1) * 86400; $ts2 = $ts + (7 - $weekDay) * 86400; break; case \CTaskFilterCtrl::OP_DATE_PREV_WEEK: $weekDay = date('N'); // numeric representation of the day of the week (1 to 7) $ts1 = $ts - ($weekDay - 1 + 7) * 86400; $ts2 = $ts - $weekDay * 86400; break; case \CTaskFilterCtrl::OP_DATE_NEXT_WEEK: $weekDay = date('N'); // numeric representation of the day of the week (1 to 7) $ts1 = $ts + (7 - $weekDay + 1) * 86400; $ts2 = $ts + (7 - $weekDay + 7) * 86400; break; case \CTaskFilterCtrl::OP_DATE_CUR_MONTH: $ts1 = mktime(0, 0, 0, date('n', $ts), 1, date('Y', $ts)); $ts2 = mktime(23, 59, 59, date('n', $ts) + 1, 0, date('Y', $ts)); break; case \CTaskFilterCtrl::OP_DATE_PREV_MONTH: $ts1 = mktime(0, 0, 0, date('n', $ts) - 1, 1, date('Y', $ts)); $ts2 = mktime(23, 59, 59, date('n', $ts), 0, date('Y', $ts)); break; case \CTaskFilterCtrl::OP_DATE_NEXT_MONTH: $ts1 = mktime(0, 0, 0, date('n', $ts) + 1, 1, date('Y', $ts)); $ts2 = mktime(23, 59, 59, date('n', $ts) + 2, 0, date('Y', $ts)); break; case \CTaskFilterCtrl::OP_DATE_LAST_DAYS: $ts1 = $ts - ((int)$val) * 86400; // val in days $ts2 = $ts; break; case \CTaskFilterCtrl::OP_DATE_NEXT_DAYS: $ts1 = $ts; $ts2 = $ts + ((int)$val) * 86400; // val in days break; case \CTaskFilterCtrl::OP_GREATER_OR_EQUAL: case \CTaskFilterCtrl::OP_LESS_OR_EQUAL: case \CTaskFilterCtrl::OP_STRICTLY_LESS: case \CTaskFilterCtrl::OP_STRICTLY_GREATER: $ts1 = $val; break; case \CTaskFilterCtrl::OP_EQUAL: $ts1 = mktime(0, 0, 0, date('n', $val), date('j', $val), date('Y', $val)); $ts2 = mktime(23, 59, 59, date('n', $val), date('j', $val), date('Y', $val)); break; case \CTaskFilterCtrl::OP_NOT_EQUAL: $ts1 = mktime(0, 0, 0, date('n', $val), date('j', $val), date('Y', $val)); $ts2 = mktime(23, 59, 59, date('n', $val), date('j', $val), date('Y', $val)); break; default: \CTaskAssert::log( 'Unknown operation code: '. $operationCode. '; $key = '. $key. '; it will be silently ignored, incorrect results expected', \CTaskAssert::ELL_ERROR // errors, incorrect results expected ); return ($arSqlSearch); break; } if ($ts1) $date1 = ConvertTimeStamp(mktime(0, 0, 0, date('n', $ts1), date('j', $ts1), date('Y', $ts1)), 'FULL'); if ($ts2) $date2 = ConvertTimeStamp(mktime(23, 59, 59, date('n', $ts2), date('j', $ts2), date('Y', $ts2)), 'FULL'); if (($cOperationType1 !== null) && ($date1 !== null)) { $arrayKey = $cOperationType1.$fieldName; while (isset($arFilter[$arrayKey])) { $arrayKey = ' '.$arrayKey; } $arFilter[$arrayKey] = $date1; } if (($cOperationType2 !== null) && ($date2 !== null)) { $arrayKey = $cOperationType2.$fieldName; while (isset($arFilter[$arrayKey])) { $arrayKey = ' '.$arrayKey; } $arFilter[$arrayKey] = $date2; } $arSqlSearch[] = self::GetSqlByFilter($arFilter, $userID, $sAliasPrefix, $bGetZombie); return ($arSqlSearch); } public static function GetFilteredKeys($arFilter) { $result = array(); if (is_array($arFilter)) { foreach ($arFilter as $key => $v) { // Skip meta-key if ($key === '::LOGIC') continue; // Skip markers if ($key === '::MARKERS') continue; // Subfilter? if (static::isSubFilterKey($key)) { $result = array_merge($result, self::GetFilteredKeys($v)); continue; } $res = self::MkOperationFilter($key); if ((string)$res['FIELD'] != '') { $result[] = $res['FIELD']; } } } return array_unique($result); } public static function isSubFilterKey($key) { return is_numeric($key) || (substr((string)$key, 0, 12) === '::SUBFILTER-'); } public static function GetFilter($arFilter, $sAliasPrefix = "", $arParams = false) { if (!is_array($arFilter)) { $arFilter = array(); } $arSqlSearch = array(); if (is_array($arParams) && array_key_exists('USER_ID', $arParams) && ($arParams['USER_ID'] > 0)) { $userID = (int)$arParams['USER_ID']; } else { $userID = User::getId(); } $bGetZombie = false; if (isset($arParams['bGetZombie'])) { $bGetZombie = (bool)$arParams['bGetZombie']; } // if TRUE will be generated constraint for members $bMembersTableJoined = false; if (isset($arParams['bMembersTableJoined'])) { $bMembersTableJoined = (bool)$arParams['bMembersTableJoined']; } $sql = self::GetSqlByFilter($arFilter, $userID, $sAliasPrefix, $bGetZombie, $bMembersTableJoined, $arParams); if (strlen($sql)) { $arSqlSearch[] = $sql; } // enable legacy access if no option passed (by default) // disable legacy access when ENABLE_LEGACY_ACCESS === true // we can not switch legacy access off by default, because getFilter() can be used separately $enableLegacyAccess = !is_array($arParams) || $arParams['ENABLE_LEGACY_ACCESS'] !== false; if ($enableLegacyAccess && static::needAccessRestriction($arFilter, $arParams)) { list($arSubSqlSearch, $fields) = static::getPermissionFilterConditions( $arParams, array('ALIAS' => $sAliasPrefix) ); if (!empty($arSubSqlSearch)) { $arSqlSearch[] = " \n/*access LEGACY BEGIN*/\n (". implode(" OR ", $arSubSqlSearch). ") \n/*access LEGACY END*/\n"; } } return $arSqlSearch; } private static function placeFieldSql($field, $behaviour, &$fields) { if ($behaviour['USE_PLACEHOLDERS']) { $fields[] = $field; return '%s'; } return $behaviour['ALIAS'].'T.'.$field; } /** * @param $arParams * @param array $behaviour * * @return array * @deprecated */ public static function getPermissionFilterConditions($arParams, $behaviour = array('ALIAS' => '', 'USE_PLACEHOLDERS' => false)) { if (!is_array($behaviour)) { $behaviour = array(); } if (!isset($behaviour['ALIAS'])) { $behaviour['ALIAS'] = ''; } if (!isset($behaviour['USE_PLACEHOLDERS'])) { $behaviour['USE_PLACEHOLDERS'] = false; } $arSubSqlSearch = array(); $fields = array(); $a = $behaviour['ALIAS']; $b = $behaviour; $f =& $fields; if (!is_array($arParams)) { $arParams = []; } if (array_key_exists('USER_ID', $arParams) && ($arParams['USER_ID'] > 0)) { $userID = (int)$arParams['USER_ID']; } else { $userID = User::getId(); } if (array_key_exists('TASK_MEMBER_JOINED', $arParams) && $arParams['TASK_MEMBER_JOINED']) { $taskMemberJoined = true; } else { $taskMemberJoined = false; } if (!User::isSuper($userID)) { // subordinate check $arParams['FIELDS'] =& $fields; if ($strSql = self::GetSubordinateSql($a, $arParams, $behaviour)) { $arSubSqlSearch[] = "EXISTS(" . $strSql . ")"; } // group permission check if ($arAllowedGroups = self::GetAllowedGroups($arParams)) { $arSubSqlSearch[] = "(" . static::placeFieldSql('GROUP_ID', $b, $f) . " IN (" . implode(",", $arAllowedGroups) . "))"; } if (!$taskMemberJoined || ($taskMemberJoined && !empty($arSubSqlSearch))) { $arSubSqlSearch[] = static::placeFieldSql('CREATED_BY', $b, $f) . " = '" . $userID . "'"; $arSubSqlSearch[] = static::placeFieldSql('RESPONSIBLE_ID', $b, $f) . " = '" . $userID . "'"; $arSubSqlSearch[] = "EXISTS( SELECT 'x' FROM b_tasks_member " . $a . "TM WHERE " . $a . "TM.TASK_ID = " . static::placeFieldSql('ID', $b, $f) . " AND " . $a . "TM.USER_ID = '" . $userID . "' )"; } } return array($arSubSqlSearch, $fields); } public static function MkOperationFilter($key) { static $arOperationsMap = null; // will be loaded on demand $key = ltrim($key); $firstSymbol = substr($key, 0, 1); $twoSymbols = substr($key, 0, 2); if ($firstSymbol == "=") //Identical { $key = substr($key, 1); $cOperationType = "I"; } elseif ($twoSymbols == "!=") //not Identical { $key = substr($key, 2); $cOperationType = "NI"; } elseif ($firstSymbol == "%") //substring { $key = substr($key, 1); $cOperationType = "S"; } elseif ($twoSymbols == "!%") //not substring { $key = substr($key, 2); $cOperationType = "NS"; } elseif ($firstSymbol == "?") //logical { $key = substr($key, 1); $cOperationType = "?"; } elseif ($twoSymbols == "><") //between { $key = substr($key, 2); $cOperationType = "B"; } elseif ($twoSymbols == "*=") // identical full text match { $key = substr($key, 2); $cOperationType = "FTI"; } elseif ($twoSymbols == "*%") // partial full text match based on LIKE { $key = substr($key, 2); $cOperationType = "FTL"; } elseif ($firstSymbol == "*") // partial full text match { $key = substr($key, 1); $cOperationType = "FT"; } elseif (substr($key, 0, 3) == "!><") //not between { $key = substr($key, 3); $cOperationType = "NB"; } elseif ($twoSymbols == ">=") //greater or equal { $key = substr($key, 2); $cOperationType = "GE"; } elseif ($firstSymbol == ">") //greater { $key = substr($key, 1); $cOperationType = "G"; } elseif ($twoSymbols == "<=") //less or equal { $key = substr($key, 2); $cOperationType = "LE"; } elseif ($firstSymbol == "<") //less { $key = substr($key, 1); $cOperationType = "L"; } elseif ($firstSymbol == "!") // not field LIKE val { $key = substr($key, 1); $cOperationType = "N"; } elseif ($firstSymbol === '#') { // Preload and cache in static variable if ($arOperationsMap === null) { $arManifest = \CTaskFilterCtrl::getManifest(); $arOperationsMap = $arManifest['Operations map']; } // Resolve operation code and cutoff operation prefix from item name $operation = null; foreach ($arOperationsMap as $operationCode => $operationPrefix) { $pattern = '/^'.preg_quote($operationPrefix).'[A-Za-z]/'; if (preg_match($pattern, $key)) { $operation = $operationCode; $key = substr($key, strlen($operationPrefix)); break; } } \CTaskAssert::assert($operation !== null); $cOperationType = "#".$operation; } else $cOperationType = "E"; // field LIKE val return array("FIELD" => $key, "OPERATION" => $cOperationType); } public static function FilterCreate($fname, $vals, $type, &$bFullJoin, $cOperationType = false, $bSkipEmpty = true) { global $DB; if (!is_array($vals)) { $vals = array($vals); } else { $vals = array_unique(array_values($vals)); } if (count($vals) < 1) return ""; if (is_bool($cOperationType)) { if ($cOperationType === true) $cOperationType = "N"; else $cOperationType = "E"; } if ($cOperationType == "G") $strOperation = ">"; elseif ($cOperationType == "GE") $strOperation = ">="; elseif ($cOperationType == "LE") $strOperation = "<="; elseif ($cOperationType == "L") $strOperation = "<"; elseif ($cOperationType === "NI") $strOperation = "!="; else $strOperation = "="; $bFullJoin = false; $bWasLeftJoin = false; // special case for array of number if ($type === 'number' && is_array($vals) && count($vals) > 1 && count($vals) < 80) { $vals = implode(', ', array_unique(array_map('intval', $vals))); $res = $fname.' '.($cOperationType == 'N' ? 'not' : '').' in ('.$vals.')'; // INNER JOIN in this case if ($cOperationType != "N") $bFullJoin = true; return $res; } $res = array(); foreach ($vals as $key => $val) { if (($type === 'number') && !$val) $val = 0; if (!$bSkipEmpty || $val <> '' || (is_bool($val) && $val === false)) { switch ($type) { case "string_equal": if ($val == '') $res[] = ($cOperationType == "N" ? "NOT" : ""). "(". $fname. " IS NULL OR ". $DB->Length($fname). "<=0)"; else $res[] = "(". ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT (" : ""). $fname. $strOperation. "'". $DB->ForSql($val). "'". ($cOperationType == "N" ? ")" : ""). ")"; break; case "string": if ($cOperationType == "?") { if ($val <> '') $res[] = GetFilterQuery($fname, $val, "Y", array(), "N"); } elseif ($cOperationType == "S") { $res[] = "(UPPER(".$fname.") LIKE UPPER('%".$DB->ForSqlLike($val)."%'))"; } elseif ($cOperationType == "NS") { $res[] = "(UPPER(".$fname.") NOT LIKE UPPER('%".$DB->ForSqlLike($val)."%'))"; } elseif ($cOperationType == "FTL") { $sqlWhere = new \CSQLWhere(); $res[] = $sqlWhere->matchLike($fname, $val); } elseif ($val == '') { $res[] = ($cOperationType == "N" ? "NOT" : ""). "(". $fname. " IS NULL OR ". $DB->Length($fname). "<=0)"; } else { if ($strOperation == "=") $res[] = "(". ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT (" : ""). (strtoupper($DB->type) == "ORACLE" ? $fname." LIKE "."'".$DB->ForSqlLike($val)."'"." ESCAPE '\\'" : $fname. " ". ($strOperation == "=" ? "LIKE" : $strOperation). " '". $DB->ForSqlLike( $val ). "'"). ($cOperationType == "N" ? ")" : ""). ")"; else $res[] = "(". ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT (" : ""). (strtoupper($DB->type) == "ORACLE" ? $fname. " ". $strOperation. " ". "'". $DB->ForSql($val). "'". " " : $fname. " ". $strOperation. " '". $DB->ForSql($val). "'"). ($cOperationType == "N" ? ")" : ""). ")"; } break; case "fulltext": echo ''; if ($cOperationType == "FT" || $cOperationType == "FTI") { $sqlWhere = new \CSQLWhere(); $res[] = $sqlWhere->match($fname, $val, $cOperationType == "FT"); } elseif ($cOperationType == "FTL") { $sqlWhere = new \CSQLWhere(); $res[] = $sqlWhere->matchLike($fname, $val); } elseif ($cOperationType == "?") { if ($val <> '') { $sr = GetFilterQuery( $fname, $val, "Y", array(), ($fname == "BE.SEARCHABLE_CONTENT" || $fname == "BE.DETAIL_TEXT" ? "Y" : "N") ); if ($sr != "0") $res[] = $sr; } } elseif (($cOperationType == "B" || $cOperationType == "NB") && is_array($val) && count($val) == 2) { $res[] = ($cOperationType == "NB" ? " ".$fname." IS NULL OR NOT " : ""). "(". \CIBlock::_Upper($fname). " ". $strOperation[0]. " '". \CIBlock::_Upper($DB->ForSql($val[0])). "' ". $strOperation[1]. " '". \CIBlock::_Upper($DB->ForSql($val[1])). "')"; } elseif ($cOperationType == "S" || $cOperationType == "NS") $res[] = ($cOperationType == "NS" ? " ".$fname." IS NULL OR NOT " : ""). "(". \CIBlock::_Upper($fname). " LIKE ". \CIBlock::_Upper("'%".\CIBlock::ForLIKE($val)."%'"). ")"; else { if ($val == '') $res[] = ($bNegative ? "NOT" : "")."(".$fname." IS NULL OR ".$DB->Length($fname)."<=0)"; else if ($strOperation == "=" && $cOperationType != "I" && $cOperationType != "NI") $res[] = ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT " : ""). "(". ($DB->type == "ORACLE" ? \CIBlock::_Upper($fname). " LIKE ". \CIBlock::_Upper("'".$DB->ForSqlLike($val)."'"). " ESCAPE '\\'" : $fname. " LIKE '". $DB->ForSqlLike($val). "'"). ")"; else $res[] = ($bNegative ? " ".$fname." IS NULL OR NOT " : ""). "(". ($DB->type == "ORACLE" ? \CIBlock::_Upper($fname). " ". $strOperation. " ". \CIBlock::_Upper("'".$DB->ForSql($val)."'"). " " : $fname. " ". $strOperation. " '". $DB->ForSql($val). "'"). ")"; } break; case "date": if ($val == '') $res[] = ($cOperationType == "N" ? "NOT" : "")."(".$fname." IS NULL)"; else $res[] = "(". ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT (" : ""). $fname. " ". $strOperation. " ". $val. "". ($cOperationType == "N" ? ")" : ""). ")"; break; case "number": if (($vals[$key] === false) || ($val == '')) $res[] = ($cOperationType == "N" ? "NOT" : "")."(".$fname." IS NULL)"; else $res[] = "(". ($cOperationType == "N" ? " ".$fname." IS NULL OR NOT (" : ""). $fname. " ". $strOperation. " '". DoubleVal($val). ($cOperationType == "N" ? "')" : "'"). ")"; break; case "number_wo_nulls": $res[] = "(". ($cOperationType == "N" ? "NOT (" : ""). $fname. " ". $strOperation. " ". DoubleVal($val). ($cOperationType == "N" ? ")" : ""). ")"; break; case "null_or_zero": if ($cOperationType == "N") $res[] = "((".$fname." IS NOT NULL) AND (".$fname." != 0))"; else $res[] = "((".$fname." IS NULL) OR (".$fname." = 0))"; break; case "left_existence": if ($strOperation != '=') { \CTaskAssert::logError('Operation type not supported for '.$fname.': '.$strOperation); } elseif ($val != 'Y' && $val != 'N' && 0) { \CTaskAssert::logError('Filter value not supported for '.$fname.': '.$val); } else { $otNot = $cOperationType == "N"; if (($val == 'Y' && !$otNot) || ($val == 'N' && $otNot)) $res[] = "(".$fname." IS NOT NULL)"; else $res[] = "(".$fname." IS NULL)"; } break; case 'reference': $val = trim($val); if (preg_match('#^[a-z0-9_]+(\.{1}[a-z0-9_]+)*$#i', $val)) { if ($cOperationType === 'E') $res[] = '('.$fname.' = '.$DB->ForSql($val).')'; elseif ($cOperationType === 'N') $res[] = '('.$fname.' != '.$DB->ForSql($val).')'; elseif ($cOperationType === 'L') $res[] = '('.$fname.' < '.$DB->ForSql($val).')'; elseif ($cOperationType === 'G') $res[] = '('.$fname.' > '.$DB->ForSql($val).')'; else \CTaskAssert::logError('[0xcf017223] Operation type not supported: '.$cOperationType); } else { \CTaskAssert::logError("Bad reference: ".$fname." => '".$val."'"); } break; } // INNER JOIN in this case if ($val <> '' && $cOperationType != "N") $bFullJoin = true; else $bWasLeftJoin = true; } } $strResult = ""; for ($i = 0, $resCnt = count($res); $i < $resCnt; $i++) { if ($i > 0) $strResult .= ($cOperationType == "N" ? " AND " : " OR "); $strResult .= $res[$i]; } if (count($res) > 1) $strResult = "(".$strResult.")"; if ($bFullJoin && $bWasLeftJoin && $cOperationType != "N") $bFullJoin = false; return $strResult; } /** * This method is deprecated. Use CTaskItem class instead. * @deprecated */ public static function GetByID($ID, $bCheckPermissions = true, $arParams = array()) { $bReturnAsArray = false; $bSkipExtraData = false; $arGetListParams = array(); if (isset($arParams['returnAsArray'])) $bReturnAsArray = ($arParams['returnAsArray'] === true); if (isset($arParams['bSkipExtraData'])) $bSkipExtraData = ($arParams['bSkipExtraData'] === true); if (isset($arParams['USER_ID'])) $arGetListParams['USER_ID'] = $arParams['USER_ID']; $arFilter = array("ID" => (int)$ID); if (!$bCheckPermissions) $arFilter["CHECK_PERMISSIONS"] = "N"; $select = ['*', 'UF_*']; if (array_key_exists('select', $arParams)) { $select = $arParams['select']; } $select = array_unique(array_merge(['ID'], $select)); $res = self::GetList(array(), $arFilter, $select, $arGetListParams); if ($res && ($task = $res->Fetch())) { if (in_array('AUDITORS', $select) || in_array('ACCOMPLICES', $select) || in_array('*', $select)) { $task["ACCOMPLICES"] = $task["AUDITORS"] = []; $rsMembers = \CTaskMembers::GetList(array(), array("TASK_ID" => $ID)); while ($arMember = $rsMembers->Fetch()) { if ($arMember["TYPE"] == "A" && (in_array('ACCOMPLICES', $select) || in_array('*', $select))) { $task["ACCOMPLICES"][] = $arMember["USER_ID"]; } elseif ($arMember["TYPE"] == "U" && (in_array('AUDITORS', $select) || in_array('*', $select))) { $task["AUDITORS"][] = $arMember["USER_ID"]; } } } if (!$bSkipExtraData) { if (in_array('TAGS', $select) || in_array('*', $select)) { $arTagsFilter = array("TASK_ID" => $ID); $arTagsOrder = array("NAME" => "ASC"); $rsTags = \CTaskTags::GetList($arTagsOrder, $arTagsFilter); $task["TAGS"] = array(); while ($arTag = $rsTags->Fetch()) { $task["TAGS"][] = $arTag["NAME"]; } } if (in_array('CHECKLIST', $select) || in_array('*', $select)) { $rsCheckList = \CTaskCheckListItem::getByTaskId($ID); $task["CHECKLIST"] = array(); while ($arCheckListItem = $rsCheckList->Fetch()) { $task["CHECKLIST"][] = $arCheckListItem; } } if (in_array('FILES', $select) || in_array('*', $select)) { $rsFiles = \CTaskFiles::GetList(array(), array("TASK_ID" => $ID)); $task["FILES"] = array(); while ($arFile = $rsFiles->Fetch()) { $task["FILES"][] = $arFile["FILE_ID"]; } } if (in_array('DEPENDS_ON', $select) || in_array('*', $select)) { $rsDependsOn = \CTaskDependence::GetList(array(), array("TASK_ID" => $ID)); $task["DEPENDS_ON"] = array(); while ($arDependsOn = $rsDependsOn->Fetch()) { $task["DEPENDS_ON"][] = $arDependsOn["DEPENDS_ON_ID"]; } } } if ($bReturnAsArray) return ($task); else { $rsTask = new \CDBResult; $rsTask->InitFromarray(array($task)); return $rsTask; } } else { if ($bReturnAsArray) return (false); else return $res; } } /** * @param null $userID * * @return array * @deprecated */ public static function GetSubordinateDeps($userID = null) { return Integration\Intranet\Department::getSubordinateIds($userID, true); } /** * @param array $arParams * * @return mixed * @deprecated * @see Integration\SocialNetwork\Group::getIdsByAllowedAction */ public static function GetAllowedGroups($arParams = array()) { global $DB; static $ALLOWED_GROUPS = array(); $userId = null; if (is_array($arParams) && isset($arParams['USER_ID'])) $userId = $arParams['USER_ID']; else { $userId = User::getId(); } if (!($userId >= 1)) $userId = 0; $bGetZombie = false; if (isset($arParams['bGetZombie'])) $bGetZombie = (bool)$arParams['bGetZombie']; if (!isset($ALLOWED_GROUPS[$userId]) && \CModule::IncludeModule("socialnetwork")) { // bottleneck $strSql = "SELECT DISTINCT(T.GROUP_ID) FROM b_tasks T WHERE T.GROUP_ID IS NOT NULL"; if (!$bGetZombie) $strSql .= " AND T.ZOMBIE = 'N'"; $rsGroups = $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); $ALLOWED_GROUPS[$userId] = $arGroupsWithTasks = array(); while ($arGroup = $rsGroups->Fetch()) { $arGroupsWithTasks[] = $arGroup["GROUP_ID"]; } if (is_array($arGroupsWithTasks) && sizeof($arGroupsWithTasks)) { if ($userId === 0) $featurePerms = \CSocNetFeaturesPerms::CurrentUserCanPerformOperation( SONET_ENTITY_GROUP, $arGroupsWithTasks, "tasks", "view_all" ); else $featurePerms = \CSocNetFeaturesPerms::CanPerformOperation( $userId, SONET_ENTITY_GROUP, $arGroupsWithTasks, "tasks", "view_all" ); if (is_array($featurePerms)) { $ALLOWED_GROUPS[$userId] = array_keys(array_filter($featurePerms)); } } } return $ALLOWED_GROUPS[$userId]; } public static function GetDepartmentManagers($arDepartments, $skipUserId = false, $arSelectFields = array('ID')) { global $CACHE_MANAGER; if ((!is_array($arDepartments)) || empty($arDepartments) || (!is_array($arSelectFields))) { return false; } // We need ID in any case if (!in_array('ID', $arSelectFields)) $arSelectFields[] = 'ID'; $arManagers = array(); $obCache = new \CPHPCache(); $lifeTime = \CTasksTools::CACHE_TTL_UNLIM; $cacheDir = "/tasks/subordinatedeps"; $cacheFPrint = sha1( serialize($arDepartments).'|'.serialize($arSelectFields) ); if ($obCache->InitCache($lifeTime, $cacheFPrint, $cacheDir)) { $arManagers = $obCache->GetVars(); } elseif ($obCache->StartDataCache()) { $IBlockID = \COption::GetOptionInt('intranet', 'iblock_structure', 0); $CACHE_MANAGER->StartTagCache($cacheDir); $CACHE_MANAGER->RegisterTag("iblock_id_".$IBlockID); $arUserIDs = self::GetDepartmentManagersIDs($arDepartments, $IBlockID); if (count($arUserIDs) > 0) { $arFilter = array( 'ID' => implode('|', $arUserIDs) ); // Prevent using users, that doesn't activate it's account // http://jabber.bx/view.php?id=29118 if (IsModuleInstalled('bitrix24')) $arFilter['!LAST_LOGIN'] = false; $dbUser = \CUser::GetList( $by = 'ID', $order = 'ASC', $arFilter, array('FIELDS' => $arSelectFields) // selects only $arSelectFields fields ); while ($arUser = $dbUser->GetNext()) { $arManagers[(int)$arUser["ID"]] = $arUser; } } $CACHE_MANAGER->EndTagCache(); $obCache->EndDataCache($arManagers); } // remove user to be skipped if (($skipUserId !== false) && (isset($arManagers[(int)$skipUserId]))) { unset ($arManagers[(int)$skipUserId]); } return $arManagers; } protected static function GetDepartmentManagersIDs($arDepartments, $IBlockID) { if (!\CModule::IncludeModule('iblock')) { return array(); } $dbSections = \CIBlockSection::GetList( array('SORT' => 'ASC'), array( 'ID' => $arDepartments, 'IBLOCK_ID' => $IBlockID, 'CHECK_PERMISSIONS' => 'N' ), false, // don't count array( 'ID', 'UF_HEAD', 'IBLOCK_SECTION_ID' ) ); $arUserIDs = array(); while ($arSection = $dbSections->Fetch()) { if ($arSection['UF_HEAD'] > 0) $arUserIDs[] = $arSection['UF_HEAD']; if ($arSection['IBLOCK_SECTION_ID'] > 0) { $arUserIDs = array_merge( $arUserIDs, self::GetDepartmentManagersIDs(array($arSection['IBLOCK_SECTION_ID']), $IBlockID) ); } } return $arUserIDs; } /** * @param $employeeID1 * @param $employeeID2 * * @return bool true if $employeeID2 is manager of $employeeID1 */ public static function IsSubordinate($employeeID1, $employeeID2) { if ($employeeID1 == $employeeID2) { return false; } $dbRes = \CUser::GetList( $by = 'ID', $order = 'ASC', array('ID' => $employeeID1), array('SELECT' => array('UF_DEPARTMENT')) ); if (($arRes = $dbRes->Fetch()) && is_array($arRes['UF_DEPARTMENT']) && (count($arRes['UF_DEPARTMENT']) > 0)) { $arManagers = array_keys(self::GetDepartmentManagers($arRes['UF_DEPARTMENT'], $employeeID1)); if (in_array($employeeID2, $arManagers)) return true; } return false; } public static function getSelectSqlByFilter(array $filter = array(), $alias = '', array $filterParams = array()) { $userId = intval($filterParams['USER_ID']); $obUserFieldsSql = new \CUserTypeSQL(); $obUserFieldsSql->SetEntity("TASKS_TASK", $alias . "T.ID"); $obUserFieldsSql->SetFilter($filter); if (isset($filter['::LOGIC'])) { \CTaskAssert::assert($filter['::LOGIC'] === 'AND'); } $optimized = static::tryOptimizeFilter($filter, $alias . "T", $alias . "TM_SPEC"); $sqlSearch = self::GetFilter($optimized['FILTER'], $alias, $filterParams); $r = $obUserFieldsSql->GetFilter(); if ($r <> '') { $sqlSearch[] = "(" . $r . ")"; } $params = [ 'USER_ID' => $userId, 'JOIN_ALIAS' => $alias, 'SOURCE_ALIAS' => $alias . "T" ]; $relatedJoins = static::getRelatedJoins([], $filter, [], $params); $relatedJoins = array_merge($relatedJoins, $optimized['JOINS']); $needGroup = isset($relatedJoins['FULL_SEARCH']) || isset($relatedJoins['COMMENT_SEARCH']); $sql = " SELECT {$alias}T.ID FROM b_tasks {$alias}T INNER JOIN b_user {$alias}CU ON {$alias}CU.ID = {$alias}T.CREATED_BY INNER JOIN b_user {$alias}RU ON {$alias}RU.ID = {$alias}T.RESPONSIBLE_ID " . implode("\n", $relatedJoins) . " " . $obUserFieldsSql->GetJoin($alias."T.ID") . " " . (sizeof($sqlSearch)? " WHERE " . implode(" AND ", $sqlSearch) : "") . " " . ($needGroup? "GROUP BY {$alias}T.ID" : ""). " "; return $sql; } /** * Get tasks fields info (for rest, etc) * * @return array */ public static function getFieldsInfo() { global $USER_FIELD_MANAGER; $fields = [ "ID" => [ 'type' => 'integer', 'primary' => true ], "PARENT_ID" => [ 'type' => 'integer', 'default' => 0 ], "TITLE" => [ 'type' => 'string', 'required' => true ], "DESCRIPTION" => [ 'type' => 'string', ], "MARK" => [ 'type' => 'enum', 'values' => [ self::MARK_NEGATIVE => Loc::getMessage('TASKS_FIELDS_MARK_NEGATIVE'), self::MARK_POSITIVE => Loc::getMessage('TASKS_FIELDS_MARK_POSITIVE') ], 'default' => null ], "PRIORITY" => [ 'type' => 'enum', 'values' => [ self::PRIORITY_HIGH => Loc::getMessage('TASKS_FIELDS_PRIORITY_HIGH'), self::PRIORITY_AVERAGE => Loc::getMessage('TASKS_FIELDS_PRIORITY_AVERAGE'), self::PRIORITY_LOW => Loc::getMessage('TASKS_FIELDS_PRIORITY_LOW') ], 'default' => self::PRIORITY_AVERAGE ], "STATUS" => [ 'type' => 'enum', 'values' => [ self::STATE_PENDING => Loc::getMessage('TASKS_FIELDS_STATUS_PENDING'), self::STATE_IN_PROGRESS => Loc::getMessage('TASKS_FIELDS_STATUS_IN_PROGRESS'), self::STATE_SUPPOSEDLY_COMPLETED => Loc::getMessage('TASKS_FIELDS_STATUS_SUPPOSEDLY_COMPLETED'), self::STATE_COMPLETED => Loc::getMessage('TASKS_FIELDS_STATUS_COMPLETED'), self::STATE_DEFERRED => Loc::getMessage('TASKS_FIELDS_STATUS_DEFERRED') ], 'default' => self::STATE_PENDING ], "MULTITASK" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "NOT_VIEWED" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "REPLICATE" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "GROUP_ID" => [ 'type' => 'integer', 'default' => 0 ], "STAGE_ID" => [ 'type' => 'integer', 'default' => 0 ], "CREATED_BY" => [ 'type' => 'integer', 'required' => true ], "CREATED_DATE" => [ 'type' => 'datetime' ], "RESPONSIBLE_ID" => [ 'type' => 'integer', 'required' => true ], "ACCOMPLICES" => [ 'type' => 'array' ], "AUDITORS" => [ 'type' => 'array' ], "CHANGED_BY" => [ 'type' => 'integer', ], "CHANGED_DATE" => [ 'type' => 'datetime' ], "STATUS_CHANGED_BY" => [ 'type' => 'integer', ], "STATUS_CHANGED_DATE" => [ 'type' => 'datetime' ], "CLOSED_BY" => [ 'type' => 'integer', 'default' => null ], "CLOSED_DATE" => [ 'type' => 'datetime', 'default' => null ], "DATE_START" => [ 'type' => 'datetime', 'default' => null ], "DEADLINE" => [ 'type' => 'datetime', 'default' => null ], "START_DATE_PLAN" => [ 'type' => 'datetime', 'default' => null ], "END_DATE_PLAN" => [ 'type' => 'datetime', 'default' => null ], 'GUID' => [ 'type' => 'string', 'default' => null ], "XML_ID" => [ 'type' => 'string', 'default' => null ], "COMMENTS_COUNT" => [ 'type' => 'integer', 'default' => 0 ], "NEW_COMMENTS_COUNT" => [ 'type' => 'integer', 'default' => 0 ], "TASK_CONTROL" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "ADD_IN_REPORT" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "FORKED_BY_TEMPLATE_ID" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => 'N' ], "TIME_ESTIMATE" => [ 'type' => 'integer', ], "TIME_SPENT_IN_LOGS" => [ 'type' => 'integer', ], "MATCH_WORK_TIME" => [ 'type' => 'integer', ], "FORUM_TOPIC_ID" => [ 'type' => 'integer', ], "FORUM_ID" => [ 'type' => 'integer', ], "SITE_ID" => [ 'type' => 'string', ], "SUBORDINATE" => [ 'type' => 'enum', 'values' => [ 'Y' => Loc::getMessage('TASKS_FIELDS_Y'), 'N' => Loc::getMessage('TASKS_FIELDS_N'), ], 'default' => null ], "EXCHANGE_MODIFIED" => [ 'type' => 'datetime', 'default' => null ], "EXCHANGE_ID" => [ 'type' => 'integer', 'default' => null ], "OUTLOOK_VERSION" => [ 'type' => 'integer', 'default' => null ], "VIEWED_DATE" => [ 'type' => 'datetime' ], "SORTING" => [ 'type' => 'double', ], "DURATION_PLAN" => [ 'type' => 'integer', ], "DURATION_FACT" => [ 'type' => 'integer', ], "DURATION_TYPE" => [ 'type' => 'enum', 'values' => [ 'secs', 'mins', 'hours', 'days', 'weeks', 'monts', 'years' ], 'default' => 'days' ] ]; foreach ($fields as $fieldId => &$fieldData) { $fieldData = array_merge(['title' => Loc::getMessage('TASKS_FIELDS_'.$fieldId)], $fieldData); } unset($fieldData); $uf = $USER_FIELD_MANAGER->GetUserFields("TASKS_TASK"); foreach ($uf as $key=>$item) { $fields[$key]=[ 'title'=>$item['USER_TYPE']['DESCRIPTION'], 'type'=>$item['USER_TYPE_ID'] ]; } return $fields; } /** * @param array $arOrder * @param array $arFilter * @param array $arSelect * @param array $arParams * @param array $arGroup * @return bool|\CDBResult * @throws \TasksException */ public static function GetList($arOrder=array(), $arFilter=array(), $arSelect = array(), $arParams = array(), array $arGroup = array()) { global $DB, $USER_FIELD_MANAGER; $bIgnoreErrors = false; $nPageTop = false; $bGetZombie = false; $deleteMessageId = false; if ( ! is_array($arParams) ) { $nPageTop = $arParams; $arParams = false; } else { if (isset($arParams['nPageTop'])) $nPageTop = $arParams['nPageTop']; if (isset($arParams['bIgnoreErrors'])) $bIgnoreErrors = (bool) $arParams['bIgnoreErrors']; if (isset($arParams['bGetZombie'])) $bGetZombie = (bool) $arParams['bGetZombie']; } $obUserFieldsSql = new \CUserTypeSQL(); $obUserFieldsSql->SetEntity("TASKS_TASK", "T.ID"); $obUserFieldsSql->SetSelect($arSelect); $obUserFieldsSql->SetFilter($arFilter); $obUserFieldsSql->SetOrder($arOrder); if (is_array($arParams) && array_key_exists('USER_ID', $arParams) && ($arParams['USER_ID'] > 0)) { $userID = (int) $arParams['USER_ID']; } else { $userID = User::getId(); } $arFields = array( "ID" => "T.ID", "TITLE" => "T.TITLE", "DESCRIPTION" => "T.DESCRIPTION", "DESCRIPTION_IN_BBCODE" => "T.DESCRIPTION_IN_BBCODE", "DECLINE_REASON" => "T.DECLINE_REASON", "PRIORITY" => "T.PRIORITY", // 1) deadline in past, real status is not STATE_SUPPOSEDLY_COMPLETED and not STATE_COMPLETED and (not STATE_DECLINED or responsible is not me (user)) // 2) viewed by noone(?) and created not by me (user) and (STATE_NEW or STATE_PENDING) "STATUS" => " CASE WHEN T.DEADLINE < DATE_ADD(".$DB->CurrentTimeFunction().", INTERVAL ". Counter::getDeadlineTimeLimit()." SECOND) AND T.DEADLINE >= ".$DB->CurrentTimeFunction()." AND T.STATUS != '4' AND T.STATUS != '5' AND ( T.STATUS != '7' OR T.RESPONSIBLE_ID != ".intval($userID)." ) THEN '-3' WHEN T.DEADLINE < ".$DB->CurrentTimeFunction()." AND T.STATUS != '4' AND T.STATUS != '5' AND (T.STATUS != '7' OR T.RESPONSIBLE_ID != ".intval($userID).") THEN '-1' WHEN TV.USER_ID IS NULL AND T.CREATED_BY != ".intval($userID)." AND (T.STATUS = 1 OR T.STATUS = 2) THEN '-2' ELSE T.STATUS END ", "NOT_VIEWED" => " CASE WHEN TV.USER_ID IS NULL AND T.CREATED_BY != ".intval($userID)." AND (T.STATUS = 1 OR T.STATUS = 2) THEN 'Y' ELSE 'N' END ", // used in ORDER BY to make completed tasks go after (or before) all other tasks "STATUS_COMPLETE" => " CASE WHEN T.STATUS = '5' THEN '2' ELSE '1' END ", "REAL_STATUS" => "T.STATUS", "MULTITASK" => "T.MULTITASK", "STAGE_ID" => "T.STAGE_ID", "RESPONSIBLE_ID" => "T.RESPONSIBLE_ID", "RESPONSIBLE_NAME" => "RU.NAME", "RESPONSIBLE_LAST_NAME" => "RU.LAST_NAME", "RESPONSIBLE_SECOND_NAME" => "RU.SECOND_NAME", "RESPONSIBLE_LOGIN" => "RU.LOGIN", "RESPONSIBLE_WORK_POSITION" => "RU.WORK_POSITION", "RESPONSIBLE_PHOTO" => "RU.PERSONAL_PHOTO", "DATE_START" => $DB->DateToCharFunction("T.DATE_START", "FULL"), "DURATION_FACT" => "(SELECT SUM(TE.MINUTES) FROM b_tasks_elapsed_time TE WHERE TE.TASK_ID = T.ID GROUP BY TE.TASK_ID)", "TIME_ESTIMATE" => "T.TIME_ESTIMATE", "TIME_SPENT_IN_LOGS" => "(SELECT SUM(TE.SECONDS) FROM b_tasks_elapsed_time TE WHERE TE.TASK_ID = T.ID GROUP BY TE.TASK_ID)", "REPLICATE" => "T.REPLICATE", "DEADLINE" => $DB->DateToCharFunction("T.DEADLINE", "FULL"), "DEADLINE_ORIG" => "T.DEADLINE", "START_DATE_PLAN" => $DB->DateToCharFunction("T.START_DATE_PLAN", "FULL"), "END_DATE_PLAN" => $DB->DateToCharFunction("T.END_DATE_PLAN", "FULL"), "CREATED_BY" => "T.CREATED_BY", "CREATED_BY_NAME" => "CU.NAME", "CREATED_BY_LAST_NAME" => "CU.LAST_NAME", "CREATED_BY_SECOND_NAME" => "CU.SECOND_NAME", "CREATED_BY_LOGIN" => "CU.LOGIN", "CREATED_BY_WORK_POSITION" => "CU.WORK_POSITION", "CREATED_BY_PHOTO" => "CU.PERSONAL_PHOTO", "CREATED_DATE" => $DB->DateToCharFunction("T.CREATED_DATE", "FULL"), "CHANGED_BY" => "T.CHANGED_BY", "CHANGED_DATE" => $DB->DateToCharFunction("T.CHANGED_DATE", "FULL"), "STATUS_CHANGED_BY" => "T.CHANGED_BY", "STATUS_CHANGED_DATE" => 'CASE WHEN T.STATUS_CHANGED_DATE IS NULL THEN ' . $DB->DateToCharFunction("T.CHANGED_DATE", "FULL") . ' ELSE ' . $DB->DateToCharFunction("T.STATUS_CHANGED_DATE", "FULL") . ' END ', "CLOSED_BY" => "T.CLOSED_BY", "CLOSED_DATE" => $DB->DateToCharFunction("T.CLOSED_DATE", "FULL"), 'GUID' => 'T.GUID', "XML_ID" => "T.XML_ID", "MARK" => "T.MARK", "ALLOW_CHANGE_DEADLINE" => "T.ALLOW_CHANGE_DEADLINE", "ALLOW_CHANGE_DEADLINE_COUNT" => "T.ALLOW_CHANGE_DEADLINE_COUNT", "ALLOW_CHANGE_DEADLINE_COUNT_VALUE" => "T.ALLOW_CHANGE_DEADLINE_COUNT_VALUE", "ALLOW_CHANGE_DEADLINE_MAXTIME" => "T.ALLOW_CHANGE_DEADLINE_MAXTIME", "ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE" => "T.ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE", "ALLOW_TIME_TRACKING" => 'T.ALLOW_TIME_TRACKING', "MATCH_WORK_TIME" => "T.MATCH_WORK_TIME", "TASK_CONTROL" => "T.TASK_CONTROL", "ADD_IN_REPORT" => "T.ADD_IN_REPORT", "GROUP_ID" => "CASE WHEN T.GROUP_ID IS NULL THEN 0 ELSE T.GROUP_ID END", "FORUM_TOPIC_ID" => "T.FORUM_TOPIC_ID", "PARENT_ID" => "T.PARENT_ID", "COMMENTS_COUNT" => "FT.POSTS", "FORUM_ID" => "FT.FORUM_ID", "MESSAGE_ID" => "MIN(TSIF.MESSAGE_ID)", "SITE_ID" => "T.SITE_ID", "SUBORDINATE" => ($strSql = self::GetSubordinateSql('', $arParams)) ? "CASE WHEN EXISTS(".$strSql.") THEN 'Y' ELSE 'N' END" : "'N'", "EXCHANGE_MODIFIED" => "T.EXCHANGE_MODIFIED", "EXCHANGE_ID" => "T.EXCHANGE_ID", "OUTLOOK_VERSION" => "T.OUTLOOK_VERSION", "VIEWED_DATE" => $DB->DateToCharFunction("TV.VIEWED_DATE", "FULL"), "DEADLINE_COUNTED" => "T.DEADLINE_COUNTED", "FORKED_BY_TEMPLATE_ID" => "T.FORKED_BY_TEMPLATE_ID", "FAVORITE" => "CASE WHEN FVT.TASK_ID IS NULL THEN 'N' ELSE 'Y' END", "SORTING" => "SRT.SORT", "DURATION_PLAN_SECONDS" => "T.DURATION_PLAN", "DURATION_TYPE_ALL" => "T.DURATION_TYPE", "DURATION_PLAN" => " case when T.DURATION_TYPE = '".self::TIME_UNIT_TYPE_MINUTE."' or T.DURATION_TYPE = '".self::TIME_UNIT_TYPE_HOUR."' then ROUND(T.DURATION_PLAN / 3600, 0) when T.DURATION_TYPE = '".self::TIME_UNIT_TYPE_DAY."' or T.DURATION_TYPE = '' or T.DURATION_TYPE is null then ROUND(T.DURATION_PLAN / 86400, 0) else T.DURATION_PLAN end ", "DURATION_TYPE" => " case when T.DURATION_TYPE = '".self::TIME_UNIT_TYPE_MINUTE."' then '".self::TIME_UNIT_TYPE_HOUR."' else T.DURATION_TYPE end " ); if ($bGetZombie) { $arFields['ZOMBIE'] = 'T.ZOMBIE'; } if (!in_array('MESSAGE_ID', $arSelect)) { $deleteMessageId = true; } if (count($arSelect) <= 0 || in_array("*", $arSelect)) { $arSelect = array_keys($arFields); } elseif (!in_array("ID", $arSelect)) { $arSelect[] = "ID"; } // add fields that are NOT selected by default //$arFields["FAVORITE"] = "CASE WHEN FVT.TASK_ID IS NULL THEN 'N' ELSE 'Y' END"; // If DESCRIPTION selected, than BBCODE flag must be selected too if ( in_array('DESCRIPTION', $arSelect) && ( ! in_array('DESCRIPTION_IN_BBCODE', $arSelect) ) ) { $arSelect[] = 'DESCRIPTION_IN_BBCODE'; } if (!Integration\Forum::isInstalled()) { $arSelect = array_diff($arSelect, array('COMMENTS_COUNT', 'FORUM_ID')); } if ($deleteMessageId) { $arSelect = array_diff($arSelect, ['MESSAGE_ID']); } if (!is_array($arOrder)) { $arOrder = array(); } $arSqlOrder = array(); foreach ($arOrder as $by => $order) { $needle = null; $by = strtolower($by); $order = strtolower($order); if ($by === 'deadline') { if ( ! in_array($order, array('asc', 'desc', 'asc,nulls', 'desc,nulls'), true) ) $order = 'asc,nulls'; } else { if ($order !== 'asc') $order = 'desc'; } switch ($by) { case 'id': $arSqlOrder[] = " ID ".$order." "; break; case 'title': $arSqlOrder[] = " TITLE ".$order." "; $needle = 'TITLE'; break; case 'time_spent_in_logs': $arSqlOrder[] = " TIME_SPENT_IN_LOGS ".$order." "; $needle = 'TIME_SPENT_IN_LOGS'; break; case 'date_start': $arSqlOrder[] = " T.DATE_START ".$order." "; $needle = 'DATE_START'; break; case 'created_date': $arSqlOrder[] = " T.CREATED_DATE ".$order." "; $needle = 'CREATED_DATE'; break; case 'changed_date': $arSqlOrder[] = " T.CHANGED_DATE ".$order." "; $needle = 'CHANGED_DATE'; break; case 'closed_date': $arSqlOrder[] = " T.CLOSED_DATE ".$order." "; $needle = 'CLOSED_DATE'; break; case 'start_date_plan': $arSqlOrder[] = " T.START_DATE_PLAN ".$order." "; $needle = 'START_DATE_PLAN'; break; case 'end_date_plan': $arSqlOrder[] = " T.END_DATE_PLAN ".$order." "; $needle = 'END_DATE_PLAN'; break; case 'deadline': $orderClause = self::getOrderSql( 'T.DEADLINE', $order, $default_order = 'asc,nulls', $nullable = true ); $needle = 'DEADLINE_ORIG'; if ( ! is_array($orderClause) ) $arSqlOrder[] = $orderClause; else // we have to add select field in order to correctly sort { // COLUMN ALIAS COLUMN EXPRESSION $arFields[$orderClause[1]] = $orderClause[0]; if ( ! in_array($orderClause[1], $arSelect) ) $arSelect[] = $orderClause[1]; $arSqlOrder[] = $orderClause[2]; // order expression } break; case 'status': // $arSqlOrder[] = " STATUS ".$order." "; // $needle = 'STATUS'; break; case 'real_status': $arSqlOrder[] = " REAL_STATUS ".$order." "; $needle = 'REAL_STATUS'; break; case 'status_complete': $arSqlOrder[] = " STATUS_COMPLETE ".$order." "; $needle = 'STATUS_COMPLETE'; break; case 'priority': $arSqlOrder[] = " PRIORITY ".$order." "; $needle = 'PRIORITY'; break; case 'mark': $arSqlOrder[] = " MARK ".$order." "; $needle = 'MARK'; break; case 'originator_name': case 'created_by': $arSqlOrder[] = " CREATED_BY_LAST_NAME ".$order." "; $needle = 'CREATED_BY_LAST_NAME'; break; case 'responsible_name': case 'responsible_id': $arSqlOrder[] = " RESPONSIBLE_LAST_NAME ".$order." "; $needle = 'RESPONSIBLE_LAST_NAME'; break; case 'group_id': $arSqlOrder[] = " GROUP_ID ".$order." "; $needle = 'GROUP_ID'; break; case 'time_estimate': $arSqlOrder[] = " TIME_ESTIMATE ".$order." "; $needle = 'TIME_ESTIMATE'; break; case 'allow_change_deadline': $arSqlOrder[] = " ALLOW_CHANGE_DEADLINE ".$order." "; $needle = 'ALLOW_CHANGE_DEADLINE'; break; case 'allow_time_tracking': $arSqlOrder[] = " ALLOW_TIME_TRACKING ".$order." "; $needle = 'ALLOW_TIME_TRACKING'; break; case 'match_work_time': $arSqlOrder[] = " MATCH_WORK_TIME ".$order." "; $needle = 'MATCH_WORK_TIME'; break; case 'favorite': $arSqlOrder[] = " FAVORITE ".$order." "; $needle = 'FAVORITE'; break; case 'sorting': $asc = stripos($order, "desc") === false; $arSqlOrder = array_merge($arSqlOrder, self::getSortingOrderBy($asc)); $needle = "SORTING"; break; case 'message_id': $arSqlOrder[] = " MESSAGE_ID " . $order . " "; $needle = 'MESSAGE_ID'; break; default: if (substr($by, 0, 3) === 'uf_') { if ($s = $obUserFieldsSql->GetOrder($by)) $arSqlOrder[$by] = " ".$s." ".$order." "; } else \CTaskAssert::logWarning('[0x9a92cf7d] invalid sort by field requested: ' . $by); break; } if ( ($needle !== null) && ( ! in_array($needle, $arSelect) ) ) { $arSelect[] = $needle; } } $arSqlSelect = array(); foreach ($arSelect as $field) { $field = strtoupper($field); if (array_key_exists($field, $arFields)) $arSqlSelect[$field] = $arFields[$field]." AS ".$field; } if (!sizeof($arSqlSelect)) { $arSqlSelect = "T.ID AS ID"; } $disableAccessOptimization = (is_array($arParams) && $arParams['DISABLE_ACCESS_OPTIMIZATION'] === true); $useAccessAsJoin = !$disableAccessOptimization; // First level logic MUST be 'AND', because of backward compatibility // and some requests for checking rights, attached at first level of filter. // Situtation when there is OR-logic at first level cannot be resolved // in general case. // So if there is OR-logic, it is FATAL error caused by programmer. // But, if you want to use OR-logic at the first level of filter, you // can do this by putting all your filter conditions to the ::SUBFILTER-xxx, // except CHECK_PERMISSIONS, SUBORDINATE_TASKS (if you don't know exactly, // what are consequences of this fields in OR-logic of subfilters). if (isset($arFilter['::LOGIC'])) { \CTaskAssert::assert($arFilter['::LOGIC'] === 'AND'); } // GET OPTIMIZED JOINS $sourceFilter = $arFilter; $optimized = static::tryOptimizeFilter($arFilter); $arFilter = $optimized['FILTER']; $optimizedJoins = implode("\n", $optimized['JOINS']); $distinct = ''; if (!empty($optimized['JOINS'])) { $distinct = 'DISTINCT'; $arParams['SOURCE_FILTER'] = $sourceFilter; } // GET RELATED JOINS $params = [ 'USER_ID' => $userID, 'VIEWED_BY' => static::getViewedBy($sourceFilter, $userID), 'SORTING_GROUP_ID' => (isset($arParams['SORTING_GROUP_ID']) && $arParams['SORTING_GROUP_ID'] > 0? $arParams['SORTING_GROUP_ID'] : false) ]; $relatedJoins = static::getRelatedJoins($arSelect, $arFilter, $arOrder, $params); $relatedJoinsForCount = array_merge( [ 'CREATOR' => "INNER JOIN " . UserTable::getTableName() . " CU ON CU.ID = T.CREATED_BY", 'RESPONSIBLE' => "INNER JOIN " . UserTable::getTableName() . " RU ON RU.ID = T.RESPONSIBLE_ID" ], static::getRelatedJoins([], $arFilter, [], $params) ); // GET ACCESS SQL $accessSql = ''; if ($useAccessAsJoin && static::needAccessRestriction($arFilter, $arParams)) { $buildAccessSql = true; $arParams['APPLY_FILTER'] = static::makePossibleForwardedFilter($arFilter); if ($arParams['MAKE_ACCESS_FILTER']) { $viewedUserId = static::getViewedUserId($sourceFilter, $userID); $runtimeOptions = static::makeAccessFilterRuntimeOptions($sourceFilter, [ 'USER_ID' => $userID, 'VIEWED_USER_ID' => $viewedUserId ]); if (!is_array($arParams['ACCESS_FILTER_RUNTIME_OPTIONS'])) { $arParams['ACCESS_FILTER_RUNTIME_OPTIONS'] = $runtimeOptions; } else { foreach ($runtimeOptions as $key => $value) { $arParams['ACCESS_FILTER_RUNTIME_OPTIONS'][$key] += $value; } } if ($viewedUserId == $userID) { $buildAccessSql = static::checkAccessSqlBuilding($runtimeOptions); } } if ($buildAccessSql) { try { $accessSql = static::appendJoinRights($accessSql, $arParams); } catch (\Exception $exception) { $hash = hash('md5', 'ACCESS_FILTER_BUILDING_ERROR'); //934afb467df6f2ed2c5ff62fc358d6fe throw new \TasksException('[' . $hash . '] ' . $exception->getMessage(), \TasksException::TE_SQL_ERROR); } } } // GET FILTER $arParams['ENABLE_LEGACY_ACCESS'] = $disableAccessOptimization; // manual legacy access switch $arSqlSearch = self::GetFilter($arFilter, '', $arParams); if (!$bGetZombie) { $arSqlSearch[] = " T.ZOMBIE = 'N' "; } $userFieldsJoin = false; $r = $obUserFieldsSql->GetFilter(); if ($r <> '') { $userFieldsJoin = true; $arSqlSearch[] = "(".$r.")"; } // GET GROUP BY if (isset($relatedJoins['FULL_SEARCH']) || isset($relatedJoins['COMMENT_SEARCH'])) { $arGroup[] = "T.ID"; } $strGroupBy = (!empty($arGroup)? 'GROUP BY ' . implode(',', $arGroup) : ""); // GET ORDER BY $strSqlOrder = ""; DelDuplicateSort($arSqlOrder); for ($i = 0, $arSqlOrderCnt = count($arSqlOrder); $i < $arSqlOrderCnt; $i++) { if ($i == 0) { $strSqlOrder = " ORDER BY "; } else { $strSqlOrder .= ","; } $strSqlOrder .= $arSqlOrder[$i]; } // BUILD QUERY $strSql = " SELECT " . $distinct . " " . implode(",\n", $arSqlSelect) . " " . $obUserFieldsSql->GetSelect(); $strFrom = " FROM b_tasks T " . $accessSql . " " . $optimizedJoins . " " . implode("\n", $relatedJoins) . " " . $obUserFieldsSql->GetJoin("T.ID") . " " . (count($arSqlSearch)? "WHERE " . implode(" AND ", $arSqlSearch) : ""); $strSql .= " " . $strFrom . " " . $strGroupBy . " " . $strSqlOrder; $strFromForCount = " FROM b_tasks T " . $accessSql . " " . $optimizedJoins . " " . implode("\n", $relatedJoinsForCount) . " " . ($userFieldsJoin? $obUserFieldsSql->GetJoin("T.ID") : "") . " " . (count($arSqlSearch)? "WHERE " . implode(" AND ", $arSqlSearch) : ""); if (($nPageTop !== false) && is_numeric($nPageTop)) { $strSql = $DB->TopSql($strSql, intval($nPageTop)); } if (is_array($arParams) && array_key_exists("NAV_PARAMS", $arParams) && is_array($arParams["NAV_PARAMS"])) { $nTopCount = intval($arParams['NAV_PARAMS']['nTopCount']); if($nTopCount > 0) { $strSql = $DB->TopSql($strSql, $nTopCount); $res = $DB->Query($strSql, $bIgnoreErrors, "File: " . __FILE__ . "<br>Line: " . __LINE__); if ($res === false) throw new \TasksException('', \TasksException::TE_SQL_ERROR); $res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("TASKS_TASK")); } else { $res_cnt = $DB->Query("SELECT COUNT(DISTINCT T.ID) as C " . $strFromForCount); $res_cnt = $res_cnt->Fetch(); $totalTasksCount = (int) $res_cnt["C"]; // unknown by default // Sync counters in case of mistiming // CTaskCountersProcessorHomeostasis::onTaskGetList($arFilter, $totalTasksCount); $res = new \CDBResult(); $res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("TASKS_TASK")); $rc = $res->NavQuery($strSql, $totalTasksCount, $arParams["NAV_PARAMS"], $bIgnoreErrors); if ($bIgnoreErrors && ($rc === false)) throw new \TasksException('', \TasksException::TE_SQL_ERROR); } } else { $res = $DB->Query($strSql, $bIgnoreErrors, "File: " . __FILE__ . "<br>Line: " . __LINE__); if ($res === false) throw new \TasksException('', \TasksException::TE_SQL_ERROR); $res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("TASKS_TASK")); } return $res; } /** * Checks if we need to build access sql * * @param $runtimeOptions * @return bool */ private static function checkAccessSqlBuilding($runtimeOptions) { $fields = $runtimeOptions['FIELDS']; foreach (array_keys($fields) as $key) { if (preg_match('/^ROLE_/', $key)) { return false; } } return true; } /** * Returns related joins * * @param $select * @param $filter * @param $order * @param $params * @return array */ public static function getRelatedJoins($select, $filter, $order, $params) { $relatedJoins = []; $userId = ($params['USER_ID']?: User::getId()); $viewedBy = ($params['VIEWED_BY']?: $userId); $sortingGroupId = $params['SORTING_GROUP_ID']; $joinAlias = ($params['JOIN_ALIAS']?: ""); $sourceAlias = ($params['SOURCE_ALIAS']?: "T"); $filterKeys = static::GetFilteredKeys($filter); $possibleJoins = ['CREATOR', 'RESPONSIBLE', 'VIEWED', 'SORTING', 'FAVORITE', 'STAGES', 'FORUM', 'FULL_SEARCH', 'COMMENT_SEARCH']; foreach ($possibleJoins as $join) { switch ($join) { case 'CREATOR': if ( in_array('CREATED_BY', $select, true) || in_array('CREATED_BY_NAME', $select, true) || in_array('CREATED_BY_LAST_NAME', $select, true) || in_array('CREATED_BY_SECOND_NAME', $select, true) || in_array('CREATED_BY_LOGIN', $select, true) || in_array('CREATED_BY_WORK_POSITION', $select, true) || in_array('CREATED_BY_PHOTO', $select, true) || array_key_exists('ORIGINATOR_NAME', $order) || array_key_exists('CREATED_BY', $order) ) { $relatedJoins[$join] = "INNER JOIN " . UserTable::getTableName() . " {$joinAlias}CU ON {$joinAlias}CU.ID = {$sourceAlias}.CREATED_BY"; } break; case 'RESPONSIBLE': if ( in_array('RESPONSIBLE_ID', $select, true) || in_array('RESPONSIBLE_NAME', $select, true) || in_array('RESPONSIBLE_LAST_NAME', $select, true) || in_array('RESPONSIBLE_SECOND_NAME', $select, true) || in_array('RESPONSIBLE_LOGIN', $select, true) || in_array('RESPONSIBLE_WORK_POSITION', $select, true) || in_array('RESPONSIBLE_PHOTO', $select, true) || array_key_exists('RESPONSIBLE_NAME', $order) || array_key_exists('RESPONSIBLE_ID', $order) ) { $relatedJoins[$join] = "INNER JOIN " . UserTable::getTableName() . " {$joinAlias}RU ON {$joinAlias}RU.ID = {$sourceAlias}.RESPONSIBLE_ID"; } break; case 'VIEWED': if ( in_array('STATUS', $select, true) || in_array('NOT_VIEWED', $select, true) || in_array('VIEWED_DATE', $select, true) || in_array('STATUS', $filterKeys, true) || in_array('VIEWED_BY', $filterKeys, true) ) { $relatedJoins[$join] = "LEFT JOIN " . ViewedTable::getTableName() . " {$joinAlias}TV ON {$joinAlias}TV.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}TV.USER_ID = " . $viewedBy; } break; case 'SORTING': if ( in_array('SORTING', $select, true) || in_array('SORTING', $filterKeys, true) || array_key_exists('SORTING', $order) ) { $relatedJoins[$join] = "LEFT JOIN " . SortingTable::getTableName() . " {$joinAlias}SRT ON {$joinAlias}SRT.TASK_ID = {$sourceAlias}.ID AND " . ($sortingGroupId > 0? "{$joinAlias}SRT.GROUP_ID = " . $sortingGroupId : "{$joinAlias}SRT.USER_ID = " . $userId); } break; case 'FAVORITE': if ( in_array('FAVORITE', $select, true) || in_array('FAVORITE', $filterKeys, true) || array_key_exists('FAVORITE', $order) ) { $relatedJoins[$join] = "LEFT JOIN " . FavoriteTable::getTableName() . " {$joinAlias}FVT ON {$joinAlias}FVT.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}FVT.USER_ID = " . $userId; } break; case 'STAGES': if (in_array('STAGES_ID', $filterKeys, true)) { $relatedJoins[$join] = "INNER JOIN " . TaskStageTable::getTableName() . " {$joinAlias}STG ON STG.TASK_ID = {$sourceAlias}.ID"; } break; case 'FORUM': if ( in_array('COMMENTS_COUNT', $select, true) || in_array('FORUM_ID', $select, true) ) { $relatedJoins[$join] = "LEFT JOIN b_forum_topic {$joinAlias}FT ON {$joinAlias}FT.ID = {$sourceAlias}.FORUM_TOPIC_ID"; } break; case 'FULL_SEARCH': if ( in_array('MESSAGE_ID', $select, true) || in_array('FULL_SEARCH_INDEX', $filterKeys, true) ) { $relatedJoins[$join] = "INNER JOIN " . SearchIndexTable::getTableName() . " {$joinAlias}TSIF ON {$joinAlias}TSIF.TASK_ID = {$sourceAlias}.ID"; } break; case 'COMMENT_SEARCH': if (in_array('COMMENT_SEARCH_INDEX', $filterKeys, true)) { $relatedJoins[$join] = "INNER JOIN " . SearchIndexTable::getTableName() . " {$joinAlias}TSIC ON {$joinAlias}TSIC.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}TSIC.MESSAGE_ID <> 0"; } break; } } return $relatedJoins; } /** * Creates filter runtime options from given sub filter * * @param $filter * @param $parameters * @return array * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\ObjectException * @throws \Bitrix\Main\SystemException */ private static function makeAccessFilterRuntimeOptions($filter, $parameters) { $runtimeOptions = [ 'FIELDS' => [], 'FILTERS' => [] ]; $fields = [ // ROLES 'CREATED_BY' => true, 'RESPONSIBLE_ID' => true, 'ACCOMPLICE' => true, 'AUDITOR' => true, 'ROLEID' => true, // TASK FIELDS 'ID' => true, 'TITLE' => true, 'PRIORITY' => true, 'STATUS' => true, 'GROUP_ID' => true, 'TAG' => true, 'MARK' => true, 'ALLOW_TIME_TRACKING' => true, // RELATED FIELDS 'FULL_SEARCH_INDEX' => true, 'COMMENT_SEARCH_INDEX' => true, // DATES 'DEADLINE' => true, 'CREATED_DATE' => true, 'CLOSED_DATE' => true, 'DATE_START' => true, 'START_DATE_PLAN' => true, 'END_DATE_PLAN' => true, // DIFFICULT PARAMS 'ACTIVE' => true, 'PARAMS' => true, 'PROBLEM' => true, ]; if (is_array($filter) && !empty($filter)) { foreach ($filter as $key => $value) { $newKey = substr((string)$key, 12); if ($newKey && $fields[$newKey]) { $fieldRuntimeOptions = static::getFieldRuntimeOptions($newKey, $value, $parameters); $runtimeOptions['FIELDS'] = array_merge($runtimeOptions['FIELDS'], $fieldRuntimeOptions['FIELDS']); $runtimeOptions['FILTERS'] = array_merge($runtimeOptions['FILTERS'], $fieldRuntimeOptions['FILTERS']); } } } return $runtimeOptions; } /** * Returns field's runtime options * * @param $key * @param $value * @param $parameters * @return array * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\ObjectException * @throws \Bitrix\Main\SystemException */ private static function getFieldRuntimeOptions($key, $value, $parameters) { $runtimeOptions = [ 'FIELDS' => [], 'FILTERS' => [] ]; $dates = ['DEADLINE', 'CREATED_DATE', 'CLOSED_DATE', 'DATE_START', 'START_DATE_PLAN', 'END_DATE_PLAN']; switch ($key) { case 'ID': case 'PRIORITY': case 'MARK': case 'ALLOW_TIME_TRACKING': case 'DEADLINE': case 'CREATED_DATE': case 'CLOSED_DATE': case 'DATE_START': case 'START_DATE_PLAN': case 'END_DATE_PLAN': foreach ($value as $name => $val) { if (in_array($key, $dates)) { $val = new Type\DateTime($val); } $fieldKeyData = static::parseFieldKey($name, $key); $runtimeOptions['FILTERS'][$key] = Query::filter()->where($key, $fieldKeyData['OPERATOR'], $val); } break; case 'TITLE': foreach ($value as $name => $val) { $fieldKeyData = static::parseFieldKey($name, $key); $field = Query::expr()->upper($key); $val = '%' . ToUpper($val) . '%'; $runtimeOptions['FILTERS'][$key] = Query::filter()->where($field, $fieldKeyData['OPERATOR'], $val); } break; case 'FULL_SEARCH_INDEX': case 'COMMENT_SEARCH_INDEX': $map = [ 'FULL_SEARCH_INDEX' => [], 'COMMENT_SEARCH_INDEX' => Query::filter()->where('ref.MESSAGE_ID', '!=', 0) ]; $runtimeOptions['FIELDS'][$key] = new Entity\ReferenceField( 'TSI' . $key[0], SearchIndexTable::class, Join::on('ref.TASK_ID', 'this.ID') ->where($map[$key]), ['join_type' => 'inner'] ); $column = "TSI{$key[0]}.SEARCH_INDEX"; $value = current($value); if (SearchIndexTable::isFullTextIndexEnabled()) { $filter = Query::filter()->whereMatch($column, $value); } else { $filter = Query::filter()->whereLike(Query::expr()->upper($column), '%' . ToUpper($value) . '%'); } $runtimeOptions['FILTERS'][$key] = $filter; break; case 'CREATED_BY': case 'RESPONSIBLE_ID': case 'GROUP_ID': $fieldKeyData = static::parseFieldKey(key($value), $key, 'in'); $runtimeOptions['FILTERS'][$key] = Query::filter()->where($key, $fieldKeyData['OPERATOR'], current($value)); break; case 'STATUS': if (!empty($value['REAL_STATUS'])) { $runtimeOptions['FILTERS'][$key] = Query::filter()->where($key, 'in', $value['REAL_STATUS']); } break; case 'ACCOMPLICE': case 'AUDITOR': case 'TAG': $parameters['USER_ID'] = $parameters['NAME'] = current($value); $parameters['TYPE_CONDITION'] = true; $runtimeOptions['FILTERS'][$key] = Query::filter()->whereExists(static::getSelectionExpressionByType($key, $parameters)); break; case 'ROLEID': case 'PROBLEM': if ($key == 'ROLEID') { $filterOptions = static::getFilterOptionsFromRoleField($value); } else { $filterOptions = static::getFilterOptionsFromProblemField($value, $parameters); } $runtimeOptions['FIELDS'] = $filterOptions['FIELDS']; $runtimeOptions['FILTERS'] = $filterOptions['FILTERS']; break; case 'ACTIVE': $date = $value[$key]; $dateStart = $dateEnd = false; if (MakeTimeStamp($date['START']) > 0) { $dateStart = new Type\DateTime($date['START']); } if (MakeTimeStamp($date['END'])) { $dateEnd = new Type\DateTime($date['END']); } if ($dateStart !== false && $dateEnd !== false) { $runtimeOptions['FILTERS'][$key] = Query::filter()->where( Query::filter() ->logic('or') ->where( Query::filter() ->where('CREATED_DATE', '>=', $dateStart) ->where('CLOSED_DATE', '<=', $dateEnd) ) ->where( Query::filter() ->where('CHANGED_DATE', '>=', $dateStart) ->where('CHANGED_DATE', '<=', $dateEnd) ) ->where( Query::filter() ->where('CREATED_DATE', '<=', $dateStart) ->where('CLOSED_DATE', '=', null) ) ); } break; case 'PARAMS': foreach ($value as $name => $val) { $fieldKeyData = static::parseFieldKey($name); $fieldName = $fieldKeyData['FIELD_NAME']; if ($fieldName == 'MARK' || $fieldName == 'ADD_IN_REPORT') { $operator = $fieldKeyData['OPERATOR']; $runtimeOptions['FILTERS'][$fieldName] = Query::filter()->where($fieldName, $operator, $val); } else if ($fieldName == 'FAVORITE') { $runtimeOptions['FIELDS'][$fieldName] = new Entity\ReferenceField( 'FVT', FavoriteTable::class, Join::on('ref.TASK_ID', 'this.ID') ->where('ref.USER_ID', $parameters['USER_ID']) ); $runtimeOptions['FILTERS'][$fieldName] = Query::filter()->where('FVT.TASK_ID', '!=', null); } else if ($fieldName == 'OVERDUED') { $runtimeOptions['FILTERS'][$fieldName] = Query::filter() ->where('DEADLINE', '!=', null) ->where('CLOSED_DATE', '!=', null) ->whereColumn('DEADLINE', '<', 'CLOSED_DATE'); } } break; } return $runtimeOptions; } /** * Tries to parse string like '>=DEADLINE' to separate operator '>=' suitable for orm and pure name 'DEADLINE' * * @param $key * @param string $fieldName * @param string $defaultOperator * @return array */ private static function parseFieldKey($key, $fieldName = '', $defaultOperator = '=') { $operators = [ '>=' => '>=', '<=' => '<=', '!=' => '!=', '%' => 'like', '=%' => 'like', '%=' => 'like', '=' => '=', '>' => '>', '<' => '<', '!' => '!=' ]; if ($fieldName) { $operator = str_replace($fieldName, '', $key); $operator = ($operator && isset($operators[$operator])? $operators[$operator] : $defaultOperator); } else { $pattern = '/^(' . implode('|', array_keys($operators)) . ')/'; $matches = []; preg_match($pattern, $key, $matches); if (!empty($matches)) { $operator = $operators[$matches[0]]; $fieldName = str_replace($matches[0], '', $key); } else { $operator = $defaultOperator; $fieldName = $key; } } return [ 'OPERATOR' => $operator, 'FIELD_NAME' => $fieldName ]; } /** * Returns role field type based on its conditions * * @param $role * @return string */ private static function getRoleFieldType($role) { if (array_key_exists('MEMBER', $role)) { return 'MEMBER'; } if (array_key_exists('=CREATED_BY', $role)) { return 'CREATED_BY'; } if (array_key_exists('=RESPONSIBLE_ID', $role)) { return 'RESPONSIBLE_ID'; } if (array_key_exists('=ACCOMPLICE', $role)) { return 'ACCOMPLICE'; } if (array_key_exists('=AUDITOR', $role)) { return 'AUDITOR'; } return ''; } /** * Returns filter options of role filter field * * @param $role * @return array * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\SystemException */ private static function getFilterOptionsFromRoleField($role) { $fields = []; $filters = []; $key = 'ROLE_'; $roleType = static::getRoleFieldType($role); $userId = $role[($roleType == 'MEMBER'? '' : '=') . $roleType]; $referenceFilter = Query::filter() ->whereColumn('ref.TASK_ID', 'this.ID') ->where('ref.USER_ID', $userId); switch ($roleType) { case 'MEMBER': $fields[$key . $roleType] = static::getMemberTableReferenceField($referenceFilter); break; case 'CREATED_BY': case 'RESPONSIBLE_ID': case 'ACCOMPLICE': case 'AUDITOR': $map = [ 'CREATED_BY' => 'O', 'RESPONSIBLE_ID' => 'R', 'ACCOMPLICE' => 'A', 'AUDITOR' => 'U' ]; $referenceFilter->where('ref.TYPE', $map[$roleType]); $fields[$key . $roleType] = static::getMemberTableReferenceField($referenceFilter); if ($roleType == 'CREATED_BY') { $filters[$key . $roleType] = Query::filter()->whereColumn('CREATED_BY', '!=', 'RESPONSIBLE_ID'); } break; } return [ 'FIELDS' => $fields, 'FILTERS' => $filters ]; } /** * Returns reference field for joining member table * * @param $referenceFilter * @return Entity\ReferenceField * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\SystemException */ private static function getMemberTableReferenceField($referenceFilter) { $joinOn = $referenceFilter; $joinType = ['join_type' => 'inner']; return new Entity\ReferenceField('TM', MemberTable::class, $joinOn, $joinType); } /** * Returns filter options of problem filter field * * @param $problem * @param $parameters * @return array * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\SystemException */ private static function getFilterOptionsFromProblemField($problem, $parameters) { $fields = []; $filters = []; if (array_key_exists('VIEWED', $problem)) { $userId = ($problem['VIEWED_BY']?: $parameters['USER_ID']); $filterKey = 'PROBLEM_NOT_VIEWED'; $fields[$filterKey] = new Entity\ReferenceField( 'TV', ViewedTable::class, Join::on('ref.TASK_ID', 'this.ID') ->where('ref.USER_ID', $userId) ); $filters[$filterKey] = Query::filter() ->where('TV.USER_ID', null) ->where('STATUS', 'in', [1, 2]); } else { $filters['PROBLEM'] = static::parseLogicProblemFilter($problem); } return [ 'FIELDS' => $fields, 'FILTERS' => $filters ]; } /** * Parse logic filter * * @param $problem * @return \Bitrix\Main\ORM\Query\Filter\ConditionTree * @throws \Bitrix\Main\ArgumentException */ private static function parseLogicProblemFilter($problem) { $filter = Query::filter(); foreach ($problem as $key => $condition) { if (static::isSubFilterKey($key)) { $filter->where(static::parseLogicProblemFilter($condition)); continue; } if ($key == '::LOGIC') { $filter->logic($condition); continue; } $fieldKeyData = static::parseFieldKey($key); $fieldKeyName = ($fieldKeyData['FIELD_NAME'] == 'REAL_STATUS'? 'STATUS' : $fieldKeyData['FIELD_NAME']); $fieldKeyOperator = $fieldKeyData['OPERATOR']; if (substr($fieldKeyName, 0, 10) == 'REFERENCE:') { $filter->whereColumn(substr($fieldKeyName, 10), $fieldKeyOperator, $condition); } else if ($condition == null && $fieldKeyOperator == '=') { $filter->whereNull($fieldKeyName); } else { $filter->where($fieldKeyName, $fieldKeyOperator, $condition); } } return $filter; } /** * Gets selection sql expression by expression type * * @param $type * @param $parameters * @return SqlExpression|string */ private static function getSelectionExpressionByType($type, $parameters) { try { switch ($type) { case 'MEMBER': case 'ACCOMPLICE': case 'AUDITOR': $userIdsConditions = []; foreach ($parameters['USER_ID'] as $userId) { $userIdsConditions[] = "(TM.USER_ID = '" . intval($userId) . "')"; } $typeCondition = ($parameters['TYPE_CONDITION'] ? ' AND TM.TYPE = ?s' : ''); $sql = new \SqlExpression( 'SELECT TM.?# FROM ?# TM WHERE TM.?# = ?#.ID AND (' . implode(" OR ", $userIdsConditions) . ')' . $typeCondition, 'TASK_ID', 'b_tasks_member', 'TASK_ID', 'tasks_internals_task', ($type == 'ACCOMPLICE' ? 'A' : 'U') ); break; case 'TAG': $sql = new \SqlExpression( 'SELECT TT.?# FROM ?# TT WHERE TT.?# = ?#.ID AND TT.NAME = ?s', 'TASK_ID', 'b_tasks_tag', 'TASK_ID', 'tasks_internals_task', $parameters['NAME'] ); break; default: $sql = ''; break; } } catch (\Exception $ex) { $sql = ''; } return $sql; } /** * @param $filter * @return array */ private static function makePossibleForwardedFilter($filter) { $result = array(); $allowedFields = array( 'ID' => true, // number_wo_nulls 'TITLE' => true, // string 'STATUS_CHANGED_BY' => true, // number 'SITE_ID' => true, // string_equal 'PRIORITY' => true, // number_wo_nulls 'STAGE_ID' => true, // number_wo_nulls 'RESPONSIBLE_ID' => true, // number_wo_nulls 'TIME_ESTIMATE' => true, // number_wo_nulls 'CREATED_BY' => true, // number_wo_nulls 'GUID' => true, // string 'XML_ID' => true, // string_equal 'MARK' => true, // string_equal 'ALLOW_CHANGE_DEADLINE' => true, // string_equal 'ALLOW_TIME_TRACKING' => true, // string_equal 'ADD_IN_REPORT' => true, // string_equal 'GROUP_ID' => true, // number 'PARENT_ID' => true, // number 'FORUM_TOPIC_ID' => true, // number 'ZOMBIE' => true, // string_equal 'MATCH_WORK_TIME' => true, // string_equal //dates /* 'DATE_START' => true, 'DEADLINE' => true, 'START_DATE_PLAN' => true, 'END_DATE_PLAN' => true, 'CREATED_DATE' => true, 'STATUS_CHANGED_DATE' => true, */ ); $stringEqual = array( 'SITE_ID' => true, // string_equal 'XML_ID' => true, // string_equal 'MARK' => true, // string_equal 'ALLOW_CHANGE_DEADLINE' => true, // string_equal 'ALLOW_TIME_TRACKING' => true, // string_equal 'ADD_IN_REPORT' => true, // string_equal 'ZOMBIE' => true, // string_equal 'MATCH_WORK_TIME' => true, // string_equal ); if(is_array($filter) && !empty($filter)) { // cannot forward filer with LOGIC OR or LOGIC NOT if(array_key_exists('LOGIC', $filter) && $filter['LOGIC'] != 'AND') { return $result; } if(array_key_exists('::LOGIC', $filter) && $filter['::LOGIC'] != 'AND') { return $result; } $filter = \Bitrix\Tasks\Internals\DataBase\Helper\Common::parseFilter($filter); foreach($filter as $k => $condition) { $field = $condition['FIELD']; if(!array_key_exists($field, $allowedFields)) { continue; } // convert like into strict check if(array_key_exists($field, $stringEqual)) { // '' => '=' if($condition['OPERATION'] == 'E') { $condition['OPERATION'] = 'I'; unset($condition['ORIG_KEY']); } // '!' => '!=' if($condition['OPERATION'] == 'N') { $condition['OPERATION'] = 'NI'; unset($condition['ORIG_KEY']); } } // actually, allow only "equal" and "not equal" $op = $condition['OPERATION']; if($op != 'E' && $op != 'I' && $op != 'N' && $op != 'NI') { continue; } $result[] = $condition; } $result = \Bitrix\Tasks\Internals\DataBase\Helper\Common::makeFilter($result); } return $result; } private static function needAccessRestriction(array $arFilter, $arParams) { if (is_array($arParams) && array_key_exists('USER_ID', $arParams) && ($arParams['USER_ID'] > 0)) $userID = (int) $arParams['USER_ID']; else $userID = User::getId(); return !User::isSuper($userID) && $arFilter["CHECK_PERMISSIONS"] != "N" // and not setted flag "skip permissions check" && $arFilter["SUBORDINATE_TASKS"] != "Y"; // and not rights via subordination } /** * @param array $filter * @param bool $getZombie * @param string $aliasPrefix * @param array $params * @return string */ private static function GetRootSubQuery($filter = [], $getZombie = false, $aliasPrefix = '', $params = []) { $filter = (isset($params['SOURCE_FILTER'])? $params['SOURCE_FILTER'] : $filter); $userId = (isset($params['USER_ID'])? $params['USER_ID'] : User::getId()); $sqlSearch = ["(PT.ID = " . $aliasPrefix . "T.PARENT_ID)"]; if (!$getZombie) { $sqlSearch[] = " (PT.ZOMBIE = 'N') "; } if ($filter["SAME_GROUP_PARENT"] == "Y") { $sqlSearch[] = "(PT.GROUP_ID = " . $aliasPrefix . "T.GROUP_ID OR (PT.GROUP_ID IS NULL AND " . $aliasPrefix . "T.GROUP_ID IS NULL) OR (PT.GROUP_ID IS NULL AND " . $aliasPrefix . "T.GROUP_ID = 0) OR (PT.GROUP_ID = 0 AND " . $aliasPrefix . "T.GROUP_ID IS NULL) )"; } unset($filter["ONLY_ROOT_TASKS"], $filter["SAME_GROUP_PARENT"]); $params = [ 'USER_ID' => $userId, 'JOIN_ALIAS' => 'P', 'SOURCE_ALIAS' => 'PT' ]; $optimized = static::tryOptimizeFilter($filter, 'PT', 'PTM_SPEC'); $sqlSearch = array_merge($sqlSearch, self::GetFilter($optimized['FILTER'], "P")); $relatedJoins = static::getRelatedJoins([], $filter, [], $params); if (isset($relatedJoins['FULL_SEARCH'])) { $relatedJoins['FULL_SEARCH'] = str_replace('INNER', 'LEFT', $relatedJoins['FULL_SEARCH']); } if (isset($relatedJoins['COMMENT_SEARCH'])) { $relatedJoins['COMMENT_SEARCH'] = str_replace('INNER', 'LEFT', $relatedJoins['COMMENT_SEARCH']); } $relatedJoins = array_merge($relatedJoins, $optimized['JOINS']); $strSql = " SELECT 'x' FROM b_tasks PT " . implode("\n", $relatedJoins) . " WHERE " . implode(" AND ", $sqlSearch) . " "; return $strSql; } /** * @param array $arFilter * @param array $arParams * @param array $arGroupBy * @return bool|\CDBResult */ public static function GetCount($arFilter=array(), $arParams = array(), $arGroupBy = array()) { /** * @global CDatabase $DB */ global $DB; $bIgnoreDbErrors = false; $bSkipUserFields = false; $bSkipExtraTables = false; $bSkipJoinTblViewed = false; $bNeedJoinMembersTable = false; if (isset($arParams['bIgnoreDbErrors'])) $bIgnoreDbErrors = (bool) $arParams['bIgnoreDbErrors']; if (isset($arParams['bSkipUserFields'])) $bSkipUserFields = (bool) $arParams['bSkipUserFields']; if (isset($arParams['bSkipExtraTables'])) $bSkipExtraTables = (bool) $arParams['bSkipExtraTables']; if (isset($arParams['bSkipJoinTblViewed'])) $bSkipJoinTblViewed = (bool) $arParams['bSkipJoinTblViewed']; if (isset($arParams['bNeedJoinMembersTable'])) $bNeedJoinMembersTable = (bool) $arParams['bNeedJoinMembersTable']; $disableOptimization = (is_array($arParams) && $arParams['DISABLE_OPTIMIZATION'] === true); $disableAccessOptimization = (is_array($arParams) && $arParams['DISABLE_ACCESS_OPTIMIZATION'] === true); // in some cases, we can replace filter conditions $canUseOptimization = !$disableOptimization && !$bNeedJoinMembersTable; if ( ! $bSkipUserFields ) { $obUserFieldsSql = new \CUserTypeSQL; $obUserFieldsSql->SetEntity("TASKS_TASK", "T.ID"); $obUserFieldsSql->SetFilter($arFilter); } if (!is_array($arFilter)) { \CTaskAssert::logError('[0x053f6639] expected array in $arFilter'); $arFilter = array(); } if (isset($arParams['USER_ID'])) $userID = (int) $arParams['USER_ID']; else $userID = User::getId(); static $arFields = array( 'GROUP_ID' => 'T.GROUP_ID', 'CREATED_BY' => 'T.CREATED_BY', 'RESPONSIBLE_ID' => 'T.RESPONSIBLE_ID', 'ACCOMPLICE' => 'TM.USER_ID', 'AUDITOR' => 'TM.USER_ID' ); $strGroupBy = ' '; $strSelect = ' '; // ignore unknown fields $arGroupBy = array_intersect($arGroupBy, array_keys($arFields)); if (is_array($arGroupBy) && ! empty($arGroupBy)) { $arGroupByFields = array(); foreach ($arGroupBy as $fieldName) { $strSelect = ', ' . $arFields[$fieldName] . ' AS ' . $fieldName; if (($fieldName === 'ACCOMPLICE') || ($fieldName === 'AUDITOR')) $bNeedJoinMembersTable = true; $arGroupByFields[] = $arFields[$fieldName]; } $strGroupBy = ' GROUP BY ' . implode(', ', $arGroupByFields); } $sourceFilter = $arFilter; // try to make some recyclebiny optimizations // (later you can remove the following block without getting any logic broken) $additionalJoins = ''; if($canUseOptimization) { $optimized = static::tryOptimizeFilter($arFilter); $arFilter = $optimized['FILTER']; $additionalJoins = implode("\n\n", $optimized['JOINS']); } if (isset($arParams['bUseRightsCheck'])) { $arFilter['CHECK_PERMISSIONS'] = ((bool) $arParams['bUseRightsCheck']) ? 'Y' : 'N'; } $fParams = array( 'bMembersTableJoined' => $bNeedJoinMembersTable, 'USER_ID' => $userID, ); $fParams['ENABLE_LEGACY_ACCESS'] = $disableAccessOptimization; // manual legacy access switch $arSqlSearch = self::GetFilter( $arFilter, '', // $sAliasPrefix $fParams ); $arSqlSearch[] = " T.ZOMBIE = 'N' "; $ufJoin = ' '; if ( ! $bSkipUserFields ) { $r = $obUserFieldsSql->GetFilter(); if ($r <> '') { $arSqlSearch[] = "(".$r.")"; } $ufJoin .= $obUserFieldsSql->GetJoin("T.ID"); } $strSql = " SELECT COUNT(".($canUseOptimization ? 'distinct ' : '')."T.ID) AS CNT " . $strSelect . " FROM "; if ($bNeedJoinMembersTable) $strSql .= "b_tasks_member TM \n INNER JOIN b_tasks T ON T.ID = TM.TASK_ID "; else $strSql .= "b_tasks T "; if ( ! $bSkipExtraTables ) { $strSql .= " INNER JOIN b_user CU ON CU.ID = T.CREATED_BY INNER JOIN b_user RU ON RU.ID = T.RESPONSIBLE_ID "; } if (!$bSkipJoinTblViewed) { $viewedBy = static::getViewedBy($arFilter, $userID); $strSql .= "\n LEFT JOIN b_tasks_viewed TV ON TV.TASK_ID = T.ID AND TV.USER_ID = " . $viewedBy; } $useAccessAsJoin = !$disableAccessOptimization; // put access check into the join if($useAccessAsJoin && static::needAccessRestriction($arFilter, $fParams)) { $fParams['APPLY_MEMBER_FILTER'] = static::makePossibleForwardedMemberFilter($sourceFilter); $fParams['APPLY_FILTER'] = static::makePossibleForwardedFilter($sourceFilter); $strSql = static::appendJoinRights($strSql, $fParams); } $strSql .= " " . $additionalJoins . " " . $ufJoin . " " . "WHERE " . implode(" AND ", $arSqlSearch) . " " . $strGroupBy; $res = $DB->Query($strSql, $bIgnoreDbErrors, "File: ".__FILE__."<br>Line: ".__LINE__); return $res; } private static function makePossibleForwardedMemberFilter($filter) { $result = array(); if (is_array($filter) && !empty($filter)) { // cannot forward filer with LOGIC OR or LOGIC NOT if (array_key_exists('LOGIC', $filter) && $filter['LOGIC'] != 'AND') { return $result; } if (array_key_exists('::LOGIC', $filter) && $filter['::LOGIC'] != 'AND') { return $result; } /** @see \self::GetSqlByFilter() */ if (array_key_exists('AUDITOR', $filter)) // we have equality to AUDITOR, not negation { $result[] = [ '=TYPE' => 'U', '=USER_ID' => $filter['AUDITOR'], ]; } else if (array_key_exists('ACCOMPLICE', $filter)) // we have equality to ACCOMPLICE, not negation { $result[] = [ '=TYPE' => 'A', '=USER_ID' => $filter['ACCOMPLICE'], ]; } } return $result; } public static function appendJoinRights($sql, $arParams) { $arParams['THIS_TABLE_ALIAS'] = 'T'; $access = \Bitrix\Tasks\Internals\RunTime\Task::getAccessCheckSql($arParams); $accessSql = $access['sql']; if ($accessSql != '') { if (isset($arParams['PUT_SELECT_INTO_WHERE']) && $arParams['PUT_SELECT_INTO_WHERE']) { $sql .= "T.ID IN ($accessSql)"; } else { $sql .= "\n\n/*access BEGIN*/\n\n inner join ($accessSql) TASKS_ACCESS on T.ID = TASKS_ACCESS.TASK_ID\n\n/*access END*/\n\n"; } } return $sql; } /** * Optimizes filter * * @param array $filter * @param $sourceTableAlias * @param $joinTableAlias * @return array */ private static function tryOptimizeFilter(array $filter, $sourceTableAlias = 'T', $joinTableAlias = 'TM') { $additionalJoins = []; $roleKey = '::SUBFILTER-ROLEID'; $joinAlias = $joinTableAlias; $sourceAlias = $sourceTableAlias; // get rid of ::SUBFILTER-ROOT if can if (array_key_exists('::SUBFILTER-ROOT', $filter) && count($filter) == 1) { if ($filter['::LOGIC'] != 'OR') { // we have only one element in the root, and logic is not "OR". then we could remove subfilter-root $filter = $filter['::SUBFILTER-ROOT']; } } // we can optimize only if there is no "or-logic" if ($filter['::LOGIC'] != 'OR' && $filter['LOGIC'] != 'OR') { // MEMBER if (array_key_exists('MEMBER', $filter) || isset($filter[$roleKey]) && array_key_exists('MEMBER', $filter[$roleKey])) { if (array_key_exists('MEMBER', $filter)) { $member = intval($filter['MEMBER']); unset($filter['MEMBER']); } else { $member = intval($filter[$roleKey]['MEMBER']); unset($filter[$roleKey]); } $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$member}"; } // DOER else if (array_key_exists('DOER', $filter)) { $doer = intval($filter['DOER']); unset($filter['DOER']); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$doer} AND {$joinAlias}.TYPE in ('R', 'A')"; } // RESPONSIBLE else if (isset($filter[$roleKey]) && array_key_exists('=RESPONSIBLE_ID', $filter[$roleKey])) { $responsible = $filter[$roleKey]['=RESPONSIBLE_ID']; unset($filter[$roleKey]); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$responsible} AND {$joinAlias}.TYPE = 'R'"; } // CREATOR else if (isset($filter[$roleKey]) && array_key_exists('=CREATED_BY', $filter[$roleKey])) { $creator = $filter[$roleKey]['=CREATED_BY']; unset($filter[$roleKey]['=CREATED_BY']); if (!empty($filter[$roleKey])) { $filter += $filter[$roleKey]; } unset($filter[$roleKey]); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$creator} AND {$joinAlias}.TYPE = 'O'"; } // ACCOMPLICE else if (array_key_exists('ACCOMPLICE', $filter) || isset($filter[$roleKey]) && array_key_exists('=ACCOMPLICE', $filter[$roleKey])) { if (array_key_exists('ACCOMPLICE', $filter)) { if (!is_array($filter['ACCOMPLICE'])) // we have single value, not array which will cause "in ()" instead of = { $accomplice = intval($filter['ACCOMPLICE']); unset($filter['ACCOMPLICE']); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$accomplice} AND {$joinAlias}.TYPE = 'A'"; } } else { if (!is_array($filter[$roleKey]['=ACCOMPLICE'])) { $accomplice = intval($filter[$roleKey]['=ACCOMPLICE']); unset($filter[$roleKey]); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$accomplice} AND {$joinAlias}.TYPE = 'A'"; } } } // AUDITOR else if (array_key_exists('AUDITOR', $filter) || isset($filter[$roleKey]) && array_key_exists('=AUDITOR', $filter[$roleKey])) { if (array_key_exists('AUDITOR', $filter)) { if (!is_array($filter['AUDITOR'])) // we have single value, not array which will cause "in ()" instead of = { $auditor = intval($filter['AUDITOR']); unset($filter['AUDITOR']); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$auditor} AND {$joinAlias}.TYPE = 'U'"; } } else { if (!is_array($filter[$roleKey]['=AUDITOR'])) { $auditor = intval($filter[$roleKey]['=AUDITOR']); unset($filter[$roleKey]); $additionalJoins[] = "INNER JOIN b_tasks_member {$joinAlias} ON {$joinAlias}.TASK_ID = {$sourceAlias}.ID AND {$joinAlias}.USER_ID = {$auditor} AND {$joinAlias}.TYPE = 'U'"; } } } } return [ 'FILTER' => $filter, 'JOINS' => $additionalJoins, ]; } /** * Gets user's id task list we are looking at * * @param $filter * @param $currentUserId * @return mixed */ private static function getViewedUserId($filter, $currentUserId) { $viewedBy = static::getViewedBy($filter, $currentUserId); if ($viewedBy !== $currentUserId) { $viewedUserId = $viewedBy; } else { if (array_key_exists('::SUBFILTER-ROLEID', $filter) && !empty($filter['::SUBFILTER-ROLEID'])) { $viewedUserId = current($filter['::SUBFILTER-ROLEID']); } else { $viewedUserId = $currentUserId; } } return $viewedUserId; } /** * Get user id b_tasks_viewed table joined on by filter or default value if filter haven't VIEWED_BY option * * @param $filter * @param $defaultValue * @return int */ private static function getViewedBy($filter, $defaultValue) { $viewedBy = $defaultValue; if ( array_key_exists('::SUBFILTER-PROBLEM', $filter) && array_key_exists('VIEWED_BY', $filter['::SUBFILTER-PROBLEM']) && intval($filter['::SUBFILTER-PROBLEM']['VIEWED_BY']) ) { $viewedBy = intval($filter['::SUBFILTER-PROBLEM']['VIEWED_BY']); } else if (array_key_exists('VIEWED_BY', $filter) && intval($filter['VIEWED_BY'])) { $viewedBy = intval($filter['VIEWED_BY']); } return $viewedBy; } public static function getUsersViewedTask($taskId) { global $DB; $taskId = (int) $taskId; $res = $DB->query( "SELECT USER_ID FROM b_tasks_viewed WHERE TASK_ID = " . $taskId, true // ignore DB errors ); if ($res === false) throw new \TasksException ('', \TasksException::TE_SQL_ERROR); $arUsers = array(); while ($ar = $res->fetch()) $arUsers[] = (int) $ar['USER_ID']; return ($arUsers); } public static function GetCountInt($arFilter=array(), $arParams = array()) { $count = 0; $rsCount = self::GetCount($arFilter, $arParams); if ($arCount = $rsCount->Fetch()) { $count = intval($arCount["CNT"]); } return $count; } public static function GetChildrenCount($filter, $parentIds) { if (!$parentIds) { return false; } global $DB; $obUserFieldsSql = new \CUserTypeSQL; $obUserFieldsSql->SetEntity("TASKS_TASK", "T.ID"); $obUserFieldsSql->SetFilter($filter); if (!is_array($filter)) { $filter = []; } $userId = User::getId(); $filter["PARENT_ID"] = $parentIds; unset($filter["ONLY_ROOT_TASKS"]); $sqlSearch = self::GetFilter($filter); $sqlSearch[] = " T.ZOMBIE = 'N' "; $r = $obUserFieldsSql->GetFilter(); if ($r <> '') { $sqlSearch[] = "(".$r.")"; } $relatedJoins = static::getRelatedJoins([], $filter, [], ['USER_ID' => $userId]); $relatedJoins = implode("\n", $relatedJoins); $strSql = " SELECT T.PARENT_ID, COUNT(T.ID) AS CNT FROM ("; $strSql .= " SELECT T.PARENT_ID AS PARENT_ID, T.ID FROM b_tasks T INNER JOIN b_user CU ON CU.ID = T.CREATED_BY INNER JOIN b_user RU ON RU.ID = T.RESPONSIBLE_ID " . $relatedJoins . " " . $obUserFieldsSql->GetJoin("T.ID") . " " . (sizeof($sqlSearch)? "WHERE " . implode(" AND ", $sqlSearch) : "") . " GROUP BY T.ID "; $strSql .= ") T GROUP BY T.PARENT_ID "; $res = $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); return $res; } /** * * @access private */ public static function GetOriginatorsByFilter($arFilter, $loggedInUserId) { return static::GetFieldGrouppedByFilter('CREATED_BY', $arFilter, $loggedInUserId); } /** * * @access private */ public static function GetResponsiblesByFilter($arFilter, $loggedInUserId) { return static::GetFieldGrouppedByFilter('RESPONSIBLE_ID', $arFilter, $loggedInUserId); } private static function GetFieldGrouppedByFilter($column, $arFilter, $loggedInUserId) { \CTaskAssert::assert($loggedInUserId && is_array($arFilter)); $arSqlSearch = self::GetFilter($arFilter, '', array('USER_ID' => $loggedInUserId)); $arSqlSearch[] = " T.ZOMBIE = 'N' "; $keysFiltered = self::GetFilteredKeys($arFilter); $bNeedJoinFavoritesTable = in_array('FAVORITE', $keysFiltered, true); $sql = "SELECT T.".$column." AS USER_ID, COUNT(T.ID) AS TASKS_CNT FROM b_tasks T LEFT JOIN b_tasks_viewed TV ON TV.TASK_ID = T.ID AND TV.USER_ID = " . $loggedInUserId . " ". ($bNeedJoinFavoritesTable ? " LEFT JOIN ".FavoriteTable::getTableName()." FVT ON FVT.TASK_ID = T.ID and FVT.USER_ID = '".$loggedInUserId/*always int, no sqli*/."' " : "")." WHERE " . implode('AND', $arSqlSearch) . " GROUP BY T.".$column; return $GLOBALS['DB']->query($sql); } public static function GetSubordinateSql($sAliasPrefix="", $arParams = array(), $behaviour = array()) { $arDepsIDs = Integration\Intranet\Department::getSubordinateIds($arParams['USER_ID'], true); if (sizeof($arDepsIDs)) { $rsDepartmentField = \CUserTypeEntity::GetList(array(), array("ENTITY_ID" => "USER", "FIELD_NAME" => "UF_DEPARTMENT")); if ($arDepartmentField = $rsDepartmentField->Fetch()) { return self::GetDeparmentSql($arDepsIDs, $sAliasPrefix, $arParams, $behaviour); } } return false; } function GetDeparmentSql($arDepsIDs, $sAliasPrefix="", $arParams = array(), $behaviour = array()) { global $DBType; if (!is_array($arDepsIDs)) { $arDepsIDs = array(intval($arDepsIDs)); } else { $arDepsIDs = array_map('intval', $arDepsIDs); } if(!is_array($behaviour)) { $behaviour = array(); } if(!isset($behaviour['ALIAS'])) { $behaviour['ALIAS'] = $sAliasPrefix; } if(!isset($arParams['FIELDS'])) { $arParams['FIELDS'] = array(); } $a = $sAliasPrefix; $b = $behaviour; $f =& $arParams['FIELDS']; //static::placeFieldSql('CREATED_BY', $b, $f) $rsDepartmentField = \CUserTypeEntity::GetList(array(), array("ENTITY_ID" => "USER", "FIELD_NAME" => "UF_DEPARTMENT")); $cntOfDepartments = count($arDepsIDs); if ($cntOfDepartments && $arDepartmentField = $rsDepartmentField->Fetch()) { if ( ($DBType === 'oracle') && ($valuesLimit = 1000) && ($cntOfDepartments > $valuesLimit) ) { $arConstraints = array(); $sliceIndex = 0; while ($sliceIndex < $cntOfDepartments) { $arConstraints[] = $sAliasPrefix . 'BUF1.VALUE_INT IN (' . implode(',', array_slice($arDepsIDs, $sliceIndex, $valuesLimit)) . ')'; $sliceIndex += $valuesLimit; } $strConstraint = '(' . implode(' OR ', $arConstraints) . ')'; } else $strConstraint = $sAliasPrefix . "BUF1.VALUE_INT IN (" . implode(",", $arDepsIDs) . ")"; // EXISTS! $strSql = " SELECT 'x' FROM b_utm_user ".$sAliasPrefix."BUF1 WHERE ".$sAliasPrefix."BUF1.FIELD_ID = ".$arDepartmentField["ID"]." AND (" . $sAliasPrefix . "BUF1.VALUE_ID = " . static::placeFieldSql('RESPONSIBLE_ID', $b, $f)." OR " . $sAliasPrefix . "BUF1.VALUE_ID = " . static::placeFieldSql('CREATED_BY', $b, $f) . " OR EXISTS( SELECT 'x' FROM b_tasks_member ".$sAliasPrefix."DSTM WHERE ".$sAliasPrefix."DSTM.TASK_ID = ".static::placeFieldSql('ID', $b, $f)." AND ".$sAliasPrefix."DSTM.USER_ID = " . $sAliasPrefix . "BUF1.VALUE_ID ) ) AND " . $strConstraint . " "; return $strSql; } return false; } /** * Use CTaskItem->update() instead (with key 'ACCOMPLICES') * * @deprecated */ function AddAccomplices($ID, $arAccompleces = array()) { if ($arAccompleces) { $arAccompleces = array_unique($arAccompleces); foreach ($arAccompleces as $accomplice) { $arMember = array( "TASK_ID" => $ID, "USER_ID" => $accomplice, "TYPE" => "A" ); $member = new \CTaskMembers(); $member->Add($arMember); } } } /** * Use CTaskItem->update() instead (with key 'AUDITORS') * * @deprecated */ function AddAuditors($ID, $arAuditors = array()) { if ($arAuditors) { $arAuditors = array_unique($arAuditors); foreach ($arAuditors as $auditor) { $arMember = array( "TASK_ID" => $ID, "USER_ID" => $auditor, "TYPE" => "U" ); $member = new \CTaskMembers(); $member->Add($arMember); } } } function AddFiles($ID, $arFiles = array(), $arParams = array()) { $arFilesIds = array(); $userId = null; $bCheckRightsOnFiles = false; if (is_array($arParams)) { if (isset($arParams['USER_ID']) && ($arParams['USER_ID'] > 0)) $userId = (int) $arParams['USER_ID']; if (isset($arParams['CHECK_RIGHTS_ON_FILES'])) $bCheckRightsOnFiles = $arParams['CHECK_RIGHTS_ON_FILES']; } if ($userId === null) { $userId = User::getId(); if(!$userId) { $userId = User::getAdminId(); } } if ($arFiles) { foreach ($arFiles as $file) $arFilesIds[] = (int) $file; if (count($arFilesIds)) { \CTaskFiles::AddMultiple( $ID, $arFilesIds, array( 'USER_ID' => $userId, 'CHECK_RIGHTS_ON_FILES' => $bCheckRightsOnFiles ) ); } } } /** * Detect tags in data array. * @param array $fields Data array. * @return array */ private function detectTags(array &$fields) { $newTags = array(); $searchFields = array('TITLE', 'DESCRIPTION'); foreach ($searchFields as $code) { if ( isset($fields[$code]) && preg_match_all('/\s#([^\s,\[\]<>]+)/is', ' ' . $fields[$code], $tags) ) { $newTags = array_merge($newTags, $tags[1]); } } return $newTags; } function AddTags($ID, $USER_ID, $arTags = array(), $effectiveUserId = null) { // delete previous $oTag = new \CTaskTags(); $oTag->DeleteByTaskID($ID); if ($arTags) { if (!is_array($arTags)) { $arTags = explode(",", $arTags); } $arTags = array_unique(array_map("trim", $arTags)); foreach ($arTags as $tag) { $arTag = array( "TASK_ID" => $ID, "USER_ID" => $USER_ID, "NAME" => $tag ); $oTag = new \CTaskTags(); $oTag->Add($arTag, $effectiveUserId); } } } function AddPrevious($ID, $arPrevious = array()) { $oDependsOn = new \CTaskDependence(); $oDependsOn->DeleteByTaskID($ID); if ($arPrevious) { $arPrevious = array_unique(array_map('intval', $arPrevious)); foreach ($arPrevious as $dependsOn) { $arDependsOn = array( "TASK_ID" => $ID, "DEPENDS_ON_ID" => $dependsOn ); $oDependsOn = new \CTaskDependence(); $oDependsOn->Add($arDependsOn); } } } function Index($arTask, $tags) { $arTask['SE_TAG'] = $tags; Integration\Search\Task::index($arTask); } function OnSearchReindex($NS=array(), $oCallback=NULL, $callback_method="") { $arResult = array(); $arOrder = array('ID' => 'ASC'); $arFilter = array(); if (isset($NS['MODULE']) && ($NS['MODULE'] === 'tasks') && isset($NS['ID']) && ($NS['ID'] > 0) ) { $arFilter['>ID'] = (int) $NS['ID']; } else $arFilter['>ID'] = 0; $rsTasks = self::GetList($arOrder, $arFilter); while ($arTask = $rsTasks->Fetch()) { $rsTags = \CTaskTags::GetList(array(), array("TASK_ID" => $arTask["ID"])); $arTags = array(); while ($arTag = $rsTags->Fetch()) { $arTags[] = $arTag["NAME"]; } $arTask["ACCOMPLICES"] = $arTask["AUDITORS"] = array(); $rsMembers = \CTaskMembers::GetList(array(), array("TASK_ID" => $arTask["ID"])); while ($arMember = $rsMembers->Fetch()) { if ($arMember["TYPE"] == "A") { $arTask["ACCOMPLICES"][] = $arMember["USER_ID"]; } elseif ($arMember["TYPE"] == "U") { $arTask["AUDITORS"][] = $arMember["USER_ID"]; } } // todo: get path form socnet if ($arTask["GROUP_ID"] > 0) { $path = str_replace("#group_id#", $arTask["GROUP_ID"], \COption::GetOptionString("tasks", "paths_task_group_entry", "/workgroups/group/#group_id#/tasks/task/view/#task_id#/", $arTask["SITE_ID"])); } else { $path = str_replace("#user_id#", $arTask["RESPONSIBLE_ID"], \COption::GetOptionString("tasks", "paths_task_user_entry", "/company/personal/user/#user_id#/tasks/task/view/#task_id#/", $arTask["SITE_ID"])); } $path = str_replace("#task_id#", $arTask["ID"], $path); $arPermissions = self::__GetSearchPermissions($arTask); $Result = array( "ID" => $arTask["ID"], "LAST_MODIFIED" => $arTask["CHANGED_DATE"] ? $arTask["CHANGED_DATE"] : $arTask["CREATED_DATE"], "TITLE" => $arTask["TITLE"], "BODY" => strip_tags($arTask["DESCRIPTION"]) ? strip_tags($arTask["DESCRIPTION"]) : $arTask["TITLE"], "TAGS" => implode(",", $arTags), "URL" => $path, "SITE_ID" => $arTask["SITE_ID"], "PERMISSIONS" => $arPermissions, ); if ($oCallback) { $index_res = call_user_func(array($oCallback, $callback_method), $Result); if(!$index_res) return $Result["ID"]; } else $arResult[] = $Result; self::UpdateForumTopicIndex($arTask["FORUM_TOPIC_ID"], "U", $arTask["RESPONSIBLE_ID"], "tasks", "view_all", $path, $arPermissions, $arTask["SITE_ID"]); } if ($oCallback) return false; return $arResult; } function UpdateForumTopicIndex($topic_id, $entity_type, $entity_id, $feature, $operation, $path, $arPermissions, $siteID) { global $DB; if(!\CModule::IncludeModule("forum")) return; $topic_id = intval($topic_id); $rsForumTopic = $DB->Query("SELECT FORUM_ID FROM b_forum_topic WHERE ID = ".$topic_id); $arForumTopic = $rsForumTopic->Fetch(); if(!$arForumTopic) return; \CSearch::ChangePermission("forum", $arPermissions, false, $arForumTopic["FORUM_ID"], $topic_id); $rsForumMessages = $DB->Query(" SELECT ID FROM b_forum_message WHERE TOPIC_ID = ".$topic_id." "); while($arMessage = $rsForumMessages->Fetch()) { \CSearch::ChangeSite("forum", array($siteID => $path), $arMessage["ID"]); } $arParams = array( "feature_id" => "S".$entity_type."_".$entity_id."_".$feature."_".$operation, "socnet_user" => $entity_id, ); \CSearch::ChangeIndex("forum", array("PARAMS" => $arParams), false, $arForumTopic["FORUM_ID"], $topic_id); } public static function __GetSearchPermissions($arTask) { $arPermissions = array(); // check task members if (!isset($arTask['ACCOMPLICES']) || !isset($arTask['AUDITORS'])) { if (!isset($arTask['ACCOMPLICES'])) $arTask['ACCOMPLICES'] = array(); if (!isset($arTask['AUDITORS'])) $arTask['AUDITORS'] = array(); $rsMembers = \CTaskMembers::GetList(array(), array("TASK_ID" => $arTask["ID"])); while ($arMember = $rsMembers->Fetch()) { if ($arMember["TYPE"] == "A") $arTask["ACCOMPLICES"][] = $arMember["USER_ID"]; elseif ($arMember["TYPE"] == "U") $arTask["AUDITORS"][] = $arMember["USER_ID"]; } } // group id is set, then take permissions from socialnetwork settings if ($arTask["GROUP_ID"] > 0 && \CModule::IncludeModule("socialnetwork")) { $prefix = "SG".$arTask["GROUP_ID"]."_"; $letter = \CSocNetFeaturesPerms::GetOperationPerm(SONET_ENTITY_GROUP, $arTask["GROUP_ID"], "tasks", "view_all"); switch($letter) { case "N"://All $arPermissions[] = 'G2'; break; case "L"://Authorized $arPermissions[] = 'AU'; break; case "K"://Group members includes moderators and admins $arPermissions[] = $prefix.'K'; case "E"://Moderators includes admins $arPermissions[] = $prefix.'E'; case "A"://Admins $arPermissions[] = $prefix.'A'; break; } } // if neither "all users" nor "authorized user" enabled, turn permissions on at least for task members if (!in_array("G2", $arPermissions) && !in_array("AU", $arPermissions)) { if (!$arTask["ACCOMPLICES"]) $arTask["ACCOMPLICES"] = array(); if (!$arTask["AUDITORS"]) $arTask["AUDITORS"] = array(); $arParticipants = array_unique(array_merge(array($arTask["CREATED_BY"], $arTask["RESPONSIBLE_ID"]), $arTask["ACCOMPLICES"], $arTask["AUDITORS"])); foreach($arParticipants as $userId) $arPermissions[] = "U".$userId; $arDepartments = array(); $arSubUsers = array_unique(array($arTask['RESPONSIBLE_ID'], $arTask['CREATED_BY'])); foreach ($arSubUsers as $subUserId) { $arUserDepartments = self::GetUserDepartments($subUserId); if (is_array($arUserDepartments) && count($arUserDepartments)) $arDepartments = array_merge($arDepartments, $arUserDepartments); } $arDepartments = array_unique($arDepartments); $arManagersTmp = self::GetDepartmentManagers($arDepartments); if (is_array($arManagersTmp)) { $arManagers = array_keys($arManagersTmp); // Remove $arSubUsers from $arManagers $arManagers = array_diff($arManagers, $arSubUsers); foreach($arManagers as $userId) { if (!in_array("U".$userId, $arPermissions)) $arPermissions[] = "U".$userId; } } } // adimins always allowed to view search result $arPermissions[] = 'G1'; return $arPermissions; } /** * Agent handler for repeating tasks. * Create new task based on given template. * * @param integer $templateId - id of task template * @param integer $flipFlop unused * @param mixed[] $debugHere * * @return string empty string. * @deprecated */ public static function RepeatTaskByTemplateId ($templateId, $flipFlop = 1, array &$debugHere = array()) { return Replicator\Task\FromTemplate::repeatTask( $templateId, array( // todo: get rid of use of CTasks one day... 'AGENT_NAME_TEMPLATE' => 'self::RepeatTaskByTemplateId(#ID#);', 'RESULT' => &$debugHere, ) ); } /** * @deprecated * * This function is deprecated and strongly discouraged to be used. * But it will not be removed, because some agents can be still active for * using this function in future for at least one year. * Current date is: 06 Oct 2012, Sat. Code written, but updater not built. * * @param $TASK_ID * @param string $time * @return string originally always returns an empty string */ function RepeatTask($TASK_ID, /** @noinspection PhpUnusedParameterInspection */ $time="") { $rsTemplate = \CTaskTemplates::GetList( array(), array('TASK_ID' => (int) $TASK_ID) ); if ( ! ($arTemplate = $rsTemplate->Fetch()) ) return (''); // Redirect call to new function if (isset($arTemplate['ID']) && ($arTemplate['ID'] > 0)) self::RepeatTaskByTemplateId( (int) $arTemplate['ID'] ); return (''); } /** * @param $arParams * @param bool $template * @param integer $agentTime Time in server timezone * @return bool|string */ public static function getNextTime($arParams, $template = false, $agentTime = false) { if(!is_array($arParams)) { return false; } $templateData = false; if(is_array($template)) { $templateData = $template; } elseif($template = intval($template)) { $item = \CTaskTemplates::getList(array(), array('ID' => $template), array(), array(), array('CREATED_BY', 'REPLICATE_PARAMS', 'TPARAM_REPLICATION_COUNT'))->fetch(); if($item) { $templateData = $item; } } if(!$templateData) { $templateData = array(); } $templateData['REPLICATE_PARAMS'] = $arParams; $result = Replicator\Task\FromTemplate::getNextTime($templateData, $agentTime); $rData = $result->getData(); return $rData['TIME'] == '' ? false : $rData['TIME']; } public static function CanGivenUserDelete($userId, $taskCreatedBy, $taskGroupId, /** @noinspection PhpUnusedParameterInspection */ $site_id = SITE_ID) { $userId = (int) $userId; $taskGroupId = (int) $taskGroupId; $site_id = null; // not used, left in function declaration for backward compatibility if ($userId <= 0) throw new \TasksException(); if ( \CTasksTools::IsAdmin($userId) || \CTasksTools::IsPortalB24Admin($userId) || ($userId == $taskCreatedBy) ) { return (true); } elseif (($taskGroupId > 0) && \CModule::IncludeModule('socialnetwork')) { return (boolean) \CSocNetFeaturesPerms::CanPerformOperation($userId, SONET_ENTITY_GROUP, $taskGroupId, "tasks", "delete_tasks"); } return false; } public static function CanCurrentUserDelete($task, $site_id = SITE_ID) { if (!$userID = User::getId()) // wtf? { return false; } return (self::CanGivenUserDelete($userID, $task['CREATED_BY'], $task['GROUP_ID'], $site_id)); } public static function CanGivenUserEdit($userId, $taskCreatedBy, $taskGroupId, /** @noinspection PhpUnusedParameterInspection */ $site_id = SITE_ID) { $userId = (int) $userId; $taskGroupId = (int) $taskGroupId; $site_id = null; // not used, left in function declaration for backward compatibility /** @noinspection PhpUnusedParameterInspection */ if ($userId <= 0) throw new \TasksException(); if ( \CTasksTools::IsAdmin($userId) || \CTasksTools::IsPortalB24Admin($userId) || ($userId == $taskCreatedBy) ) { return (true); } elseif (($taskGroupId > 0) && \CModule::IncludeModule('socialnetwork')) { return (boolean) \CSocNetFeaturesPerms::CanPerformOperation($userId, SONET_ENTITY_GROUP, $taskGroupId, "tasks", "edit_tasks"); } return false; } public static function CanCurrentUserEdit($task, $site_id = SITE_ID) { if (!$userID = User::getId()) { return false; } return (self::CanGivenUserEdit($userID, $task['CREATED_BY'], $task['GROUP_ID'], $site_id)); } public static function UpdateViewed($TASK_ID, $USER_ID) { self::__updateViewed($TASK_ID, $USER_ID); } public static function __updateViewed($TASK_ID, $USER_ID, $onTaskAdd = false) { $USER_ID = (int) $USER_ID; $TASK_ID = (int) $TASK_ID; $list = \Bitrix\Tasks\Internals\Task\ViewedTable::getList(array( "select" => array("TASK_ID", "USER_ID"), "filter" => array( "=TASK_ID" => $TASK_ID, "=USER_ID" => $USER_ID, ), )); if ($item = $list->fetch()) { \Bitrix\Tasks\Internals\Task\ViewedTable::update($item, array( "VIEWED_DATE" => new \Bitrix\Main\Type\DateTime(), )); } else { \Bitrix\Tasks\Internals\Task\ViewedTable::add(array( "TASK_ID" => $TASK_ID, "USER_ID" => $USER_ID, "VIEWED_DATE" => new \Bitrix\Main\Type\DateTime(), )); } //CTaskCountersProcessor::onAfterTaskViewedFirstTime($TASK_ID, $USER_ID, $onTaskAdd); \Bitrix\Tasks\Internals\Counter::onAfterTaskViewedFirstTime($TASK_ID, $USER_ID, $onTaskAdd); $event = new \Bitrix\Main\Event( 'tasks', 'onTaskUpdateViewed', array( 'taskId' => $TASK_ID, 'userId' => $USER_ID ) ); $event->send(); } function GetUpdatesCount($arViewed) { global $DB; if ($userID = User::getId()) { $arSqlSearch = array(); $arUpdatesCount = array(); foreach($arViewed as $key=>$val) { $arSqlSearch[] = "(CREATED_DATE > " . $DB->CharToDateFunction($val) . " AND TASK_ID = " . (int) $key . ")"; $arUpdatesCount[$key] = 0; } if ( ! empty($arSqlSearch) ) { $strSql = " SELECT TL.TASK_ID AS TASK_ID, COUNT(TL.TASK_ID) AS CNT FROM b_tasks_log TL WHERE USER_ID != " . $userID . " AND ( ".implode(" OR ", $arSqlSearch)." ) GROUP BY TL.TASK_ID "; $rsUpdatesCount = $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); while($arUpdate = $rsUpdatesCount->Fetch()) { $arUpdatesCount[$arUpdate["TASK_ID"]] = $arUpdate["CNT"]; } return $arUpdatesCount; } } return false; } function GetFilesCount($arTasksIDs) { global $DB; $arFilesCount = array(); $arTasksIDs = array_filter($arTasksIDs); if (sizeof($arTasksIDs)) { $strSql = " SELECT TF.TASK_ID, COUNT(TF.FILE_ID) AS CNT FROM b_tasks_file TF WHERE TF.TASK_ID IN (".implode(",", $arTasksIDs).") "; $rsFilesCount = $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); while($arFile = $rsFilesCount->Fetch()) { $arFilesCount[$arFile["TASK_ID"]] = $arFile["CNT"]; } } return $arFilesCount; } function CanCurrentUserViewTopic($topicID) { $isSocNetModuleIncluded = \CModule::IncludeModule("socialnetwork"); if (($topicID = intval($topicID)) && User::getId()) { if (User::isSuper()) { return true; } $rsTask = $res = self::GetList(array(), array("FORUM_TOPIC_ID" => $topicID)); if ($arTask = $rsTask->Fetch()) { if ( ((int)$arTask['GROUP_ID']) > 0 ) { if (in_array(\CSocNetFeaturesPerms::GetOperationPerm(SONET_ENTITY_GROUP, $arTask["GROUP_ID"], "tasks", "view_all"), array("G2", "AU"))) return true; elseif ( $isSocNetModuleIncluded && (false !== \CSocNetFeaturesPerms::CurrentUserCanPerformOperation(SONET_ENTITY_GROUP, $arTask['GROUP_ID'], 'tasks', 'view_all')) ) { return (true); } } $arTask["ACCOMPLICES"] = $arTask["AUDITORS"] = array(); $rsMembers = \CTaskMembers::GetList(array(), array("TASK_ID" => $arTask["ID"])); while ($arMember = $rsMembers->Fetch()) { if ($arMember["TYPE"] == "A") { $arTask["ACCOMPLICES"][] = $arMember["USER_ID"]; } elseif ($arMember["TYPE"] == "U") { $arTask["AUDITORS"][] = $arMember["USER_ID"]; } } if (in_array(User::getId(), array_unique(array_merge(array($arTask["CREATED_BY"], $arTask["RESPONSIBLE_ID"]), $arTask["ACCOMPLICES"], $arTask["AUDITORS"])))) return true; $dbRes = \CUser::GetList($by='ID', $order='ASC', array('ID' => $arTask["RESPONSIBLE_ID"]), array('SELECT' => array('UF_DEPARTMENT'))); if (($arRes = $dbRes->Fetch()) && is_array($arRes['UF_DEPARTMENT']) && count($arRes['UF_DEPARTMENT']) > 0) if (in_array(User::getId(), array_keys(self::GetDepartmentManagers($arRes['UF_DEPARTMENT'], $arTask["RESPONSIBLE_ID"])))) return true; } } return false; } public static function getParentOfTask($taskId) { $taskId = intval($taskId); if(!$taskId) { return false; } global $DB; $item = $DB->query("select PARENT_ID from b_tasks where ID = '".$taskId."'")->fetch(); return intval($item['PARENT_ID']) ? intval($item['PARENT_ID']) : false; } public static function GetUserDepartments($USER_ID) { static $cache = array(); $USER_ID = (int) $USER_ID; if (!isset($cache[$USER_ID])) { $dbRes = \CUser::GetList($by='ID', $order='ASC', array('ID' => $USER_ID), array('SELECT' => array('UF_DEPARTMENT'))); if ($arRes = $dbRes->Fetch()) $cache[$USER_ID] = $arRes['UF_DEPARTMENT']; else $cache[$USER_ID] = false; } return $cache[$USER_ID]; } public static function onBeforeSocNetGroupDelete($inGroupId) { global $DB, $APPLICATION; $bCanDelete = false; // prohibit group removing by default $groupId = (int) $inGroupId; $strSql = "SELECT ID AS TASK_ID FROM b_tasks WHERE GROUP_ID = $groupId AND ZOMBIE = 'N' "; $result = $DB->Query($strSql, false, 'File: ' . __FILE__ . '<br>Line: ' . __LINE__); if ($result === false) { $APPLICATION->ThrowException('EA_SQL_ERROR_OCCURED'); return (false); } $arResult = $result->Fetch(); // permit group deletion only when there is no tasks if ($arResult === false) $bCanDelete = true; else $APPLICATION->ThrowException(GetMessage('TASKS_ERR_GROUP_IN_USE')); return ($bCanDelete); } public static function OnBeforeUserDelete($inUserID) { global $DB, $APPLICATION; $userID = (int) $inUserID; if ( ! ($userID > 0) ) { $APPLICATION->ThrowException(GetMessage('TASKS_BAD_USER_ID')); return (false); } // check for tasks $strSql = "SELECT ID AS TASK_ID FROM b_tasks WHERE ( CREATED_BY = $userID OR RESPONSIBLE_ID = $userID ) AND ZOMBIE = 'N' UNION SELECT TASK_ID FROM b_tasks_member WHERE USER_ID = $userID"; $result = $DB->Query($strSql, false, 'File: ' . __FILE__ . '<br>Line: ' . __LINE__); if ($result === false) { $APPLICATION->ThrowException('EA_SQL_ERROR_OCCURED'); return (false); } $tasks = array(); while($item = $result->fetch()) { $tasks[$item['TASK_ID']] = true; } $templates = array(); $res = \Bitrix\Tasks\TemplateTable::getList(array('filter' => array( 'LOGIC' => 'OR', array('=CREATED_BY' => $userID), array('=RESPONSIBLE_ID' => $userID) ), 'select' => array('ID'))); while($item = $res->fetch()) { $templates[$item['ID']] = true; } $errorMessages = array(); if(!empty($tasks)) { $tasks = array_keys($tasks); $tail = ''; $count = count($tasks); if($count > 10) { $tasks = array_slice($tasks, 0, 10); $tail = GetMessage('TASKS_ERR_USER_IN_USE_TAIL', array('#N#' => $count - 10)); } $errorMessages[] = GetMessage('TASKS_ERR_USER_IN_USE_TASKS', array('#IDS#' => implode(', ', $tasks))).$tail; } if(!empty($templates)) { $templates = array_keys($templates); $tail = ''; $count = count($templates); if($count > 10) { $templates = array_slice($templates, 0, 10); $tail = GetMessage('TASKS_ERR_USER_IN_USE_TAIL', array('#N#' => $count - 10)); } $errorMessages[] = GetMessage('TASKS_ERR_USER_IN_USE_TEMPLATES', array('#IDS#' => implode(', ', $templates))).$tail; } $errorMessages = implode(', ', $errorMessages); if((string) $errorMessages != '') $APPLICATION->ThrowException(GetMessage('TASKS_ERR_USER_IN_USE_TASKS_PREFIX', array('#ENTITIES#' => $errorMessages))); return (empty($tasks) && empty($templates)); } // $value comes in units of $type, we must translate to seconds private static function convertDurationToSeconds($value, $type) { if($type == self::TIME_UNIT_TYPE_HOUR) { // hours to seconds return intval($value) * 3600; } elseif($type == self::TIME_UNIT_TYPE_DAY || (string) $type == ''/*days by default, see install/db*/) { // days to seconds return intval($value) * 86400; } return $value; } // $value comes in seconds, we must translate to units of $type public static function convertDurationFromSeconds($value, $type) { if($type == self::TIME_UNIT_TYPE_HOUR) { // hours to seconds return round(intval($value) / 3600, 0); } elseif($type == self::TIME_UNIT_TYPE_DAY || (string) $type == ''/*days by default, see install/db*/) { // days to seconds return round(intval($value) / 86400, 0); } return $value; } public static function OnUserDelete($USER_ID) { global $CACHE_MANAGER, $DB; $USER_ID = intval($USER_ID); $strSql = " SELECT RESPONSIBLE_ID AS USER_ID FROM b_tasks WHERE CREATED_BY = ".$USER_ID." AND CREATED_BY != RESPONSIBLE_ID UNION SELECT CREATED_BY AS USER_ID FROM b_tasks WHERE RESPONSIBLE_ID = ".$USER_ID." AND CREATED_BY != RESPONSIBLE_ID UNION SELECT USER_ID FROM b_tasks_member WHERE TASK_ID IN (SELECT TASK_ID FROM b_tasks_member WHERE USER_ID = ".$USER_ID.") "; $result = $DB->Query($strSql, false, "File: ".__FILE__."<br>Line: ".__LINE__); while($arResult = $result->Fetch()) { $CACHE_MANAGER->ClearByTag("tasks_user_".$arResult["USER_ID"]); } } public static function EmitPullWithTagPrefix($arRecipients, $tagPrefix, $cmd, $arParams) { if ( ! is_array($arRecipients) ) throw new \TasksException('EA_PARAMS', \TasksException::TE_WRONG_ARGUMENTS); $arRecipients = array_unique($arRecipients); if ( ! \CModule::IncludeModule('pull') ) return; /* $arEventData = array( 'module_id' => 'tasks', 'command' => 'notify', 'params' => CIMNotify::GetFormatNotify( array( 'ID' => -3 ) ), ); */ $bWasFatalError = false; foreach ($arRecipients as $userId) { $userId = (int) $userId; if ($userId < 1) { $bWasFatalError = true; continue; // skip invalid items } //\Bitrix\Pull\Event::add($userId, $arEventData); \CPullWatch::AddToStack( $tagPrefix . $userId, array( 'module_id' => 'tasks', 'command' => $cmd, 'params' => $arParams ) ); } if ($bWasFatalError) throw new \TasksException('EA_PARAMS', \TasksException::TE_WRONG_ARGUMENTS); } public static function EmitPullWithTag($arRecipients, $tag, $cmd, $arParams) { if ( ! is_array($arRecipients) ) throw new \TasksException('EA_PARAMS', \TasksException::TE_WRONG_ARGUMENTS); $arRecipients = array_unique($arRecipients); if ( ! \CModule::IncludeModule('pull') ) return; $bWasFatalError = false; foreach ($arRecipients as $userId) { $userId = (int) $userId; if ($userId < 1) { $bWasFatalError = true; continue; // skip invalid items } \CPullWatch::Add($userId, $tag); //\Bitrix\Pull\Event::add($userId, $arEventData); \CPullWatch::AddToStack( $tag, array( 'module_id' => 'tasks', 'command' => $cmd, 'params' => $arParams ) ); } if ($bWasFatalError) throw new \TasksException('EA_PARAMS', \TasksException::TE_WRONG_ARGUMENTS); } /** * Get list of IDs groups, which contains tasks where given user is member * * @param integer $userId * @throws \TasksException * @return array */ public static function GetGroupsWithTasksForUser($userId) { global $DB; $userId = (int) $userId; // EXISTS! $rc = $DB->Query( "SELECT GROUP_ID FROM b_tasks T WHERE ( T.CREATED_BY = $userId OR T.RESPONSIBLE_ID = $userId OR EXISTS( SELECT 'x' FROM b_tasks_member TM WHERE TM.TASK_ID = T.ID AND TM.USER_ID = $userId ) ) AND T.ZOMBIE = 'N' AND GROUP_ID IS NOT NULL AND GROUP_ID != 0 GROUP BY GROUP_ID " ); if ( ! $rc ) throw new \TasksException(); $arGroups = array(); while ($ar = $rc->Fetch()) $arGroups[] = (int) $ar['GROUP_ID']; return (array_unique($arGroups)); } /** * Convert every given string in array from BB-code to HTML * * @param array $arStringsInBbcode * * @throws \TasksException * @return array of strings converted to HTML, keys maintaned */ public static function convertBbcode2Html($arStringsInBbcode) { if ( ! is_array($arStringsInBbcode) ) throw new \TasksException(); static $delimiter = '--------This is unique BB-code strings delimiter at high confidence level (CL)--------'; $stringsCount = count($arStringsInBbcode); $arStringsKeys = array_keys($arStringsInBbcode); $concatenatedStrings = implode($delimiter, $arStringsInBbcode); // While not unique identifier, try to $i = -150; while (count(explode($delimiter, $concatenatedStrings)) !== $stringsCount) { // prevent an infinite loop if ( ! ($i++) ) throw new \TasksException(); $delimiter = '--------' . sha1(uniqid()) . '--------'; $concatenatedStrings = implode($delimiter, $arStringsInBbcode); } $oParser = new \CTextParser(); $arHtmlStringsWoKeys = explode( $delimiter, str_replace( "\t", ' ', $oParser->convertText($concatenatedStrings) ) ); $arHtmlStrings = array(); // Do job in compatibility mode, if count of resulted strings not match source if (count($arHtmlStringsWoKeys) !== $stringsCount) { foreach ($arStringsInBbcode as $key => $str) { $oParser = new \CTextParser(); $arHtmlStrings[$key] = str_replace( "\t", ' ', $oParser->convertText($str) ); unset($oParser); } } else { // Maintain original array keys $i = 0; foreach ($arStringsKeys as $key) $arHtmlStrings[$key] = $arHtmlStringsWoKeys[$i++]; } return ($arHtmlStrings); } public static function getTaskSubTree($taskId) { $taskId = intval($taskId); if(!$taskId) { return array(); } $queue = array($taskId); $met = array(); $limit = 1000; $result = array(); $i = 0; while(true) { if($i > $limit) { break; } $next = array_shift($queue); if(isset($met[$next])) { break; } if(!intval($next)) { break; } $subTasks = self::getSubTaskIdsForTask($next); foreach($subTasks as $sTId) { $result[] = $sTId; $queue[] = $sTId; } $met[$next] = true; $i++; } return $result; } private static function getSubTaskIdsForTask($taskId) { global $DB; $taskId = intval($taskId); $result = array(); $res = $DB->query("select ID from b_tasks where ZOMBIE != 'Y' and ".($taskId ? "PARENT_ID = '".$taskId."'" : "PARENT_ID is null or PARENT_ID = '0'")); while($item = $res->fetch()) { if(intval($item['ID'])) { $result[] = $item['ID']; } } return array_unique($result); } public static function runRestMethod($executiveUserId, $methodName, $args, $navigation) { \CTaskAssert::assert($methodName === 'getlist'); // Force & limit NAV_PARAMS (in 4th argument) while (count($args) < 4) $args[] = array(); // All params in self::GetList() by default are empty arrays $arParams = & $args[3]; if ($navigation['iNumPage'] > 1) { $arParams['NAV_PARAMS'] = array( 'nPageSize' => \CTaskRestService::TASKS_LIMIT_PAGE_SIZE, 'iNumPage' => (int) $navigation['iNumPage'] ); } else if (isset($arParams['NAV_PARAMS'])) { if (isset($arParams['NAV_PARAMS']['nPageTop'])) $arParams['NAV_PARAMS']['nPageTop'] = min(\CTaskRestService::TASKS_LIMIT_TOP_COUNT, (int) $arParams['NAV_PARAMS']['nPageTop']); if (isset($arParams['NAV_PARAMS']['nPageSize'])) $arParams['NAV_PARAMS']['nPageSize'] = min(\CTaskRestService::TASKS_LIMIT_PAGE_SIZE, (int) $arParams['NAV_PARAMS']['nPageSize']); if ( ( ! isset($arParams['NAV_PARAMS']['nPageTop']) ) && ( ! isset($arParams['NAV_PARAMS']['nPageSize']) ) ) { $arParams['NAV_PARAMS'] = array( 'nPageSize' => \CTaskRestService::TASKS_LIMIT_PAGE_SIZE, 'iNumPage' => 1 ); } } else { $arParams['NAV_PARAMS'] = array( 'nPageSize' => \CTaskRestService::TASKS_LIMIT_PAGE_SIZE, 'iNumPage' => 1 ); } // Check and parse params $argsParsed = \CTaskRestService::_parseRestParams('ctasks', $methodName, $args); $arParams['USER_ID'] = $executiveUserId; // TODO: remove this hack (needs for select tasks with GROUP_ID === NULL or 0) if (isset($argsParsed[1])) { $arFilter = $argsParsed[1]; foreach ($arFilter as $key => $value) { if (($key === 'GROUP_ID') && ($value == 0)) { $argsParsed[1]['META:GROUP_ID_IS_NULL_OR_ZERO'] = 1; unset($argsParsed[1][$key]); break; } } if ( isset($argsParsed[1]['ID']) && is_array($argsParsed[1]['ID']) && empty($argsParsed[1]['ID']) ) { $argsParsed[1]['ID'] = -1; } } $rsTasks = call_user_func_array(array('self', 'getlist'), $argsParsed); $arTasks = array(); while ($arTask = $rsTasks->fetch()) $arTasks[] = $arTask; return (array($arTasks, $rsTasks)); } public static function getPublicFieldMap() { // READ, WRITE, SORT, FILTER, DATE return array( 'TITLE' => array(1, 1, 1, 1, 0), 'STAGE_ID' => array(1, 1, 0, 1, 0), 'STAGES_ID' => array(0, 0, 0, 1, 0), 'DESCRIPTION' => array(1, 1, 0, 0, 0), 'DEADLINE' => array(1, 1, 1, 1, 1), 'START_DATE_PLAN' => array(1, 1, 1, 1, 1), 'END_DATE_PLAN' => array(1, 1, 1, 1, 1), 'PRIORITY' => array(1, 1, 1, 1, 0), 'ACCOMPLICES' => array(1, 1, 0, 0, 0), 'AUDITORS' => array(1, 1, 0, 0, 0), 'TAGS' => array(1, 1, 0, 0, 0), 'ALLOW_CHANGE_DEADLINE' => array(1, 1, 1, 0, 0), 'ALLOW_CHANGE_DEADLINE_COUNT' => array(1, 1, 1, 1, 0), 'ALLOW_CHANGE_DEADLINE_COUNT_VALUE' => array(1, 1, 1, 1, 0), 'ALLOW_CHANGE_DEADLINE_MAXTIME' => array(1, 1, 1, 1, 1), 'ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE' => array(1, 1, 1, 1, 1), 'TASK_CONTROL' => array(1, 1, 0, 0, 0), 'PARENT_ID' => array(1, 1, 0, 1, 0), 'DEPENDS_ON' => array(1, 1, 0, 1, 0), 'GROUP_ID' => array(1, 1, 1, 1, 0), 'RESPONSIBLE_ID' => array(1, 1, 1, 1, 0), 'TIME_ESTIMATE' => array(1, 1, 1, 1, 0), 'ID' => array(1, 0, 1, 1, 0), 'CREATED_BY' => array(1, 1, 1, 1, 0), 'DESCRIPTION_IN_BBCODE' => array(1, 0, 0, 0, 0), 'DECLINE_REASON' => array(1, 1, 0, 0, 0), 'REAL_STATUS' => array(1, 0, 0, 1, 0), 'STATUS' => array(1, 1, 1, 1, 0), 'RESPONSIBLE_NAME' => array(1, 0, 0, 0, 0), 'RESPONSIBLE_LAST_NAME' => array(1, 0, 0, 0, 0), 'RESPONSIBLE_SECOND_NAME' => array(1, 0, 0, 0, 0), 'DATE_START' => array(1, 0, 1, 1, 1), 'DURATION_FACT' => array(1, 0, 0, 0, 0), 'DURATION_PLAN' => array(1, 1, 0, 0, 0), 'DURATION_TYPE' => array(1, 1, 0, 0, 0), 'CREATED_BY_NAME' => array(1, 0, 0, 0, 0), 'CREATED_BY_LAST_NAME' => array(1, 0, 0, 0, 0), 'CREATED_BY_SECOND_NAME' => array(1, 0, 0, 0, 0), 'CREATED_DATE' => array(1, 0, 1, 1, 1), 'CHANGED_BY' => array(1, 1, 0, 1, 0), 'CHANGED_DATE' => array(1, 1, 1, 1, 1), 'STATUS_CHANGED_BY' => array(1, 0, 0, 1, 0), 'STATUS_CHANGED_DATE' => array(1, 0, 0, 0, 1), 'CLOSED_BY' => array(1, 0, 0, 0, 0), 'CLOSED_DATE' => array(1, 0, 1, 1, 1), 'GUID' => array(1, 0, 0, 1, 0), 'MARK' => array(1, 1, 1, 1, 0), 'VIEWED_DATE' => array(1, 0, 0, 0, 1), 'TIME_SPENT_IN_LOGS' => array(1, 0, 0, 0, 0), 'FAVORITE' => array(1, 0, 1, 1, 0), 'ALLOW_TIME_TRACKING' => array(1, 1, 1, 1, 0), 'MATCH_WORK_TIME' => array(1, 1, 1, 1, 0), 'ADD_IN_REPORT' => array(1, 1, 0, 1, 0), 'FORUM_ID' => array(1, 0, 0, 0, 0), 'FORUM_TOPIC_ID' => array(1, 0, 0, 1, 0), 'COMMENTS_COUNT' => array(1, 0, 0, 0, 0), 'SITE_ID' => array(1, 1, 0, 1, 0), 'SUBORDINATE' => array(1, 0, 0, 0, 0), 'FORKED_BY_TEMPLATE_ID' => array(1, 0, 0, 0, 0), 'MULTITASK' => array(1, 0, 0, 0, 0), 'ACCOMPLICE' => array(0, 0, 0, 1, 0), 'AUDITOR' => array(0, 0, 0, 1, 0), 'DOER' => array(0, 0, 0, 1, 0), 'MEMBER' => array(0, 0, 0, 1, 0), 'TAG' => array(0, 0, 0, 1, 0), 'ONLY_ROOT_TASKS' => array(0, 0, 0, 1, 0), ); } public static function getManifest() { static $fieldMap; if($fieldMap == null) { $fieldMap = static::getPublicFieldMap(); } static $fieldManifest; if($fieldManifest === null) { foreach($fieldMap as $field => $permissions) { if($permissions[0]) // read { $fieldManifest['READ'][] = $field; } if($permissions[1]) // write { $fieldManifest['WRITE'][] = $field; } if($permissions[2]) // sort { $fieldManifest['SORT'][] = $field; } if($permissions[3]) // filter { $fieldManifest['FILTER'][] = $field; } if($permissions[4]) // filter { $fieldManifest['DATE'][] = $field; } } } return(array( 'Manifest version' => '2.1', 'Warning' => 'don\'t rely on format of this manifest, it can be changed without any notification', 'REST: shortname alias to class' => 'items', 'REST: writable task data fields' => $fieldManifest['WRITE'], 'REST: readable task data fields' => $fieldManifest['READ'], 'REST: sortable task data fields' => $fieldManifest['SORT'], 'REST: filterable task data fields' => $fieldManifest['FILTER'], 'REST: date fields' => $fieldManifest['DATE'], 'REST: available methods' => array( 'getlist' => array( 'mandatoryParamsCount' => 0, 'params' => array( array( 'description' => 'arOrder', 'type' => 'array', 'allowedKeys' => $fieldManifest['SORT'] ), array( 'description' => 'arFilter', 'type' => 'array', 'allowedKeys' => $fieldManifest['FILTER'], 'allowedKeyPrefixes' => array( '=', '!=', '%', '!%', '?', '><', '!><', '>=', '>', '<', '<=', '!' ) ), array( 'description' => 'arSelect', 'type' => 'array', 'allowedValues' => $fieldManifest['READ'] ), array( 'description' => 'arParams', 'type' => 'array', 'allowedKeys' => array('NAV_PARAMS', 'bGetZombie') ) ), 'allowedKeysInReturnValue' => $fieldManifest['READ'], 'collectionInReturnValue' => true ) ) )); } private static function getSortingOrderBy($asc = true) { $order = array(); $direction = $asc ? "ASC" : "DESC"; $connection = Application::getConnection(); if ($connection instanceof MysqlCommonConnection) { $order[] = " ISNULL(SORTING) ".$direction." "; $order[] = " SORTING ".$direction." "; } elseif ($connection instanceof MssqlConnection) { $order[] = "CASE WHEN SRT.SORT IS".($asc ? "" : " NOT")." NULL THEN 1 ELSE 0 END"; $order[] = " SRT.SORT ".$direction." "; } elseif ($connection instanceof OracleConnection) { $order[] = " SORTING ".$direction." ".($asc ? "NULLS LAST " : "NULLS FIRST " ); } return $order; } private static function getOrderSql($by, $order, $default_order, $nullable = true) { global $DBType; static $dbtype = null; if ($dbtype === null) $dbtype = strtolower($DBType); switch ($dbtype) { case 'mysql': return (self::getOrderSql_mysql($by, $order, $default_order, $nullable = true)); break; case 'mssql': return (self::getOrderSql_mssql($by, $order, $default_order, $nullable = true)); break; case 'oracle': return (self::getOrderSql_oracle($by, $order, $default_order, $nullable = true)); break; default: \CTaskAssert::log('unknown DB type: ' . $dbtype, \CTaskAssert::ELL_ERROR); return ' '; break; } } private static function getOrderSql_mysql($by, $order, $default_order, $nullable = true) { $o = self::parseOrder($by, $order, $default_order, $nullable); //$o[0] - bNullsFirst //$o[1] - asc|desc if($o[0]) { if($o[1] == "asc") return $by." asc"; else return "length(".$by.")>0 asc, ".$by." desc"; } else { if($o[1] == "asc") return "length(".$by.")>0 desc, ".$by." asc"; else return $by." desc"; } } private static function getOrderSql_mssql($by, $order, $default_order, $nullable = true) { static $temp_by = 0; $o = self::parseOrder($by, $order, $default_order, $nullable); //$o[0] - bNullsFirst //$o[1] - asc|desc if($o[0]) { if($o[1] == "asc") return $by." asc";// else return array( "case when len(".$by.") > 0 then 1 else 0 end", "_IS_NULL_".$temp_by, "_IS_NULL_".($temp_by++)." asc, ".$by." desc", ); } else { if($o[1] == "asc") return array( "case when len(".$by.") > 0 then 1 else 0 end", "_IS_NULL_".$temp_by, "_IS_NULL_".($temp_by++)." desc, ".$by." asc", ); else return $by." desc";// } } private static function getOrderSql_oracle($by, $order, $default_order, $nullable = true) { $o = self::parseOrder($by, $order, $default_order, $nullable); //$o[0] - bNullsFirst //$o[1] - asc|desc if($o[0]) { if($o[1] == "asc") { if($nullable) return $by." asc nulls first"; else return $by." asc"; } else { return $by." desc"; } } else { if($o[1] == "asc") { return $by." asc"; } else { if($nullable) return $by." desc nulls last"; else return $by." desc"; } } } private static function parseOrder($by, $order, $default_order, $nullable = true) { static $arOrder = array( "nulls,asc" => array(true, "asc" ), "asc,nulls" => array(false, "asc" ), "nulls,desc" => array(true, "desc"), "desc,nulls" => array(false, "desc"), "asc" => array(true, "asc" ), "desc" => array(false, "desc"), ); $order = strtolower(trim($order)); if(array_key_exists($order, $arOrder)) $o = $arOrder[$order]; elseif(array_key_exists($default_order, $arOrder)) $o = $arOrder[$default_order]; else $o = $arOrder["desc,nulls"]; //There is no need to "reverse" nulls order when //column can not contain nulls if(!$nullable) { if($o[1] == "asc") $o[0] = true; else $o[0] = false; } return $o; } }