1<?php 2// Copyright (C) 2010-2018 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19 20/** 21 * Data Exchange - synchronization with external applications (incoming data) 22 * 23 * @copyright Copyright (C) 2010-2018 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27 28class SynchroExceptionNotStarted extends CoreException 29{ 30} 31 32class SynchroDataSource extends cmdbAbstractObject 33{ 34 public static function Init() 35 { 36 $aParams = array 37 ( 38 "category" => "core/cmdb,view_in_gui,grant_by_profile", 39 "key_type" => "autoincrement", 40 "name_attcode" => array('name'), 41 "state_attcode" => "", 42 "reconc_keys" => array(), 43 "db_table" => "priv_sync_datasource", 44 "db_key_field" => "id", 45 "db_finalclass_field" => "realclass", 46 "display_template" => "", 47 "icon" => "../images/synchro.png", 48 ); 49 MetaModel::Init_Params($aParams); 50 //MetaModel::Init_InheritAttributes(); 51 MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); 52 MetaModel::Init_AddAttribute(new AttributeText("description", array("allowed_values" => null, "sql" => "description", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); 53 MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('implementation,production,obsolete'), "sql"=>"status", "default_value"=>"implementation", "is_null_allowed"=>false, "depends_on"=>array()))); 54 MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=>null, "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); 55 MetaModel::Init_AddAttribute(new AttributeExternalKey("notify_contact_id", array("targetclass"=>"Contact", "jointype"=>null, "allowed_values"=>null, "sql"=>"notify_contact_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); 56 MetaModel::Init_AddAttribute(new AttributeClass("scope_class", array("class_category"=>"bizmodel,addon/authentication,application", "more_values"=>"", "sql"=>"scope_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); 57 MetaModel::Init_AddAttribute(new AttributeString("database_table_name", array("allowed_values"=>null, "sql"=>"database_table_name", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array(), "validation_pattern" => "^[A-Za-z0-9_]*$"))); 58 59 // Declared here for a future usage, but ignored so far 60 MetaModel::Init_AddAttribute(new AttributeString("scope_restriction", array("allowed_values"=>null, "sql"=>"scope_restriction", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 61 62 //MetaModel::Init_AddAttribute(new AttributeDateTime("last_synchro_date", array("allowed_values"=>null, "sql"=>"last_synchro_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); 63 64 // Format: seconds (int) 65 MetaModel::Init_AddAttribute(new AttributeDuration("full_load_periodicity", array("allowed_values"=>null, "sql"=>"full_load_periodicity", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); 66 67// MetaModel::Init_AddAttribute(new AttributeString("reconciliation_list", array("allowed_values"=>null, "sql"=>"reconciliation_list", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); 68 MetaModel::Init_AddAttribute(new AttributeEnum("reconciliation_policy", array("allowed_values"=>new ValueSetEnum('use_primary_key,use_attributes'), "sql"=>"reconciliation_policy", "default_value"=>"use_attributes", "is_null_allowed"=>false, "depends_on"=>array()))); 69 MetaModel::Init_AddAttribute(new AttributeEnum("action_on_zero", array("allowed_values"=>new ValueSetEnum('create,error'), "sql"=>"action_on_zero", "default_value"=>"create", "is_null_allowed"=>false, "depends_on"=>array()))); 70 MetaModel::Init_AddAttribute(new AttributeEnum("action_on_one", array("allowed_values"=>new ValueSetEnum('update,error'), "sql"=>"action_on_one", "default_value"=>"update", "is_null_allowed"=>false, "depends_on"=>array()))); 71 MetaModel::Init_AddAttribute(new AttributeEnum("action_on_multiple", array("allowed_values"=>new ValueSetEnum('take_first,create,error'), "sql"=>"action_on_multiple", "default_value"=>"error", "is_null_allowed"=>false, "depends_on"=>array()))); 72 73 MetaModel::Init_AddAttribute(new AttributeEnum("delete_policy", array("allowed_values"=>new ValueSetEnum('ignore,delete,update,update_then_delete'), "sql"=>"delete_policy", "default_value"=>"ignore", "is_null_allowed"=>false, "depends_on"=>array()))); 74 MetaModel::Init_AddAttribute(new AttributeString("delete_policy_update", array("allowed_values"=>null, "sql"=>"delete_policy_update", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 75 76 // Format: seconds (unsigned int) 77 MetaModel::Init_AddAttribute(new AttributeDuration("delete_policy_retention", array("allowed_values"=>null, "sql"=>"delete_policy_retention", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 78 79 MetaModel::Init_AddAttribute(new AttributeLinkedSet("attribute_list", array("linked_class"=>"SynchroAttribute", "ext_key_to_me"=>"sync_source_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array(), 'tracking_level' => LINKSET_TRACKING_DETAILS))); 80 // Not used yet ! 81 MetaModel::Init_AddAttribute(new AttributeEnum("user_delete_policy", array("allowed_values"=>new ValueSetEnum('everybody,administrators,nobody'), "sql"=>"user_delete_policy", "default_value"=>"nobody", "is_null_allowed"=>true, "depends_on"=>array()))); 82 83 MetaModel::Init_AddAttribute(new AttributeURL("url_icon", array("allowed_values"=>null, "sql"=>"url_icon", "default_value"=>null, "is_null_allowed"=>true, "target"=> '_top', "depends_on"=>array()))); 84 // The field below is not a real URL since it can contain placeholders like $replica->primary_key$ which are not syntactically allowed in a real URL 85 MetaModel::Init_AddAttribute(new AttributeString("url_application", array("allowed_values"=>null, "sql"=>"url_application", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 86 87 // Display lists 88 MetaModel::Init_SetZListItems('details', array( 89 'col:0'=> array( 90 'fieldset:SynchroDataSource:Description' => array('name','description','status','scope_class','user_id','notify_contact_id','url_icon','url_application', 'database_table_name')), 91 'col:1'=> array( 92 'fieldset:SynchroDataSource:Reconciliation' => array('reconciliation_policy','action_on_zero','action_on_one','action_on_multiple'), 93 'fieldset:SynchroDataSource:Deletion' => array('user_delete_policy','full_load_periodicity','delete_policy','delete_policy_update','delete_policy_retention')) 94 ) 95 ); 96 MetaModel::Init_SetZListItems('list', array('scope_class', 'status', 'user_id', 'full_load_periodicity')); // Attributes to be displayed for a list 97 // Search criteria 98 MetaModel::Init_SetZListItems('standard_search', array('name', 'status', 'scope_class', 'user_id')); // Criteria of the std search form 99 MetaModel::Init_SetZListItems('default_search', array('name', 'status', 'scope_class')); // Criteria of the defaut search form 100 // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 101 } 102 103 public function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) 104 { 105 if (!$this->IsNew()) 106 { 107 $this->Set('database_table_name', $this->GetDataTable()); 108 } 109 return parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams); 110 } 111 112 public function DisplayBareRelations(WebPage $oPage, $bEditMode = false) 113 { 114 if (!$this->IsNew()) 115 { 116 $oPage->SetCurrentTab(Dict::S('Core:SynchroAttributes')); 117 $oAttributeSet = $this->Get('attribute_list'); 118 $aAttributes = array(); 119 120 while($oAttribute = $oAttributeSet->Fetch()) 121 { 122 $aAttributes[$oAttribute->Get('attcode')] = $oAttribute; 123 } 124 // Columns of the form 125 $aAttribs = array(); 126 foreach(array('attcode', 'reconciliation', 'update', 'update_policy', 'reconciliation_attcode') as $s ) 127 { 128 $aAttribs[$s] = array( 'label' => Dict::S("Core:SynchroAtt:$s"), "description" => Dict::S("Core:SynchroAtt:$s+")); 129 } 130 // Rows of the form 131 $aValues = array(); 132 foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef) 133 { 134 if ($oAttDef->IsWritable()) 135 { 136 if (isset($aAttributes[$sAttCode])) 137 { 138 $oAttribute = $aAttributes[$sAttCode]; 139 } 140 else 141 { 142 if ($oAttDef->IsExternalKey()) 143 { 144 $oAttribute = new SynchroAttExtKey(); 145 $oAttribute->Set('reconciliation_attcode', ''); // Blank means by pkey 146 } 147 elseif ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) 148 { 149 $oAttribute = new SynchroAttLinkSet(); 150 // Todo - add these settings into the form 151 $oAttribute->Set('row_separator', MetaModel::GetConfig()->Get('link_set_item_separator')); 152 $oAttribute->Set('attribute_separator', MetaModel::GetConfig()->Get('link_set_attribute_separator')); 153 $oAttribute->Set('value_separator', MetaModel::GetConfig()->Get('link_set_value_separator')); 154 $oAttribute->Set('attribute_qualifier', MetaModel::GetConfig()->Get('link_set_attribute_qualifier')); 155 } 156 elseif ($oAttDef->IsScalar()) 157 { 158 $oAttribute = new SynchroAttribute(); 159 } 160 else 161 { 162 $oAttribute = null; 163 } 164 165 if (!is_null($oAttribute)) 166 { 167 $oAttribute->Set('sync_source_id', $this->GetKey()); 168 $oAttribute->Set('attcode', $sAttCode); 169 $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0); 170 $oAttribute->Set('update', 1); 171 $oAttribute->Set('update_policy', 'master_locked'); 172 } 173 } 174 if (!is_null($oAttribute)) 175 { 176 if (!$bEditMode) 177 { 178 // Read-only mode 179 $aRow['reconciliation'] = $oAttribute->Get('reconcile') == 1 ? Dict::S('Core:SynchroReconcile:Yes') : Dict::S('Core:SynchroReconcile:No'); 180 $aRow['update'] = $oAttribute->Get('update') == 1 ? Dict::S('Core:SynchroUpdate:Yes') : Dict::S('Core:SynchroUpdate:No'); 181 $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode')).' ('.$oAttribute->Get('attcode').')'; 182 $aRow['update_policy'] = $oAttribute->GetAsHTML('update_policy'); 183 if ($oAttDef->IsExternalKey()) 184 { 185 $sReconciliationAttCode = $oAttribute->Get('reconciliation_attcode'); 186 switch($sReconciliationAttCode) 187 { 188 case '': 189 $sDisplayReconciliationAttCode = Dict::S('Core:SynchroAttExtKey:ReconciliationById'); 190 break; 191 192 default: 193 $sDisplayReconciliationAttCode = MetaModel::GetLabel($oAttDef->GetTargetClass(), $sReconciliationAttCode); 194 } 195 $aRow['reconciliation_attcode'] = $sDisplayReconciliationAttCode; 196 } 197 else 198 { 199 $aRow['reconciliation_attcode'] = ' '; 200 } 201 } 202 else 203 { 204 // Edit mode 205 $sAttCode = $oAttribute->Get('attcode'); 206 $sChecked = $oAttribute->Get('reconcile') == 1 ? 'checked' : ''; 207 $aRow['reconciliation'] = "<input type=\"checkbox\" name=\"reconciliation[$sAttCode]\" $sChecked/>"; 208 $sChecked = $oAttribute->Get('update') == 1 ? 'checked' : ''; 209 $aRow['update'] = "<input type=\"checkbox\" name=\"update[$sAttCode]\" $sChecked/>"; 210 $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode')).' ('.$oAttribute->Get('attcode').')'; 211 $oUpdateAttDef = MetaModel::GetAttributeDef(get_class($oAttribute), 'update_policy'); 212 $aRow['update_policy'] = cmdbAbstractObject::GetFormElementForField($oPage, get_class($oAttribute), 'update_policy', $oUpdateAttDef, $oAttribute->Get('update_policy'), '', 'update_policy_'.$sAttCode, "[$sAttCode]"); 213 if ($oAttDef->IsExternalKey()) 214 { 215 $aRow['reconciliation_attcode'] = $oAttribute->GetReconciliationFormElement($oAttDef->GetTargetClass(), "attr_reconciliation_attcode[$sAttCode]"); 216 } 217 else 218 { 219 $aRow['reconciliation_attcode'] = ' '; 220 } 221 } 222 $aValues[] = $aRow; 223 } 224 } 225 } 226 $oPage->p(Dict::Format('Class:SynchroDataSource:DataTable', $this->GetDataTable())); 227 $oPage->Table($aAttribs, $aValues); 228 $this->DisplayStatusTab($oPage); 229 } 230 parent::DisplayBareRelations($oPage, $bEditMode); 231 } 232 233 /** 234 * Displays the status (SynchroLog) of the datasource in a graphical manner 235 * @param $oPage WebPage 236 * @return void 237 */ 238 protected function DisplayStatusTab(WebPage $oPage) 239 { 240 $oPage->SetCurrentTab(Dict::S('Core:SynchroStatus')); 241 242 $sSelectSynchroLog = 'SELECT SynchroLog WHERE sync_source_id = :source_id'; 243 $oSetSynchroLog = new CMDBObjectSet(DBObjectSearch::FromOQL($sSelectSynchroLog), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey())); 244 $oSetSynchroLog->SetLimit(100); // Display only the 100 latest runs 245 246 if ($oSetSynchroLog->Count() > 0) 247 { 248 $oLastLog = $oSetSynchroLog->Fetch(); 249 $sStartDate = $oLastLog->GetAsHTML('start_date'); 250 $oLastLog->Get('stats_nb_replica_seen'); 251 $iLastLog = 0; 252 $iDSid = $this->GetKey(); 253 if ($oLastLog->Get('status') == 'running') 254 { 255 // Still running ! 256 $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroRunningStartedOn_Date', $sStartDate).'</h2>'); 257 } 258 else 259 { 260 $sEndDate = $oLastLog->GetAsHTML('end_date'); 261 $iLastLog = $oLastLog->GetKey(); 262 $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroEndedOn_Date', $sEndDate).'</h2>'); 263 $sOQL = "SELECT SynchroReplica WHERE sync_source_id=$iDSid"; 264 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL)); 265 $iCountAllReplicas = $oSet->Count(); 266 $sAllReplicas = "<a href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\">$iCountAllReplicas</a>"; 267 $sOQL = "SELECT SynchroReplica WHERE sync_source_id=$iDSid AND status_last_error !=''"; 268 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL)); 269 $iCountAllErrors = $oSet->Count(); 270 $sAllErrors = "<a href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\">$iCountAllErrors</a>"; 271 $sOQL = "SELECT SynchroReplica WHERE sync_source_id=$iDSid AND status_last_warning !=''"; 272 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL)); 273 $iCountAllWarnings = $oSet->Count(); 274 $sAllWarnings = "<a href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\">$iCountAllWarnings</a>"; 275 $oPage->p('<h2>'.Dict::Format('Core:Synchro:ListReplicas_AllReplicas_Errors_Warnings', $sAllReplicas, $sAllErrors, $sAllWarnings).'</h2>'); 276 } 277 278 $oPage->add('<table class="synoptics"><tr><td style="color:#333;vertical-align:top">'); 279 280 // List all the log entries for the user to select 281 $oPage->add('<h2 style="line-height:55px;">'.Dict::S('Core:Synchro:History').'</h2>'); 282 $oSetSynchroLog->Rewind(); 283 $oPage->add('<select size="25" onChange="UpdateSynoptics(this.value);">'); 284 $sSelected = ' selected'; // First log is selected by default 285 $sScript = "var aSynchroLog = {\n"; 286 while($oLog = $oSetSynchroLog->Fetch()) 287 { 288 $sLogTitle = Dict::Format('Core:SynchroLogTitle', $oLog->Get('status'), $oLog->GetEditValue('start_date')); 289 $oPage->add('<option value="'.$oLog->GetKey().'" '.$sSelected.'>'.$sLogTitle.'</option>'); 290 $sSelected = ''; // only the first log is selected by default 291 $aData = $this->ProcessLog($oLog); 292 $sScript .= '"'.$oLog->GetKey().'": '.json_encode($aData).",\n"; 293 } 294 $sScript .= "end: 'Done'"; 295 $sScript .= "};\n"; 296 $sScript .= <<<EOF 297 var sLastLog = '$iLastLog'; 298 function ToggleSynoptics(sId, bShow) 299 { 300 if (bShow) 301 { 302 $(sId).show(); 303 } 304 else 305 { 306 $(sId).hide(); 307 } 308 } 309 310 function UpdateSynoptics(id) 311 { 312 var aValues = aSynchroLog[id]; 313 if (aValues == undefined) return; 314 315 for (var sKey in aValues) 316 { 317 $('#c_'+sKey).html(aValues[sKey]); 318 var fOpacity = (aValues[sKey] == 0) ? 0.3 : 1; 319 $('#'+sKey).fadeTo("slow", fOpacity); 320 } 321 //alert('id = '+id+', lastLog='+sLastLog+', id==sLastLog: '+(id==sLastLog)+' obj_updated_errors: '+aValues['obj_updated_errors']); 322 if ( (id == sLastLog) && (aValues['obj_new_errors'] > 0) ) 323 { 324 $('#new_errors_link').show(); 325 } 326 else 327 { 328 $('#new_errors_link').hide(); 329 } 330 331 if ( (id == sLastLog) && (aValues['obj_updated_errors'] > 0) ) 332 { 333 $('#updated_errors_link').show(); 334 } 335 else 336 { 337 $('#updated_errors_link').hide(); 338 } 339 340 if ( (id == sLastLog) && (aValues['obj_disappeared_errors'] > 0) ) 341 { 342 $('#disappeared_errors_link').show(); 343 } 344 else 345 { 346 $('#disappeared_errors_link').hide(); 347 } 348 349 ToggleSynoptics('#cw_obj_created_warnings', aValues['obj_created_warnings'] > 0); 350 ToggleSynoptics('#cw_obj_new_updated_warnings', aValues['obj_new_updated_warnings'] > 0); 351 ToggleSynoptics('#cw_obj_new_unchanged_warnings', aValues['obj_new_unchanged_warnings'] > 0); 352 ToggleSynoptics('#cw_obj_updated_warnings', aValues['obj_updated_warnings'] > 0); 353 ToggleSynoptics('#cw_obj_unchanged_warnings', aValues['obj_unchanged_warnings'] > 0); 354 $('#status_traces').html(aValues['traces']); 355 } 356EOF 357; 358 $oPage->add_script($sScript); 359 $oPage->add('</select>'); 360 361 $oPage->add('</td><td style="vertical-align:top;">'); 362 363 // Now build the big "synoptics" view 364 $aData = $this->ProcessLog($oLastLog); 365 366 $sNbReplica = $this->GetIcon()." ".Dict::Format('Core:Synchro:Nb_Replica', "<span id=\"c_nb_replica_total\">{$aData['nb_replica_total']}</span>"); 367 $sNbObjects = MetaModel::GetClassIcon($this->GetTargetClass())." ".Dict::Format('Core:Synchro:Nb_Class:Objects', $this->GetTargetClass(), "<span id=\"c_nb_obj_total\">{$aData['nb_obj_total']}</span>"); 368 $oPage->add( 369<<<EOF 370 <table class="synoptics"> 371 <tr class="synoptics_header"> 372 <td>$sNbReplica</td><td> </td><td>$sNbObjects</td> 373 </tr> 374 <tr> 375EOF 376); 377 $sBaseOQL = "SELECT SynchroReplica WHERE sync_source_id=".$this->GetKey()." AND status_last_error!=''"; 378 $oPage->add($this->HtmlBox('repl_ignored', $aData, '#999').'<td colspan="2"> </td>'); 379 $oPage->add("</tr>\n<tr>"); 380 $oPage->add($this->HtmlBox('repl_disappeared', $aData, '#630', 'rowspan="4"').'<td rowspan="4" class="arrow">=></td>'.$this->HtmlBox('obj_disappeared_no_action', $aData, '#333')); 381 $oPage->add("</tr>\n<tr>"); 382 $oPage->add($this->HtmlBox('obj_deleted', $aData, '#000')); 383 $oPage->add("</tr>\n<tr>"); 384 $oPage->add($this->HtmlBox('obj_obsoleted', $aData, '#630')); 385 $oPage->add("</tr>\n<tr>"); 386 $sOQL = urlencode($sBaseOQL." AND status='obsolete'"); 387 $oPage->add($this->HtmlBox('obj_disappeared_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"disappeared_errors_link\">Show</a>")); 388 $oPage->add("</tr>\n<tr>"); 389 $oPage->add($this->HtmlBox('repl_existing', $aData, '#093', 'rowspan="3"').'<td rowspan="3" class="arrow">=></td>'.$this->HtmlBox('obj_unchanged', $aData, '#393')); 390 $oPage->add("</tr>\n<tr>"); 391 $oPage->add($this->HtmlBox('obj_updated', $aData, '#3C3')); 392 $oPage->add("</tr>\n<tr>"); 393 $sOQL = urlencode($sBaseOQL." AND status='modified'"); 394 $oPage->add($this->HtmlBox('obj_updated_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"updated_errors_link\">Show</a>")); 395 $oPage->add("</tr>\n<tr>"); 396 $oPage->add($this->HtmlBox('repl_new', $aData, '#339', 'rowspan="4"').'<td rowspan="4" class="arrow">=></td>'.$this->HtmlBox('obj_new_unchanged', $aData, '#393')); 397 $oPage->add("</tr>\n<tr>"); 398 $oPage->add($this->HtmlBox('obj_new_updated', $aData, '#3C3')); 399 $oPage->add("</tr>\n<tr>"); 400 $oPage->add($this->HtmlBox('obj_created', $aData, '#339')); 401 $oPage->add("</tr>\n<tr>"); 402 $sOQL = urlencode($sBaseOQL." AND status='new'"); 403 $oPage->add($this->HtmlBox('obj_new_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"new_errors_link\">Show</a>")); 404 $oPage->add("</tr>\n</table>\n"); 405 $oPage->add('</td></tr></table>'); 406 $oPage->add('<div id="status_traces" style="overflow-x:auto"></div>'); 407 $oPage->add_ready_script("UpdateSynoptics('$iLastLog')"); 408 } 409 else 410 { 411 $oPage->p('<h2>'.Dict::S('Core:Synchro:NeverRun').'</h2>'); 412 } 413 } 414 415 protected function HtmlBox($sId, $aData, $sColor, $sHTMLAttribs = '', $sErrorLink = '') 416 { 417 $iCount = $aData[$sId]; 418 $sCount = "<span id=\"c_{$sId}\">$iCount</span>"; 419 $sLabel = Dict::Format('Core:Synchro:label_'.$sId, $sCount); 420 $sOpacity = ($iCount==0) ? "opacity:0.3;" : ""; 421 if (isset($aData[$sId.'_warnings'])) 422 { 423 $sLabel .= " <span id=\"cw_{$sId}_warnings\"><img src=\"../images/error.png\" style=\"vertical-align:middle\"/> (<span id=\"c_{$sId}_warnings\">".$aData[$sId.'_warnings']."</span>)</span>"; 424 } 425 426 return "<td id=\"$sId\" style=\"background-color:$sColor;$sOpacity;\" {$sHTMLAttribs}>{$sLabel}{$sErrorLink}</td>"; 427 } 428 429 protected function ProcessLog($oLastLog) 430 { 431 $aData = array( 432 'obj_deleted' => $oLastLog->Get('stats_nb_obj_deleted'), 433 'obj_obsoleted' => $oLastLog->Get('stats_nb_obj_obsoleted'), 434 'obj_disappeared_errors' => $oLastLog->Get('stats_nb_obj_obsoleted_errors') + $oLastLog->Get('stats_nb_obj_deleted_errors'), 435 'obj_disappeared_no_action' => $oLastLog->Get('stats_nb_replica_disappeared_no_action'), 436 'obj_updated' => $oLastLog->Get('stats_nb_obj_updated'), 437 'obj_updated_warnings' => $oLastLog->Get('stats_nb_obj_updated_warnings'), 438 'obj_updated_errors' => $oLastLog->Get('stats_nb_obj_updated_errors'), 439 'obj_new_updated' => $oLastLog->Get('stats_nb_obj_new_updated'), 440 'obj_new_updated_warnings' => $oLastLog->Get('stats_nb_obj_new_updated_warnings'), 441 'obj_new_unchanged' => $oLastLog->Get('stats_nb_obj_new_unchanged'), 442 'obj_created' => $oLastLog->Get('stats_nb_obj_created'), 443 'obj_created_warnings' => $oLastLog->Get('stats_nb_obj_created_warnings'), 444 'obj_created_errors' => $oLastLog->Get('stats_nb_obj_created_errors'), 445 'obj_unchanged_warnings' => $oLastLog->Get('stats_nb_obj_unchanged_warnings'), 446 ); 447 $oLastLog->Get('stats_nb_replica_reconciled_errors'); 448 $iDisappeared = $aData['obj_disappeared_errors'] + $aData['obj_obsoleted'] + $aData['obj_deleted'] + $aData['obj_disappeared_no_action']; 449 $aData['repl_disappeared'] = $iDisappeared; 450 $iNewErrors = $aData['obj_created_errors'] + $oLastLog->Get('stats_nb_replica_reconciled_errors'); 451 $aData['obj_new_errors'] = $iNewErrors; 452 $iNew = $aData['obj_created'] + $iNewErrors + $aData['obj_new_updated'] + $aData['obj_new_unchanged']; 453 $aData['repl_new'] = $iNew; 454 $iExisting = $oLastLog->Get('stats_nb_replica_seen') - $iNew; 455 $aData['repl_existing'] = $iExisting; 456 $aData['obj_unchanged'] = $iExisting - $aData['obj_updated'] - $aData['obj_updated_errors']; 457 $iIgnored = $oLastLog->Get('stats_nb_replica_total') - $iNew - $iExisting - $iDisappeared; 458 $aData['repl_ignored'] = $iIgnored; 459 $aData['nb_obj_total'] = $iNew + $iExisting + $iDisappeared; 460 $aData['nb_replica_total'] = $aData['nb_obj_total'] + $iIgnored; 461 if(strlen($oLastLog->Get('traces')) > 0) 462 { 463 $aData['traces'] = '<fieldset><legend>Debug traces</legend><pre>'.htmlentities($oLastLog->Get('traces'), ENT_QUOTES, 'UTF-8').'</pre></fieldset>'; 464 } 465 else 466 { 467 $aData['traces'] = ''; 468 } 469 return $aData; 470 } 471 472 public function GetIcon($bImgTag = true, $sMoreStyles = '') 473 { 474 if ($this->Get('url_icon') == '') return MetaModel::GetClassIcon(get_class($this), $bImgTag); 475 if ($bImgTag) 476 { 477 return "<img src=\"".$this->Get('url_icon')."\" style=\"vertical-align:middle;$sMoreStyles\"/>"; 478 479 } 480 return $this->Get('url_icon'); 481 } 482 483 /** 484 * Get the actual hyperlink to the remote application for the given replica and dest object 485 * 486 * @param \DBObject $oDestObj 487 * @param \SynchroReplica $oReplica 488 * 489 * @return string 490 */ 491 public function GetApplicationUrl(DBObject $oDestObj, SynchroReplica $oReplica) 492 { 493 if ($this->Get('url_application') == '') return ''; 494 $aSearches = array(); 495 $aReplacements = array(); 496 foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef) 497 { 498 if ($oAttDef->IsScalar()) 499 { 500 $aSearches[] = '$this->'.$sAttCode.'$'; 501 $aReplacements[] = $oDestObj->Get($sAttCode); 502 } 503 } 504 $aData = $oReplica->LoadExtendedDataFromTable($this->GetDataTable()); 505 506 foreach($aData as $sColumn => $value) 507 { 508 $aSearches[] = '$replica->'.$sColumn.'$'; 509 $aReplacements[] = $value; 510 } 511 return str_replace($aSearches, $aReplacements, $this->Get('url_application')); 512 } 513 514 public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') 515 { 516 if ( (($sAttCode == 'scope_class') || ($sAttCode == 'database_table_name')) && (!$this->IsNew())) 517 { 518 return OPT_ATT_READONLY; 519 } 520 return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState); 521 } 522 523 public function UpdateObjectFromPostedForm($sFormPrefix = '', $sAttList = null, $aAttFlags = array()) 524 { 525 parent::UpdateObjectFromPostedForm($sFormPrefix, $sAttList, $aAttFlags); 526 // And now read the other post parameters... 527 $oAttributeSet = $this->Get('attribute_list'); 528 $aAttributes = array(); 529 while($oAttribute = $oAttributeSet->Fetch()) 530 { 531 $aAttributes[$oAttribute->Get('attcode')] = $oAttribute; 532 } 533 $aReconcile = utils::ReadPostedParam('reconciliation', array()); 534 $aUpdate = utils::ReadPostedParam('update', array()); 535 $aUpdatePolicy = utils::ReadPostedParam('attr_update_policy', array()); 536 $aReconciliation = utils::ReadPostedParam('attr_reconciliation_attcode', array()); 537 // update_policy cannot be empty, so there is one entry per attribute, use this to iterate 538 // through all the writable attributes 539 foreach($aUpdatePolicy as $sAttCode => $sValue) 540 { 541 if(!isset($aAttributes[$sAttCode])) 542 { 543 $oAttribute = $this->CreateSynchroAtt($sAttCode); 544 } 545 else 546 { 547 $oAttribute = $aAttributes[$sAttCode]; 548 } 549 $bReconcile = 0; 550 if (isset($aReconcile[$sAttCode])) 551 { 552 $bReconcile = $aReconcile[$sAttCode] == 'on' ? 1 : 0; 553 } 554 $bUpdate = 0 ; // Default / initial value 555 if (isset($aUpdate[$sAttCode])) 556 { 557 $bUpdate = $aUpdate[$sAttCode] == 'on' ? 1 : 0; 558 } 559 $oAttribute->Set('reconcile', $bReconcile); 560 $oAttribute->Set('update', $bUpdate); 561 $oAttribute->Set('update_policy', $sValue); 562 if ($oAttribute instanceof SynchroAttExtKey) 563 { 564 $oAttribute->Set('reconciliation_attcode', $aReconciliation[$sAttCode]); 565 } 566 elseif ($oAttribute instanceof SynchroAttLinkSet) 567 { 568 } 569 $oAttributeSet->AddItem($oAttribute); 570 } 571 $this->Set('attribute_list', $oAttributeSet); 572 } 573 574 /** 575 * Creates a new SynchroAttXXX object in memory with the default values 576 * 577 * @param string $sAttCode 578 * 579 * @return \SynchroAttExtKey|\SynchroAttLinkSet|\SynchroAttribute 580 */ 581 protected function CreateSynchroAtt($sAttCode) 582 { 583 $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode); 584 if ($oAttDef->IsExternalKey()) 585 { 586 $oAttribute = new SynchroAttExtKey(); 587 $oAttribute->Set('reconciliation_attcode', ''); // Blank means by pkey 588 } 589 elseif ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) 590 { 591 $oAttribute = new SynchroAttLinkSet(); 592 // Todo - set those value from the form 593 $oAttribute->Set('row_separator', MetaModel::GetConfig()->Get('link_set_item_separator')); 594 $oAttribute->Set('attribute_separator', MetaModel::GetConfig()->Get('link_set_attribute_separator')); 595 $oAttribute->Set('value_separator', MetaModel::GetConfig()->Get('link_set_value_separator')); 596 $oAttribute->Set('attribute_qualifier', MetaModel::GetConfig()->Get('link_set_attribute_qualifier')); 597 } 598 else 599 { 600 $oAttribute = new SynchroAttribute(); 601 } 602 $oAttribute->Set('sync_source_id', $this->GetKey()); 603 $oAttribute->Set('attcode', $sAttCode); 604 $oAttribute->Set('reconcile', 0); 605 $oAttribute->Set('update', 0); 606 $oAttribute->Set('update_policy', 'master_locked'); 607 return $oAttribute; 608 } 609 /** 610 * Overload the standard behavior 611 */ 612 public function ComputeValues() 613 { 614 parent::ComputeValues(); 615 616 if ($this->IsNew()) 617 { 618 // Compute the database_table_name 619 $sDataTable = $this->Get('database_table_name'); 620 if (!empty($sDataTable)) 621 { 622 $this->Set('database_table_name', $this->ComputeDataTableName()); 623 } 624 625 // When inserting a new datasource object, also create the SynchroAttribute objects 626 // for each field of the target class 627 // Create all the SynchroAttribute records 628 $oAttributeSet = $this->Get('attribute_list'); 629 if ($oAttributeSet->Count() == 0) 630 { 631 foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef) 632 { 633 if ($oAttDef->IsWritable()) 634 { 635 $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode); 636 if ($oAttDef->IsExternalKey()) 637 { 638 $oAttribute = new SynchroAttExtKey(); 639 $oAttribute->Set('reconciliation_attcode', ''); // Blank means by pkey 640 } 641 elseif ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) 642 { 643 $oAttribute = new SynchroAttLinkSet(); 644 // Todo - set those value from the form 645 $oAttribute->Set('row_separator', MetaModel::GetConfig()->Get('link_set_item_separator')); 646 $oAttribute->Set('attribute_separator', MetaModel::GetConfig()->Get('link_set_attribute_separator')); 647 $oAttribute->Set('value_separator', MetaModel::GetConfig()->Get('link_set_value_separator')); 648 $oAttribute->Set('attribute_qualifier', MetaModel::GetConfig()->Get('link_set_attribute_qualifier')); 649 } 650 elseif ($oAttDef->IsScalar()) 651 { 652 $oAttribute = new SynchroAttribute(); 653 } 654 else 655 { 656 $oAttribute = null; 657 } 658 659 if (!is_null($oAttribute)) 660 { 661 $oAttribute->Set('attcode', $sAttCode); 662 $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0); 663 $oAttribute->Set('update', 1); 664 $oAttribute->Set('update_policy', 'master_locked'); 665 $oAttributeSet->AddItem($oAttribute); 666 } 667 } 668 } 669 $this->Set('attribute_list', $oAttributeSet); 670 } 671 } 672 else 673 { 674 $sDataTable = $this->Get('database_table_name'); 675 if (empty($sDataTable)) 676 { 677 $this->Set('database_table_name', $this->ComputeDataTableName()); 678 } 679 } 680 } 681 public function DoCheckToWrite() 682 { 683 parent::DoCheckToWrite(); 684 685 // Check that there is at least one reconciliation key defined 686 if ($this->Get('reconciliation_policy') == 'use_attributes') 687 { 688 $oSet = $this->Get('attribute_list'); 689 $bReconciliationKey = false; 690 foreach($oSet as $oSynchroAttribute) 691 { 692 if ($oSynchroAttribute->Get('reconcile') == 1) 693 { 694 $bReconciliationKey = true; // At least one key is defined 695 break; 696 } 697 } 698 if (!$bReconciliationKey) 699 { 700 $this->m_aCheckIssues[] = Dict::Format('Class:SynchroDataSource/Error:AtLeastOneReconciliationKeyMustBeSpecified'); 701 } 702 } 703 704 // If 'update_then_delete' is specified there must be a delete_retention_period 705 if (($this->Get('delete_policy') == 'update_then_delete') && ($this->Get('delete_policy_retention') == 0)) 706 { 707 $this->m_aCheckIssues[] = Dict::Format('Class:SynchroDataSource/Error:DeleteRetentionDurationMustBeSpecified'); 708 } 709 710 // If update is specified, then something to update must be defined 711 if ((($this->Get('delete_policy') == 'update_then_delete') || ($this->Get('delete_policy') == 'update')) 712 && ($this->Get('delete_policy_update') == '')) 713 { 714 $this->m_aCheckIssues[] = Dict::Format('Class:SynchroDataSource/Error:DeletePolicyUpdateMustBeSpecified'); 715 } 716 717 // When creating the data source with a specified database_table_name, this table must NOT exist 718 if ($this->IsNew()) 719 { 720 $sDataTable = $this->GetDataTable(); 721 if (!empty($sDataTable) && CMDBSource::IsTable($this->GetDataTable())) 722 { 723 // Hmm, the synchro_data_xxx table already exists !! 724 $this->m_aCheckIssues[] = Dict::Format('Class:SynchroDataSource/Error:DataTableAlreadyExists', $this->GetDataTable()); 725 } 726 } 727 } 728 729 public function GetTargetClass() 730 { 731 return $this->Get('scope_class'); 732 } 733 734 public function GetDataTable() 735 { 736 $sTable = $this->Get('database_table_name'); 737 if (empty($sTable)) 738 { 739 $sTable = $this->ComputeDataTableName(); 740 } 741 return $sTable; 742 } 743 744 protected function ComputeDataTableName() 745 { 746 $sDBTableName = $this->Get('database_table_name'); 747 if (empty($sDBTableName)) 748 { 749 $sDBTableName = strtolower($this->GetTargetClass()); 750 $sDBTableName = preg_replace('/[^A-za-z0-9_]/', '_', $sDBTableName); // Remove forbidden characters from the table name 751 $sDBTableName .= '_'.$this->GetKey(); // Add a suffix for unicity 752 } 753 else 754 { 755 $sDBTableName = preg_replace('/[^A-za-z0-9_]/', '_', $sDBTableName); // Remove forbidden characters from the table name 756 } 757 $sPrefix = MetaModel::GetConfig()->Get('db_subname')."synchro_data_"; 758 if (strpos($sDBTableName, $sPrefix) !== 0) 759 { 760 $sDBTableName = $sPrefix.$sDBTableName; 761 } 762 return $sDBTableName; 763 } 764 765 /** 766 * When the new datasource has been created, let's create the synchro_data table 767 * that will hold the data records and the correspoding triggers which will maintain 768 * both tables in sync 769 */ 770 protected function AfterInsert() 771 { 772 parent::AfterInsert(); 773 774 $sTable = $this->GetDataTable(); 775 776 $aColumns = $this->GetSQLColumns(); 777 778 $aFieldDefs = array(); 779 // Allow '0', otherwise mysql will render an error when the id is not given 780 // (the trigger is expected to set the value, but it is not executed soon enough) 781 $aFieldDefs[] = "id INTEGER(11) NOT NULL DEFAULT 0 "; 782 $aFieldDefs[] = "`primary_key` VARCHAR(255) NULL DEFAULT NULL"; 783 foreach($aColumns as $sColumn => $ColSpec) 784 { 785 $aFieldDefs[] = "`$sColumn` $ColSpec NULL DEFAULT NULL"; 786 } 787 $aFieldDefs[] = "INDEX (id)"; 788 $aFieldDefs[] = "INDEX (primary_key)"; 789 $sFieldDefs = implode(', ', $aFieldDefs); 790 791 $sDbCharset = DEFAULT_CHARACTER_SET; 792 $sDbCollation = DEFAULT_COLLATION; 793 $sCreateTable = "CREATE TABLE `$sTable` ($sFieldDefs) ENGINE = ".MYSQL_ENGINE." CHARACTER SET ".$sDbCharset." COLLATE ".$sDbCollation.";"; 794 CMDBSource::Query($sCreateTable); 795 796 $aTriggers = $this->GetTriggersDefinition(); 797 foreach($aTriggers as $key => $sTriggerSQL) 798 { 799 CMDBSource::Query($sTriggerSQL); 800 } 801 802 $sDataTable = $this->Get('database_table_name'); 803 if (empty($sDataTable)) 804 { 805 $this->Set('database_table_name', $this->ComputeDataTableName()); 806 $this->DBUpdate(); 807 } 808 809 } 810 811 /** 812 * Gets the definitions of the 3 triggers: before insert, before update and after delete 813 * @return array An array with 3 entries, one for each of the SQL queries 814 */ 815 protected function GetTriggersDefinition() 816 { 817 $sTable = $this->GetDataTable(); 818 $sReplicaTable = MetaModel::DBGetTable('SynchroReplica'); 819 $aColumns = $this->GetSQLColumns(); 820 $aResult = array(); 821 822 $sTriggerInsert = "CREATE TRIGGER `{$sTable}_bi` BEFORE INSERT ON `$sTable`"; 823 $sTriggerInsert .= " FOR EACH ROW"; 824 $sTriggerInsert .= " BEGIN"; 825 $sTriggerInsert .= " INSERT INTO `{$sReplicaTable}` (`sync_source_id`, `status_last_seen`, `status`) VALUES ({$this->GetKey()}, NOW(), 'new');"; 826 $sTriggerInsert .= " SET NEW.id = LAST_INSERT_ID();"; 827 $sTriggerInsert .= " END;"; 828 $aResult['bi'] = $sTriggerInsert; 829 830 $aModified = array(); 831 foreach($aColumns as $sColumn => $ColSpec) 832 { 833 // <=> is a null-safe 'EQUALS' operator (there is no equivalent for "DIFFERS FROM") 834 $aModified[] = "NOT(NEW.`$sColumn` <=> OLD.`$sColumn`)"; 835 } 836 $sIsModified = '('.implode(') OR (', $aModified).')'; 837 838 // Update the replica 839 // 840 // status is forced to "new" if the replica was obsoleted directly from the state "new" (dest_id = null) 841 // otherwise, if status was either 'obsolete' or 'synchronized' it is turned into 'modified' or 'synchronized' depending on the changes 842 // otherwise, the status is left as is 843 $sTriggerUpdate = "CREATE TRIGGER `{$sTable}_bu` BEFORE UPDATE ON `$sTable`"; 844 $sTriggerUpdate .= " FOR EACH ROW"; 845 $sTriggerUpdate .= " BEGIN"; 846 $sTriggerUpdate .= " IF @itopuser is null THEN"; 847 $sTriggerUpdate .= " UPDATE `{$sReplicaTable}` SET status_last_seen = NOW(), `status` = IF(`status` = 'obsolete', IF(`dest_id` IS NULL, 'new', 'modified'), IF(`status` IN ('synchronized') AND ($sIsModified), 'modified', `status`)) WHERE sync_source_id = {$this->GetKey()} AND id = OLD.id;"; 848 $sTriggerUpdate .= " SET NEW.id = OLD.id;"; // make sure this id won't change 849 $sTriggerUpdate .= " END IF;"; 850 $sTriggerUpdate .= " END;"; 851 $aResult['bu'] = $sTriggerUpdate; 852 853 $sTriggerDelete = "CREATE TRIGGER `{$sTable}_ad` AFTER DELETE ON `$sTable`"; 854 $sTriggerDelete .= " FOR EACH ROW"; 855 $sTriggerDelete .= " BEGIN"; 856 $sTriggerDelete .= " DELETE FROM `{$sReplicaTable}` WHERE id = OLD.id;"; 857 $sTriggerDelete .= " END;"; 858 $aResult['ad'] = $sTriggerDelete; 859 return $aResult; 860 } 861 862 protected function AfterDelete() 863 { 864 parent::AfterDelete(); 865 866 $sTable = $this->GetDataTable(); 867 868 $sDropTable = "DROP TABLE IF EXISTS `$sTable`"; // Do not fail if the table is already deleted (corrupted database) 869 CMDBSource::Query($sDropTable); 870 // TO DO - check that triggers get dropped with the table 871 } 872 873 /** 874 * Checks if the data source definition is consistent with the schema of the target class 875 * 876 * @param boolean $bDiagnostics boolean True to only diagnose the consistency, false to actually apply some changes 877 * @param boolean $bVerbose boolean True to get some information in the std output (echo) 878 * @param null $oChange //FIXME never used, should we drop this ? 879 * 880 * @return bool Whether or not the database needs fixing for this data source 881 */ 882 public function CheckDBConsistency($bDiagnostics, $bVerbose, $oChange = null) 883 { 884 $bFixNeeded = false; 885 $bTriggerRebuildNeeded = false; 886 $aMissingFields = array(); 887 $oAttributeSet = $this->Get('attribute_list'); 888 $aAttributes = array(); 889 890 while($oAttribute = $oAttributeSet->Fetch()) 891 { 892 $sAttCode = $oAttribute->Get('attcode'); 893 if (MetaModel::IsValidAttCode($this->GetTargetClass(), $sAttCode)) 894 { 895 $aAttributes[$sAttCode] = $oAttribute; 896 } 897 else 898 { 899 // Old field remaining 900 $bTriggerRebuildNeeded = true; 901 if ($bVerbose) 902 { 903 echo "Irrelevant field description for the field '$sAttCode', for the data synchro task ".$this->GetName()." (".$this->GetKey()."), will be removed.\n"; 904 } 905 $bFixNeeded = true; 906 if (!$bDiagnostics) 907 { 908 // Fix the issue 909 $oAttribute->DBDelete(); 910 } 911 } 912 } 913 914 $sTable = $this->GetDataTable(); 915 foreach($this->ListTargetAttributes() as $sAttCode=>$oAttDef) 916 { 917 if (!isset($aAttributes[$sAttCode])) 918 { 919 $bFixNeeded = true; 920 $aMissingFields[] = $sAttCode; 921 $bTriggerRebuildNeeded = true; 922 // New field missing... 923 if ($bVerbose) 924 { 925 echo "Missing field description for the field '$sAttCode', for the data synchro task ".$this->GetName()." (".$this->GetKey()."), will be created with default values.\n"; 926 } 927 if (!$bDiagnostics) 928 { 929 // Fix the issue 930 $oAttribute = $this->CreateSynchroAtt($sAttCode); 931 $oAttribute->DBInsert(); 932 } 933 } 934 else 935 { 936 $aColumns = $this->GetSQLColumns(array($sAttCode)); 937 foreach($aColumns as $sColName => $sColumnDef) 938 { 939 $bOneColIsMissing = false; 940 if (!CMDBSource::IsField($sTable, $sColName)) 941 { 942 $bFixNeeded = true; 943 $bOneColIsMissing = true; 944 if ($bVerbose) 945 { 946 if (count($aColumns) > 1) 947 { 948 echo "Missing column '$sColName', in the table '$sTable' for the data synchro task ".$this->GetName()." (".$this->GetKey()."). The columns '".implode("', '", $aColumns )." will be re-created.'.\n"; 949 } 950 else 951 { 952 echo "Missing column '$sColName', in the table '$sTable' for the data synchro task ".$this->GetName()." (".$this->GetKey()."). The column '$sColName' will be added.\n"; 953 } 954 } 955 } 956 else if (strcasecmp(CMDBSource::GetFieldType($sTable, $sColName), $sColumnDef) != 0) 957 { 958 $bFixNeeded = true; 959 $bOneColIsMissing = true; 960 if (count($aColumns) > 1) 961 { 962 echo "Incorrect column '$sColName' (".CMDBSource::GetFieldType($sTable, $sColName)." instead of ".$sColumnDef."), in the table '$sTable' for the data synchro task ".$this->GetName()." (".$this->GetKey()."). The columns '".implode("', '", $aColumns )." will be re-created.'.\n"; 963 } 964 else 965 { 966 echo "Incorrect column '$sColName' (".CMDBSource::GetFieldType($sTable, $sColName)." instead of ".$sColumnDef."), in the table '$sTable' for the data synchro task ".$this->GetName()." (".$this->GetKey()."). The column '$sColName' will be added.\n"; 967 } 968 } 969 if ($bOneColIsMissing) 970 { 971 $bTriggerRebuildNeeded = true; 972 $aMissingFields[] = $sAttCode; 973 } 974 } 975 } 976 } 977 978 $sDBName = MetaModel::GetConfig()->Get('db_name'); 979 try 980 { 981 // Note: as per the MySQL documentation, using information_schema behaves exactly like SHOW TRIGGERS (user privileges) 982 // and this is in fact the recommended way for better portability 983 $iTriggerCount = CMDBSource::QueryToScalar("select count(*) from information_schema.triggers where EVENT_OBJECT_SCHEMA='$sDBName' and EVENT_OBJECT_TABLE='$sTable'"); 984 } 985 catch (Exception $e) 986 { 987 if ($bVerbose) 988 { 989 echo "Failed to investigate on the synchro triggers (skipping the check): ".$e->getMessage().".\n"; 990 } 991 // Ignore this error: consider that the trigger are there 992 $iTriggerCount = 3; 993 } 994 if ($iTriggerCount < 3) 995 { 996 $bFixNeeded = true; 997 $bTriggerRebuildNeeded = true; 998 if ($bVerbose) 999 { 1000 echo "Missing trigger(s) for the data synchro task ".$this->GetName()." (table {$sTable}).\n"; 1001 } 1002 } 1003 1004 $aRepairQueries = array(); 1005 1006 if (count($aMissingFields) > 0) 1007 { 1008 // The structure of the table needs adjusting 1009 $aColumns = $this->GetSQLColumns($aMissingFields); 1010 $aFieldDefs = array(); 1011 foreach($aColumns as $sAttCode => $sColumnDef) 1012 { 1013 if (CMDBSource::IsField($sTable, $sAttCode)) 1014 { 1015 $aRepairQueries[] = "ALTER TABLE `$sTable` CHANGE `$sAttCode` `$sAttCode` $sColumnDef"; 1016 } 1017 else 1018 { 1019 $aFieldDefs[] = "`$sAttCode` $sColumnDef"; 1020 } 1021 1022 } 1023 if (count($aFieldDefs) > 0) 1024 { 1025 $aRepairQueries[] = "ALTER TABLE `$sTable` ADD (".implode(',', $aFieldDefs).");"; 1026 } 1027 1028 if ($bDiagnostics) 1029 { 1030 if ($bVerbose) 1031 { 1032 echo "The structure of the table $sTable for the data synchro task ".$this->GetName()." (".$this->GetKey().") must be altered (missing or incorrect fields: ".implode(',', $aMissingFields).").\n"; 1033 } 1034 } 1035 } 1036 1037 // Repair the triggers 1038 // Must be done after updating the columns because MySQL does check the validity of the query found into the procedure! 1039 if ($bTriggerRebuildNeeded) 1040 { 1041 // The triggers as well must be adjusted 1042 $aTriggersDefs = $this->GetTriggersDefinition(); 1043 $aTriggerRepair = array(); 1044 $aTriggerRepair[] = "DROP TRIGGER IF EXISTS `{$sTable}_bi`;"; 1045 $aTriggerRepair[] = $aTriggersDefs['bi']; 1046 $aTriggerRepair[] = "DROP TRIGGER IF EXISTS `{$sTable}_bu`;"; 1047 $aTriggerRepair[] = $aTriggersDefs['bu']; 1048 $aTriggerRepair[] = "DROP TRIGGER IF EXISTS `{$sTable}_ad`;"; 1049 $aTriggerRepair[] = $aTriggersDefs['ad']; 1050 1051 if ($bDiagnostics) 1052 { 1053 if ($bVerbose) 1054 { 1055 echo "The triggers {$sTable}_bi, {$sTable}_bu, {$sTable}_ad for the data synchro task ".$this->GetName()." (".$this->GetKey().") must be re-created.\n"; 1056 echo implode("\n", $aTriggerRepair)."\n"; 1057 } 1058 } 1059 $aRepairQueries = array_merge($aRepairQueries, $aTriggerRepair); // The order matters! 1060 } 1061 1062 // Execute the repair statements 1063 // 1064 if (!$bDiagnostics && (count($aRepairQueries) > 0)) 1065 { 1066 // Fix the issue 1067 foreach($aRepairQueries as $sSQL) 1068 { 1069 CMDBSource::Query($sSQL); 1070 if ($bVerbose) 1071 { 1072 echo "$sSQL\n"; 1073 } 1074 } 1075 } 1076 return $bFixNeeded; 1077 } 1078 1079 public function SendNotification($sSubject, $sBody) 1080 { 1081 $iContact = $this->Get('notify_contact_id'); 1082 if ($iContact == 0) 1083 { 1084 // Leave silently... 1085 return; 1086 } 1087 $oContact = MetaModel::GetObject('Contact', $iContact); 1088 1089 // Determine the email attribute (the first one will be our choice) 1090 $sEmailAttCode = null; 1091 foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef) 1092 { 1093 if ($oAttDef instanceof AttributeEmailAddress) 1094 { 1095 $sEmailAttCode = $sAttCode; 1096 // we've got one, exit the loop 1097 break; 1098 } 1099 } 1100 if (is_null($sEmailAttCode)) 1101 { 1102 // Leave silently... 1103 return; 1104 } 1105 1106 $sTo = $oContact->Get($sEmailAttCode); 1107 $sBody = '<p>Data synchronization: '.$this->GetHyperlink().'</p>'.$sBody; 1108 1109 $sSubject = 'iTop Data Sync - '.$this->GetName().' - '.$sSubject; 1110 1111 $oEmail = new Email(); 1112 $oEmail->SetRecipientTO($sTo); 1113 $oEmail->SetSubject($sSubject); 1114 $oEmail->SetBody($sBody); 1115 if ($oEmail->Send($aIssues) == EMAIL_SEND_ERROR) 1116 { 1117 // mmmm, what can I do? 1118 } 1119 } 1120 1121 /** 1122 * Get the list of attributes eligible to the synchronization 1123 */ 1124 public function ListTargetAttributes() 1125 { 1126 $aRet = array(); 1127 foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode => $oAttDef) 1128 { 1129 if ($sAttCode == 'finalclass') continue; 1130 if (!$oAttDef->IsWritable()) continue; 1131 if ($oAttDef->IsLinkSet() && !$oAttDef->IsIndirect()) continue; 1132 1133 $aRet[$sAttCode] = $oAttDef; 1134 } 1135 return $aRet; 1136 } 1137 1138 1139 /** 1140 * @param null|string[] $aAttributeCodes attribute codes list 1141 * 1142 * @return string[] corresponding current class SQL columns, all of the table columns if null was provided 1143 */ 1144 public function GetSQLColumns($aAttributeCodes = null) 1145 { 1146 $aColumns = array(); 1147 $sClass = $this->GetTargetClass(); 1148 1149 if (is_null($aAttributeCodes)) 1150 { 1151 $aAttributeCodes = array(); 1152 foreach($this->ListTargetAttributes() as $sAttCode => $oAttDef) 1153 { 1154 $aAttributeCodes[] = $sAttCode; 1155 } 1156 } 1157 1158 foreach($aAttributeCodes as $sAttCode) 1159 { 1160 $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); 1161 1162 if ($oAttDef->IsExternalKey()) 1163 { 1164 // The pkey might be used as well as any other key column 1165 $aColumns[$sAttCode] = 'VARCHAR(255)'; 1166 } 1167 else 1168 { 1169 foreach($oAttDef->GetImportColumns() as $sField => $sDBFieldType) 1170 { 1171 $aColumns[$sField] = $sDBFieldType; 1172 } 1173 } 1174 } 1175 return $aColumns; 1176 } 1177 1178 /** 1179 * DEPRECATED - Get the list of Date and Datetime SQL columns 1180 */ 1181 public function GetDateSQLColumns() 1182 { 1183 $aDateAttributes = array(); 1184 1185 $sClass = $this->GetTargetClass(); 1186 foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 1187 { 1188 if ($oAttDef instanceof AttributeDateTime) 1189 { 1190 $aDateAttributes[] = $sAttCode; 1191 } 1192 } 1193 return $this->GetSQLColumns($aDateAttributes); 1194 } 1195 1196 public function IsRunning() 1197 { 1198 $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='running'"; 1199 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */); 1200 if ($oSet->Count() < 1) 1201 { 1202 $bRet = false; 1203 } 1204 else 1205 { 1206 $bRet = true; 1207 } 1208 return $bRet; 1209 } 1210 1211 public function GetLatestLog() 1212 { 1213 $oLog = null; 1214 1215 $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id"; 1216 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */); 1217 if ($oSet->Count() >= 1) 1218 { 1219 $oLog = $oSet->Fetch(); 1220 } 1221 return $oLog; 1222 } 1223 1224 // TO DO: remove if still unused 1225 /** 1226 * Retrieve from the log, the date of the last completed import 1227 * @return DateTime 1228 */ 1229 public function GetLastCompletedImportDate() 1230 { 1231 $date = null; 1232 $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='completed'"; 1233 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('end_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 0 /* limitCount */, 0 /* limitStart */); 1234 if ($oSet->Count() >= 1) 1235 { 1236 $oLog = $oSet->Fetch(); 1237 $date = $oLog->Get('end_date'); 1238 } 1239 return $date; 1240 } 1241} 1242 1243class SynchroAttribute extends cmdbAbstractObject 1244{ 1245 public static function Init() 1246 { 1247 $aParams = array 1248 ( 1249 "category" => "core/cmdb,view_in_gui,grant_by_profile", 1250 "key_type" => "autoincrement", 1251 "name_attcode" => "attcode", 1252 "state_attcode" => "", 1253 "reconc_keys" => array(), 1254 "db_table" => "priv_sync_att", 1255 "db_key_field" => "id", 1256 "db_finalclass_field" => "", 1257 "display_template" => "", 1258 ); 1259 MetaModel::Init_Params($aParams); 1260 MetaModel::Init_InheritAttributes(); 1261 MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); 1262 MetaModel::Init_AddAttribute(new AttributeExternalField("sync_source_name", array("allowed_values"=>null, "extkey_attcode"=> 'sync_source_id', "target_attcode"=>"name"))); 1263 MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); 1264 MetaModel::Init_AddAttribute(new AttributeBoolean("update", array("allowed_values"=>null, "sql"=>"update", "default_value"=>true, "is_null_allowed"=>false, "depends_on"=>array()))); 1265 MetaModel::Init_AddAttribute(new AttributeBoolean("reconcile", array("allowed_values"=>null, "sql"=>"reconcile", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array()))); 1266 MetaModel::Init_AddAttribute(new AttributeEnum("update_policy", array("allowed_values"=>new ValueSetEnum('master_locked,master_unlocked,write_if_empty'), "sql"=>"update_policy", "default_value"=>"master_locked", "is_null_allowed"=>false, "depends_on"=>array()))); 1267 1268 // Display lists 1269 MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for the complete details 1270 MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list 1271 // Search criteria 1272// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form 1273// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 1274 } 1275} 1276 1277class SynchroAttExtKey extends SynchroAttribute 1278{ 1279 public static function Init() 1280 { 1281 $aParams = array 1282 ( 1283 "category" => "core/cmdb,view_in_gui,grant_by_profile", 1284 "key_type" => "autoincrement", 1285 "name_attcode" => "attcode", 1286 "state_attcode" => "", 1287 "reconc_keys" => array(), 1288 "db_table" => "priv_sync_att_extkey", 1289 "db_key_field" => "id", 1290 "db_finalclass_field" => "", 1291 "display_template" => "", 1292 ); 1293 MetaModel::Init_Params($aParams); 1294 MetaModel::Init_InheritAttributes(); 1295 MetaModel::Init_AddAttribute(new AttributeString("reconciliation_attcode", array("allowed_values"=>null, "sql"=>"reconciliation_attcode", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 1296 1297 // Display lists 1298 MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'reconciliation_attcode')); // Attributes to be displayed for the complete details 1299 MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list 1300 1301 // Search criteria 1302// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form 1303// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 1304 } 1305 1306 public function GetReconciliationFormElement($sTargetClass, $sFieldName) 1307 { 1308 $sHtml = "<select name=\"$sFieldName\">\n"; 1309 // Id 1310 $sSelected = (''== $this->Get('reconciliation_attcode')) ? ' selected' : ''; 1311 $sHtml .= "<option value=\"\" $sSelected>".Dict::S('Core:SynchroAttExtKey:ReconciliationById')."</option>\n"; 1312 1313 // Friendly name 1314 $sSelected = ('friendlyname' == $this->Get('reconciliation_attcode')) ? ' selected' : ''; 1315 $sHtml .= "<option value=\"friendlyname\" $sSelected>".MetaModel::GetLabel($sTargetClass, 'friendlyname')."</option>\n"; 1316 1317 // Separator 1318 $sHtml .= '<option value="" disabled>———————————</option>'; // Note: using the em-dash character which has no space between 2 characters 1319 1320 // Then add all remaining scalar attributes, sorted alphabetically 1321 $aMoreOptions = array(); 1322 foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) 1323 { 1324 if ($oAttDef->IsScalar() && ($sAttCode != 'friendlyname')) 1325 { 1326 $sSelected = ($sAttCode == $this->Get('reconciliation_attcode')) ? ' selected' : ''; 1327 $aMoreOptions[MetaModel::GetLabel($sTargetClass, $sAttCode)] = "<option value=\"$sAttCode\" $sSelected>".MetaModel::GetLabel($sTargetClass, $sAttCode)."</option>\n"; 1328 } 1329 } 1330 ksort($aMoreOptions); 1331 foreach($aMoreOptions as $sOption) 1332 { 1333 $sHtml .= $sOption; 1334 } 1335 1336 $sHtml .= "</select>\n"; 1337 return $sHtml; 1338 } 1339 1340} 1341 1342class SynchroAttLinkSet extends SynchroAttribute 1343{ 1344 public static function Init() 1345 { 1346 $aParams = array 1347 ( 1348 "category" => "core/cmdb,view_in_gui,grant_by_profile", 1349 "key_type" => "autoincrement", 1350 "name_attcode" => "attcode", 1351 "state_attcode" => "", 1352 "reconc_keys" => array(), 1353 "db_table" => "priv_sync_att_linkset", 1354 "db_key_field" => "id", 1355 "db_finalclass_field" => "", 1356 "display_template" => "", 1357 ); 1358 MetaModel::Init_Params($aParams); 1359 MetaModel::Init_InheritAttributes(); 1360 MetaModel::Init_AddAttribute(new AttributeString("row_separator", array("allowed_values"=>null, "sql"=>"row_separator", "default_value"=>'|', "is_null_allowed"=>true, "depends_on"=>array()))); 1361 MetaModel::Init_AddAttribute(new AttributeString("attribute_separator", array("allowed_values"=>null, "sql"=>"attribute_separator", "default_value"=>';', "is_null_allowed"=>true, "depends_on"=>array()))); 1362 MetaModel::Init_AddAttribute(new AttributeString("value_separator", array("allowed_values"=>null, "sql"=>"value_separator", "default_value"=>':', "is_null_allowed"=>true, "depends_on"=>array()))); 1363 MetaModel::Init_AddAttribute(new AttributeString("attribute_qualifier", array("allowed_values"=>null, "sql"=>"attribute_qualifier", "default_value"=>'\'', "is_null_allowed"=>true, "depends_on"=>array()))); 1364 1365 // Display lists 1366 MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'row_separator', 'attribute_separator', 'value_separator', 'attribute_qualifier')); // Attributes to be displayed for the complete details 1367 MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list 1368 1369 // Search criteria 1370// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form 1371// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 1372 } 1373 1374} 1375 1376//class SynchroLog extends Event 1377class SynchroLog extends DBObject 1378{ 1379 public static function Init() 1380 { 1381 $aParams = array 1382 ( 1383 "category" => "core/cmdb,view_in_gui", 1384 "key_type" => "autoincrement", 1385 "name_attcode" => "", 1386 "state_attcode" => "", 1387 "reconc_keys" => array(), 1388 "db_table" => "priv_sync_log", 1389 "db_key_field" => "id", 1390 "db_finalclass_field" => "", 1391 "display_template" => "", 1392 ); 1393 MetaModel::Init_Params($aParams); 1394 MetaModel::Init_InheritAttributes(); 1395// MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); 1396 MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); 1397 MetaModel::Init_AddAttribute(new AttributeDateTime("start_date", array("allowed_values"=>null, "sql"=>"start_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); 1398 MetaModel::Init_AddAttribute(new AttributeDateTime("end_date", array("allowed_values"=>null, "sql"=>"end_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); 1399 MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('running,completed,error'), "sql"=>"status", "default_value"=>"running", "is_null_allowed"=>false, "depends_on"=>array()))); 1400 MetaModel::Init_AddAttribute(new AttributeInteger("status_curr_job", array("allowed_values"=>null, "sql"=>"status_curr_job", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); 1401 MetaModel::Init_AddAttribute(new AttributeInteger("status_curr_pos", array("allowed_values"=>null, "sql"=>"status_curr_pos", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); 1402 1403 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_seen", array("allowed_values"=>null, "sql"=>"stats_nb_replica_seen", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1404 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_total", array("allowed_values"=>null, "sql"=>"stats_nb_replica_total", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1405 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_deleted", array("allowed_values"=>null, "sql"=>"stats_nb_obj_deleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1406 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_deleted_errors", array("allowed_values"=>null, "sql"=>"stats_deleted_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1407 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_obsoleted", array("allowed_values"=>null, "sql"=>"stats_nb_obj_obsoleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1408 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_obsoleted_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_obsoleted_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1409 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_created", array("allowed_values"=>null, "sql"=>"stats_nb_obj_created", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1410 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_created_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_created_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1411 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_created_warnings", array("allowed_values"=>null, "sql"=>"stats_nb_obj_created_warnings", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1412 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_updated", array("allowed_values"=>null, "sql"=>"stats_nb_obj_updated", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1413 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_updated_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_updated_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1414 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_updated_warnings", array("allowed_values"=>null, "sql"=>"stats_nb_obj_updated_warnings", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1415 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_unchanged_warnings", array("allowed_values"=>null, "sql"=>"stats_nb_obj_unchanged_warnings", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1416 // MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_reconciled", array("allowed_values"=>null, "sql"=>"stats_nb_replica_reconciled", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1417 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_reconciled_errors", array("allowed_values"=>null, "sql"=>"stats_nb_replica_reconciled_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1418 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_disappeared_no_action", array("allowed_values"=>null, "sql"=>"stats_nb_replica_disappeared_no_action", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1419 1420 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_updated", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_updated", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1421 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_updated_warnings", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_updated_warnings", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1422 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_unchanged", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_unchanged", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1423 MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_unchanged_warnings", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_unchanged_warnings", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1424 1425 MetaModel::Init_AddAttribute(new AttributeText("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); 1426 MetaModel::Init_AddAttribute(new AttributeLongText("traces", array("allowed_values"=>null, "sql"=>"traces", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); 1427 1428 MetaModel::Init_AddAttribute(new AttributeInteger("memory_usage_peak", array("allowed_values"=>null, "sql"=>"memory_usage_peak", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); 1429 1430 // Display lists 1431 MetaModel::Init_SetZListItems('details', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_replica_total', 'stats_nb_replica_seen', 'stats_nb_obj_created', /*'stats_nb_replica_reconciled',*/ 'stats_nb_obj_updated', 'stats_nb_obj_obsoleted', 'stats_nb_obj_deleted', 1432 'stats_nb_obj_created_errors', 'stats_nb_replica_reconciled_errors', 'stats_nb_replica_disappeared_no_action', 'stats_nb_obj_updated_errors', 'stats_nb_obj_obsoleted_errors', 'stats_nb_obj_deleted_errors', 'stats_nb_obj_new_unchanged', 'stats_nb_obj_new_updated', 'traces')); // Attributes to be displayed for the complete details 1433 MetaModel::Init_SetZListItems('list', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_replica_seen')); // Attributes to be displayed for a list 1434 // Search criteria 1435// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form 1436// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 1437 } 1438 1439 /** 1440 * Helper 1441 */ 1442 function GetErrorCount() 1443 { 1444 return $this->Get('stats_nb_obj_deleted_errors') 1445 + $this->Get('stats_nb_obj_obsoleted_errors') 1446 + $this->Get('stats_nb_obj_created_errors') 1447 + $this->Get('stats_nb_obj_updated_errors') 1448 + $this->Get('stats_nb_replica_reconciled_errors'); 1449 } 1450 1451 /** 1452 * Increments a statistics counter 1453 * 1454 * @param string $sCode 1455 */ 1456 function Inc($sCode) 1457 { 1458 $this->Set($sCode, 1+$this->Get($sCode)); 1459 } 1460 1461 1462 /** 1463 * Implement traces management 1464 */ 1465 protected $m_aTraces = array(); 1466 public function AddTrace($sMsg, $oReplica = null) 1467 { 1468 if (MetaModel::GetConfig()->Get('synchro_trace') == 'none') 1469 { 1470 return; 1471 } 1472 1473 if ($oReplica) 1474 { 1475 $sDestClass = $oReplica->Get('dest_class'); 1476 if (!empty($sDestClass)) 1477 { 1478 $sPrefix = $oReplica->GetKey().','.$sDestClass.'::'.$oReplica->Get('dest_id').','; 1479 } 1480 else 1481 { 1482 $sPrefix = $oReplica->GetKey().',,'; 1483 } 1484 } 1485 else 1486 { 1487 $sPrefix = ',,'; 1488 } 1489 $this->m_aTraces[] = $sPrefix.$sMsg; 1490 } 1491 1492 public function GetTraces() 1493 { 1494 return $this->m_aTraces; 1495 } 1496 1497 protected function TraceToText() 1498 { 1499 if (MetaModel::GetConfig()->Get('synchro_trace') != 'save') 1500 { 1501 // none, or display only 1502 return; 1503 } 1504 1505 $sPrevTrace = $this->Get('traces'); 1506 1507 $oAttDef = MetaModel::GetAttributeDef(get_class($this), 'traces'); 1508 $iMaxSize = $oAttDef->GetMaxSize(); 1509 if (strlen($sPrevTrace) > 0) 1510 { 1511 $sTrace = $sPrevTrace."\n".implode("\n", $this->m_aTraces); 1512 } 1513 else 1514 { 1515 $sTrace = implode("\n", $this->m_aTraces); 1516 } 1517 if (strlen($sTrace) >= $iMaxSize) 1518 { 1519 $sTrace = substr($sTrace, 0, $iMaxSize - 255)."...\nTruncated (size: ".strlen($sTrace).')'; 1520 } 1521 $this->Set('traces', $sTrace); 1522 1523 //DBUpdate may be called many times... the operation should not be repeated 1524 $this->m_aTraces = array(); 1525 } 1526 1527 protected function OnInsert() 1528 { 1529 $this->TraceToText(); 1530 parent::OnInsert(); 1531 } 1532 1533 protected function OnUpdate() 1534 { 1535 $this->TraceToText(); 1536 $sMemPeak = max($this->Get('memory_usage_peak'), ExecutionKPI::memory_get_peak_usage()); 1537 $this->Set('memory_usage_peak', $sMemPeak); 1538 parent::OnUpdate(); 1539 } 1540} 1541 1542 1543class SynchroReplica extends DBObject implements iDisplay 1544{ 1545 static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source) 1546 protected $aWarnings; 1547 1548 public static function Init() 1549 { 1550 $aParams = array 1551 ( 1552 "category" => "core/cmdb,view_in_gui", 1553 "key_type" => "autoincrement", 1554 "name_attcode" => "", 1555 "state_attcode" => "", 1556 "reconc_keys" => array(), 1557 "db_table" => "priv_sync_replica", 1558 "db_key_field" => "id", 1559 "db_finalclass_field" => "", 1560 "display_template" => "", 1561 "indexes" => array( array ('dest_class', 'dest_id'), ), 1562 ); 1563 MetaModel::Init_Params($aParams); 1564 MetaModel::Init_InheritAttributes(); 1565 1566 MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); 1567 MetaModel::Init_AddAttribute(new AttributeExternalField("base_class", array("allowed_values"=>null, "extkey_attcode"=> 'sync_source_id', "target_attcode"=>"scope_class"))); 1568 1569 MetaModel::Init_AddAttribute(new AttributeObjectKey("dest_id", array("allowed_values"=>null, "class_attcode"=>"dest_class", "sql"=>"dest_id", "is_null_allowed"=>true, "depends_on"=>array()))); 1570 MetaModel::Init_AddAttribute(new AttributeClass("dest_class", array("class_category"=>"", "more_values"=>"", "sql"=>"dest_class", "default_value"=>'Organization', "is_null_allowed"=>true, "depends_on"=>array()))); 1571 1572 MetaModel::Init_AddAttribute(new AttributeDateTime("status_last_seen", array("allowed_values"=>null, "sql"=>"status_last_seen", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); 1573 MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('new,synchronized,modified,orphan,obsolete'), "sql"=>"status", "default_value"=>"new", "is_null_allowed"=>false, "depends_on"=>array()))); 1574 MetaModel::Init_AddAttribute(new AttributeBoolean("status_dest_creator", array("allowed_values"=>null, "sql"=>"status_dest_creator", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); 1575 MetaModel::Init_AddAttribute(new AttributeString("status_last_error", array("allowed_values"=>null, "sql"=>"status_last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); 1576 MetaModel::Init_AddAttribute(new AttributeString("status_last_warning", array("allowed_values"=>null, "sql"=>"status_last_warning", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); 1577 1578 MetaModel::Init_AddAttribute(new AttributeDateTime("info_creation_date", array("allowed_values"=>null, "sql"=>"info_creation_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); 1579 MetaModel::Init_AddAttribute(new AttributeDateTime("info_last_modified", array("allowed_values"=>null, "sql"=>"info_last_modified", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); 1580 1581 // Display lists 1582 MetaModel::Init_SetZListItems('details', array('' . 1583 'col:0'=> array( 1584 'fieldset:SynchroDataSource:Definition' => array('sync_source_id','dest_id','dest_class'), 1585 'fieldset:SynchroDataSource:Status' => array('status','status_last_seen','status_dest_creator','status_last_error','status_last_warning'), 1586 'fieldset:SynchroDataSource:Information' => array('info_creation_date','info_last_modified')) 1587 ) 1588 ); 1589 MetaModel::Init_SetZListItems('list', array('sync_source_id', 'dest_id', 'dest_class', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error', 'status_last_warning')); // Attributes to be displayed for a list 1590 // Search criteria 1591 MetaModel::Init_SetZListItems('standard_search', array('sync_source_id', 'status_last_seen', 'status', 'status_dest_creator', 'dest_class', 'dest_id', 'status_last_error', 'status_last_warning')); // Criteria of the std search form 1592// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form 1593 } 1594 1595 public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) 1596 { 1597 parent::__construct($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec); 1598 $this->aWarnings = array(); 1599 } 1600 1601 protected function AddWarning($sWarningMessage) 1602 { 1603 $this->aWarnings[] = $sWarningMessage; 1604 } 1605 1606 protected function ResetWarnings() 1607 { 1608 $this->aWarnings = array(); 1609 } 1610 1611 protected function HasWarnings() 1612 { 1613 return (count($this->aWarnings) > 0); 1614 } 1615 1616 protected function RecordWarnings() 1617 { 1618 $MAX_WARNING_LENGTH = 255; 1619 switch(count($this->aWarnings)) 1620 { 1621 case 0: 1622 $sWarningMessage = ''; 1623 break; 1624 1625 case 1: 1626 $sWarningMessage = $this->aWarnings[0]; 1627 break; 1628 1629 default: 1630 $sWarningMessage = count($this->aWarnings)." warnings: ".implode(' ', $this->aWarnings); 1631 break; 1632 } 1633 1634 if (strlen($sWarningMessage) > $MAX_WARNING_LENGTH) 1635 { 1636 $sWarningMessage = substr($sWarningMessage, 0, $MAX_WARNING_LENGTH - 3).'...'; 1637 } 1638 1639 $this->Set('status_last_warning', $sWarningMessage); 1640 } 1641 1642 public function DBInsert() 1643 { 1644 throw new CoreException('A synchronization replica must be created only by the mean of triggers'); 1645 } 1646 1647 // Overload the deletion -> the replica has been created by the mean of a trigger, 1648 // it will be deleted by the mean of a trigger too 1649 protected function DBDeleteSingleObject() 1650 { 1651 $this->OnDelete(); 1652 1653 if (!MetaModel::DBIsReadOnly()) 1654 { 1655 $oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'), false); 1656 if ($oDataSource) 1657 { 1658 $sTable = $oDataSource->GetDataTable(); 1659 1660 $sSQL = "DELETE FROM `$sTable` WHERE id = '{$this->GetKey()}'"; 1661 CMDBSource::Query($sSQL); 1662 } 1663 // else the whole datasource has probably been already deleted 1664 } 1665 1666 $this->AfterDelete(); 1667 1668 $this->m_bIsInDB = false; 1669 $this->m_iKey = null; 1670 } 1671 1672 public function SetLastError($sMessage, $oException = null) 1673 { 1674 if ($oException) 1675 { 1676 $sText = $sMessage.$oException->getMessage(); 1677 } 1678 else 1679 { 1680 $sText = $sMessage; 1681 } 1682 if (strlen($sText) > 255) 1683 { 1684 $sText = substr($sText, 0, 200).'...('.strlen($sText).' chars)...'; 1685 } 1686 $this->Set('status_last_error', $sText); 1687 } 1688 1689 1690 public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange, &$oStatLog) 1691 { 1692 $oStatLog->AddTrace(">>> Beginning of SynchroReplica::Synchro, replica status is '".$this->Get('status')."'.", $this); 1693 $this->ResetWarnings(); 1694 switch($this->Get('status')) 1695 { 1696 case 'new': 1697 $this->Set('status_dest_creator', false); 1698 // If needed, construct the query used for the reconciliation 1699 if (!isset(self::$aSearches[$oDataSource->GetKey()])) 1700 { 1701 $aCriterias = array(); 1702 foreach($aReconciliationKeys as $sFilterCode => $oSyncAtt) 1703 { 1704 $aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode; 1705 } 1706 $sOQL = "SELECT ".$oDataSource->GetTargetClass()." WHERE ".implode(' AND ', $aCriterias); 1707 self::$aSearches[$oDataSource->GetKey()] = DBObjectSearch::FromOQL($sOQL); 1708 } 1709 // Get the criterias for the search 1710 $aFilterValues = array(); 1711 foreach($aReconciliationKeys as $sFilterCode => $oSyncAtt) 1712 { 1713 $value = $this->GetValueFromExtData($sFilterCode, $oSyncAtt, $oStatLog); 1714 if (!is_null($value)) 1715 { 1716 $aFilterValues[$sFilterCode] = $value; 1717 } 1718 else 1719 { 1720 // TO DO: can we retry this ?? 1721 // Reconciliation could not be performed - log and EXIT 1722 $oStatLog->AddTrace("Could not reconcile on null value for attribute '$sFilterCode'", $this); 1723 $this->SetLastError("Could not reconcile on null value for attribute '$sFilterCode'"); 1724 $oStatLog->Inc('stats_nb_replica_reconciled_errors'); 1725 $oStatLog->AddTrace("<<< End of SyncroReplica::Synchro (error could not reconcile on null value for attribute '$sFilterCode').", $this); 1726 return; 1727 } 1728 } 1729 $oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues); 1730 $iCount = $oDestSet->Count(); 1731 $sDebugOQL = $oDestSet->GetFilter()->ToOQL(true); 1732 $oStatLog->AddTrace("Reconciliation query: '$sDebugOQL' returned $iCount object(s).", $this); 1733 $aConditions = array(); 1734 foreach($aFilterValues as $sCode => $sValue) 1735 { 1736 $aConditions[] = $sCode.'='.$sValue; 1737 } 1738 $sConditionDesc = implode(' AND ', $aConditions); 1739 // How many objects match the reconciliation criterias 1740 switch($iCount) 1741 { 1742 case 0: 1743 $oStatLog->AddTrace("Nothing found on: $sConditionDesc", $this); 1744 if ($oDataSource->Get('action_on_zero') == 'create') 1745 { 1746 $oStatLog->AddTrace("Calling CreateObjectFromReplica", $this); 1747 $bCreated = $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog); 1748 if ($bCreated) 1749 { 1750 if ($this->HasWarnings()) 1751 { 1752 $oStatLog->Inc('stats_nb_obj_created_warnings'); 1753 } 1754 } 1755 else 1756 { 1757 // Creation error has precedence over any warning 1758 $this->ResetWarnings(); 1759 } 1760 } 1761 else // assumed to be 'error' 1762 { 1763 $oStatLog->AddTrace("Failed to reconcile (no match)", $this); 1764 // Recoverable error 1765 $this->SetLastError('Could not find a match for reconciliation'); 1766 $oStatLog->Inc('stats_nb_replica_reconciled_errors'); 1767 } 1768 break; 1769 1770 case 1: 1771 $oStatLog->AddTrace("Found 1 object on: $sConditionDesc", $this); 1772 if ($oDataSource->Get('action_on_one') == 'update') 1773 { 1774 $oDestObj = $oDestSet->Fetch(); 1775 $oStatLog->AddTrace("Calling UpdateObjectFromReplica(".(get_class($oDestObj).'::'.$oDestObj->GetKey()).")", $this); 1776 $bModified = $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, 'stats_nb_obj_new', 'stats_nb_replica_reconciled_errors'); 1777 $this->Set('dest_id', $oDestObj->GetKey()); 1778 $this->Set('dest_class', get_class($oDestObj)); 1779 if ($this->HasWarnings()) 1780 { 1781 if ($bModified) 1782 { 1783 $oStatLog->Inc('stats_nb_obj_new_updated_warnings'); 1784 } 1785 else 1786 { 1787 $oStatLog->Inc('stats_nb_obj_new_unchanged_warnings'); 1788 } 1789 } 1790 } 1791 else 1792 { 1793 // assumed to be 'error' 1794 $oStatLog->AddTrace("Failed to reconcile (1 match)", $this); 1795 // Recoverable error 1796 $this->SetLastError('Found a match while expecting several'); 1797 $oStatLog->Inc('stats_nb_replica_reconciled_errors'); 1798 } 1799 break; 1800 1801 default: 1802 $oStatLog->AddTrace("Found $iCount objects on: $sConditionDesc", $this); 1803 if ($oDataSource->Get('action_on_multiple') == 'error') 1804 { 1805 $oStatLog->AddTrace("Failed to reconcile (N>1 matches)", $this); 1806 // Recoverable error 1807 $this->SetLastError($iCount.' destination objects match the reconciliation criterias: '.$sConditionDesc); 1808 $oStatLog->Inc('stats_nb_replica_reconciled_errors'); 1809 } 1810 elseif ($oDataSource->Get('action_on_multiple') == 'create') 1811 { 1812 $bCreated = $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog); 1813 if ($bCreated) 1814 { 1815 if ($this->HasWarnings()) 1816 { 1817 $oStatLog->Inc('stats_nb_obj_created_warnings'); 1818 } 1819 } 1820 else 1821 { 1822 // Creation error has precedence over any warning 1823 $this->ResetWarnings(); 1824 } 1825 } 1826 else 1827 { 1828 // assumed to be 'take_first' 1829 $oDestObj = $oDestSet->Fetch(); 1830 $bModified = $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, 'stats_nb_obj_new', 'stats_nb_replica_reconciled_errors'); 1831 $this->Set('dest_id', $oDestObj->GetKey()); 1832 $this->Set('dest_class', get_class($oDestObj)); 1833 if ($this->HasWarnings()) 1834 { 1835 if ($bModified) 1836 { 1837 $oStatLog->Inc('stats_nb_obj_new_updated_warnings'); 1838 } 1839 else 1840 { 1841 $oStatLog->Inc('stats_nb_obj_new_unchanged_warnings'); 1842 } 1843 } 1844 } 1845 } 1846 $this->RecordWarnings(); 1847 break; 1848 1849 case 'synchronized': // try to recover synchronized replicas with warnings 1850 case 'modified': 1851 $oDestObj = MetaModel::GetObject($oDataSource->GetTargetClass(), $this->Get('dest_id')); 1852 if ($oDestObj == null) 1853 { 1854 $this->Set('status', 'orphan'); // The destination object has been deleted ! 1855 $this->SetLastError('Destination object deleted unexpectedly'); 1856 $oStatLog->Inc('stats_nb_obj_updated_errors'); 1857 } 1858 else 1859 { 1860 $bModified = $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, 'stats_nb_obj', 'stats_nb_obj_updated_errors'); 1861 if ($this->HasWarnings()) 1862 { 1863 if ($bModified) 1864 { 1865 $oStatLog->Inc('stats_nb_obj_updated_warnings'); 1866 } 1867 else 1868 { 1869 $oStatLog->Inc('stats_nb_obj_unchanged_warnings'); 1870 } 1871 } 1872 } 1873 $this->RecordWarnings(); 1874 break; 1875 1876 default: // Do nothing in all other cases 1877 } 1878 $oStatLog->AddTrace("<<< End of SynchroReplica::Synchro.", $this); 1879 } 1880 1881 /** 1882 * Updates the destination object with the Extended data found in the synchro_data_XXXX table 1883 * 1884 * @param $oDestObj 1885 * @param string[] $aAttributes 1886 * @param $oChange 1887 * @param $oStatLog 1888 * @param string $sStatsCode 1889 * @param string $sStatsCodeError 1890 * 1891 * @return bool 1892 */ 1893 protected function UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, &$oStatLog, $sStatsCode, $sStatsCodeError) 1894 { 1895 $aValueTrace = array(); 1896 $bModified = false; 1897 try 1898 { 1899 foreach($aAttributes as $sAttCode => $oSyncAtt) 1900 { 1901 $value = $this->GetValueFromExtData($sAttCode, $oSyncAtt, $oStatLog); 1902 if (!is_null($value)) 1903 { 1904 if ($oSyncAtt->Get('update_policy') == 'write_if_empty') 1905 { 1906 $oAttDef = MetaModel::GetAttributeDef(get_class($oDestObj), $sAttCode); 1907 if ($oAttDef->IsNull($oDestObj->Get($sAttCode))) 1908 { 1909 // The value is still "empty" in the target object, we are allowed to write the new value 1910 $oDestObj->Set($sAttCode, $value); 1911 $aValueTrace[] = "$sAttCode: $value"; 1912 } 1913 } 1914 else 1915 { 1916 $oDestObj->Set($sAttCode, $value); 1917 $aValueTrace[] = "$sAttCode: $value"; 1918 } 1919 } 1920 } 1921 // Really modified ? 1922 if ($oDestObj->IsModified()) 1923 { 1924 $oDestObj->DBUpdateTracked($oChange); 1925 $bModified = true; 1926 $oStatLog->AddTrace('Updated object - Values: {'.implode(', ', $aValueTrace).'}', $this); 1927 if (($sStatsCode != '') &&(MetaModel::IsValidAttCode(get_class($oStatLog), $sStatsCode.'_updated'))) 1928 { 1929 $oStatLog->Inc($sStatsCode.'_updated'); 1930 } 1931 $this->Set('info_last_modified', date(AttributeDateTime::GetSQLFormat())); 1932 } 1933 else 1934 { 1935 $oStatLog->AddTrace('Unchanged object', $this); 1936 if (($sStatsCode != '') &&(MetaModel::IsValidAttCode(get_class($oStatLog), $sStatsCode.'_unchanged'))) 1937 { 1938 $oStatLog->Inc($sStatsCode.'_unchanged'); 1939 } 1940 } 1941 1942 $this->Set('status_last_error', ''); 1943 $this->Set('status', 'synchronized'); 1944 } 1945 catch(Exception $e) 1946 { 1947 $oStatLog->AddTrace("Failed to update destination object: {$e->getMessage()}", $this); 1948 $this->SetLastError('Unable to update destination object: ', $e); 1949 $oStatLog->Inc($sStatsCodeError); 1950 } 1951 return $bModified; 1952 } 1953 1954 /** 1955 * Creates the destination object populating it with the Extended data found in the synchro_data_XXXX table 1956 * 1957 * @param string $sClass 1958 * @param string[] $aAttributes 1959 * @param $oChange 1960 * @param $oStatLog 1961 * 1962 * @return bool Whether or not the object was created 1963 */ 1964 protected function CreateObjectFromReplica($sClass, $aAttributes, $oChange, &$oStatLog) 1965 { 1966 $bCreated = false; 1967 $oDestObj = MetaModel::NewObject($sClass); 1968 try 1969 { 1970 $aValueTrace = array(); 1971 foreach($aAttributes as $sAttCode => $oSyncAtt) 1972 { 1973 $value = $this->GetValueFromExtData($sAttCode, $oSyncAtt, $oStatLog); 1974 if (!is_null($value)) 1975 { 1976 $oDestObj->Set($sAttCode, $value); 1977 $aValueTrace[] = "$sAttCode: $value"; 1978 } 1979 } 1980 $iNew = $oDestObj->DBInsertTracked($oChange); 1981 1982 $this->Set('dest_id', $oDestObj->GetKey()); 1983 $this->Set('dest_class', get_class($oDestObj)); 1984 $this->Set('status_dest_creator', true); 1985 $this->Set('status_last_error', ''); 1986 $this->Set('status', 'synchronized'); 1987 $this->Set('info_creation_date', date(AttributeDateTime::GetSQLFormat())); 1988 $bCreated = true; 1989 1990 $oStatLog->AddTrace("Created (".implode(', ', $aValueTrace).")", $this); 1991 $oStatLog->Inc('stats_nb_obj_created'); 1992 } 1993 catch(Exception $e) 1994 { 1995 $oStatLog->AddTrace("Failed to create $sClass ({$e->getMessage()})", $this); 1996 $this->SetLastError('Unable to create destination object: ', $e); 1997 $oStatLog->Inc('stats_nb_obj_created_errors'); 1998 } 1999 return $bCreated; 2000 } 2001 2002 /** 2003 * Update the destination object with given values 2004 */ 2005 public function UpdateDestObject($aValues, $oChange, &$oStatLog) 2006 { 2007 try 2008 { 2009 if ($this->Get('dest_class') == '') 2010 { 2011 $this->SetLastError('No destination object to update'); 2012 $oStatLog->Inc('stats_nb_obj_obsoleted_errors'); 2013 } 2014 else 2015 { 2016 $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id')); 2017 foreach($aValues as $sAttCode => $value) 2018 { 2019 if (!MetaModel::IsValidAttCode(get_class($oDestObj), $sAttCode)) 2020 { 2021 throw new Exception("Unknown attribute code '$sAttCode'"); 2022 } 2023 $oDestObj->Set($sAttCode, $value); 2024 } 2025 $this->Set('info_last_modified', date(AttributeDateTime::GetSQLFormat())); 2026 $oDestObj->DBUpdateTracked($oChange); 2027 $oStatLog->AddTrace("Replica marked as obsolete", $this); 2028 $oStatLog->Inc('stats_nb_obj_obsoleted'); 2029 } 2030 } 2031 catch(Exception $e) 2032 { 2033 $this->SetLastError('Unable to update the destination object: ', $e); 2034 $oStatLog->Inc('stats_nb_obj_obsoleted_errors'); 2035 } 2036 } 2037 2038 /** 2039 * Delete the destination object 2040 */ 2041 public function DeleteDestObject($oChange, &$oStatLog) 2042 { 2043 if($this->Get('status_dest_creator')) 2044 { 2045 try 2046 { 2047 $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id')); 2048 $oCheckDeletionPlan = new DeletionPlan(); 2049 if ($oDestObj->CheckToDelete($oCheckDeletionPlan)) 2050 { 2051 $oActualDeletionPlan = new DeletionPlan(); 2052 $oDestObj->DBDeleteTracked($oChange, null, $oActualDeletionPlan); 2053 $this->DBDeleteTracked($oChange); 2054 $oStatLog->Inc('stats_nb_obj_deleted'); 2055 } 2056 else 2057 { 2058 $sIssues = implode("\n", $oCheckDeletionPlan->GetIssues()); 2059 throw(new Exception($sIssues)); 2060 } 2061 } 2062 catch(Exception $e) 2063 { 2064 $this->SetLastError('Unable to delete the destination object: ', $e); 2065 $this->Set('status', 'obsolete'); 2066 $this->DBUpdateTracked($oChange); 2067 $oStatLog->Inc('stats_nb_obj_deleted_errors'); 2068 } 2069 } 2070 else 2071 { 2072 $this->DBDeleteTracked($oChange); 2073 $oStatLog->Inc('stats_nb_replica_disappeared_no_action'); 2074 } 2075 } 2076 2077 /** 2078 * Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica 2079 * 2080 * @param string $sExtAttCode could be a standard attcode, or 'primary_key' 2081 * @param $oSyncAtt 2082 * @param $oStatLog 2083 * 2084 * @return mixed , or null (leave unchanged), or '' (reset) 2085 */ 2086 protected function GetValueFromExtData($sExtAttCode, $oSyncAtt, &$oStatLog) 2087 { 2088 // $aData should contain attributes defined either for reconciliation or create/update 2089 $aData = $this->GetExtendedData(); 2090 2091 if ($sExtAttCode == 'primary_key') 2092 { 2093 return $aData['primary_key']; 2094 } 2095 2096 // $sExtAttCode is a valid attribute code 2097 // 2098 $sClass = $this->Get('base_class'); 2099 2100 $oAttDef = MetaModel::GetAttributeDef($sClass, $sExtAttCode); 2101 2102 if (!is_null($oSyncAtt) && ($oSyncAtt instanceof SynchroAttExtKey)) 2103 { 2104 $rawValue = $aData[$sExtAttCode]; 2105 if (is_null($rawValue)) 2106 { 2107 // Null means "ignore" this attribute 2108 return null; 2109 } 2110 2111 $sReconcAttCode = $oSyncAtt->Get('reconciliation_attcode'); 2112 if (!empty($sReconcAttCode)) 2113 { 2114 $sRemoteClass = $oAttDef->GetTargetClass(); 2115 $oObj = MetaModel::GetObjectByColumn($sRemoteClass, $sReconcAttCode, $rawValue, false); 2116 if ($oObj) 2117 { 2118 $retValue = $oObj->GetKey(); 2119 } 2120 else 2121 { 2122 if ($rawValue != '') 2123 { 2124 // Note: differs from null (in which case the value would be left unchanged) 2125 $oStatLog->AddTrace("Could not find [unique] object for '$sExtAttCode': searched on $sReconcAttCode = '$rawValue'", $this); 2126 $this->AddWarning("Could not find [unique] object for '$sExtAttCode': searched on $sReconcAttCode = '$rawValue'"); 2127 } 2128 $retValue = 0; 2129 } 2130 } 2131 else 2132 { 2133 $retValue = $rawValue; 2134 } 2135 } 2136 elseif (!is_null($oSyncAtt) && ($oSyncAtt instanceof SynchroAttLinkSet)) 2137 { 2138 $rawValue = $aData[$sExtAttCode]; 2139 if (is_null($rawValue)) 2140 { 2141 // Null means "ignore" this attribute 2142 return null; 2143 } 2144 // MakeValueFromString() throws an exception in case of failure 2145 $bLocalizedValue = false; 2146 $retValue = $oAttDef->MakeValueFromString($rawValue, $bLocalizedValue, $oSyncAtt->Get('row_separator'), $oSyncAtt->Get('attribute_separator'), $oSyncAtt->Get('value_separator'), $oSyncAtt->Get('attribute_qualifier')); 2147 } 2148 else 2149 { 2150 $aColumns = $oAttDef->GetImportColumns(); 2151 foreach($aColumns as $sColumn => $sFormat) 2152 { 2153 // In any case, a null column means "ignore this attribute" 2154 // 2155 if (is_null($aData[$sColumn])) 2156 { 2157 return null; 2158 } 2159 } 2160 // No null column has been found 2161 $retValue = $oAttDef->FromImportToValue($aData, $sExtAttCode); 2162 if (is_null($retValue)) 2163 { 2164 // This is a reset 2165 $retValue = ''; 2166 } 2167 } 2168 2169 return $retValue; 2170 } 2171 2172 /** 2173 * Maps the given context parameter name to the appropriate filter/search code for this class 2174 * @param string $sContextParam Name of the context parameter, i.e. 'org_id' 2175 * @return string Filter code, i.e. 'customer_id' 2176 */ 2177 public static function MapContextParam($sContextParam) 2178 { 2179 if ($sContextParam == 'menu') 2180 { 2181 return null; 2182 } 2183 else 2184 { 2185 return $sContextParam; 2186 } 2187 } 2188 2189 /** 2190 * This function returns a 'hilight' CSS class, used to hilight a given row in a table 2191 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, 2192 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE 2193 * To Be overridden by derived classes 2194 * @param void 2195 * @return String The desired higlight class for the object/row 2196 */ 2197 public function GetHilightClass() 2198 { 2199 // Possible return values are: 2200 // HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE 2201 return HILIGHT_CLASS_NONE; // Not hilighted by default 2202 } 2203 2204 public static function GetUIPage() 2205 { 2206 return '../synchro/replica.php'; 2207 } 2208 2209 function DisplayDetails(WebPage $oPage, $bEditMode = false) 2210 { 2211 // Object's details 2212 //$this->DisplayBareHeader($oPage, $bEditMode); 2213 $oPage->AddTabContainer(OBJECT_PROPERTIES_TAB); 2214 $oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB); 2215 $oPage->SetCurrentTab(Dict::S('UI:PropertiesTab')); 2216 $this->DisplayBareProperties($oPage, $bEditMode); 2217 } 2218 2219 function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) 2220 { 2221 if ($bEditMode) return; // Not editable 2222 2223 $oPage->add('<table style="vertical-align:top"><tr style="vertical-align:top"><td>'); 2224 $aDetails = array(); 2225 $sClass = get_class($this); 2226 $oPage->add('<fieldset>'); 2227 $oPage->add('<legend>'.Dict::S('Core:SynchroReplica:PrivateDetails').'</legend>'); 2228 $aZList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); 2229 foreach( $aZList as $sAttCode) 2230 { 2231 $sDisplayValue = $this->GetAsHTML($sAttCode); 2232 $aDetails[] = array('label' => '<span title="'.MetaModel::GetDescription($sClass, $sAttCode).'">'.MetaModel::GetLabel($sClass, $sAttCode).'</span>', 'value' => $sDisplayValue); 2233 } 2234 $oPage->Details($aDetails); 2235 $oPage->add('</fieldset>'); 2236 if (strlen($this->Get('dest_class')) > 0) 2237 { 2238 $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'), false); 2239 if (is_object($oDestObj)) 2240 { 2241 $oPage->add('<fieldset>'); 2242 $oPage->add('<legend>'.Dict::Format('Core:SynchroReplica:TargetObject', $oDestObj->GetHyperlink()).'</legend>'); 2243 $oDestObj->DisplayBareProperties($oPage, false, $sPrefix, $aExtraParams); 2244 $oPage->add('<fieldset>'); 2245 } 2246 } 2247 $oPage->add('</td><td>'); 2248 $oPage->add('<fieldset>'); 2249 $oPage->add('<legend>'.Dict::S('Core:SynchroReplica:PublicData').'</legend>'); 2250 $oSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id')); 2251 2252 $sSQLTable = $oSource->GetDataTable(); 2253 $aData = $this->LoadExtendedDataFromTable($sSQLTable); 2254 2255 $aHeaders = array('attcode' => array('label' => 'Attribute Code', 'description' => ''), 2256 'data' => array('label' => 'Value', 'description' => '')); 2257 $aRows = array(); 2258 foreach($aData as $sKey => $value) 2259 { 2260 $aRows[] = array('attcode' => $sKey, 'data' => $value); 2261 } 2262 $oPage->Table($aHeaders, $aRows); 2263 $oPage->add('</fieldset>'); 2264 $oPage->add('</td></tr></table>'); 2265 2266 } 2267 2268 public function LoadExtendedDataFromTable($sSQLTable) 2269 { 2270 $sSQL = "SELECT * FROM $sSQLTable WHERE id=".$this->GetKey(); 2271 2272 $rQuery = CMDBSource::Query($sSQL); 2273 return CMDBSource::FetchArray($rQuery); 2274 } 2275} 2276 2277/** 2278 * Context of an ongoing synchronization 2279 * Two usages: 2280 * 1) Public usage: execute the synchronization 2281 * $oSynchroExec = new SynchroExecution($oDataSource[, $iLastFullLoad]); 2282 * $oSynchroExec->Process($iMaxChunkSize); 2283 * 2284 * 2) Internal usage: continue the synchronization (split into chunks, each performed in a separate process) 2285 * This is implemented in the page priv_sync_chunk.php 2286 * $oSynchroExec = SynchroExecution::Resume($oDataSource, $iLastFullLoad, $iSynchroLog, $iChange, $iMaxToProcess, $iJob, $iNextInJob); 2287 * $oSynchroExec->Process() 2288 */ 2289class SynchroExecution 2290{ 2291 protected $m_oDataSource = null; 2292 protected $m_oLastFullLoadStartDate = null; 2293 2294 protected $m_oChange = null; 2295 /** @var SynchroLog */ 2296 protected $m_oStatLog = null; 2297 2298 // Context computed one for optimization and report inconsistencies ASAP 2299 protected $m_aExtDataSpec = array(); 2300 protected $m_aReconciliationKeys = array(); 2301 protected $m_aAttributes = array(); 2302 protected $m_iCountAllReplicas = 0; 2303 protected $m_oCtx; 2304 protected $m_oCtx1; 2305 2306 /** 2307 * Constructor 2308 * @param SynchroDataSource $oDataSource Synchronization task 2309 * @param DateTime $oLastFullLoadStartDate Date of the last full load (start date/time), if known 2310 */ 2311 public function __construct($oDataSource, $oLastFullLoadStartDate = null) 2312 { 2313 $this->m_oDataSource = $oDataSource; 2314 $this->m_oLastFullLoadStartDate = $oLastFullLoadStartDate; 2315 $this->m_oCtx = new ContextTag('Synchro'); 2316 $this->m_oCtx1 = new ContextTag('Synchro:'.$oDataSource->GetRawName()); // More precise context information 2317 } 2318 2319 /** 2320 * Create the persistant information records, for the current synchronization 2321 * In fact, those records ARE defining what is the "current" synchronization 2322 */ 2323 protected function PrepareLogs() 2324 { 2325 if (!is_null($this->m_oChange)) 2326 { 2327 return; 2328 } 2329 2330 // Create a change used for logging all the modifications/creations happening during the synchro 2331 $this->m_oChange = MetaModel::NewObject("CMDBChange"); 2332 $this->m_oChange->Set("date", time()); 2333 $sUserString = CMDBChange::GetCurrentUserName(); 2334 $this->m_oChange->Set("userinfo", $sUserString.' '.Dict::S('Core:SyncDataExchangeComment')); 2335 $this->m_oChange->Set("origin", 'synchro-data-source'); 2336 $this->m_oChange->DBInsert(); 2337 2338 // Start logging this execution (stats + protection against reentrance) 2339 // 2340 $this->m_oStatLog = new SynchroLog(); 2341 $this->m_oStatLog->Set('sync_source_id', $this->m_oDataSource->GetKey()); 2342 $this->m_oStatLog->Set('start_date', time()); 2343 $this->m_oStatLog->Set('status', 'running'); 2344 $this->m_oStatLog->Set('stats_nb_replica_seen', 0); 2345 $this->m_oStatLog->Set('stats_nb_replica_total', 0); 2346 $this->m_oStatLog->Set('stats_nb_obj_deleted', 0); 2347 $this->m_oStatLog->Set('stats_nb_obj_deleted_errors', 0); 2348 $this->m_oStatLog->Set('stats_nb_obj_obsoleted', 0); 2349 $this->m_oStatLog->Set('stats_nb_obj_obsoleted_errors', 0); 2350 $this->m_oStatLog->Set('stats_nb_obj_created', 0); 2351 $this->m_oStatLog->Set('stats_nb_obj_created_errors', 0); 2352 $this->m_oStatLog->Set('stats_nb_obj_created_warnings', 0); 2353 $this->m_oStatLog->Set('stats_nb_obj_updated', 0); 2354 $this->m_oStatLog->Set('stats_nb_obj_updated_warnings', 0); 2355 $this->m_oStatLog->Set('stats_nb_obj_updated_errors', 0); 2356 $this->m_oStatLog->Set('stats_nb_obj_unchanged_warnings', 0); 2357 // $this->m_oStatLog->Set('stats_nb_replica_reconciled', 0); 2358 $this->m_oStatLog->Set('stats_nb_replica_reconciled_errors', 0); 2359 $this->m_oStatLog->Set('stats_nb_replica_disappeared_no_action', 0); 2360 $this->m_oStatLog->Set('stats_nb_obj_new_updated', 0); 2361 $this->m_oStatLog->Set('stats_nb_obj_new_updated_warnings', 0); 2362 $this->m_oStatLog->Set('stats_nb_obj_new_unchanged',0); 2363 $this->m_oStatLog->Set('stats_nb_obj_new_unchanged_warnings',0); 2364 2365 $sSelectTotal = "SELECT SynchroReplica WHERE sync_source_id = :source_id"; 2366 $oSetTotal = new DBObjectSet(DBObjectSearch::FromOQL($sSelectTotal), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey())); 2367 $this->m_iCountAllReplicas = $oSetTotal->Count(); 2368 $this->m_oStatLog->Set('stats_nb_replica_total', $this->m_iCountAllReplicas); 2369 2370 $this->m_oStatLog->DBInsertTracked($this->m_oChange); 2371 $sLastFullLoad = is_object($this->m_oLastFullLoadStartDate) ? $this->m_oLastFullLoadStartDate->format('Y-m-d H:i:s') : 'not specified'; 2372 $this->m_oStatLog->AddTrace("###### STARTING SYNCHRONIZATION ##### Total: {$this->m_iCountAllReplicas} replica(s). Last full load: '$sLastFullLoad' "); 2373 $sSql = 'SELECT NOW();'; 2374 $sDBNow = CMDBSource::QueryToScalar($sSql); 2375 $this->m_oStatLog->AddTrace("Database server current date/time is '$sDBNow', web server current date/time is: '".date('Y-m-d H:i:s')."'"); 2376 } 2377 2378 /** 2379 * Prevent against the reentrance... or allow the current task to do things forbidden by the others ! 2380 */ 2381 public static $m_oCurrentTask = null; 2382 public static function GetCurrentTaskId() 2383 { 2384 if (is_object(self::$m_oCurrentTask)) 2385 { 2386 return self::$m_oCurrentTask->GetKey(); 2387 } 2388 else 2389 { 2390 return null; 2391 } 2392 } 2393 2394 /** 2395 * Prepare structures in memory, to speedup the processing of a given replica 2396 * 2397 * @param bool $bFirstPass 2398 * 2399 * @throws \SynchroExceptionNotStarted 2400 */ 2401 public function PrepareProcessing($bFirstPass = true) 2402 { 2403 if ($this->m_oDataSource->Get('status') == 'obsolete') 2404 { 2405 throw new SynchroExceptionNotStarted(Dict::S('Core:SyncDataSourceObsolete')); 2406 } 2407 if (!UserRights::IsAdministrator() && $this->m_oDataSource->Get('user_id') != UserRights::GetUserId()) 2408 { 2409 throw new SynchroExceptionNotStarted(Dict::S('Core:SyncDataSourceAccessRestriction')); 2410 } 2411 2412 // Get the list of SQL columns 2413 $aAttCodesExpected = array(); 2414 $aAttCodesToReconcile = array(); 2415 $aAttCodesToUpdate = array(); 2416 $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND (update = 1 OR reconcile = 1)"; 2417 $oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey()) /* aArgs */); 2418 while ($oSyncAtt = $oSetAtt->Fetch()) 2419 { 2420 if ($oSyncAtt->Get('update')) 2421 { 2422 $aAttCodesToUpdate[$oSyncAtt->Get('attcode')] = $oSyncAtt; 2423 } 2424 if ($oSyncAtt->Get('reconcile')) 2425 { 2426 $aAttCodesToReconcile[$oSyncAtt->Get('attcode')] = $oSyncAtt; 2427 } 2428 $aAttCodesExpected[$oSyncAtt->Get('attcode')] = $oSyncAtt; 2429 } 2430 $aColumns = $this->m_oDataSource->GetSQLColumns(array_keys($aAttCodesExpected)); 2431 $aExtDataFields = array_keys($aColumns); 2432 $aExtDataFields[] = 'primary_key'; 2433 2434 $this->m_aExtDataSpec = array( 2435 'table' => $this->m_oDataSource->GetDataTable(), 2436 'join_key' => 'id', 2437 'fields' => $aExtDataFields 2438 ); 2439 2440 // Get the list of attributes, determine reconciliation keys and update targets 2441 // 2442 if ($this->m_oDataSource->Get('reconciliation_policy') == 'use_attributes') 2443 { 2444 $this->m_aReconciliationKeys = $aAttCodesToReconcile; 2445 } 2446 elseif ($this->m_oDataSource->Get('reconciliation_policy') == 'use_primary_key') 2447 { 2448 // Override the settings made at the attribute level ! 2449 $this->m_aReconciliationKeys = array("primary_key" => null); 2450 } 2451 2452 if ($bFirstPass) 2453 { 2454 $this->m_oStatLog->AddTrace("Update of: {".implode(', ', array_keys($aAttCodesToUpdate))."}"); 2455 $this->m_oStatLog->AddTrace("Reconciliation on: {".implode(', ', array_keys($this->m_aReconciliationKeys))."}"); 2456 } 2457 2458 if (count($aAttCodesToUpdate) == 0) 2459 { 2460 $this->m_oStatLog->AddTrace("No attribute to update"); 2461 throw new SynchroExceptionNotStarted('There is no attribute to update'); 2462 } 2463 if (count($this->m_aReconciliationKeys) == 0) 2464 { 2465 $this->m_oStatLog->AddTrace("No attribute for reconciliation"); 2466 throw new SynchroExceptionNotStarted('No attribute for reconciliation'); 2467 } 2468 2469 $this->m_aAttributes = array(); 2470 foreach($aAttCodesToUpdate as $sAttCode => $oSyncAtt) 2471 { 2472 $oAttDef = MetaModel::GetAttributeDef($this->m_oDataSource->GetTargetClass(), $sAttCode); 2473 if ($oAttDef->IsWritable()) 2474 { 2475 $this->m_aAttributes[$sAttCode] = $oSyncAtt; 2476 } 2477 } 2478 2479 // Compute and keep track of the limit date taken into account for obsoleting replicas 2480 // 2481 if ($this->m_oLastFullLoadStartDate == null) 2482 { 2483 $this->m_oLastFullLoadStartDate = new DateTime('1970-01-01'); 2484 } 2485 if ($bFirstPass) 2486 { 2487 $this->m_oStatLog->AddTrace("Limit Date: ".$this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s')); 2488 } 2489 } 2490 2491 2492 /** 2493 * Perform a synchronization between the data stored in the replicas (&synchro_data_xxx_xx table) 2494 * and the iTop objects. If the lastFullLoadStartDate is NOT specified then the full_load_periodicity 2495 * is used to determine which records are obsolete. 2496 * 2497 * @return SynchroLog 2498 */ 2499 public function Process() 2500 { 2501 $this->PrepareLogs(); 2502 2503 self::$m_oCurrentTask = $this->m_oDataSource; 2504 2505 $oMutex = new iTopMutex('synchro_process_'.$this->m_oDataSource->GetKey()); 2506 try 2507 { 2508 $oMutex->Lock(); 2509 $this->DoSynchronize(); 2510 $oMutex->Unlock(); 2511 2512 $this->m_oStatLog->Set('end_date', time()); 2513 $this->m_oStatLog->Set('status', 'completed'); 2514 $this->m_oStatLog->DBUpdateTracked($this->m_oChange); 2515 2516 $iErrors = $this->m_oStatLog->GetErrorCount(); 2517 if ($iErrors > 0) 2518 { 2519 $sIssuesOQL = "SELECT SynchroReplica WHERE sync_source_id=".$this->m_oDataSource->GetKey()." AND status_last_error!=''"; 2520 $sAbsoluteUrl = utils::GetAbsoluteUrlAppRoot(); 2521 $sIssuesURL = "{$sAbsoluteUrl}synchro/replica.php?operation=oql&datasource=".$this->m_oDataSource->GetKey()."&oql=".urlencode($sIssuesOQL); 2522 2523 $sStatistics = "<h1>Statistics</h1>\n"; 2524 $sStatistics .= "<ul>\n"; 2525 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('start_date').": ".$this->m_oStatLog->Get('start_date')."</li>\n"; 2526 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('end_date').": ".$this->m_oStatLog->Get('end_date')."</li>\n"; 2527 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_replica_seen').": ".$this->m_oStatLog->Get('stats_nb_replica_seen')."</li>\n"; 2528 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_replica_total').": ".$this->m_oStatLog->Get('stats_nb_replica_total')."</li>\n"; 2529 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_deleted').": ".$this->m_oStatLog->Get('stats_nb_obj_deleted')."</li>\n"; 2530 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_deleted_errors').": ".$this->m_oStatLog->Get('stats_nb_obj_deleted_errors')."</li>\n"; 2531 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_obsoleted').": ".$this->m_oStatLog->Get('stats_nb_obj_obsoleted')."</li>\n"; 2532 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_obsoleted_errors').": ".$this->m_oStatLog->Get('stats_nb_obj_obsoleted_errors')."</li>\n"; 2533 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_created').": ".$this->m_oStatLog->Get('stats_nb_obj_created')." (".$this->m_oStatLog->Get('stats_nb_obj_created_warnings')." warnings)"."</li>\n"; 2534 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_created_errors').": ".$this->m_oStatLog->Get('stats_nb_obj_created_errors')."</li>\n"; 2535 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_updated').": ".$this->m_oStatLog->Get('stats_nb_obj_updated')." (".$this->m_oStatLog->Get('stats_nb_obj_updated_warnings')." warnings)"."</li>\n"; 2536 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_updated_errors').": ".$this->m_oStatLog->Get('stats_nb_obj_updated_errors')."</li>\n"; 2537 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_replica_reconciled_errors').": ".$this->m_oStatLog->Get('stats_nb_replica_reconciled_errors')."</li>\n"; 2538 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_replica_disappeared_no_action').": ".$this->m_oStatLog->Get('stats_nb_replica_disappeared_no_action')."</li>\n"; 2539 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_new_updated').": ".$this->m_oStatLog->Get('stats_nb_obj_new_updated')." (".$this->m_oStatLog->Get('stats_nb_obj_new_updated_warnings')." warnings)"."</li>\n"; 2540 $sStatistics .= "<li>".$this->m_oStatLog->GetLabel('stats_nb_obj_new_unchanged').": ".$this->m_oStatLog->Get('stats_nb_obj_new_unchanged')." (".$this->m_oStatLog->Get('stats_nb_obj_new_unchanged_warnings')." warnings)"."</li>\n"; 2541 $sStatistics .= "</ul>\n"; 2542 2543 $this->m_oDataSource->SendNotification("errors ($iErrors)", "<p>The synchronization has been executed, $iErrors errors have been encountered. Click <a href=\"$sIssuesURL\">here</a> to see the records being currently in error.</p>".$sStatistics); 2544 } 2545 else 2546 { 2547 //$this->m_oDataSource->SendNotification('success', '<p>The synchronization has been successfully executed.</p>'); 2548 } 2549 } 2550 catch (SynchroExceptionNotStarted $e) 2551 { 2552 $oMutex->Unlock(); 2553 // Set information for reporting... but delete the object in DB 2554 $this->m_oStatLog->Set('end_date', time()); 2555 $this->m_oStatLog->Set('status', 'error'); 2556 $this->m_oStatLog->Set('last_error', $e->getMessage()); 2557 $this->m_oStatLog->DBDeleteTracked($this->m_oChange); 2558 $this->m_oDataSource->SendNotification('fatal error', '<p>The synchronization could not start: \''.$e->getMessage().'\'</p><p>Please check its configuration</p>'); 2559 } 2560 catch (Exception $e) 2561 { 2562 $oMutex->Unlock(); 2563 $this->m_oStatLog->Set('end_date', time()); 2564 $this->m_oStatLog->Set('status', 'error'); 2565 $this->m_oStatLog->Set('last_error', $e->getMessage()); 2566 $this->m_oStatLog->DBUpdateTracked($this->m_oChange); 2567 $this->m_oDataSource->SendNotification('exception', '<p>The synchronization has been interrupted: \''.$e->getMessage().'\'</p><p>Please contact the application support team</p>'); 2568 } 2569 self::$m_oCurrentTask = null; 2570 2571 return $this->m_oStatLog; 2572 } 2573 2574 /** 2575 * Do the entire synchronization job 2576 */ 2577 protected function DoSynchronize() 2578 { 2579 $this->m_oStatLog->Set('status_curr_job', 1); 2580 $this->m_oStatLog->Set('status_curr_pos', -1); 2581 2582 $iMaxChunkSize = utils::ReadParam('max_chunk_size', 0, true /* allow CLI */); 2583 if ($iMaxChunkSize > 0) 2584 { 2585 // Split the execution into several processes 2586 // Each process will call DoSynchronizeChunk() 2587 // The loop will end when a process does not reply "continue" on the last line of its output 2588 if (!utils::IsModeCLI()) 2589 { 2590 throw new SynchroExceptionNotStarted(Dict::S('Core:SyncSplitModeCLIOnly')); 2591 } 2592 $aArguments = array(); 2593 $aArguments['source'] = $this->m_oDataSource->GetKey(); 2594 $aArguments['log'] = $this->m_oStatLog->GetKey(); 2595 $aArguments['change'] = $this->m_oChange->GetKey(); 2596 $aArguments['chunk'] = $iMaxChunkSize; 2597 if ($this->m_oLastFullLoadStartDate) 2598 { 2599 $aArguments['last_full_load'] = $this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s'); 2600 } 2601 else 2602 { 2603 $aArguments['last_full_load'] = ''; 2604 } 2605 2606 $this->m_oStatLog->DBUpdate($this->m_oChange); 2607 2608 $iStepCount = 0; 2609 do 2610 { 2611 $aArguments['step_count'] = $iStepCount; 2612 $iStepCount++; 2613 2614 set_time_limit(0); // On Linux the time spent outside of the script does not count, but on Windows it does, so let give us time ! 2615 list ($iRes, $aOut) = utils::ExecITopScript('synchro/priv_sync_chunk.php', $aArguments); 2616 2617 // Reload the log that has been modified by the processes 2618 $this->m_oStatLog->Reload(); 2619 2620 $sLastRes = strtolower(trim(end($aOut))); 2621 switch($sLastRes) 2622 { 2623 case 'continue': 2624 $bContinue = true; 2625 break; 2626 2627 case 'finished': 2628 $bContinue = false; 2629 break; 2630 2631 default: 2632 $this->m_oStatLog->AddTrace("The script did not reply with the expected keywords:"); 2633 $aIndentedOut = array(); 2634 foreach ($aOut as $sOut) 2635 { 2636 $aIndentedOut[] = "-> $sOut"; 2637 $this->m_oStatLog->AddTrace(">>> $sOut"); 2638 } 2639 throw new Exception("Encountered an error in an underspinned process:\n".implode("\n", $aIndentedOut)); 2640 } 2641 } 2642 while ($bContinue); 2643 } 2644 else 2645 { 2646 $this->PrepareProcessing(/* first pass */); 2647 $this->DoJob1(); 2648 $this->DoJob2(); 2649 $this->DoJob3(); 2650 } 2651 } 2652 2653 /** 2654 * Do the synchronization job, limited to some amount of work 2655 * This verb has been designed to be called from within a separate process 2656 * @return true if the process has to be continued 2657 */ 2658 public function DoSynchronizeChunk($oLog, $oChange, $iMaxChunkSize) 2659 { 2660 // Initialize the structures... 2661 self::$m_oCurrentTask = $this->m_oDataSource; 2662 $this->m_oStatLog = $oLog; 2663 $this->m_oChange = $oChange; 2664 2665 // Prepare internal structures (not the first pass) 2666 $this->PrepareProcessing(false); 2667 2668 $iCurrJob = $this->m_oStatLog->Get('status_curr_job'); 2669 $iCurrPos = $this->m_oStatLog->Get('status_curr_pos'); 2670 2671 $this->m_oStatLog->AddTrace("Synchronizing chunk - curr_job:$iCurrJob, curr_pos:$iCurrPos, max_chunk_size:$iMaxChunkSize"); 2672 2673 switch ($iCurrJob) 2674 { 2675 case 1: 2676 default: 2677 $this->DoJob1($iMaxChunkSize, $iCurrPos); 2678 $bContinue = true; 2679 break; 2680 2681 case 2: 2682 $this->DoJob2($iMaxChunkSize, $iCurrPos); 2683 $bContinue = true; 2684 break; 2685 2686 case 3: 2687 $bContinue = $this->DoJob3($iMaxChunkSize, $iCurrPos); 2688 break; 2689 } 2690 $this->m_oStatLog->DBUpdate($this->m_oChange); 2691 self::$m_oCurrentTask = null; 2692 return $bContinue; 2693 } 2694 2695 /** 2696 * Do the synchronization job #1: Obsolete replica "untouched" for some time 2697 * @param integer $iMaxReplica Limit the number of replicas to process 2698 * @param integer $iCurrPos Current position where to resume the processing 2699 * @return true if the process must be continued 2700 */ 2701 protected function DoJob1($iMaxReplica = null, $iCurrPos = -1) 2702 { 2703 $this->m_oStatLog->AddTrace(">>> Beginning of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)"); 2704 $sLastFullLoadStartDate = $this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s'); 2705 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 2706 2707 // Get all the replicas that were not seen in the last import and mark them as obsolete 2708 $sDeletePolicy = $this->m_oDataSource->Get('delete_policy'); 2709 if ($sDeletePolicy != 'ignore') 2710 { 2711 $oLimitDate = clone($this->m_oLastFullLoadStartDate); 2712 $iLoadPeriodicity = $this->m_oDataSource->Get('full_load_periodicity'); // Duration in seconds 2713 if ($iLoadPeriodicity > 0) 2714 { 2715 $sInterval = "-$iLoadPeriodicity seconds"; 2716 $oLimitDate->Modify($sInterval); 2717 } 2718 $sLimitDate = $oLimitDate->Format('Y-m-d H:i:s'); 2719 $sSelectToObsolete = "SELECT SynchroReplica WHERE id > :curr_pos AND sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import"; 2720 $oSetScope = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos)); 2721 $iCountScope = $oSetScope->Count(); 2722 $sDebugOql = $oSetScope->GetFilter()->ToOQL(true); 2723 $this->m_oStatLog->AddTrace("Searching for replicas to mark as obsolete using query: '$sDebugOql', returned $iCountScope replica(s)."); 2724 if (($this->m_iCountAllReplicas > 10) && ($this->m_iCountAllReplicas == $iCountScope) && MetaModel::GetConfig()->Get('synchro_prevent_delete_all')) 2725 { 2726 throw new SynchroExceptionNotStarted(Dict::S('Core:SyncTooManyMissingReplicas')); 2727 } 2728 2729 if ($iMaxReplica) 2730 { 2731 // Consider a given subset, starting from replica iCurrPos, limited to the count of iMaxReplica 2732 // The replica have to be ordered by id 2733 $oSetToProcess = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array('id'=>true) /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos)); 2734 $oSetToProcess->SetLimit($iMaxReplica); 2735 } 2736 else 2737 { 2738 $oSetToProcess = $oSetScope; 2739 } 2740 2741 $iLastReplicaProcessed = -1; 2742 while($oReplica = $oSetToProcess->Fetch()) 2743 { 2744 set_time_limit($iLoopTimeLimit); 2745 $iLastReplicaProcessed = $oReplica->GetKey(); 2746 switch ($sDeletePolicy) 2747 { 2748 case 'update': 2749 case 'update_then_delete': 2750 $this->m_oStatLog->AddTrace("Destination object to be updated", $oReplica); 2751 $aToUpdate = array(); 2752 $aToUpdateSpec = explode(';', $this->m_oDataSource->Get('delete_policy_update')); //ex: 'status:obsolete;description:stopped', 2753 foreach($aToUpdateSpec as $sUpdateSpec) 2754 { 2755 $aUpdateSpec = explode(':', $sUpdateSpec); 2756 if (count($aUpdateSpec) == 2) 2757 { 2758 $sAttCode = $aUpdateSpec[0]; 2759 $sValue = $aUpdateSpec[1]; 2760 $aToUpdate[$sAttCode] = $sValue; 2761 } 2762 } 2763 $oReplica->Set('status_last_error', ''); 2764 if ($oReplica->Get('dest_id') == '') 2765 { 2766 $oReplica->Set('status', 'obsolete'); 2767 $this->m_oStatLog->Inc('stats_nb_replica_disappeared_no_action'); 2768 } 2769 else 2770 { 2771 $oReplica->UpdateDestObject($aToUpdate, $this->m_oChange, $this->m_oStatLog); 2772 if ($oReplica->Get('status_last_error') == '') 2773 { 2774 // Change the status of the replica IIF 2775 $oReplica->Set('status', 'obsolete'); 2776 } 2777 } 2778 $oReplica->DBUpdateTracked($this->m_oChange); 2779 break; 2780 2781 case 'delete': 2782 default: 2783 $this->m_oStatLog->AddTrace("Destination object to be DELETED", $oReplica); 2784 $oReplica->DeleteDestObject($this->m_oChange, $this->m_oStatLog); 2785 } 2786 } 2787 if ($iMaxReplica) 2788 { 2789 if ($iMaxReplica < $iCountScope) 2790 { 2791 // Continue with this job! 2792 $this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed); 2793 $this->m_oStatLog->AddTrace("<<< End of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)"); 2794 return true; 2795 } 2796 } 2797 } // if ($sDeletePolicy != 'ignore' 2798 2799 //Count "seen" objects 2800 $sSelectSeen = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen >= :last_import"; 2801 $oSetSeen = new DBObjectSet(DBObjectSearch::FromOQL($sSelectSeen), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLastFullLoadStartDate)); 2802 $this->m_oStatLog->Set('stats_nb_replica_seen', $oSetSeen->Count()); 2803 2804 2805 // Job complete! 2806 $this->m_oStatLog->Set('status_curr_job', 2); 2807 $this->m_oStatLog->Set('status_curr_pos', -1); 2808 $this->m_oStatLog->AddTrace("<<< End of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)"); 2809 return false; 2810 } 2811 2812 /** 2813 * Do the synchronization job #2: Create and modify object for new/modified replicas 2814 * @param integer $iMaxReplica Limit the number of replicas to process 2815 * @param integer $iCurrPos Current position where to resume the processing 2816 * @return true if the process must be continued 2817 */ 2818 protected function DoJob2($iMaxReplica = null, $iCurrPos = -1) 2819 { 2820 $this->m_oStatLog->AddTrace(">>> Beginning of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)"); 2821 $sLimitDate = $this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s'); 2822 $this->m_oStatLog->AddTrace("\$sLimitDate = '$sLimitDate'"); 2823 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 2824 2825 // Get all the replicas that are 'new' or modified or synchronized with a warning 2826 // 2827 $sSelectToSync = "SELECT SynchroReplica WHERE id > :curr_pos AND (status = 'new' OR status = 'modified' OR (status = 'synchronized' AND status_last_warning != '')) AND sync_source_id = :source_id AND status_last_seen >= :last_import"; 2828 $oSetScope = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos), $this->m_aExtDataSpec); 2829 $iCountScope = $oSetScope->Count(); 2830 $sDebugOQL = $oSetScope->GetFilter()->ToOQL(true); 2831 $this->m_oStatLog->AddTrace("Looking for - new, modified or synchonized with a warning - replicas using the OQL query: '$sDebugOQL', returned $iCountScope replicas."); 2832 2833 if ($iMaxReplica) 2834 { 2835 // Consider a given subset, starting from replica iCurrPos, limited to the count of iMaxReplica 2836 // The replica have to be ordered by id 2837 $oSetToProcess = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array('id'=>true) /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos), $this->m_aExtDataSpec); 2838 $oSetToProcess->SetLimit($iMaxReplica); 2839 } 2840 else 2841 { 2842 $oSetToProcess = $oSetScope; 2843 } 2844 2845 $iLastReplicaProcessed = -1; 2846 while($oReplica = $oSetToProcess->Fetch()) 2847 { 2848 set_time_limit($iLoopTimeLimit); 2849 $iLastReplicaProcessed = $oReplica->GetKey(); 2850 $this->m_oStatLog->AddTrace("Synchronizing replica id=$iLastReplicaProcessed."); 2851 $oReplica->Synchro($this->m_oDataSource, $this->m_aReconciliationKeys, $this->m_aAttributes, $this->m_oChange, $this->m_oStatLog); 2852 $this->m_oStatLog->AddTrace("Updating replica id=$iLastReplicaProcessed."); 2853 $oReplica->DBUpdateTracked($this->m_oChange); 2854 } 2855 2856 if ($iMaxReplica) 2857 { 2858 if ($iMaxReplica < $iCountScope) 2859 { 2860 // Continue with this job! 2861 $this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed); 2862 $this->m_oStatLog->AddTrace("<<< End of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)"); 2863 return true; 2864 } 2865 } 2866 2867 // Job complete! 2868 $this->m_oStatLog->Set('status_curr_job', 3); 2869 $this->m_oStatLog->Set('status_curr_pos', -1); 2870 $this->m_oStatLog->AddTrace("<<< End of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)"); 2871 return false; 2872 } 2873 2874 /** 2875 * Do the synchronization job #3: Delete replica depending on the obsolescence scheme 2876 * @param integer $iMaxReplica Limit the number of replicas to process 2877 * @param integer $iCurrPos Current position where to resume the processing 2878 * @return true if the process must be continued 2879 */ 2880 protected function DoJob3($iMaxReplica = null, $iCurrPos = -1) 2881 { 2882 $this->m_oStatLog->AddTrace(">>> Beginning of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)"); 2883 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 2884 2885 $sDeletePolicy = $this->m_oDataSource->Get('delete_policy'); 2886 if ($sDeletePolicy != 'update_then_delete') 2887 { 2888 $this->m_oStatLog->AddTrace("\$sDeletePoliciy = $sDeletePolicy != 'update_then_delete', nothing to do!"); 2889 // Job complete! 2890 $this->m_oStatLog->Set('status_curr_job', 0); 2891 $this->m_oStatLog->Set('status_curr_pos', -1); 2892 $this->m_oStatLog->AddTrace("<<< End of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)"); 2893 return false; 2894 } 2895 2896 $bFirstPass = ($iCurrPos == -1); 2897 2898 // Get all the replicas that are to be deleted 2899 // 2900 $oDeletionDate = $this->m_oLastFullLoadStartDate; 2901 $iDeleteRetention = $this->m_oDataSource->Get('delete_policy_retention'); // Duration in seconds 2902 if ($iDeleteRetention > 0) 2903 { 2904 $sInterval = "-$iDeleteRetention seconds"; 2905 $oDeletionDate->Modify($sInterval); 2906 } 2907 $sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s'); 2908 if ($bFirstPass) 2909 { 2910 $this->m_oStatLog->AddTrace("Deletion date: $sDeletionDate"); 2911 } 2912 $sSelectToDelete = "SELECT SynchroReplica WHERE id > :curr_pos AND sync_source_id = :source_id AND status IN ('obsolete') AND status_last_seen < :last_import"; 2913 $oSetScope = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sDeletionDate, 'curr_pos' => $iCurrPos)); 2914 $iCountScope = $oSetScope->Count(); 2915 2916 if ($iMaxReplica) 2917 { 2918 // Consider a given subset, starting from replica iCurrPos, limited to the count of iMaxReplica 2919 // The replica have to be ordered by id 2920 $oSetToProcess = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array('id'=>true) /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sDeletionDate, 'curr_pos' => $iCurrPos)); 2921 $oSetToProcess->SetLimit($iMaxReplica); 2922 } 2923 else 2924 { 2925 $oSetToProcess = $oSetScope; 2926 } 2927 2928 $iLastReplicaProcessed = -1; 2929 while($oReplica = $oSetToProcess->Fetch()) 2930 { 2931 set_time_limit($iLoopTimeLimit); 2932 $iLastReplicaProcessed = $oReplica->GetKey(); 2933 $this->m_oStatLog->AddTrace("Destination object to be DELETED", $oReplica); 2934 $oReplica->DeleteDestObject($this->m_oChange, $this->m_oStatLog); 2935 } 2936 2937 if ($iMaxReplica) 2938 { 2939 if ($iMaxReplica < $iCountScope) 2940 { 2941 // Continue with this job! 2942 $this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed); 2943 $this->m_oStatLog->AddTrace("<<< End of DoJob3\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)"); 2944 return true; 2945 } 2946 } 2947 // Job complete! 2948 $this->m_oStatLog->Set('status_curr_job', 0); 2949 $this->m_oStatLog->Set('status_curr_pos', -1); 2950 $this->m_oStatLog->AddTrace("<<< End of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)"); 2951 return false; 2952 } 2953} 2954