mirror of
https://github.com/VTECRM/vtenext.git
synced 2026-02-26 16:18:47 +00:00
582 lines
18 KiB
PHP
582 lines
18 KiB
PHP
<?php
|
|
/*************************************
|
|
* SPDX-FileCopyrightText: 2009-2020 Vtenext S.r.l. <info@vtenext.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
************************************/
|
|
/* crmv@31780 - fix vari */
|
|
/* crmv@33097 - fix vari */
|
|
/* crmv@71388 - file upload */
|
|
/* crmv@85213 - collision handling */
|
|
|
|
class TouchSaveRecord extends TouchWSClass {
|
|
|
|
public $validateModule = true;
|
|
public $useChangelog = true;
|
|
|
|
protected $rawValues = array();
|
|
|
|
function process(&$request) {
|
|
global $currentModule, $touchInst, $touchUtils;
|
|
|
|
// save the values as they arrive, without any validation, for customization
|
|
$this->rawValues = Zend_Json::decode($request['values']);
|
|
|
|
$module = $request['module'];
|
|
$recordid = intval($request['record']);
|
|
$values = vtlib_purify($request['values']);
|
|
$postActions = Zend_Json::decode($request['post_save_actions']);
|
|
|
|
$currentModule = $module;
|
|
|
|
$returndata = null;
|
|
$errormsg = '';
|
|
|
|
$newRecord = Zend_Json::decode($values);
|
|
if (empty($newRecord)) {
|
|
return $this->error('Invalid values passed');
|
|
}
|
|
|
|
if ($recordid > 0) {
|
|
// UPDATE
|
|
$this->preSaveRecord($module, $recordid, 'update', $newRecord);
|
|
|
|
// collision handling
|
|
$coll = 0;
|
|
if (!empty($touchInst->collision_strategy)) {
|
|
$coll = $this->checkCollisions($touchInst->collision_strategy, $touchInst->collision_level, $module, $recordid, $newRecord);
|
|
}
|
|
if ($coll != 2) {
|
|
$response = $this->updateRecord($module, $recordid, $newRecord, $returndata);
|
|
} else {
|
|
// there was a collision and I don't have to save the record
|
|
$returndata['crmid'] = $recordid;
|
|
$response = array('success' => true);
|
|
}
|
|
$returndata['collision_result'] = $coll;
|
|
} else {
|
|
// CREATE
|
|
$tempCrmid = intval($newRecord['temp_crmid']);
|
|
$this->preSaveRecord($module, $tempCrmid, 'create', $newRecord);
|
|
$response = $this->createRecord($module, $tempCrmid, $newRecord, $returndata);
|
|
}
|
|
$returnok = ($response['success'] === true);
|
|
|
|
if ($returnok) {
|
|
// handles for post save customizations
|
|
$this->postSaveRecord($returndata);
|
|
// process post save actions
|
|
if (is_array($postActions)) {
|
|
$message = '';
|
|
$r = $this->processPostActions($returndata, $postActions, $message);
|
|
if (!$r) {
|
|
$returnok = false;
|
|
$errormsg = $message;
|
|
}
|
|
}
|
|
} else {
|
|
$errormsg = $response['error']->message;
|
|
}
|
|
|
|
return $touchInst->createOutput(array('result' => $returndata),$errormsg, $returnok);
|
|
}
|
|
|
|
/**
|
|
* Check for collisions. Returns:
|
|
* 0: no collisions
|
|
* 1: collision resolved, save the record
|
|
* 2: collision resolved, don't save the record
|
|
* 3: collision resolved, record merged
|
|
* 5: collision not resolved
|
|
* 10: no collision check possible (missing timestamps)
|
|
* 11: error retrieving record
|
|
*/
|
|
public function checkCollisions($strategy, $level, $module, $crmid, &$values) {
|
|
global $touchInst, $touchUtils;
|
|
|
|
$appTS = intval($values['timestamp']);
|
|
$vteTS = $this->getRecordTs($module, $crmid);
|
|
|
|
if ($level == 'field') {
|
|
// TODO:
|
|
// 1. retrieve old record, diff, and find the changed fields
|
|
// 2. find last changelog with at least one of those fields changed and use that timestamp
|
|
// 3. compare the ts
|
|
}
|
|
|
|
if ($appTS && $vteTS) {
|
|
if ($appTS > $vteTS) {
|
|
// no collisions
|
|
return 0;
|
|
} else {
|
|
if ($strategy == 'App') {
|
|
// do nothing, the app will overwrite the VTE
|
|
return 1;
|
|
} else {
|
|
// VTE wins or last, which means the VTE in this case
|
|
return 2;
|
|
}
|
|
}
|
|
} else {
|
|
return 10;
|
|
}
|
|
}
|
|
|
|
// crmv@167773
|
|
protected function setRecordTs($module, $crmid, $ts, $mode = 'update') {
|
|
global $adb, $table_prefix;
|
|
|
|
$date = date('Y-m-d H:i:s', $ts);
|
|
|
|
$table = "{$table_prefix}_crmentity";
|
|
$index = "crmid";
|
|
|
|
// TODO: don't check for the name, but for $focus->tab_name
|
|
// but this might degrade performance, check first!
|
|
if ($module == 'Messages') {
|
|
$table = $table_prefix.'_messages';
|
|
$index = 'messagesid';
|
|
}
|
|
|
|
if ($mode == 'update') {
|
|
$sql = "UPDATE $table SET modifiedtime = ? WHERE $index = ?";
|
|
$params = array($date, $crmid);
|
|
} elseif ($mode == 'create') {
|
|
$sql = "UPDATE $table SET createdtime = ?, modifiedtime = ? WHERE $index = ?";
|
|
$params = array($date, $date, $crmid);
|
|
}
|
|
|
|
$r = $adb->pquery($sql, $params);
|
|
}
|
|
// crmv@167773e
|
|
|
|
protected function getRecordTs($module, $crmid) {
|
|
global $adb, $table_prefix, $touchInst, $touchUtils;
|
|
|
|
if ($module == 'Messages') {
|
|
$ts = strtotime(getSingleFieldValue($table_prefix.'_messages', 'modifiedtime', 'messagesid', $crmid));
|
|
} else {
|
|
$ts = strtotime(getSingleFieldValue($table_prefix.'_crmentity', 'modifiedtime', 'crmid', $crmid));
|
|
}
|
|
|
|
// try to use the changelog to skip non-field modifications (ex: related lists)
|
|
if ($this->useChangelog && isModuleInstalled('ChangeLog') && vtlib_isModuleActive('ChangeLog')) {
|
|
$clfocus = $touchUtils->getModuleInstance('ChangeLog');
|
|
if ($clfocus && $clfocus->isEnabled($module)) {
|
|
$changes = array();
|
|
// crmv@161363 crmv@164120
|
|
$res = $adb->limitPquery(
|
|
"SELECT modified_date, description
|
|
FROM {$clfocus->table_name}
|
|
WHERE parent_id = ?
|
|
ORDER BY modified_date DESC, {$clfocus->table_index} DESC",
|
|
0,50,
|
|
array($crmid)
|
|
);
|
|
// crmv@161363e crmv@164120e
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
while ($row = $adb->FetchByAssoc($res, -1, false)) {
|
|
$change = $this->parseChangeLog($row['description'], $module, $crmid);
|
|
$change['timestamp'] = strtotime($row['modified_date']); // crmv@167773
|
|
$changes[] = $change;
|
|
}
|
|
}
|
|
// now examine the logs (ordered from the newest)
|
|
if (count($changes) > 0) {
|
|
foreach ($changes as $change) {
|
|
if ($change['type'] == 'relation') continue;
|
|
if ($change['timestamp'] < $ts) {
|
|
$ts = $change['timestamp'];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $ts;
|
|
}
|
|
|
|
// the function in the ChangeLog module, returns partial informations
|
|
protected function parseChangeLog($description, $module, $crmid) {
|
|
$ret = array();
|
|
$description_elements = Zend_Json::decode($description);
|
|
|
|
if (is_array($description_elements)) {
|
|
if ($description_elements[0] == 'GenericChangeLog') {
|
|
$ret['type'] = 'generic';
|
|
$ret['text'] = $description_elements[1];
|
|
} elseif ($description_elements[0] == 'ModNotification_Relation') {
|
|
$ret['type'] = 'relation';
|
|
|
|
$tmp = $description_elements[1];
|
|
$tmp = explode(' LBL_LINKED_TO ',$tmp);
|
|
|
|
$tmp1 = substr($tmp[0],0,strpos($tmp[0],' ('));
|
|
$url1 = preg_match("/<a href=[\"'](.+)[\"']>/", $tmp1, $match1);
|
|
$info1 = parse_url($match1[1]);
|
|
parse_str($info1['query'], $info1);
|
|
$module1 = $info1['module'];
|
|
$record1 = $info1['record'];
|
|
|
|
$tmp2 = substr($tmp[1],0,strpos($tmp[1],' ('));
|
|
$url2 = preg_match("/<a href=[\"'](.+)[\"']>/", $tmp2, $match2);
|
|
$info2 = parse_url($match2[1]);
|
|
parse_str($info2['query'], $info2);
|
|
$module2 = $info2['module'];
|
|
$record2 = $info2['record'];
|
|
|
|
if ($module1 == $module && $record1 == $crmid) {
|
|
$ret['module'] = $module2;
|
|
$ret['crmid'] = $record2;
|
|
} else {
|
|
$ret['module'] = $module1;
|
|
$ret['crmid'] = $record1;
|
|
}
|
|
|
|
} else {
|
|
$ret['type'] = 'fields';
|
|
|
|
foreach($description_elements as $value){
|
|
$fieldname = $value[0];
|
|
|
|
$ret['changes'][$fieldname] = array(
|
|
'fieldname' => $fieldname,
|
|
// not needed here, so not retrieved
|
|
//'previous' => $previous_value,
|
|
//'current' => $current_value,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
public function preSaveRecord($module, $crmid, $mode, &$values) {
|
|
global $touchInst;
|
|
// fix for calendar
|
|
if ($module == 'Events') {
|
|
$start = substr($values['date_start'], 0, 10).'T'.substr($values['time_start'], 0, 5).':00';
|
|
$end = substr($values['due_date'], 0, 10).'T'.substr($values['time_end'], 0, 5).':00';
|
|
$tsStart = DateTime::createFromFormat('Y-m-d\TH:i:s', $start);
|
|
$tsEnd = DateTime::createFromFormat('Y-m-d\TH:i:s', $end);
|
|
$diff = $tsEnd->getTimestamp() - $tsStart->getTimestamp();
|
|
$delta_hours = floor($diff / 3600);
|
|
$delta_min = floor($diff / 60) % 60;
|
|
$values['duration_hours'] = $delta_hours;
|
|
$values['duration_minutes'] = $delta_min;
|
|
} else if ($module == 'Processes' && isset($values['dynaform'])) { // crmv@100158
|
|
$dynaform = Zend_Json::decode($values['dynaform']) ?: array();
|
|
$blocks = $dynaform['blocks'] ?: array();
|
|
foreach ($blocks as $blocks) {
|
|
$fields = $blocks['fields'] ?: array();
|
|
foreach ($fields as $field) {
|
|
$fieldname = $field['name'];
|
|
$value = $field['value'];
|
|
|
|
$type = $field['type']['name'];
|
|
if ($type === 'reference') {
|
|
$_REQUEST[$fieldname] = $value;
|
|
} else {
|
|
$_REQUEST[$fieldname] = $touchInst->touch2Field($module, $fieldname, $value, $field);
|
|
}
|
|
}
|
|
}
|
|
} else if ($module == 'HelpDesk') {
|
|
// crmv@104567
|
|
if (isset($values['signature'])) {
|
|
$path = 'storage/signatures/';
|
|
if (!file_exists($path)) {
|
|
mkdir($path);
|
|
chmod($path, 0755);
|
|
}
|
|
$img = $values['signature'];
|
|
$img = str_replace('data:image/png;base64,', '', $img);
|
|
$img = str_replace(' ', '+', $img);
|
|
$data = base64_decode($img);
|
|
$file = $path . 'signature_' . md5($crmid) . '.png';
|
|
$success = file_put_contents($file, $data);
|
|
if ($success) {
|
|
$values['signature'] = $file;
|
|
}
|
|
}
|
|
// crmv@104567e
|
|
}
|
|
}
|
|
|
|
public function postSaveRecord(&$returndata = null) {
|
|
// do nothing for now... you can extend the class and do something
|
|
}
|
|
|
|
protected function retrieveOldRecord($module, $crmid) {
|
|
global $touchInst, $touchUtils, $current_user;
|
|
|
|
$wsrecordid = vtws_getWebserviceEntityId($module, $crmid);
|
|
|
|
$wsname = 'retrieve';
|
|
if (isInventoryModule($module)) $wsname = 'retrieveInventory';
|
|
$response = $touchUtils->wsRequest($current_user->id,$wsname, array('id'=>$wsrecordid));
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function updateRecord($module, $crmid, $newRecord, &$returndata = null) {
|
|
global $touchInst, $touchUtils, $current_user;
|
|
|
|
// retrieve old record
|
|
$response = $this->retrieveOldRecord($module, $crmid);
|
|
if (!empty($response['error'])) {
|
|
return $response;
|
|
}
|
|
$record = $response['result'];
|
|
|
|
// other fix for stupid calendar
|
|
if ($module == 'Events') {
|
|
unset($record['contact_id']);
|
|
}
|
|
|
|
if (is_array($newRecord) && is_array($record)) {
|
|
// aggiungo blocco prodotti se manca
|
|
if (!array_key_exists('product_block', $record) && !empty($newRecord['product_block'])) $record['product_block'] = array();
|
|
foreach ($newRecord as $fldname => $fldval) {
|
|
if (array_key_exists($fldname, $record)) {
|
|
// to prevent notes html content from being stripped out
|
|
if ($module == 'Documents' && $fldname == 'notecontent') {
|
|
if (is_array($this->rawValues)) {
|
|
$fldval = $this->rawValues[$fldname];
|
|
}
|
|
}
|
|
$fldval = $touchInst->touch2Field($module, $fldname, $fldval);
|
|
$record[$fldname] = $fldval;
|
|
} else {
|
|
// crmv@100158
|
|
if ($module === 'Processes') {
|
|
$hidden_fields = array('processmaker', 'running_process');
|
|
if (in_array($fldname, $hidden_fields)) {
|
|
$fldval = $touchInst->touch2Field($module, $fldname, $fldval);
|
|
$record[$fldname] = $fldval;
|
|
}
|
|
}
|
|
// crmv@100158e
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($module == 'MyNotes' && !array_key_exists('assigned_user_id', $record)) {
|
|
$record['assigned_user_id'] = $current_user->id;
|
|
}
|
|
|
|
$wsclass = $touchInst->getWSClassInstance('UploadFile', $this->requestedVersion);
|
|
$uploads = $this->checkUploadedFile($module, $newRecord, $record, $wsclass); // crmv@182645
|
|
|
|
$response = $touchUtils->wsRequest($current_user->id,'update', array('element'=>$record) );
|
|
|
|
if ($response['success'] === true) {
|
|
// and delete the uploads
|
|
if ($uploads && count($uploads) > 0) {
|
|
$this->createDocsFromUploads($module, $crmid, $uploads, $wsclass); // crmv@182645
|
|
$wsclass->removeUploads($uploads);
|
|
}
|
|
|
|
// crmv@167773
|
|
// update the timestamp to the one passed by the app, to
|
|
if (!empty($touchInst->collision_strategy) && !empty($newRecord['timestamp'])) {
|
|
$this->setRecordTs($module, $crmid, $newRecord['timestamp'], 'update');
|
|
$record['modifiedtime'] = date('Y-m-d H:i:s', $newRecord['timestamp']);
|
|
}
|
|
// crmv@167773e
|
|
|
|
$record['crmid'] = $crmid;
|
|
$returndata = $record;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function createRecord($module, $tempCrmid, $newRecord, &$returndata = null) {
|
|
global $touchInst, $touchUtils, $current_user;
|
|
|
|
$updateRecord = array();
|
|
|
|
if (is_array($newRecord)) {
|
|
foreach ($newRecord as $fldname => $fldval) {
|
|
$fldval = $touchInst->touch2Field($module, $fldname, $fldval);
|
|
if ($fldval == '') continue;
|
|
$updateRecord[$fldname] = $fldval;
|
|
}
|
|
// aggiunta campi per calendario
|
|
if ($module == 'Calendar') {
|
|
$updateRecord['activitytype'] = 'Task';
|
|
$updateRecord['visibility'] = 'Standard';
|
|
}
|
|
|
|
if ($module == 'MyNotes' && !array_key_exists('assigned_user_id', $updateRecord)) {
|
|
$updateRecord['assigned_user_id'] = $current_user->id;
|
|
}
|
|
}
|
|
|
|
// add attachment uploaded from the app
|
|
$wsclass = $touchInst->getWSClassInstance('UploadFile', $this->requestedVersion);
|
|
$uploads = $this->checkUploadedFile($module, $newRecord, $updateRecord, $wsclass); // crmv@182645
|
|
|
|
$response = $touchUtils->wsRequest($current_user->id,'create',
|
|
array(
|
|
'elementType'=>$module,
|
|
'element'=>$updateRecord,
|
|
)
|
|
);
|
|
//$record = $response['result'];
|
|
|
|
// in caso di creazione ritorno il record appena creato
|
|
if ($response['success'] === true) {
|
|
list($modid, $recordid) = explode('x', $response['result']['id'], 2);
|
|
|
|
// and delete the uploads
|
|
if ($uploads && count($uploads) > 0) {
|
|
$this->createDocsFromUploads($module, $recordid, $uploads, $wsclass); // crmv@182645
|
|
$wsclass->removeUploads($uploads);
|
|
}
|
|
|
|
$focus = $touchUtils->getModuleInstance($module);
|
|
$focus->retrieve_entity_info($recordid, $module);
|
|
$record = $focus->column_fields;
|
|
foreach ($record as $fldname=>$fldvalue) {
|
|
$record[$fldname] = $touchInst->field2Touch($module, $fldname, $fldvalue);
|
|
}
|
|
|
|
$touchInst->setTempId($current_user-id, null, $recordid, $tempCrmid); // crmv@106521
|
|
|
|
// crmv@167773
|
|
// update the timestamp to the one passed by the app, to
|
|
if (!empty($touchInst->collision_strategy) && !empty($newRecord['timestamp'])) {
|
|
$this->setRecordTs($module, $crmid, $newRecord['timestamp'], 'create');
|
|
$record['createdtime'] = date('Y-m-d H:i:s', $newRecord['timestamp']);
|
|
$record['modifiedtime'] = date('Y-m-d H:i:s', $newRecord['timestamp']);
|
|
}
|
|
// crmv@167773e
|
|
|
|
$record['crmid'] = $recordid;
|
|
$record['temp_crmid'] = $tempCrmid; // pass old temporary id so the app knows what to update
|
|
$returndata = $record;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
protected function checkUploadedFile($module, $inputValues, &$outputValues, $wsclass) { // crmv@182645
|
|
|
|
$uploads = array_filter(array_unique(explode(',', $inputValues['upload_ids'])), function($v) {
|
|
return $v !== "" && $v >= 0;
|
|
});
|
|
|
|
$docMods = array('Documents', 'Myfiles'); // crmv@182645
|
|
|
|
// if it's a document module, include the file info into the record
|
|
// otherwise link it as a document later
|
|
if (in_array($module, $docMods) && count($uploads) > 0) { // crmv@182645
|
|
|
|
// add other uploaded files
|
|
// retrieve the file information
|
|
$list = $wsclass->getTouchUploadList($uploads);
|
|
$base = 'storage/touch_uploads/';
|
|
$_FILES = array();
|
|
foreach ($list as $uinfo) {
|
|
if (is_readable($base.$uinfo['path'])) {
|
|
$_FILES['filename'] = array(
|
|
'name' => $uinfo['realname'] ?: $uinfo['path'],
|
|
'tmp_name' => $base.$uinfo['path'],
|
|
'type' => $uinfo['filetype'],
|
|
'size' => filesize($base.$uinfo['path'])
|
|
);
|
|
$_POST['copy_not_move'] = 'true';
|
|
$outputValues['filelocationtype'] = 'I';
|
|
unset($outputValues['upload_ids']);
|
|
return $uploads;
|
|
break;
|
|
}
|
|
}
|
|
// crmv@182645
|
|
} elseif (count($uploads) > 0) {
|
|
return $uploads;
|
|
}
|
|
// crmv@182645e
|
|
return false;
|
|
}
|
|
|
|
// crmv@182645
|
|
protected function createDocsFromUploads($module, $crmid, $uploads, $wsclass) {
|
|
|
|
$docMods = array('Documents', 'Myfiles');
|
|
$base = 'storage/touch_uploads/';
|
|
$docids = array();
|
|
|
|
// check if already saved with the record
|
|
if (in_array($module, $docMods)) return $docids;
|
|
|
|
// otherwise, create a document and link it to the record
|
|
|
|
$docFocus = CRMEntity::getInstance('Documents');
|
|
|
|
$list = $wsclass->getTouchUploadList($uploads);
|
|
foreach ($list as $uinfo) {
|
|
$docid = $docFocus->createDocumentFromPathFile($base.$uinfo['path'], null, $crmid);
|
|
if ($docid > 0) {
|
|
$docids[] = $docid;
|
|
}
|
|
}
|
|
|
|
return $docids;
|
|
}
|
|
// crmv@182645e
|
|
|
|
/**
|
|
* Executes extra operations after the save operation
|
|
*/
|
|
protected function processPostActions($record, $postActions, &$message) {
|
|
global $touchInst, $touchUtils;
|
|
|
|
$returnok = true;
|
|
|
|
foreach ($postActions as $action) {
|
|
$actionName = $action['name'];
|
|
$actionParams = $action['params'];
|
|
|
|
if ($actionName == 'LinkToRecord' || $actionName == 'LinkToRecordRev') {
|
|
$linkModule = $actionParams['module'];
|
|
$linkCrmid = $actionParams['crmid'];
|
|
if (empty($linkModule) || empty($linkCrmid)) {
|
|
$returnok = false;
|
|
$message = "Wrong parameters for post action: $actionName";
|
|
break;
|
|
} else {
|
|
if ($actionName == 'LinkToRecord') {
|
|
$req = array(
|
|
'module_from' => $record['record_module'],
|
|
'crmid_from' => $record['record_id'],
|
|
'module_to' => $linkModule,
|
|
'crmid_to' => $linkCrmid,
|
|
);
|
|
} elseif ($actionName == 'LinkToRecordRev') {
|
|
$req = array(
|
|
'module_to' => $record['record_module'],
|
|
'crmid_to' => $record['record_id'],
|
|
'module_from' => $linkModule,
|
|
'crmid_from' => $linkCrmid,
|
|
);
|
|
}
|
|
$r = $this->subcall('LinkModules', $req);
|
|
if (!$r['success']) {
|
|
$returnok = false;
|
|
$message = "Post action failed: ".$r['error'];
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$returnok = false;
|
|
$message = "Unknown post action: $actionName";
|
|
break;
|
|
}
|
|
}
|
|
return $returnok;
|
|
}
|
|
|
|
} |