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 * Execute and shows the data quality audit 21 * 22 * @copyright Copyright (C) 2010-2017 Combodo SARL 23 * @license http://opensource.org/licenses/AGPL-3.0 24 */ 25/** 26 * Adds the context parameters to the audit rule query 27 * 28 * @param DBSearch $oFilter 29 * @param ApplicationContext $oAppContext 30 * 31 * @throws \CoreException 32 * @throws \CoreWarning 33 * @throws \Exception 34 */ 35function FilterByContext(DBSearch &$oFilter, ApplicationContext $oAppContext) 36{ 37 $sObjClass = $oFilter->GetClass(); 38 $aContextParams = $oAppContext->GetNames(); 39 $aCallSpec = array($sObjClass, 'MapContextParam'); 40 if (is_callable($aCallSpec)) 41 { 42 foreach($aContextParams as $sParamName) 43 { 44 $sValue = $oAppContext->GetCurrentValue($sParamName, null); 45 if ($sValue != null) 46 { 47 $sAttCode = call_user_func($aCallSpec, $sParamName); // Returns null when there is no mapping for this parameter 48 if ( ($sAttCode != null) && MetaModel::IsValidAttCode($sObjClass, $sAttCode)) 49 { 50 // Check if the condition points to a hierarchical key 51 $bConditionAdded = false; 52 if ($sAttCode == 'id') 53 { 54 // Filtering on the objects themselves 55 $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass($sObjClass); 56 57 if ($sHierarchicalKeyCode !== false) 58 { 59 $oRootFilter = new DBObjectSearch($sObjClass); 60 $oRootFilter->AddCondition($sAttCode, $sValue); 61 $oFilter->AddCondition_PointingTo($oRootFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); // Use the 'below' operator by default 62 $bConditionAdded = true; 63 } 64 } 65 else 66 { 67 $oAttDef = MetaModel::GetAttributeDef($sObjClass, $sAttCode); 68 $bConditionAdded = false; 69 if ($oAttDef->IsExternalKey()) 70 { 71 $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass()); 72 73 if ($sHierarchicalKeyCode !== false) 74 { 75 $oRootFilter = new DBObjectSearch($oAttDef->GetTargetClass()); 76 $oRootFilter->AddCondition('id', $sValue); 77 $oHKFilter = new DBObjectSearch($oAttDef->GetTargetClass()); 78 $oHKFilter->AddCondition_PointingTo($oRootFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); // Use the 'below' operator by default 79 $oFilter->AddCondition_PointingTo($oHKFilter, $sAttCode); 80 $bConditionAdded = true; 81 } 82 } 83 } 84 if (!$bConditionAdded) 85 { 86 $oFilter->AddCondition($sAttCode, $sValue); 87 } 88 } 89 } 90 } 91 } 92} 93 94/** 95 * @param int $iRuleId Audit rule ID 96 * @param DBObjectSearch $oDefinitionFilter Created from the audit category's OQL 97 * @param ApplicationContext $oAppContext 98 * 99 * @return mixed 100 * @throws \ArchivedObjectException 101 * @throws \CoreException 102 * @throws \OQLException 103 */ 104function GetRuleResultFilter($iRuleId, $oDefinitionFilter, $oAppContext) 105{ 106 $oRule = MetaModel::GetObject('AuditRule', $iRuleId); 107 $sOql = $oRule->Get('query'); 108 $oRuleFilter = DBObjectSearch::FromOQL($sOql); 109 $oRuleFilter->UpdateContextFromUser(); 110 FilterByContext($oRuleFilter, $oAppContext); // Not needed since this filter is a subset of the definition filter, but may speedup things 111 112 if ($oRule->Get('valid_flag') == 'false') 113 { 114 // The query returns directly the invalid elements 115 $oFilter = $oRuleFilter->Intersect($oDefinitionFilter); 116 } 117 else 118 { 119 // The query returns only the valid elements, all the others are invalid 120 // Warning : we're generating a `WHERE ID IN`... query, and this could be very slow if there are lots of id ! 121 $aValidRows = $oRuleFilter->ToDataArray(array('id')); 122 $aValidIds = array(); 123 foreach($aValidRows as $aRow) 124 { 125 $aValidIds[] = $aRow['id']; 126 } 127 /** @var \DBObjectSearch $oFilter */ 128 $oFilter = $oDefinitionFilter->DeepClone(); 129 if (count($aValidIds) > 0) 130 { 131 $aInDefSet = array(); 132 foreach($oDefinitionFilter->ToDataArray(array('id')) as $aRow) 133 { 134 $aInDefSet[] = $aRow['id']; 135 } 136 $aInvalids = array_diff($aInDefSet, $aValidIds); 137 if (count($aInvalids) > 0) 138 { 139 $oFilter->AddConditionForInOperatorUsingParam('id', $aInvalids, true); 140 } 141 else 142 { 143 $oFilter->AddCondition('id', 0, '='); 144 } 145 } 146 } 147 return $oFilter; 148} 149 150function GetReportColor($iTotal, $iErrors) 151{ 152 $sResult = 'red'; 153 if ( ($iTotal == 0) || ($iErrors / $iTotal) <= 0.05 ) 154 { 155 $sResult = 'green'; 156 } 157 else if ( ($iErrors / $iTotal) <= 0.25 ) 158 { 159 $sResult = 'orange'; 160 } 161 return $sResult; 162} 163 164try 165{ 166 require_once('../approot.inc.php'); 167 require_once(APPROOT.'/application/application.inc.php'); 168 require_once(APPROOT.'/application/itopwebpage.class.inc.php'); 169 require_once(APPROOT.'/application/csvpage.class.inc.php'); 170 171 172 require_once(APPROOT.'/application/startup.inc.php'); 173 $operation = utils::ReadParam('operation', ''); 174 $oAppContext = new ApplicationContext(); 175 176 require_once(APPROOT.'/application/loginwebpage.class.inc.php'); 177 LoginWebPage::DoLogin(); // Check user rights and prompt if needed 178 179 $oP = new iTopWebPage(Dict::S('UI:Audit:Title')); 180 181 switch($operation) 182 { 183 case 'csv': 184 $oP->DisableBreadCrumb(); 185 // Big result sets cause long OQL that cannot be passed (serialized) as a GET parameter 186 // Therefore we don't use the standard "search_oql" operation of UI.php to display the CSV 187 $iCategory = utils::ReadParam('category', ''); 188 $iRuleIndex = utils::ReadParam('rule', 0); 189 190 $oAuditCategory = MetaModel::GetObject('AuditCategory', $iCategory); 191 $oDefinitionFilter = DBObjectSearch::FromOQL($oAuditCategory->Get('definition_set')); 192 $oDefinitionFilter->UpdateContextFromUser(); 193 FilterByContext($oDefinitionFilter, $oAppContext); 194 $oDefinitionSet = new CMDBObjectSet($oDefinitionFilter); 195 $oFilter = GetRuleResultFilter($iRuleIndex, $oDefinitionFilter, $oAppContext); 196 $oErrorObjectSet = new CMDBObjectSet($oFilter); 197 $oAuditRule = MetaModel::GetObject('AuditRule', $iRuleIndex); 198 $sFileName = utils::ReadParam('filename', null, true, 'string'); 199 $bAdvanced = utils::ReadParam('advanced', false); 200 $sAdvanced = $bAdvanced ? '&advanced=1' : ''; 201 202 if ($sFileName != null) 203 { 204 $oP = new CSVPage("iTop - Export"); 205 $sCharset = MetaModel::GetConfig()->Get('csv_file_default_charset'); 206 $sCSVData = cmdbAbstractObject::GetSetAsCSV($oErrorObjectSet, array('localize_values' => true, 'fields_advanced' => $bAdvanced), $sCharset); 207 if ($sCharset == 'UTF-8') 208 { 209 $sOutputData = UTF8_BOM.$sCSVData; 210 } 211 else 212 { 213 $sOutputData = $sCSVData; 214 } 215 if ($sFileName == '') 216 { 217 // Plain text => Firefox will NOT propose to download the file 218 $oP->add_header("Content-type: text/plain; charset=$sCharset"); 219 } 220 else 221 { 222 $sCSVName = basename($sFileName); // pseudo sanitization, just in case 223 // Force the name of the downloaded file, since windows gives precedence to the extension over of the mime type 224 $oP->add_header("Content-disposition: attachment; filename=\"$sCSVName\""); 225 $oP->add_header("Content-type: text/csv; charset=$sCharset"); 226 } 227 $oP->add($sOutputData); 228 $oP->TrashUnexpectedOutput(); 229 $oP->output(); 230 exit; 231 } 232 else 233 { 234 $oP->add('<div class="page_header"><h1>Audit Errors: <span class="hilite">'.$oAuditRule->Get('description').'</span></h1><img style="margin-top: -20px; margin-right: 10px; float: right;" src="../images/stop.png"/></div>'); 235 $oP->p('<a href="./audit.php?'.$oAppContext->GetForLink().'">[Back to audit results]</a>'); 236 $sBlockId = 'audit_errors'; 237 $oP->p("<div id=\"$sBlockId\" style=\"clear:both\">\n"); 238 $oBlock = DisplayBlock::FromObjectSet($oErrorObjectSet, 'csv', array('show_obsolete_data' => true)); 239 $oBlock->Display($oP, 1); 240 $oP->p("</div>\n"); 241 // Adjust the size of the Textarea containing the CSV to fit almost all the remaining space 242 $oP->add_ready_script(" $('#1>textarea').height(400);"); // adjust the size of the block 243 $sExportUrl = utils::GetAbsoluteUrlAppRoot()."pages/audit.php?operation=csv&category=".$oAuditCategory->GetKey()."&rule=".$oAuditRule->GetKey(); 244 $oP->add_ready_script("$('a[href*=\"webservices/export.php?expression=\"]').attr('href', '".$sExportUrl."&filename=audit.csv".$sAdvanced."');"); 245 $oP->add_ready_script("$('#1 :checkbox').removeAttr('onclick').click( function() { var sAdvanced = ''; if (this.checked) sAdvanced = '&advanced=1'; window.location.href='$sExportUrl'+sAdvanced; } );"); 246 } 247 break; 248 249 case 'errors': 250 $sTitle = 'Audit Errors'; 251 $oP->SetBreadCrumbEntry('ui-tool-auditerrors', $sTitle, '', '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png'); 252 $iCategory = utils::ReadParam('category', ''); 253 $iRuleIndex = utils::ReadParam('rule', 0); 254 255 $oAuditCategory = MetaModel::GetObject('AuditCategory', $iCategory); 256 $oDefinitionFilter = DBObjectSearch::FromOQL($oAuditCategory->Get('definition_set')); 257 $oDefinitionFilter->UpdateContextFromUser(); 258 FilterByContext($oDefinitionFilter, $oAppContext); 259 $oDefinitionSet = new CMDBObjectSet($oDefinitionFilter); 260 $oFilter = GetRuleResultFilter($iRuleIndex, $oDefinitionFilter, $oAppContext); 261 $oErrorObjectSet = new CMDBObjectSet($oFilter); 262 $oAuditRule = MetaModel::GetObject('AuditRule', $iRuleIndex); 263 $oP->add('<div class="page_header"><h1>Audit Errors: <span class="hilite">'.$oAuditRule->Get('description').'</span></h1><img style="margin-top: -20px; margin-right: 10px; float: right;" src="../images/stop.png"/></div>'); 264 $oP->p('<a href="./audit.php?'.$oAppContext->GetForLink().'">[Back to audit results]</a>'); 265 $sBlockId = 'audit_errors'; 266 $oP->p("<div id=\"$sBlockId\" style=\"clear:both\">\n"); 267 $oBlock = DisplayBlock::FromObjectSet($oErrorObjectSet, 'list', array('show_obsolete_data' => true)); 268 $oBlock->Display($oP, 1); 269 $oP->p("</div>\n"); 270 $sExportUrl = utils::GetAbsoluteUrlAppRoot()."pages/audit.php?operation=csv&category=".$oAuditCategory->GetKey()."&rule=".$oAuditRule->GetKey(); 271 $oP->add_ready_script("$('a[href*=\"pages/UI.php?operation=search\"]').attr('href', '".$sExportUrl."')"); 272 break; 273 274 case 'audit': 275 default: 276 $oP->SetBreadCrumbEntry('ui-tool-audit', Dict::S('Menu:Audit'), Dict::S('UI:Audit:InteractiveAudit'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png'); 277 $oP->add('<div class="page_header"><h1>'.Dict::S('UI:Audit:InteractiveAudit').'</h1><img style="margin-top: -20px; margin-right: 10px; float: right;" src="../images/clean.png"/></div>'); 278 $oAuditFilter = new DBObjectSearch('AuditCategory'); 279 $oCategoriesSet = new DBObjectSet($oAuditFilter); 280 $oP->add("<table style=\"margin-top: 1em; padding: 0px; border-top: 3px solid #f6f6f1; border-left: 3px solid #f6f6f1; border-bottom: 3px solid #e6e6e1; border-right: 3px solid #e6e6e1;\">\n"); 281 $oP->add("<tr><td>\n"); 282 $oP->add("<table>\n"); 283 $oP->add("<tr>\n"); 284 $oP->add("<th><img src=\"../images/minus.gif\"></th><th class=\"alignLeft\">".Dict::S('UI:Audit:HeaderAuditRule')."</th><th>".Dict::S('UI:Audit:HeaderNbObjects')."</th><th>".Dict::S('UI:Audit:HeaderNbErrors')."</th><th>".Dict::S('UI:Audit:PercentageOk')."</th>\n"); 285 $oP->add("</tr>\n"); 286 while($oAuditCategory = $oCategoriesSet->fetch()) 287 { 288 try 289 { 290 $oDefinitionFilter = DBObjectSearch::FromOQL($oAuditCategory->Get('definition_set')); 291 $oDefinitionFilter->UpdateContextFromUser(); 292 FilterByContext($oDefinitionFilter, $oAppContext); 293 294 $aObjectsWithErrors = array(); 295 if (!empty($currentOrganization)) 296 { 297 if (MetaModel::IsValidFilterCode($oDefinitionFilter->GetClass(), 'org_id')) 298 { 299 $oDefinitionFilter->AddCondition('org_id', $currentOrganization, '='); 300 } 301 } 302 $aResults = array(); 303 $oDefinitionSet = new CMDBObjectSet($oDefinitionFilter); 304 $iCount = $oDefinitionSet->Count(); 305 $oRulesFilter = new DBObjectSearch('AuditRule'); 306 $oRulesFilter->AddCondition('category_id', $oAuditCategory->GetKey(), '='); 307 $oRulesSet = new DBObjectSet($oRulesFilter); 308 while($oAuditRule = $oRulesSet->fetch() ) 309 { 310 $aRow = array(); 311 $aRow['description'] = $oAuditRule->GetName(); 312 if ($iCount == 0) 313 { 314 // nothing to check, really ! 315 $aRow['nb_errors'] = "<a href=\"audit.php?operation=errors&category=".$oAuditCategory->GetKey()."&rule=".$oAuditRule->GetKey()."\">0</a>"; 316 $aRow['percent_ok'] = '100.00'; 317 $aRow['class'] = GetReportColor($iCount, 0); 318 } 319 else 320 { 321 try 322 { 323 $oFilter = GetRuleResultFilter($oAuditRule->GetKey(), $oDefinitionFilter, $oAppContext); 324 $aErrors = $oFilter->ToDataArray(array('id')); 325 $iErrorsCount = count($aErrors); 326 foreach($aErrors as $aErrorRow) 327 { 328 $aObjectsWithErrors[$aErrorRow['id']] = true; 329 } 330 $aRow['nb_errors'] = ($iErrorsCount == 0) ? '0' : "<a href=\"?operation=errors&category=".$oAuditCategory->GetKey()."&rule=".$oAuditRule->GetKey()."&".$oAppContext->GetForLink()."\">$iErrorsCount</a> <a href=\"?operation=csv&category=".$oAuditCategory->GetKey()."&rule=".$oAuditRule->GetKey()."&".$oAppContext->GetForLink()."\">(CSV)</a>"; 331 $aRow['percent_ok'] = sprintf('%.2f', 100.0 * (($iCount - $iErrorsCount) / $iCount)); 332 $aRow['class'] = GetReportColor($iCount, $iErrorsCount); 333 } 334 catch(Exception $e) 335 { 336 $aRow['nb_errors'] = "OQL Error"; 337 $aRow['percent_ok'] = 'n/a'; 338 $aRow['class'] = 'red'; 339 $sMessage = Dict::Format('UI:Audit:ErrorIn_Rule_Reason', $oAuditRule->GetHyperlink(), $e->getMessage()); 340 $oP->p("<img style=\"vertical-align:middle\" src=\"../images/stop-mid.png\"/> ".$sMessage); 341 } 342 } 343 $aResults[] = $aRow; 344 } 345 $iTotalErrors = count($aObjectsWithErrors); 346 $sOverallPercentOk = ($iCount == 0) ? '100.00' : sprintf('%.2f', 100.0 * (($iCount - $iTotalErrors) / $iCount)); 347 $sClass = GetReportColor($iCount, $iTotalErrors); 348 } 349 catch(Exception $e) 350 { 351 $aRow = array(); 352 $aRow['description'] = "OQL error"; 353 $aRow['nb_errors'] = "n/a"; 354 $aRow['percent_ok'] = ''; 355 $aRow['class'] = 'red'; 356 $sMessage = Dict::Format('UI:Audit:ErrorIn_Category_Reason', $oAuditCategory->GetHyperlink(), $e->getMessage()); 357 $oP->p("<img style=\"vertical-align:middle\" src=\"../images/stop-mid.png\"/> ".$sMessage); 358 $aResults[] = $aRow; 359 360 $sClass = 'red'; 361 $iTotalErrors = 'n/a'; 362 $sOverallPercentOk = ''; 363 } 364 $oP->add("<tr>\n"); 365 $oP->add("<th><img src=\"../images/minus.gif\"></th><th class=\"alignLeft\">".$oAuditCategory->GetName()."</th><th class=\"alignRight\">$iCount</th><th class=\"alignRight\">$iTotalErrors</th><th class=\"alignRight $sClass\">$sOverallPercentOk %</th>\n"); 366 $oP->add("</tr>\n"); 367 foreach($aResults as $aRow) 368 { 369 $oP->add("<tr>\n"); 370 $oP->add("<td> </td><td colspan=\"2\">".$aRow['description']."</td><td class=\"alignRight\">".$aRow['nb_errors']."</td><td class=\"alignRight ".$aRow['class']."\">".$aRow['percent_ok']." %</td>\n"); 371 $oP->add("</tr>\n"); 372 } 373 } 374 $oP->add("</table>\n"); 375 $oP->add("</td></tr>\n"); 376 $oP->add("</table>\n"); 377 } 378 $oP->output(); 379} 380catch(CoreException $e) 381{ 382 require_once(APPROOT.'/setup/setuppage.class.inc.php'); 383 $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); 384 $oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n"); 385 $oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc())); 386 $oP->output(); 387 388 if (MetaModel::IsLogEnabledIssue()) 389 { 390 if (MetaModel::IsValidClass('EventIssue')) 391 { 392 $oLog = new EventIssue(); 393 394 $oLog->Set('message', $e->getMessage()); 395 $oLog->Set('userinfo', ''); 396 $oLog->Set('issue', $e->GetIssue()); 397 $oLog->Set('impact', 'Page could not be displayed'); 398 $oLog->Set('callstack', $e->getTrace()); 399 $oLog->Set('data', $e->getContextData()); 400 $oLog->DBInsertNoReload(); 401 } 402 403 IssueLog::Error($e->getMessage()); 404 } 405 406 // For debugging only 407 //throw $e; 408} 409catch(Exception $e) 410{ 411 require_once(APPROOT.'/setup/setuppage.class.inc.php'); 412 $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); 413 $oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n"); 414 $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); 415 $oP->output(); 416 417 if (MetaModel::IsLogEnabledIssue()) 418 { 419 if (MetaModel::IsValidClass('EventIssue')) 420 { 421 $oLog = new EventIssue(); 422 423 $oLog->Set('message', $e->getMessage()); 424 $oLog->Set('userinfo', ''); 425 $oLog->Set('issue', 'PHP Exception'); 426 $oLog->Set('impact', 'Page could not be displayed'); 427 $oLog->Set('callstack', $e->getTrace()); 428 $oLog->Set('data', array()); 429 $oLog->DBInsertNoReload(); 430 } 431 432 IssueLog::Error($e->getMessage()); 433 } 434} 435