* SPDX-License-Identifier: AGPL-3.0-only ************************************/ require_once('include/Webservices/GetUpdates.php'); require_once('include/utils/SessionValidator.php'); // crmv@91082 require_once('modules/Users/LoginHistory.php'); // crmv@91082 require_once('include/utils/EmailDirectory.php'); // crmv@107655 class TouchGetChanges extends TouchWSClass { public $listLimit = 50; // return max 50 records per module public $zeroDate = '1970-01-02 00:00:00'; public $reloadDate = null; // date in which a complete meta reload is needed protected $rawRequest = null; protected $relatedCache = array(); /* * Get the changes that should be propagated to the app */ public function process(&$request) { global $adb, $table_prefix, $current_user; global $touchInst, $touchUtils; $this->rawRequest = $request; $modules = Zend_Json::decode($request['modules']); // if this is a valid array, these modules will be used instead, if it's empty, all the available mods will be used $deviceTime = $request['devicetime']; $limit = intval($request['limit']); if ($limit > 0) $this->listLimit = $limit; // crmv@173184 $since = intval($request['since']); if ($request['since_multiple']) { $sinceAll = Zend_Json::decode($request['since_multiple']); if (is_array($sinceAll)) { foreach ($sinceAll as &$ts) { $ts = $this->ts2date($ts); } } else { $sinceAll = $this->ts2date($since); } } else { $sinceAll = $this->ts2date($since); } // crmv@173184e $metaSince = intval($request['meta_since']); $metaSinceDate = ($metaSince > 0 ? date('Y-m-d H:i:s', $metaSince) : $this->zeroDate); $skip_timecheck = ($request['skip_time_check'] == 1); $skip_related = ($request['skip_record_related'] == 1); $skip_content = ($request['skip_record_content'] == 1); $skip_filter = ($request['skip_filter_check'] == 1); $skip_folder = ($request['skip_folder_check'] == 1); $skip_recents = ($request['skip_recents'] == 1); $skip_emaildir = ($request['skip_emaildir'] == 1); // TODO: do this comparison with a token and not with a date or pass the server time and the client resend that // It's already like this for the modules, but should be extended and this check removed // first compare the server and the device time, if the difference is more than 60 seconds... too much! $localtime = time(); $diff = abs($localtime - $deviceTime); if ($diff > 60 && !$skip_timecheck) { return $this->error('Time difference is too big ('.$diff.'s), you must synchronize the server and the client first.'); } // Check the session // crmv@91082 $SV = SessionValidator::getInstance(); if ($SV->isStarted()) { if ($SV->isValid()) { $SV->refresh(); } else { $loghistory = LoginHistory::getInstance(); $loghistory->user_logout($current_user->user_name, null, null, 'auto', true); $touchInst->destroyWSSession(); $touchInst->outputFailure('Session expired', array('logout_reason' => 'concurrent')); exit; } } else { $SV->refresh(); } // crmv@91082e // crmv@161368 // get the wipedate if the variable here is not set $wipeDate = $touchInst->getWipeDate($current_user->id); if (!empty($wipeDate) && $metaSinceDate != $this->zeroDate && ($wipeDate >= $metaSinceDate)) { return $this->success(array( 'meta_changed' => true, 'require_reload' => true, 'require_wipe' => true, 'meta'=> $metaSinceDate, 'login_data' => $touchInst->retrieveLoginData($request['deviceid']) )); } // crmv@161368e // check reload date if ($this->reloadDate && ($this->reloadDate >= $metaSinceDate)) { return $this->success(array('meta_changed' => true, 'require_reload' => true, 'meta'=> $metaSinceDate)); } $requireReload = false; // crmv@93148 - close the session, to allow concurrent requests $touchInst->closeWSSession(); // meta changes $meta = $this->getMetaChanges($metaSinceDate); if ($meta && $meta['total'] > 0) $requireReload = true; // get modified/deleted records and filters $changes = $this->getChangedRecords($modules, $sinceAll, !$skip_content, !$skip_filter, !$skip_folder, !$skip_related); // crmv@173184 // users $users = $this->getChangedUsers($metaSinceDate); // groups $groups = $this->getChangedGroups($metaSinceDate); // recents if ($skip_recents) { $recents = array(); } else { $recents = $this->getRecents(); } // crmv@107655 // email directory if ($skip_emaildir) { $emailDir = array(); } else { $emailDir = $this->getEmailDirectory($metaSinceDate); } // crmv@107655e $metaChanged = $requireReload || ($users['total'] > 0) || ($groups['total'] > 0); $output = array( 'records'=>$changes, 'users' => $users, 'groups' => $groups, 'recents'=>$recents, 'email_directory' => $emailDir, // crmv@107655 'meta_changed' => $metaChanged, 'meta'=>$meta, 'require_reload' => $requireReload ); if ($touchInst->wsExists('GetOfflineData')) { $req = array(); $offlineInfo = $this->subcall('GetOfflineData', $req); if ($offlineInfo && $offlineInfo['success']) { $output['offline_info'] = $offlineInfo; } } $output['login_data'] = $touchInst->retrieveLoginData($request['deviceid']); return $this->success($output); } // crmv@173184 protected function ts2date($ts) { if ($ts > 0) { $date = max(date('Y-m-d H:i:s', $ts), $this->zeroDate); } else { $date = $this->zeroDate; } return $date; } // crmv@173184e function getChangedRecords($modules = null, $sinceDates = null, $content = false, $filters = false, $folders = false, $related = false) { // crmv@173184 global $adb, $table_prefix; global $touchInst, $touchUtils, $current_user; // max number of records to return for each module // this amount is not strictly observed, since if there are many records with the same timestamp, // they will be all returned since there is no way to partition them with several requests $limitRecords = $this->listLimit; // get the list of modules $modulesList = array(); if (!empty($modules)) { if (!is_array($modules)) $modules = array($modules); $modulesList = $modules; } else { $entityModules = array(); $modulesDetails = vtws_listtypes(null,$current_user); $modulesInformation = $modulesDetails["information"]; foreach ($modulesInformation as $moduleName=>$entityInformation){ if($entityInformation["isEntity"]) $modulesList[] = $moduleName; } } // crmv@173184 // find minimum date if (is_array($sinceDates)) { $minDate = min($sinceDates); } else { $minDate = $sinceDates; } if (empty($minDate)) $minDate = $this->zeroDate; // crmv@173184e // Users are done separately $modulesList = array_diff($modulesList,$touchInst->excluded_modules, array('Users')); if (empty($modulesList)) return array('changes'=>array(), 'total'=>0); // do a very quick check to see if globally there are changes (there should be an index, otherwise it's pointless) // crmv@177095 $res = $adb->query( "SELECT MAX(maxtime) AS maxmod FROM ( SELECT MAX(modifiedtime) AS maxtime FROM {$table_prefix}_crmentity UNION ALL SELECT MAX(modifiedtime) AS maxtime FROM {$table_prefix}_messages ) tt" ); // crmv@177095e if ($res && $adb->num_rows($res) > 0) { $maxmod = $adb->query_result_no_html($res, 0, 'maxmod'); // crmv@164122 // check also timestamp of notifications if (in_array('ModNotifications', $modulesList)) { $res = $adb->query("SELECT MAX(modifiedtime) AS maxmod FROM {$table_prefix}_modnotifications"); if ($res && $adb->num_rows($res) > 0) { $maxmod = max($maxmod, $adb->query_result_no_html($res, 0, 'maxmod')); } } // crmv@164122e if ($maxmod && $maxmod < $minDate) { // crmv@173184 // last global mod time is older than sincetime, therefore no nchanges will be found for any module return array('changes'=>array(), 'total'=>0, 'last_timestamp' => time()); } } $CustomView = CRMEntity::getInstance('CustomView'); // crmv@115329 $changes = array(); $globalLastTs = null; // crmv@173184 $total = 0; // now check for changes foreach ($modulesList as $elementType) { // crmv@173184 if (is_array($sinceDates)) { $sinceDate = $sinceDates[$elementType]; } else { $sinceDate = $sinceDates; } if (empty($sinceDate)) $sinceDate = $this->zeroDate; // crmv@173184e // crmv@177095 - speed up for messages the first time if ($sinceDate == $this->zeroDate && $elementType == 'Messages') { $oldRelated = $related; $related = false; } elseif (isset($oldRelated)) { $related = $oldRelated; } // crmv@177095e $moduleInstance = $touchUtils->getModuleInstance($elementType); $queries = $this->getChangeQuery($elementType, $sinceDate); $query = $queries['main']; $params = $queries['params']; $countQuery = $queries['count']; $idColumn = $queries['idcolumn']; $res = $adb->limitPQuery($query, 0, $limitRecords+1, $params); // crmv@164122 - removed line if ($res && $adb->num_rows($res) > 0) { $modCounter = 0; $lastTs = 0; $secondStep = false; $relateds_ids = array(); while ($row = $adb->fetchByAssoc($res, -1, false)) { $crmid = $row[$idColumn]; $entry = array('crmid'=>$crmid); $entry['timestamp'] = strtotime($row['modifiedtime']); $isDeleted = ($row['is_deleted'] == '1'); if (!$this->rawRequest['latest'] && !$secondStep && $modCounter == $limitRecords) { // last+1, check the timestamp if ($lastTs != $entry['timestamp']) break; // ok, different, so end // re-run the query and find the first != timestamp // suppose no more than 1000 records with same modifiedtime are present $query2 = preg_replace('/\) sync_table/i', ' AND modifiedtime < ?) sync_table', $query); $params[] = date('Y-m-d H:i:s', $lastTs+1); // overwrite old res $res = $adb->limitPQuery($query2, $limitRecords, 1000, $params); $secondStep = true; continue; } // fetch the content of the record if ($content && !$isDeleted) { $req = array( 'module' => $elementType, 'record' => $crmid, 'set_recent' => 0, 'set_seen' => 0, ); $record = $this->subcall('GetRecord', $req); if (!empty($record['crmid']) || !empty($record['record_id'])) { // this is for modComments (they can return a different crmid) $entry['crmid'] = ($record['crmid'] ? $record['crmid'] : $record['record_id']); } $entry['content'] = $record; } if ($filters && !$isDeleted) { $CustomView = CRMEntity::getInstance('CustomView'); // crmv@115329 $filterList = $CustomView->getRecordViews($elementType, $crmid, true); $entry['filters'] = $filterList; } if ($folders && !$isDeleted && $moduleInstance->hasFolders()) { if ($entry['content']) { $folderid = $entry['content']['folderid']; } else { $folderid = getSingleFieldValue($moduleInstance->table_name, 'folderid', $moduleInstance->table_index, $crmid); } $entry['folders'] = array(intval($folderid)); } if ($related && !$isDeleted) { // changed into multi-id retrieval //$entry['related'] = $this->getAllRelated($elementType, $crmid); $relateds_ids['related_list'][] = $crmid; $relateds_ids['related_list_idx'][$crmid] = count($changes[$elementType]['updated']); // Changed into a multi-id retrieval //$entry['related_n1'] = $this->getN1Related($elementType, $crmid); $relateds_ids['related_n1_list'][] = $crmid; $relateds_ids['related_n1_list_idx'][$crmid] = count($changes[$elementType]['updated']); } $changes[$elementType][$isDeleted ? 'deleted' : 'updated'][] = $entry; if ($this->rawRequest['latest']) { if (!$lastTs) $lastTs = $entry['timestamp']; } else { $lastTs = $entry['timestamp']; } ++$modCounter; ++$total; } // retrieve multiple related ids all at once if (!empty($relateds_ids['related_list'])) { $list = $this->getAllRelated($elementType, $relateds_ids['related_list']); foreach ($list as $lid => $lists) { $index = $relateds_ids['related_list_idx'][$lid]; if (is_int($index) && $index >= 0) { $changes[$elementType]['updated'][$index]['related'] = $lists; } } } // retrieve multiple related ids all at once if (!empty($relateds_ids['related_n1_list'])) { $list = $this->getN1Related($elementType, $relateds_ids['related_n1_list']); foreach ($list as $lid => $lists) { $index = $relateds_ids['related_n1_list_idx'][$lid]; if (is_int($index) && $index >= 0) { $changes[$elementType]['updated'][$index]['related_n1'] = $lists; } } } } if (!empty($changes[$elementType])) { $changes[$elementType]['last_timestamp'] = $lastTs; $globalLastTs = max($globalLastTs, $lastTs); // crmv@173184 } } return array('changes'=>$changes, 'total'=>$total, 'last_timestamp' => $globalLastTs ?: time()); // crmv@173184 } // crmv@164122 protected function getChangeQuery($elementType, $sinceDate) { global $adb, $table_prefix, $current_user; global $touchUtils; $eventsBadType = array('Task', 'Emails', 'Webmails'); if ($elementType == 'ModNotifications') { $idColumn = 'modnotificationsid'; $query = "SELECT 0 as is_deleted, modifiedtime, $idColumn FROM {$table_prefix}_modnotifications WHERE smownerid = '{$current_user->id}'"; if (!$this->rawRequest['latest']) { $query .= " AND modifiedtime >= ?"; $params = array($sinceDate); $query .= " ORDER BY modifiedtime ASC, $idColumn ASC"; } else { $query .= " ORDER BY modifiedtime ASC"; } $countQuery = preg_replace('/select.*?from/i', "SELECT COUNT($idColumn) AS count FROM", $query); } else { $handler = vtws_getModuleHandlerFromName($elementType, $current_user); $moduleMeta = $handler->getMeta(); $deletedQueryCondition = $moduleMeta->getEntityDeletedQuery(); $moduleInstance = $touchUtils->getModuleInstance($elementType); if ($elementType == 'Messages') { global $current_account, $current_folder; //config account $mail_accountid = vtlib_purify($this->rawRequest['mail_accountid']); if ($mail_accountid) $current_account = $mail_accountid; if (empty($current_account)) { $current_account = $moduleInstance->getMainUserAccount(); $current_account = $current_account['id']; } $moduleInstance->setAccount($current_account); if ($current_account != 'all') $specialFolders = $moduleInstance->getSpecialFolders(false); // crmv@170276 // config folder $mail_folder = $this->rawRequest['mail_folder']; if ($mail_folder) $current_folder = $mail_folder; } $queryGenerator = QueryGenerator::getInstance($elementType, $current_user); $queryGenerator->setFields(array('id')); // create the extra join $baseTable = "{$table_prefix}_crmentity"; $baseId = 'crmid'; // crmv@173184 $modField = $queryGenerator->getWSField('modifiedtime', false); $modTable = $modField->getTableName() ?: $table_prefix.'_crmentity'; // crmv@173184e // condition on the date $setype = $elementType; if ($elementType == 'Events') { $setype = 'Calendar'; } if ($elementType == 'Users') { $dateSql = "INNER JOIN (SELECT id AS crmid FROM {$table_prefix}_users WHERE date_entered >= ?) sync_table ON sync_table.crmid = {$table_prefix}_users.id"; $params = array($sinceDate); } elseif (in_array("{$table_prefix}_crmentity", $moduleInstance->tab_name)) { // crmv@173184 crmv@200661 // TODO: fix per modcomments, prendere solo il padre $dateSql = "INNER JOIN (SELECT crmid FROM $baseTable MARK1 WHERE MARK2 setype = ? AND modifiedtime >= ?) sync_table ON sync_table.crmid = $baseTable.crmid"; $params = array($setype, $sinceDate); } if (!$this->rawRequest['latest']) { $queryGenerator->appendToFromClause($dateSql); } if ($elementType == 'Calendar') { $queryGenerator->appendToWhereClause(" AND {$table_prefix}_activity.activitytype = 'Task'"); } elseif ($elementType == 'Events') { if (count($eventsBadType) > 0) { foreach ($eventsBadType as $caltype) $blist[] = "'$caltype'"; $queryGenerator->appendToWhereClause(" AND {$table_prefix}_activity.activitytype NOT IN (".implode(',',$blist).")"); } // TODO: tirare su anche gli eventi a cui sono invitato? } elseif ($elementType == 'ModComments') { // not needed // retrieve only parent comments //$queryGenerator->appendToWhereClause(" AND {$table_prefix}_modcomments.parent_comments = 0"); } elseif ($elementType == 'Messages') { // Messages in the Shared folder are retrieved when getting the ModComments // For all the other folders, messages are private, so I just take mines // crmv@173184 $ownerField = $queryGenerator->getWSField('assigned_user_id', false); $ownerTable = $ownerField->getTableName(); $queryGenerator->appendToWhereClause(" AND $ownerTable.smownerid = ".$current_user->id); $queryGenerator->appendToWhereClause(" AND $modTable.modifiedtime >= ?"); $params[] = $sinceDate; // crmv@173184e } // generate query $queryGenerator->setSkipDeletedQuery(true); $query = $queryGenerator->getQuery(); // crmv@95586 if ($elementType == 'Messages') { // fix forced index which is not needed here and slows things down $query = preg_replace('/force index\s+\([a-z0-9_ ]+\)/i', '', $query); } // crmv@95586e // crmv@173184 // fix where because some ugly things are added $query = preg_replace('/MARK1.+MARK2/', 'WHERE', $query); $query = preg_replace('/^select /i', 'select CASE WHEN ('.$deletedQueryCondition.") THEN 0 ELSE 1 END AS is_deleted, $modTable.modifiedtime, ", $query); $countQuery = preg_replace('/select.*?from/', 'SELECT COUNT(crmid) AS count FROM', $query); $idColumn = $moduleMeta->getIdColumn(); $baseTable = $moduleMeta->getEntityBaseTable(); if ($this->rawRequest['latest']) { if ($elementType = 'Messages') { $query .= " ORDER BY {$table_prefix}_messages.mdate DESC"; } else { $query .= " ORDER BY $modTable.modifiedtime DESC"; } } else { if ($baseTable == $table_prefix.'_crmentity') { $query .= " ORDER BY $modTable.modifiedtime ASC, {$table_prefix}_crmentity.crmid ASC"; } else { $query .= " ORDER BY $modTable.modifiedtime ASC, $baseTable.$idColumn ASC"; } } // crmv@173184e } $queries = array('main' => $query, 'params' => $params, 'count' => $countQuery, 'idcolumn' => $idColumn); return $queries; } // crmv@164122e // TODO: add special related also (invitees....) // $crmid can be an array. in this case, optimized subcalls will be used // in this case the result will be in the format array(crmid1 => [lists], crmid2 => [lists]... ) public function getAllRelated($module, $crmid) { // crmv@177095 global $touchInst, $touchUtils; global $adb, $table_prefix; if (empty($this->relatedCache[$module])) { $req = array('module' => $module, 'type'=>'RELATED'); $blocks = $this->subcall('GetBlocks', $req); if (isInventoryModule($module)) { $req = array('module' => $module, 'type'=>'PRODUCTS'); $blocks_prod = $this->subcall('GetBlocks', $req); $blocks['blocks'] = array_merge($blocks['blocks'], $blocks_prod['blocks']); } $this->relatedCache[$module] = array_filter($blocks['blocks']); } $relateds = $this->relatedCache[$module]; if (empty($relateds)) return array(); $lists = array(); foreach ($relateds as $relinfo) { $relationid = intval($relinfo['blockid']); if ($relationid <= 0) continue; if (is_array($crmid)) { $relatedModule = $relinfo['related_module']; $query = $this->getRelationQuery($module, $relinfo, $crmid); if ($query) { $res = $adb->limitQuery($query, 0, $this->listLimit * count($crmid)); if ($res) { $rlists = array(); while ($row = $adb->FetchByAssoc($res, -1, false)) { $mainCrmid = intval($row['crmid']); $relcrmid = intval($row['relcrmid']); if ($mainCrmid > 0 && $relcrmid > 0) { $rlists[$mainCrmid][] = $relcrmid; } } foreach ($rlists as $mainCrmid => $listrel) { $lists[$mainCrmid][] = array( 'relationid' => $relationid, 'module' => $module, 'relmodule' => $relatedModule, 'extra' => null, 'total' => count($listrel), 'list' => $listrel, 'lineid' => null, ); } $missing_crmids = array_diff($crmid, array_keys($rlists)); foreach ($missing_crmids as $mainCrmid) { $lists[$mainCrmid][] = array( 'relationid' => $relationid, 'module' => $module, 'relmodule' => $relatedModule, 'extra' => null, 'total' => 0, 'list' => array(), 'lineid' => null, ); } } } } else { $onlyIds = 1; if ($relinfo['type'] == 'PRODUCTS') $onlyIds = 0; $crmRelationId = $this->getCrmRelationId($relationid); $req = array('module' => $module, 'recordid' => $crmid, 'relationid' => $crmRelationId, 'limit' => $this->listLimit, 'onlyids' => $onlyIds); $rlist = $this->subcall('GetRelated', $req); if ($rlist['success'] && $rlist['entries']) { $idlist = $touchUtils->arrayPluck('crmid', $rlist['entries']); $extra = ($onlyIds ? null : $touchUtils->arrayPluck('extrafields', $rlist['entries'])); $lineidlist = ($onlyIds ? null : $touchUtils->arrayPluck('lineid', $rlist['entries'])); $lists[] = array( 'relationid' => $relationid, 'module' => $module, 'relmodule' => $relinfo['related_module'], 'extra' => $extra, 'total' => count($idlist), 'list' => $idlist, 'lineid' => $lineidlist, ); } } } return $lists; } protected function getFakeRecordId($module) { global $adb, $table_prefix; $setype = $module; if ($setype == 'Events') $setype = 'Calendar'; $crmid = 1234567; $res = $adb->limitpQuery("SELECT crmid FROM {$table_prefix}_crmentity WHERE setype = ? AND deleted = 0", 0, 1, array($setype)); // crmv@93148 if ($res && $adb->num_rows($res) > 0) { $crmid = $adb->query_result_no_html($res, 0, 'crmid'); } return intval($crmid) ?: 1234567; } protected function getRelationQuery($module, $blockinfo, $crmids) { global $touchInst, $touchUtils, $onlyquery; global $adb, $table_prefix; $crmids = array_filter(array_map('intval', $crmids)); $relationId = $blockinfo['blockid']; $relatedModule = $blockinfo['related_module']; $crmRelationId = $this->getCrmRelationId($relationId); $relationInfo = getRelatedListInfoById($crmRelationId); $function_name = $relationInfo['functionName']; if (empty($function_name)) return null; if ($relatedModule == 'Calendar' || $relatedModule == 'Events') // crmv@54449 $mod_listquery = "activity_listquery"; else $mod_listquery = strtolower($relatedModule)."_listquery"; $modObj = $touchUtils->getModuleInstance(($module == 'Events' ? 'Calendar' : $module)); if (!$modObj) return null; $relmodObj = $touchUtils->getModuleInstance(($relatedModule == 'Events' ? 'Calendar' : $relatedModule)); // crmv@173184 $onlyquery = true; $fakeRecordId = $this->getFakeRecordId($module); // crmv@177095 if ($module == 'Messages') { global $current_user; $qgen = QueryGenerator::getInstance('Messages', $current_user); $qgen->setFields(array('id')); $qgen->appendToFromClause( "INNER JOIN {$table_prefix}_messagesrel ON {$table_prefix}_messagesrel.messagehash = {$table_prefix}_messages.messagehash INNER JOIN {$relmodObj->table_name} ON {$relmodObj->table_name}.{$relmodObj->table_index} = {$table_prefix}_messagesrel.crmid INNER JOIN {$table_prefix}_crmentity c2 ON c2.crmid = {$relmodObj->table_name}.{$relmodObj->table_index} AND c2.deleted = 0" ); $qgen->appendToWhereClause("AND {$table_prefix}_messagesrel.module = '".$adb->sql_escape_string($relatedModule)."'"); $qgen->appendToWhereClause("AND {$table_prefix}_messages.messagesid = ".intval($fakeRecordId)); $query = $qgen->getQuery(); } else { // no need to reopen the session, since I don't care to save it VteSession::set($mod_listquery, ''); $relatedListData = $modObj->$function_name($fakeRecordId, $relationInfo['tabid'], $relationInfo['relatedTabId'], array()); $query = VteSession::get($mod_listquery); } // crmv@177095e // remove the order by clause (it's slow) if (preg_match('/order by.+$/i', $query, $orderByMatch)) { $query = preg_replace('/\s*order by.+$/i', '', $query); } if (preg_match('/([^\s()]+)\s*=\s*'.$fakeRecordId.'/', $query, $matches)) { $relatedIndex = $matches[1]; //$query = preg_replace('/(and|or)\s*[^\s()]+\s*=\s*'.$fakeRecordId.'/i', '', $query); $query = preg_replace('/=\s*'.$fakeRecordId.'/i', ' IN ('.implode(',', $crmids).')', $query); $query = preg_replace('/^select .+? from/i', "SELECT {$relmodObj->table_name}.{$relmodObj->table_index} as relcrmid, $relatedIndex AS crmid FROM", $query); // crmv@173184 // crmv@177095 if (in_array($table_prefix.'_crmentity', $modObj->tab_name)) { // now add a join with crmentity of first module because I have to filter by module $query = $this->sql_preg_replace('/\s+where\s+/i', " INNER JOIN {$table_prefix}_crmentity crm1 ON crm1.crmid = $relatedIndex WHERE ", $query, 1); //and add the condition $query .= " AND crm1.setype = '$module'"; } // crmv@177095e } // crmv@54449, really terrible tricks, please please, someone destroy that ugly calendar!! if ($relatedModule == 'Events') { // only events $query = preg_replace("/'Task'/", "'NOTATASK'", $query); } else { // only tasks $query = preg_replace("/activitytype in \(.*?\)/i", "activitytype = 'NOTANEVENT'", $query); } // crmv@54449e // restore the order by /*if ($orderByMatch[0]) { $query .= " ".$orderByMatch[0]; }*/ // now replace the crmid return $query; } //TODO: this is duplicated code, please remove me!! // replaces pieces of a query, without replacing inside subqueries // assume subquery starts with "(select" protected function sql_preg_replace($regexp, $replace, $sql, $limit = -1) { $subQueries = array(); // first replace subqueries with safe strings while (preg_match('/\(\s*select/i', $sql, $smatches, PREG_OFFSET_CAPTURE)) { $rpos1 = $smatches[0][1]; $rpos2 = 0; $bcount = 1; // now walk to find the closing bracket for ($pos = $smatches[0][1]+1; $pos $rpos1) { $subid = '###SUBQUERY'.count($subQueries).'###'; $subQueries[$subid] = substr($sql, $rpos1, $rpos2-$rpos1); $sql = substr($sql, 0, $rpos1) . $subid . substr($sql, $rpos2); } else { // if the closing bracket wasn't found break; } } // now do the replace $sql = preg_replace($regexp, $replace, $sql, $limit); // and restore the subqueries if (count($subQueries) > 0) { $sql = str_replace(array_keys($subQueries), array_values($subQueries), $sql); } return $sql; } protected function getCrmRelationId($relationId) { global $touchUtils; if ($touchUtils->isInviteesRelated($relationId)) { // invitees } elseif ($touchUtils->isProductsRelated($relationId)) { // products } elseif ($touchUtils->isNotesRelated($relationId)) { // MyNotes } elseif ($relationId >= $touchUtils->related_blockids['related_events']) { $relationId -= $touchUtils->related_blockids['related_events']; } else { $relationId -= $touchUtils->related_blockids['related']; } return $relationId; } // $crmid can be an array. in this case, optimized subcalls will be used // in this case the result will be in the format array(crmid1 => [lists], crmid2 => [lists]... ) protected function getN1Related($module, $crmid) { global $touchUtils, $touchInst, $touchCache; $RM = RelationManager::getInstance(); $relations = $RM->getRelations($module, ModuleRelation::$TYPE_NTO1, array(), $touchInst->excluded_modules); $lists = array(); if (is_array($relations)) { foreach ($relations as $rel) { $relmodule = $rel->getSecondModule(); // crmv@93148 if (empty($this->relatedCache[$relmodule])) { $key = 'relcache_'.$relmodule; $relateds = $touchCache->get($key); if ($relateds === false) { $req = array('module' => $relmodule, 'type'=>'RELATED'); $blocks = $this->subcall('GetBlocks', $req); $relateds = array_filter($blocks['blocks']); $touchCache->set($key, $relateds); } $this->relatedCache[$relmodule] = $relateds; } $relationid = null; $relateds = $this->relatedCache[$relmodule]; // crmv@93148e // find correct related foreach ($relateds as $relinfo) { $relationid = intval($relinfo['blockid']) - $touchInst->related_blockids['related']; if ($relationid <= 0) continue; if ($relinfo['related_module'] == $module) break; } if (empty($relationid)) continue; if (is_array($crmid)) { $idlist = $this->getRelatedIdsNTO1($rel, $crmid); if (is_array($idlist) && count($idlist) > 0) { foreach ($idlist as $id => $listrel) { $lists[$id][] = array( 'relationid' => ($rel->relationid ? $rel->relationid : $relationid), 'module' => $module, 'relmodule' => $relmodule, 'extra' => null, 'total' => count($listrel), 'list' => $listrel ); } } $missing_crmids = array_diff($crmid, array_keys($idlist)); foreach ($missing_crmids as $id) { $lists[$id][] = array( 'relationid' => ($rel->relationid ? $rel->relationid : $relationid), 'module' => $module, 'relmodule' => $relmodule, 'extra' => null, 'total' => 0, 'list' => array() ); } } else { $idlist = $rel->getRelatedIds($crmid); $lists[] = array( 'relationid' => ($rel->relationid ? $rel->relationid : $relationid), 'module' => $module, 'relmodule' => $relmodule, 'extra' => null, 'total' => count($idlist), 'list' => $idlist ); } } } return $lists; } // fast version of the ModuleRelation::getRelatedIds, which retrieves multiple ids at once protected function getRelatedIdsNTO1($relation, $crmids) { global $adb, $table_prefix, $touchInst, $touchUtils; $ret = array(); if (!empty($relation->fieldid)) { // inline getFieldValue, optimized for many records $res = $adb->pquery( "select {$table_prefix}_tab.name as modulename, fieldid, fieldname, tablename, columnname from {$table_prefix}_field inner join {$table_prefix}_tab on {$table_prefix}_tab.tabid = {$table_prefix}_field.tabid where fieldid=? and {$table_prefix}_field.presence in (0,2)", array($relation->fieldid) ); if ($res && $adb->num_rows($res) > 0) { $row = $adb->FetchByAssoc($res, -1, false); $focus = $touchUtils->getModuleInstance($row['modulename']); if (empty($focus)) return $ret; $indexname = $focus->tab_name_index[$row['tablename']]; if (empty($indexname)) return $ret; if ($row['tablename'] != $table_prefix.'_crmentity') { $join = "inner join {$table_prefix}_crmentity on {$table_prefix}_crmentity.crmid = {$row['tablename']}.$indexname"; } else { $join = ""; } $res2 = $adb->pquery( "select {$row['tablename']}.$indexname AS srcid, {$row['tablename']}.{$row['columnname']} as fieldval from {$row['tablename']} $join inner join {$table_prefix}_crmentity crm2 ON crm2.crmid = {$row['tablename']}.{$row['columnname']} where {$table_prefix}_crmentity.deleted = 0 AND crm2.setype = ? AND {$row['tablename']}.$indexname IN (".generateQuestionMarks($crmid).")", array($relation->getSecondModule(), $crmids) ); if ($res2 && $adb->num_rows($res2) > 0) { while ($row2 = $adb->FetchByAssoc($res2, -1, false)) { $ret[$row2['srcid']][] = $row2['fieldval']; } } } } return $ret; } // REMOVE ME protected function getRecents() { $recents = array(); $req = array('module' => ''); $result = $this->subcall('GetRecents', $req); if ($result && $result['success']) { $recents = $result['list']; } return $recents; } // warning, retrieve all the folders, not only the one visible to the user /*function getFoldersForRecord($moduleInstance, $recordid) { global $adb, $table_prefix; $table = $moduleInstance->table_name; $index = $moduleInstance->table_index; $res = $adb->pquery("select crm.crmid from {$table_prefix}_crmentity crm inner join $table on $table.$index = crm.crmid where crm.deleted = 0 and $table.folderid = ?") }*/ function getChangedUsers($sinceDate = null) { global $adb, $table_prefix, $metaLogs, $touchInst, $touchUtils, $current_user; // crmv@90935 if (empty($sinceDate)) $sinceDate = $this->zeroDate; if (!$metaLogs) $metaLogs = MetaLogs::getInstance(); $query = "SELECT id FROM {$table_prefix}_users WHERE deleted = 0 AND status = ? AND date_modified >= ?"; $params = array('Active', $sinceDate); $list = array(); $res = $adb->pquery($query, $params); if ($res && $adb->num_rows($res) > 0) { while ($row = $adb->fetchByAssoc($res, -1, false)) { $sparams = array('userid' => $row['id']); $user = $this->subcall('GetUsers', $sparams); if ($user['success'] && $user['total'] > 0) { $list[] = $user['users'][0]; } } } // now get also deleted users $delUsers = $metaLogs->getChanges($metaLogs::OPERATION_DELUSER, $sinceDate); if ($delUsers && count($delUsers) > 0) { // add a fake user, just to know something has changes $list[] = array('userid' => 0, 'deleted' => true); } //crmv@90935 // check if my password has changed $changePwd = $metaLogs->getChanges($metaLogs::OPERATION_CHANGEUSERPWD, $sinceDate); if ($changePwd && count($changePwd) > 0) { foreach ($changePwd as $change) { if ($change['objectid'] == $current_user->id) { $list[] = array('userid' => $current_user->id, 'changed_password' => true); } } } //crmv@90935e // clear user cache if (count($list) > 0) { $touchUtils->clearWSCache('GetUsers'); } return array('changes' => $list, 'total' => count($list)); } function getChangedGroups($sinceDate = null) { global $adb, $table_prefix, $metaLogs, $touchUtils; if (empty($sinceDate)) $sinceDate = $this->zeroDate; if (!$metaLogs) $metaLogs = MetaLogs::getInstance(); $query = "SELECT groupid FROM {$table_prefix}_groups WHERE date_modified >= ?"; $params = array($sinceDate); $list = array(); $res = $adb->pquery($query, $params); if ($res && $adb->num_rows($res) > 0) { while ($row = $adb->fetchByAssoc($res, -1, false)) { $sparams = array('groupid' => $row['groupid']); $group = $this->subcall('GetGroups', $sparams); if ($group['success'] && $group['total'] > 0) { $list[] = $group['groups'][0]; } } } // now get also deleted groups $delGroups = $metaLogs->getChanges($metaLogs::OPERATION_DELGROUP, $sinceDate); if ($delGroups && count($delGroups) > 0) { // add a fake user, just to know something has changes $list[] = array('groupid' => 0, 'deleted' => true); } // clear user cache if (count($list) > 0) { $touchUtils->clearWSCache('GetGroups'); } return array('changes' => $list, 'total' => count($list)); } function getMetaChanges($sinceDate = null) { global $adb, $table_prefix, $metaLogs, $touchCache; if (empty($sinceDate)) $sinceDate = $this->zeroDate; if (!$metaLogs) $metaLogs = MetaLogs::getInstance(); // ignore these types of operations $ignoreTypes = array( $metaLogs::OPERATION_UNKNOWN, $metaLogs::OPERATION_REBUILDSHARES, $metaLogs::OPERATION_ADDUSER, $metaLogs::OPERATION_EDITUSER, $metaLogs::OPERATION_DELUSER, $metaLogs::OPERATION_ADDGROUP, $metaLogs::OPERATION_EDITGROUP, $metaLogs::OPERATION_DELGROUP, $metaLogs::OPERATION_CHANGEUSERPWD, // crmv@90935 //crmv@146434 $metaLogs::OPERATION_ADDPANEL, $metaLogs::OPERATION_DELPANEL, $metaLogs::OPERATION_EDITPANEL, $metaLogs::OPERATION_ADDRELATEDLISTTOTAB, $metaLogs::OPERATION_EDITRELATEDLISTTOTAB, $metaLogs::OPERATION_DELRELATEDLISTTOTAB, //crmv@146434e ); $req = array('onlynames' => 1); $appModules = $this->subcall('ModulesList', $req); $modules = array(); $changes = $metaLogs->getChanges(null, $sinceDate); if (is_array($changes)) { foreach ($changes as $k=>$c) { // exclude some if (in_array($c['operation'], $ignoreTypes)) { unset($changes[$k]); continue; } // get modules $mods = $metaLogs->getAffectedModules($c); $common = array_intersect($mods, $appModules); if (in_array('All', $mods) || count($common) > 0) { // module is relevant $changes[$k]['modules'] = $mods; } else { unset($changes[$k]); } // crmv@57366 // now check if I need to keep it if (!$this->filterMetaChange($changes[$k])) { unset($changes[$k]); } // crmv@57366e } } // clear cache if (count($changes) > 0) { $touchCache->clear(); } return array('changes'=>$changes, 'total'=>count($changes), 'last_timestamp' => time()); // crmv@107655 } // crmv@57366 function filterMetaChange($change) { global $adb, $table_prefix, $metaLogs; $filterchanges = array( $metaLogs::OPERATION_ADDFILTER, $metaLogs::OPERATION_EDITFILTER, // always pass the delete, because I don't know if it was mobile or not //$metaLogs::OPERATION_DELFILTER, ); // check if it's a filter edit, of a non-mobile filter if (in_array($change['operation'], $filterchanges)) { $filterid = intval($change['objectid']); if ($filterid > 0) { $res = $adb->pquery("SELECT setmobile FROM {$table_prefix}_customview WHERE cvid = ?", array($filterid)); if ($res && $adb->num_rows($res) > 0) { $isMobile = $adb->query_result_no_html($res, 0, 'setmobile'); if (!$isMobile) return false; } } } return true; } // crmv@57366e // crmv@107655 function getEmailDirectory($sinceDate = null) { $list = array(); $fakeRequest = array( 'since' => $sinceDate ? strtotime($sinceDate) : 0, ); $result = $this->subcall('GetEmailDirectory', $fakeRequest); if ($result['success']) { $list = $result['entries']; } return array('entries' => $list, 'total' => count($list), 'last_timestamp' => time()); } // crmv@107655e }