diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 4a34a2f97..4e6a00e74 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -45,6 +45,11 @@ require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php'); require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php'); require_once(APPROOT.'/application/datatable.class.inc.php'); require_once(APPROOT.'/sources/renderer/console/consoleformrenderer.class.inc.php'); +require_once(APPROOT.'/sources/application/search/searchform.class.inc.php'); +require_once(APPROOT.'/sources/application/search/criterionparser.class.inc.php'); +require_once(APPROOT.'/sources/application/search/criterionconversionabstract.class.inc.php'); +require_once(APPROOT.'/sources/application/search/criterionconversion/criteriontooql.class.inc.php'); +require_once(APPROOT.'/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php'); abstract class cmdbAbstractObject extends CMDBObject implements iDisplay { @@ -604,7 +609,7 @@ EOF function GetBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix, $aExtraParams = array()) { $sHtml = ''; - $oAppContext = new ApplicationContext(); + $oAppContext = new ApplicationContext(); $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); $aDetails = array(); $sClass = get_class($this); @@ -620,28 +625,28 @@ EOF $aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array(); $aExtraFlags = (isset($aExtraParams['fieldsFlags'])) ? $aExtraParams['fieldsFlags'] : array(); $bFieldComments = (count($aFieldsComments) > 0); - - foreach($aDetailsStruct as $sTab => $aCols ) + + foreach($aDetailsStruct as $sTab => $aCols) { $aDetails[$sTab] = array(); - $aTableStyles[] = 'vertical-align:top'; - $aTableClasses = array(); + $aTableStyles[] = 'vertical-align:top'; + $aTableClasses = array(); $aColStyles[] = 'vertical-align:top'; $aColClasses = array(); - ksort($aCols); + ksort($aCols); $iColCount = count($aCols); - if($iColCount > 1) - { - $aTableClasses[] = 'n-cols-details'; - $aTableClasses[] = $iColCount.'-cols-details'; + if ($iColCount > 1) + { + $aTableClasses[] = 'n-cols-details'; + $aTableClasses[] = $iColCount.'-cols-details'; - $aColStyles[] = 'width:'.floor(100/$iColCount).'%'; - } - else - { - $aTableClasses[] = 'one-col-details'; - } + $aColStyles[] = 'width:'.floor(100 / $iColCount).'%'; + } + else + { + $aTableClasses[] = 'one-col-details'; + } $oPage->SetCurrentTab(Dict::S($sTab)); $oPage->add(''); @@ -654,7 +659,7 @@ EOF $aDetails[$sTab][$sColIndex] = array(); foreach($aFieldsets as $sFieldsetName => $aFields) { - if (!empty($sFieldsetName) && ($sFieldsetName[0] != '_')) + if (!empty($sFieldsetName) && ($sFieldsetName[0] != '_')) { $sLabel = $sFieldsetName; } @@ -683,90 +688,93 @@ EOF { - $sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ''; - $sInfos = ''; - $iFlags = $this->GetFormAttributeFlags($sAttCode); - if (array_key_exists($sAttCode, $aExtraFlags)) - { - // the caller may override some flags if needed - $iFlags = $iFlags | $aExtraFlags[$sAttCode]; - } - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ( (!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0)) - { - $sInputId = $this->m_iFormId.'_'.$sAttCode; - if ($oAttDef->IsWritable()) + $sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ''; + $sInfos = ''; + $iFlags = $this->GetFormAttributeFlags($sAttCode); + if (array_key_exists($sAttCode, $aExtraFlags)) { - if ($sStateAttCode == $sAttCode) + // the caller may override some flags if needed + $iFlags = $iFlags | $aExtraFlags[$sAttCode]; + } + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0)) + { + $sInputId = $this->m_iFormId.'_'.$sAttCode; + if ($oAttDef->IsWritable()) { - // State attribute is always read-only from the UI - $sHTMLValue = $this->GetStateLabel(); - $val = array('label' => '', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode); - } - else - { - if ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE)) + if ($sStateAttCode == $sAttCode) { - // Check if the attribute is not read-only because of a synchro... - if ($iFlags & OPT_ATT_SLAVE) - { - $aReasons = array(); - $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); - $sSynchroIcon = " "; - $sTip = ''; - foreach($aReasons as $aRow) - { - $sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8'); - $sDescription = str_replace(array("\r\n", "\n"), "
", $sDescription); - $sTip .= "
"; - $sTip .= "
Synchronized with {$aRow['name']}
"; - $sTip .= "
$sDescription
"; - } - $sTip = addslashes($sTip); - $oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); - $sComments = $sSynchroIcon; - } - - // Attribute is read-only - $sHTMLValue = "".$this->GetAsHTML($sAttCode).''; + // State attribute is always read-only from the UI + $sHTMLValue = $this->GetStateLabel(); + $val = array('label' => '', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode); } else { - $sValue = $this->Get($sAttCode); - $sDisplayValue = $this->GetEditValue($sAttCode); - $aArgs = array('this' => $this, 'formPrefix' => $sPrefix); - $sHTMLValue = "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).''; + if ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE)) + { + // Check if the attribute is not read-only because of a synchro... + if ($iFlags & OPT_ATT_SLAVE) + { + $aReasons = array(); + $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); + $sSynchroIcon = " "; + $sTip = ''; + foreach($aReasons as $aRow) + { + $sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8'); + $sDescription = str_replace(array("\r\n", "\n"), "
", $sDescription); + $sTip .= "
"; + $sTip .= "
Synchronized with {$aRow['name']}
"; + $sTip .= "
$sDescription
"; + } + $sTip = addslashes($sTip); + $oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); + $sComments = $sSynchroIcon; + } + + // Attribute is read-only + $sHTMLValue = "".$this->GetAsHTML($sAttCode).''; + } + else + { + $sValue = $this->Get($sAttCode); + $sDisplayValue = $this->GetEditValue($sAttCode); + $aArgs = array('this' => $this, 'formPrefix' => $sPrefix); + $sHTMLValue = "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).''; + } + $aFieldsMap[$sAttCode] = $sInputId; + $val = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode); } + } + else + { + $val = array( + 'label' => ''.$oAttDef->GetLabel().'', + 'value' => "".$this->GetAsHTML($sAttCode)."", + 'comments' => $sComments, + 'infos' => $sInfos, + 'attcode' => $sAttCode + ); $aFieldsMap[$sAttCode] = $sInputId; - $val = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode); + } + + // Checking how the field should be rendered + // Note: For view mode, this is done in cmdbAbstractObject::GetFieldAsHtml() + // Note 2: Shouldn't this be a property of the AttDef instead an array that we have to maintain? + if (in_array($oAttDef->GetEditClass(), array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression'))) + { + $val['layout'] = 'large'; + } + else + { + $val['layout'] = 'small'; } } else { - $val = array('label' => ''.$oAttDef->GetLabel().'', 'value' => "".$this->GetAsHTML($sAttCode)."", 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode); - $aFieldsMap[$sAttCode] = $sInputId; + $val = null; // Skip this field } - // Checking how the field should be rendered - // Note: For view mode, this is done in cmdbAbstractObject::GetFieldAsHtml() - // Note 2: Shouldn't this be a property of the AttDef instead an array that we have to maintain? - if(in_array($oAttDef->GetEditClass(), array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression'))) - { - $val['layout'] = 'large'; - } - else - { - $val['layout'] = 'small'; - } - } - else - { - $val = null; // Skip this field - } - - - - } else { @@ -776,10 +784,10 @@ EOF if ($val != null) { - // The field is visible, add it to the current column + // The field is visible, add it to the current column $aDetails[$sTab][$sColIndex][] = $val; $iInputId++; - } + } } } if (!empty($sPreviousLabel)) @@ -796,6 +804,7 @@ EOF } $oPage->add('
'); } + return $aFieldsMap; } @@ -814,6 +823,7 @@ EOF { // Object's details // template not found display the object using the *old style* + $oPage->add('
'); $this->DisplayBareHeader($oPage, $bEditMode); $oPage->AddTabContainer(OBJECT_PROPERTIES_TAB); $oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB); @@ -823,6 +833,7 @@ EOF //$oPage->SetCurrentTab(Dict::S('UI:HistoryTab')); //$this->DisplayBareHistory($oPage, $bEditMode); $oPage->AddAjaxTab(Dict::S('UI:HistoryTab'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=history&class='.get_class($this).'&id='.$this->GetKey()); + $oPage->add('
'); } } @@ -1527,238 +1538,21 @@ EOF $oPage->add(self::GetSearchForm($oPage, $oSet, $aExtraParams)); } - + + /** + * @param WebPage $oPage + * @param CMDBObjectSet $oSet + * @param array $aExtraParams + * + * @return string + * @throws CoreException + * @throws DictExceptionMissingString + */ public static function GetSearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array()) { - static $iSearchFormId = 0; - $bMultiSelect = false; - $oAppContext = new ApplicationContext(); - $sHtml = ''; - $numCols=4; - $sClassName = $oSet->GetFilter()->GetClass(); + $oSearchForm = new \Combodo\iTop\Application\Search\SearchForm(); - // Romain: temporarily removed the tab "OQL query" because it was not finalized - // (especially when used to add a link) - /* - $sHtml .= "
\n"; - */ - // Simple search form - if (isset($aExtraParams['currentId'])) - { - $sSearchFormId = $aExtraParams['currentId']; - } - else - { - $iSearchFormId = $oPage->GetUniqueId(); - $sSearchFormId = 'SimpleSearchForm'.$iSearchFormId; - $sHtml .= "
\n"; - } - // Check if the current class has some sub-classes - if (isset($aExtraParams['baseClass'])) - { - $sRootClass = $aExtraParams['baseClass']; - } - else - { - $sRootClass = $sClassName; - } - $aSubClasses = MetaModel::GetSubclasses($sRootClass); - if (count($aSubClasses) > 0) - { - $aOptions = array(); - $aOptions[MetaModel::GetName($sRootClass)] = "
\n"; - } - if ($bMultiSelect) - { - $aOptions = array( - 'header' => true, - 'checkAllText' => Dict::S('UI:SearchValue:CheckAll'), - 'uncheckAllText' => Dict::S('UI:SearchValue:UncheckAll'), - 'noneSelectedText' => Dict::S('UI:SearchValue:Any'), - 'selectedText' => Dict::S('UI:SearchValue:NbSelected'), - 'selectedList' => 1, - ); - $sJSOptions = json_encode($aOptions); - $oPage->add_ready_script("$('.multiselect').multiselect($sJSOptions);"); - } -/* - // OQL query builder - $sHtml .= "
\n"; - $sHtml .= "

".Dict::S('UI:OQLQueryBuilderTitle')."

\n"; - $sHtml .= "
\n"; - $sHtml .= "\n"; - $sHtml .= "\n"; - $sHtml .= "\n"; - foreach($aExtraParams as $sName => $sValue) - { - if (is_scalar($sValue)) - { - $sHtml .= "\n"; - } - } - $sHtml .= "\n"; - $sHtml .= $oAppContext->GetForForm(); - $sHtml .= "
 \n"; - $sHtml .= "
\n"; - $sHtml .= "
\n"; -*/ - return $sHtml; + return $oSearchForm->GetSearchForm($oPage, $oSet, $aExtraParams); } /** diff --git a/application/datatable.class.inc.php b/application/datatable.class.inc.php index 672cc40b2..86f13aada 100644 --- a/application/datatable.class.inc.php +++ b/application/datatable.class.inc.php @@ -571,33 +571,6 @@ EOF { $oPage->add_ready_script("oTable.trigger(\"fakesorton\", [$sFakeSortList]);"); } - //if ($iNbPages == 1) - if (false) - { - if (isset($aExtraParams['cssCount'])) - { - $sCssCount = $aExtraParams['cssCount']; - if ($sSelectMode == 'single') - { - $sSelectSelector = ":radio[name^=selectObj]"; - } - else if ($sSelectMode == 'multiple') - { - $sSelectSelector = ":checkbox[name^=selectObj]"; - } - $oPage->add_ready_script( -<<iListId} table.listResults $sSelectSelector').change(function() { - var c = $('{$sCssCount}'); - var v = $('#{$this->iListId} table.listResults $sSelectSelector:checked').length; - c.val(v); - $('#{$this->iListId} .selectedCount').text(v); - c.trigger('change'); - }); -EOF - ); - } - } return $sHtml; } diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 10366d534..c23abd980 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -220,46 +220,13 @@ class DisplayBlock $aExtraParams['currentId'] = $sId; $sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them - $bAutoReload = false; - if (isset($aExtraParams['auto_reload'])) - { - if ($aExtraParams['auto_reload'] === true) - { - // Note: does not work in the switch (case true) because a positive number evaluates to true!!! - $aExtraParams['auto_reload'] = 'standard'; - } - switch($aExtraParams['auto_reload']) - { - case 'fast': - $bAutoReload = true; - $iReloadInterval = MetaModel::GetConfig()->GetFastReloadInterval()*1000; - break; - - case 'standard': - case 'true': - $bAutoReload = true; - $iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000; - break; - - default: - if (is_numeric($aExtraParams['auto_reload']) && ($aExtraParams['auto_reload'] > 0)) - { - $bAutoReload = true; - $iReloadInterval = max(MetaModel::GetConfig()->Get('min_reload_interval'), $aExtraParams['auto_reload'])*1000; - } - else - { - // incorrect config, ignore it - $bAutoReload = false; - } - } - } + $sFilter = $this->m_oFilter->serialize(); // Used either for asynchronous or auto_reload if (!$this->m_bAsynchronous) { // render now - $sHtml .= "
\n"; + $sHtml .= "
\n"; $sHtml .= $this->GetRenderContent($oPage, $aExtraParams, $sId); $sHtml .= "
\n"; } @@ -273,17 +240,36 @@ class DisplayBlock $.post("ajax.render.php?style='.$this->m_sStyle.'", { operation: "ajax", filter: "'.$sFilter.'", extra_params: "'.$sExtraParams.'" }, function(data){ - $("#'.$sId.'").empty(); - $("#'.$sId.'").append(data); - $("#'.$sId.'").removeClass("loading"); + $("#'.$sId.'") + .empty() + .append(data) + .removeClass("loading") + ; } ); '); } - if (($bAutoReload) && ($this->m_sStyle != 'search')) // Search form do NOT auto-reload - { - $oPage->add_script('setInterval("ReloadBlock(\''.$sId.'\', \''.$this->m_sStyle.'\', \''.$sFilter.'\', \"'.$sExtraParams.'\")", '.$iReloadInterval.');'); - } + + + if ($this->m_sStyle == 'list') // Search form need to extract result list extra data, the simplest way is to expose this configuration + { + + $listJsonExtraParams = json_encode(json_encode($aExtraParams)); + $oPage->add_ready_script(" + $('#$sId').data('sExtraParams', ".$listJsonExtraParams."); +// console.debug($('#$sId').data()); +// console.debug($('#$sId')); +// console.debug('#$sId'); + "); + + + + +// $oPage->add_ready_script("console.debug($('#Menu_UserRequest_OpenRequests').data());"); + + } + + return $sHtml; } @@ -919,21 +905,10 @@ class DisplayBlock case 'search': if (!$oPage->IsPrintableVersion()) { - $sStyle = (isset($aExtraParams['open']) && ($aExtraParams['open'] == 'true')) ? 'SearchDrawer' : 'SearchDrawer DrawerClosed'; - $sHtml .= "
\n"; - $oPage->add_ready_script( -<<\n"; $aExtraParams['currentId'] = $sId; $sHtml .= cmdbAbstractObject::GetSearchForm($oPage, $this->m_oSet, $aExtraParams); $sHtml .= "
\n"; - $sHtml .= "
\n"; - $sHtml .= "
".Dict::S('UI:SearchToggle')."
\n"; } break; @@ -1126,6 +1101,56 @@ EOF // Unsupported style, do nothing. $sHtml .= Dict::format('UI:Error:UnsupportedStyleOfBlock', $this->m_sStyle); } + + + $bAutoReload = false; + if (isset($aExtraParams['auto_reload'])) + { + if ($aExtraParams['auto_reload'] === true) + { + // Note: does not work in the switch (case true) because a positive number evaluates to true!!! + $aExtraParams['auto_reload'] = 'standard'; + } + switch($aExtraParams['auto_reload']) + { + case 'fast': + $bAutoReload = true; + $iReloadInterval = MetaModel::GetConfig()->GetFastReloadInterval()*1000; + break; + + case 'standard': + case 'true': + $bAutoReload = true; + $iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000; + break; + + default: + if (is_numeric($aExtraParams['auto_reload']) && ($aExtraParams['auto_reload'] > 0)) + { + $bAutoReload = true; + $iReloadInterval = max(MetaModel::GetConfig()->Get('min_reload_interval'), $aExtraParams['auto_reload'])*1000; + } + else + { + // incorrect config, ignore it + $bAutoReload = false; + } + } + } + if (($bAutoReload) && ($this->m_sStyle != 'search')) // Search form do NOT auto-reload + { + $sFilter = $this->m_oFilter->serialize(); // Used either for asynchronous or auto_reload + $sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them + + $oPage->add_script('if (typeof window.oAutoReloadBlock == "undefined") { + window.oAutoReloadBlock = {}; + } + if (typeof window.oAutoReloadBlock[\''.$sId.'\'] != "undefined") { + clearInterval(window.oAutoReloadBlock[\''.$sId.'\']); + } + window.oAutoReloadBlock[\''.$sId.'\'] = setInterval("ReloadBlock(\''.$sId.'\', \''.$this->m_sStyle.'\', \''.$sFilter.'\', \"'.$sExtraParams.'\")", '.$iReloadInterval.');'); + } + return $sHtml; } diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 666236257..9cc27ac98 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -111,8 +111,13 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $sSearchAny = addslashes(Dict::S('UI:SearchValue:Any')); $sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected')); $this->add_dict_entry('UI:FillAllMandatoryFields'); - $this->add_dict_entry('UI:Button:Cancel'); - $this->add_dict_entry('UI:Button:Done'); + + $this->add_dict_entries('Error:'); + $this->add_dict_entries('UI:Button:'); + $this->add_dict_entries('UI:Search:'); + $this->add_dict_entry('UI:UndefinedObject'); + $this->add_dict_entries('Enum:Undefined'); + if (!$this->IsPrintableVersion()) { diff --git a/application/nicewebpage.class.inc.php b/application/nicewebpage.class.inc.php index bfae9321d..61c2bdcaf 100644 --- a/application/nicewebpage.class.inc.php +++ b/application/nicewebpage.class.inc.php @@ -41,6 +41,7 @@ class NiceWebPage extends WebPage $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-migrate-1.4.1.min.js'); // Needed since many other plugins still rely on oldies like $.browser $this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/ui-lightness/jquery-ui-1.11.4.custom.css'); $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-ui-1.11.4.custom.min.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/utils.js'); $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/hovertip.js'); // table sorting $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.js'); @@ -50,7 +51,24 @@ class NiceWebPage extends WebPage $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/datatable.js'); $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.positionBy.js'); $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.popupmenu.js'); - $this->add_ready_script( + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/searchformforeignkeys.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler_history.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_raw.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_string.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_field.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_numeric.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_enum.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_key.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_hierarchical_key.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_abstract.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_time.js'); + + $this->add_dict_entries('UI:Combo'); + + $this->add_ready_script( <<< EOF //add new widget called TruncatedList to properly display truncated lists when they are sorted $.tablesorter.addWidget({ diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index 8d412b04d..34f7ed506 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -19,15 +19,15 @@ * Class UIExtKeyWidget * UI wdiget for displaying and editing external keys when * A simple drop-down list is not enough... - * + * * The layout is the following - * + * * +-- #label_ (input)-------+ +-----------+ * | | | Browse... | * +-----------------------------+ +-----------+ - * + * * And the popup dialog has the following layout: - * + * * +------------------- ac_dlg_ (div)-----------+ * + +--- ds_ (div)---------------------------+ | * | | +------------- fs_ (form)------------+ | | @@ -61,13 +61,16 @@ require_once(APPROOT.'/application/webpage.class.inc.php'); require_once(APPROOT.'/application/displayblock.class.inc.php'); -class UIExtKeyWidget +class UIExtKeyWidget { + const ENUM_OUTPUT_FORMAT_CSV = 'csv'; + const ENUM_OUTPUT_FORMAT_JSON = 'json'; + protected $iId; protected $sTargetClass; protected $sAttCode; protected $bSearchMode; - + //public function __construct($sAttCode, $sClass, $sTitle, $oAllowedValues, $value, $iInputId, $bMandatory, $sNameSuffix = '', $sFieldPrefix = '', $sFormPrefix = '') static public function DisplayFromAttCode($oPage, $sAttCode, $sClass, $sTitle, $oAllowedValues, $value, $iInputId, $bMandatory, $sFieldName = '', $sFormPrefix = '', $aArgs, $bSearchMode = false) { @@ -94,7 +97,7 @@ class UIExtKeyWidget $this->sAttCode = $sAttCode; $this->bSearchMode = $bSearchMode; } - + /** * Get the HTML fragment corresponding to the ext key editing widget * @param WebPage $oP The web page used for all the output @@ -107,10 +110,10 @@ class UIExtKeyWidget { $this->bSearchMode = $bSearchMode; } - $sTitle = addslashes($sTitle); + $sTitle = addslashes($sTitle); $oPage->add_linked_script('../js/extkeywidget.js'); $oPage->add_linked_script('../js/forms-json-utils.js'); - + $bCreate = (!$this->bSearchMode) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_BULK_MODIFY) && $bAllowTargetCreation); $bExtensions = true; $sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm'); @@ -123,7 +126,7 @@ class UIExtKeyWidget $sWizHelper = 'null'; $sWizHelperJSON = "''"; $sJSSearchMode = 'true'; - } + } else { if (isset($aArgs['wizHelper'])) @@ -159,7 +162,7 @@ class UIExtKeyWidget while($oObj = $oAllowedValues->Fetch()) { $aAllowedValues[$oObj->GetKey()] = $oObj->GetName(); - } + } $sHTMLValue .= $oPage->GetRadioButtons($aAllowedValues, $value, $this->iId, "{$sAttrFieldPrefix}{$sFieldName}", false /* $bMandatory will be placed manually */, $bVertical, $sValidationField); $aEventsList[] ='change'; break; @@ -168,7 +171,7 @@ class UIExtKeyWidget case 'list': default: $sSelectMode = 'true'; - + $sHelpText = ''; //$this->oAttDef->GetHelpOnEdition(); $sHTMLValue .= "
\n"; @@ -196,7 +199,7 @@ class UIExtKeyWidget { $key = $oObj->GetKey(); $display_value = $oObj->GetName(); - + if (($oAllowedValues->Count() == 1) && ($bMandatory == 'true') ) { // When there is only once choice, select it by default @@ -239,7 +242,7 @@ EOF { // Too many choices, use an autocomplete $sSelectMode = 'false'; - + // Check that the given value is allowed $oSearch = $oAllowedValues->GetFilter(); $oSearch->AddCondition('id', $value); @@ -259,15 +262,15 @@ EOF } $iMinChars = isset($aArgs['iMinChars']) ? $aArgs['iMinChars'] : 3; //@@@ $this->oAttDef->GetMinAutoCompleteChars(); $iFieldSize = isset($aArgs['iFieldSize']) ? $aArgs['iFieldSize'] : 20; //@@@ $this->oAttDef->GetMaxSize(); - + // the input for the auto-complete $sHTMLValue .= "Count()."\" type=\"text\" id=\"label_$this->iId\" value=\"$sDisplayValue\"/>"; $sHTMLValue .= "iId}\" style=\"border:0;vertical-align:middle;cursor:pointer;\" src=\"../images/mini_search.gif?itopversion=".ITOP_VERSION."\" onClick=\"oACWidget_{$this->iId}.Search();\"/>"; - + // another hidden input to store & pass the object's Id $sHTMLValue .= "iId\" name=\"{$sAttrFieldPrefix}{$sFieldName}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\" />\n"; - $JSSearchMode = $this->bSearchMode ? 'true' : 'false'; + $JSSearchMode = $this->bSearchMode ? 'true' : 'false'; // Scripts to start the autocomplete and bind some events to it $oPage->add_ready_script( <<iId.'">'; @@ -341,7 +344,17 @@ EOF $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open'); $oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); $oBlock = new DisplayBlock($oFilter, 'search', false, $aParams); - $sHTML .= $oBlock->GetDisplay($oPage, $this->iId, array('open' => $bOpen, 'currentId' => $this->iId)); + $sHTML .= $oBlock->GetDisplay($oPage, $this->iId, + array( + 'menu' => false, + 'open' => $bOpen, + 'currentId' => $this->iId, + 'table_id' => "dr_{$this->iId}", + 'table_inner_id' => "{$this->iId}_results", + 'selection_mode' => true, + 'selection_type' => 'single', + 'cssCount' => '#count_'.$this->iId) + ); $sHTML .= "
iId}\" OnSubmit=\"return oACWidget_{$this->iId}.DoOk();\">\n"; $sHTML .= "
iId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; $sHTML .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; @@ -375,7 +388,7 @@ EOF { throw new Exception('Implementation: null value for allowed values definition'); } - + $oFilter = DBObjectSearch::FromOQL($sFilter); if (strlen($sRemoteClass) > 0) { @@ -389,7 +402,7 @@ EOF $oBlock = new DisplayBlock($oFilter, 'list', false, array('query_params' => array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId))); $oBlock->Display($oP, $this->iId.'_results', array('this' => $oObj, 'cssCount'=> '#count_'.$this->iId, 'menu' => false, 'selection_mode' => true, 'selection_type' => 'single', 'table_id' => 'select_'.$this->sAttCode)); // Don't display the 'Actions' menu on the results } - + /** * Search for objects to be selected * @param WebPage $oP The page used for the output (usually an AjaxWebPage) @@ -397,7 +410,7 @@ EOF * @param DBObject $oObj The current object for the OQL context * @param string $sContains The text of the autocomplete to filter the results */ - public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains) + public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains, $sOutputFormat = self::ENUM_OUTPUT_FORMAT_CSV) { if (is_null($sFilter)) { @@ -410,12 +423,27 @@ EOF $oValuesSet = new ValueSetObjects($sFilter, 'friendlyname'); // Bypass GetName() to avoid the encoding by htmlentities $oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); $aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains); - foreach($aValues as $sKey => $sFriendlyName) + + switch($sOutputFormat) { - $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + case static::ENUM_OUTPUT_FORMAT_JSON: + // Array flip to preserve values order on the label, otherwise the JS will re-order regarding the keys. + $oP->SetContentType('application/json'); + $oP->add(json_encode(array_flip($aValues))); + break; + + case static::ENUM_OUTPUT_FORMAT_CSV: + foreach($aValues as $sKey => $sFriendlyName) + { + $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + } + break; + default: + throw new Exception('Invalid output format, "'.$sOutputFormat.'" given.'); + break; } } - + /** * Get the display name of the selected object, to fill back the autocomplete */ @@ -505,7 +533,7 @@ EOF } } } - + // 3rd - set values from the page argument 'default' $oNewObj->UpdateObjectFromArg('default'); @@ -522,7 +550,7 @@ EOF $aFieldsComments[$sAttCode] = ' '; } } - cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), array('formPrefix' => $this->iId, 'noRelations' => true, 'fieldsFlags' => $aFieldsFlags, 'fieldsComments' => $aFieldsComments)); + cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), array('formPrefix' => $this->iId, 'noRelations' => true, 'fieldsFlags' => $aFieldsFlags, 'fieldsComments' => $aFieldsComments)); $oPage->add('
'); // $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$sDialogTitle'});\n"); $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitle'});\n"); @@ -566,7 +594,7 @@ EOF $oPage->add(''); $oPage->add("iId}\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_tree_{$this->iId}').dialog('close');\">  "); $oPage->add("iId}\" value=\"".Dict::S('UI:Button:Ok')."\" onClick=\"oACWidget_{$this->iId}.DoHKOk();\">"); - + $oPage->add(''); $oPage->add_ready_script("\$('#tree_$this->iId ul').treeview();\n"); $oPage->add_ready_script("\$('#dlg_tree_$this->iId').dialog({ width: 'auto', height: 'auto', autoOpen: true, modal: true, title: '$sDialogTitle', resizeStop: oACWidget_{$this->iId}.OnHKResize, close: oACWidget_{$this->iId}.OnHKClose });\n"); @@ -588,7 +616,7 @@ EOF } else { - return array('error' => implode(' ', $aErrors), 'id' => 0); + return array('error' => implode(' ', $aErrors), 'id' => 0); } } catch(Exception $e) @@ -611,7 +639,7 @@ EOF $aTree[$iParentId][$oObj->GetKey()] = $oObj->GetName(); $aNodes[$oObj->GetKey()] = $oObj; } - + $aParents = array_keys($aTree); $aRoots = array(); foreach($aParents as $id) @@ -626,7 +654,7 @@ EOF $this->DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue); } } - + function DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue) { $bSelect = true; diff --git a/application/ui.linksdirectwidget.class.inc.php b/application/ui.linksdirectwidget.class.inc.php index e62729f8b..b38db95c3 100644 --- a/application/ui.linksdirectwidget.class.inc.php +++ b/application/ui.linksdirectwidget.class.inc.php @@ -278,14 +278,36 @@ class UILinksWidgetDirect /** * @param WebPage $oPage * @param DBObject $oCurrentObj - * @throws Exception + * @param $aAlreadyLinked + * + * @throws \CoreException + * @throws \Exception + * @throws \OQLException */ - public function GetObjectsSelectionDlg($oPage, $oCurrentObj) + public function GetObjectsSelectionDlg($oPage, $oCurrentObj, $aAlreadyLinked) { $sHtml = "
\n"; - - $oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode); - $valuesDef = $oLinksetDef->GetValuesDef(); + + $oHiddenFilter = new DBObjectSearch($this->sLinkedClass); + if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($this->sLinkedClass, $this->sClass)) + { + // Prevent linking to self if the linked object is of the same family + // and already present in the database + if (!$oCurrentObj->IsNew()) + { + $oHiddenFilter->AddCondition('id', $oCurrentObj->GetKey(), '!='); + } + } + if (count($aAlreadyLinked) > 0) + { + $oHiddenFilter->AddCondition('id', $aAlreadyLinked, 'NOTIN'); + } + $oHiddenCriteria = $oHiddenFilter->GetCriteria(); + $aArgs = $oHiddenFilter->GetInternalParams(); + $sHiddenCriteria = $oHiddenCriteria->Render($aArgs); + + $oLinkSetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode); + $valuesDef = $oLinkSetDef->GetValuesDef(); if ($valuesDef === null) { $oFilter = new DBObjectSearch($this->sLinkedClass); @@ -298,13 +320,27 @@ class UILinksWidgetDirect } $oFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression()); } + if ($oCurrentObj != null) { $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + + $aArgs = array_merge($oCurrentObj->ToArgs('this'), $oFilter->GetInternalParams()); + $oFilter->SetInternalParams($aArgs); } $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open'); $oBlock = new DisplayBlock($oFilter, 'search', false); - $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}", array('open' => $bOpen)); + $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}", + array( + 'open' => $bOpen, + 'result_list_outer_selector' => "SearchResultsToAdd_{$this->sInputid}", + 'table_id' => "add_{$this->sInputid}", + 'table_inner_id' => "ResultsToAdd_{$this->sInputid}", + 'cssCount' => "#count_{$this->sInputid}", + 'query_params' => $oFilter->GetInternalParams(), + 'hidden_criteria' => $sHiddenCriteria, + ) + ); $sHtml .= "sInputid}\">\n"; $sHtml .= "
sInputid}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; $sHtml .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; @@ -330,8 +366,8 @@ class UILinksWidgetDirect { $sRemoteClass = $this->sLinkedClass; } - $oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode); - $valuesDef = $oLinksetDef->GetValuesDef(); + $oLinkSetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode); + $valuesDef = $oLinkSetDef->GetValuesDef(); if ($valuesDef === null) { $oFilter = new DBObjectSearch($sRemoteClass); @@ -348,7 +384,7 @@ class UILinksWidgetDirect if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($sRemoteClass, $this->sClass)) { // Prevent linking to self if the linked object is of the same family - // and laready present in the database + // and already present in the database if (!$oCurrentObj->IsNew()) { $oFilter->AddCondition('id', $oCurrentObj->GetKey(), '!='); @@ -360,6 +396,8 @@ class UILinksWidgetDirect } if ($oCurrentObj != null) { + $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + $aArgs = array_merge($oCurrentObj->ToArgs('this'), $oFilter->GetInternalParams()); $oFilter->SetInternalParams($aArgs); } diff --git a/application/ui.linkswidget.class.inc.php b/application/ui.linkswidget.class.inc.php index 60a746dc8..deef928b2 100644 --- a/application/ui.linkswidget.class.inc.php +++ b/application/ui.linkswidget.class.inc.php @@ -386,21 +386,55 @@ EOF /** * @param WebPage $oPage * @param DBObject $oCurrentObj + * @param $sJson + * @param array $aAlreadyLinkedIds + * + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception */ - public function GetObjectPickerDialog($oPage, $oCurrentObj) + public function GetObjectPickerDialog($oPage, $oCurrentObj, $sJson, $aAlreadyLinkedIds = array()) { $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open'); $sHtml = "
\n"; + + $oAlreadyLinkedFilter = new DBObjectSearch($this->m_sRemoteClass); + if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) + { + $oAlreadyLinkedFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); + $oAlreadyLinkedExpression = $oAlreadyLinkedFilter->GetCriteria(); + $sAlreadyLinkedExpression = $oAlreadyLinkedExpression->Render(); + } + else + { + $sAlreadyLinkedExpression = ''; + } + $oFilter = new DBObjectSearch($this->m_sRemoteClass); - $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + if ($oCurrentObj != null) + { + $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + } $oBlock = new DisplayBlock($oFilter, 'search', false); - $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", array('open' => $bOpen)); - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}\" OnSubmit=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\">\n"; + $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + array( + 'open' => $bOpen, + 'menu' => false, + 'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + 'table_id' => 'add_'.$this->m_sAttCode, + 'table_inner_id' => "ResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + 'selection_mode' => true, + 'json' => $sJson, + 'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix, + 'query_params' => $oFilter->GetInternalParams(), + 'hidden_criteria' => $sAlreadyLinkedExpression, + )); + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}\">\n"; $sHtml .= "
m_sAttCode}{$this->m_sNameSuffix}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; $sHtml .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; $sHtml .= "
\n"; $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}\" value=\"0\"/>"; - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">  m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"submit\" value=\"".Dict::S('UI:Button:Add')."\">"; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">  m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"button\" onclick=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">"; $sHtml .= "
\n"; $sHtml .= "\n"; $oPage->add($sHtml); @@ -416,7 +450,7 @@ EOF * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass * @param Array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of the search */ - public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array()) + public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array(), $oCurrentObj = null) { if ($sRemoteClass != '') { @@ -432,6 +466,7 @@ EOF { $oFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); } + $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); $oBlock = new DisplayBlock($oFilter, 'list', false); $oBlock->Display($oP, "ResultsToAdd_{$this->m_sAttCode}", array('menu' => false, 'cssCount'=> '#count_'.$this->m_sAttCode.$this->m_sNameSuffix , 'selection_mode' => true, 'table_id' => 'add_'.$this->m_sAttCode)); // Don't display the 'Actions' menu on the results } diff --git a/application/ui.searchformforeignkeys.class.inc.php b/application/ui.searchformforeignkeys.class.inc.php new file mode 100644 index 000000000..1b97ac808 --- /dev/null +++ b/application/ui.searchformforeignkeys.class.inc.php @@ -0,0 +1,117 @@ + + * + */ + + +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/displayblock.class.inc.php'); + +class UISearchFormForeignKeys +{ + public function __construct($sTargetClass, $iInputId = null) + { + $this->m_sRemoteClass = $sTargetClass; + $this->m_iInputId = $iInputId; + } + + /** + * @param WebPage $oPage + * + * @param $sTitle + * + * @throws \Exception + */ + public function ShowModalSearchForeignKeys($oPage, $sTitle) + { + $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open'); + $sHtml = "
\n"; + + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + + $oBlock = new DisplayBlock($oFilter, 'search', false); + $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_iInputId}", + array( + 'open' => $bOpen, + 'menu' => false, + 'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_iInputId}", + 'table_id' => "add_{$this->m_iInputId}", + 'table_inner_id' => "ResultsToAdd_{$this->m_iInputId}", + 'selection_mode' => true, + 'cssCount' => "#count_{$this->m_iInputId}", + 'query_params' => $oFilter->GetInternalParams(), + )); + $sHtml .= "
m_iInputId}\">\n"; + $sHtml .= "
m_iInputId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; + $sHtml .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; + $sHtml .= "
\n"; + $sHtml .= "m_iInputId}\" value=\"0\"/>"; + $sHtml .= "m_iInputId}').dialog('close');\">  m_iInputId}\" disabled=\"disabled\" type=\"button\" onclick=\"return oForeignKeysWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">"; + $sHtml .= "
\n"; + $sHtml .= "\n"; + $oPage->add($sHtml); + $oPage->add_ready_script("$('#dlg_{$this->m_iInputId}').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, resizeStop: oForeignKeysWidget{$this->m_iInputId}.UpdateSizes });"); + $oPage->add_ready_script("$('#dlg_{$this->m_iInputId}').dialog('option', {title:'$sTitle'});"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_iInputId} form').bind('submit.uilinksWizard', oForeignKeysWidget{$this->m_iInputId}.SearchObjectsToAdd);"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_iInputId}').resize(oForeignKeysWidget{$this->m_iInputId}.UpdateSizes);"); + } + + public function GetFullListForeignKeysFromSelection($oPage, $oFullSetFilter) + { + try + { + $aLinkedObjects = utils::ReadMultipleSelectionWithFriendlyname($oFullSetFilter); + $oPage->add(json_encode($aLinkedObjects)); + } + catch (CoreException $e) + { + http_response_code(500); + $oPage->add(json_encode(array('error' => $e->GetMessage()))); + IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); + } + } + + /** + * Search for objects to be linked to the current object (i.e "remote" objects) + * + * @param WebPage $oP The page used for the output (usually an AjaxWebPage) + * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass + * + * @throws \Exception + */ + public function ListResultsSearchForeignKeys(WebPage $oP, $sRemoteClass = '') + { + if ($sRemoteClass != '') + { + // assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass)); + $oFilter = new DBObjectSearch($sRemoteClass); + } + else + { + // No remote class specified use the one defined in the linkedset + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + } + + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, "ResultsToAdd_{$this->m_iInputId}", + array('menu' => false, 'cssCount' => "#count_{$this->m_iInputId}", 'selection_mode' => true, 'table_id' => "add_{$this->m_iInputId}")); + } + +} \ No newline at end of file diff --git a/application/utils.inc.php b/application/utils.inc.php index 019e2f50f..7e44ac4f8 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -413,8 +413,10 @@ class utils /** * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected - * @return Array An arry of object IDs corresponding to the objects selected in the set + * + * @return Array An array of object IDs corresponding to the objects selected in the set */ public static function ReadMultipleSelection($oFullSetFilter) { @@ -448,6 +450,47 @@ class utils return $aSelectedObj; } + /** + * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * + * @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected + * + * @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set + * @throws \CoreException + */ + public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter) + { + $sSelectionMode = utils::ReadParam('selectionMode', ''); + + if ($sSelectionMode === '') + { + throw new CoreException('selectionMode is mandatory'); + } + + // Paginated selection + $aSelectedIds = utils::ReadParam('storedSelection', array()); + if ($sSelectionMode == 'positive') + { + // Only the explicitly listed items are selected + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN'); + } + else + { + // All items of the set are selected, except the one explicitly listed + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN'); + } + $aSelectedObj = array(); + $oFullSet = new DBObjectSet($oFullSetFilter); + $sClassAlias = $oFullSetFilter->GetClassAlias(); + $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field + while ($oObj = $oFullSet->Fetch()) + { + $aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname'); + } + + return $aSelectedObj; + } + public static function GetNewTransactionId() { return privUITransaction::GetNewTransactionId(); diff --git a/application/webpage.class.inc.php b/application/webpage.class.inc.php index 9af9a8ce8..018d57251 100644 --- a/application/webpage.class.inc.php +++ b/application/webpage.class.inc.php @@ -58,6 +58,7 @@ class WebPage implements Page protected $s_deferred_content; protected $a_scripts; protected $a_dict_entries; + protected $a_dict_entries_prefixes; protected $a_styles; protected $a_include_scripts; protected $a_include_stylesheets; @@ -80,6 +81,7 @@ class WebPage implements Page $this->s_deferred_content = ''; $this->a_scripts = array(); $this->a_dict_entries = array(); + $this->a_dict_entries_prefixes = array(); $this->a_styles = array(); $this->a_linked_scripts = array(); $this->a_linked_stylesheets = array(); @@ -243,10 +245,39 @@ class WebPage implements Page /** * Add a dictionary entry for the Javascript side */ - public function add_dict_entry($s_entryId) - { - $this->a_dict_entries[$s_entryId] = Dict::S($s_entryId); - } + public function add_dict_entry($s_entryId) + { + $this->a_dict_entries[] = $s_entryId; + } + + /** + * Add a set of dictionary entries (based on the given prefix) for the Javascript side + */ + public function add_dict_entries($s_entriesPrefix) + { + $this->a_dict_entries_prefixes[] = $s_entriesPrefix; + } + + protected function get_dict_signature() + { + return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',', $this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes)); + } + + protected function get_dict_file_content() + { + $aEntries = array(); + foreach($this->a_dict_entries as $sCode) + { + $aEntries[$sCode] = Dict::S($sCode); + } + foreach($this->a_dict_entries_prefixes as $sPrefix) + { + $aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix)); + } + $sJSFile = 'var aDictEntries = '.json_encode($aEntries); + + return $sJSFile; + } /** @@ -347,7 +378,7 @@ class WebPage implements Page */ public function GetDetails($aFields) { - $sHtml = "
\n"; + $sHtml = "
\n"; foreach($aFields as $aAttrib) { $sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : ''; @@ -501,6 +532,9 @@ class WebPage implements Page echo ""; echo "".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."\n"; echo $this->get_base_tag(); + + $this->output_dict_entries(); + foreach($this->a_linked_scripts as $s_script) { // Make sure that the URL to the script contains the application's version number @@ -524,7 +558,6 @@ class WebPage implements Page } echo "\n"; } - $this->output_dict_entries(); foreach($this->a_linked_stylesheets as $a_stylesheet) { if (strpos($a_stylesheet['link'], '?') === false) @@ -778,36 +811,21 @@ class WebPage implements Page protected function output_dict_entries($bReturnOutput = false) { - $sHtml = ''; - if (count($this->a_dict_entries)>0) + if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0)) { - $sHtml .= "\n"; - } - - if ($bReturnOutput) - { - return $sHtml; - } - else - { - echo $sHtml; } } } diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 3218e4e9d..5b84748eb 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -111,6 +111,18 @@ define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/re */ abstract class AttributeDefinition { + const SEARCH_WIDGET_TYPE_RAW = 'raw'; + const SEARCH_WIDGET_TYPE_STRING = 'string'; + const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric'; + const SEARCH_WIDGET_TYPE_ENUM = 'enum'; + const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key'; + const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key'; + const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field'; + const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time'; + const SEARCH_WIDGET_TYPE_DATE = 'date'; + + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function GetType() { return Dict::S('Core:'.get_class($this)); @@ -122,6 +134,24 @@ abstract class AttributeDefinition abstract public function GetEditClass(); + /** + * Return the search widget type corresponding to this attribute + * + * @return string + */ + public function GetSearchType() + { + return static::SEARCH_WIDGET_TYPE; + } + + /** + * @return bool + */ + public function IsSearchable() + { + return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW; + } + protected $m_sCode; private $m_aParams = array(); protected $m_sHostClass = '!undefined!'; @@ -1617,6 +1647,8 @@ class AttributeDBField extends AttributeDBFieldVoid */ class AttributeInteger extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -1710,6 +1742,8 @@ class AttributeInteger extends AttributeDBField */ class AttributeObjectKey extends AttributeDBFieldVoid { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed')); @@ -1765,6 +1799,8 @@ class AttributeObjectKey extends AttributeDBFieldVoid */ class AttributePercentage extends AttributeInteger { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { $iWidth = 5; // Total width of the percentage bar graph, in em... @@ -1804,6 +1840,8 @@ class AttributePercentage extends AttributeInteger */ class AttributeDecimal extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); @@ -1902,6 +1940,8 @@ class AttributeDecimal extends AttributeDBField */ class AttributeBoolean extends AttributeInteger { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -2098,6 +2138,8 @@ class AttributeBoolean extends AttributeInteger */ class AttributeString extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -2248,6 +2290,8 @@ class AttributeString extends AttributeDBField */ class AttributeClass extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("class_category", "more_values")); @@ -2300,6 +2344,8 @@ class AttributeClass extends AttributeString */ class AttributeApplicationLanguage extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -2336,6 +2382,8 @@ class AttributeApplicationLanguage extends AttributeString */ class AttributeFinalClass extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function __construct($sCode, $aParams) { $this->m_sCode = $sCode; @@ -2490,6 +2538,8 @@ class AttributeFinalClass extends AttributeString */ class AttributePassword extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -2542,6 +2592,8 @@ class AttributePassword extends AttributeString */ class AttributeEncryptedString extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static $sKey = null; // Encryption key used for all encrypted fields public function __construct($sCode, $aParams) @@ -2984,6 +3036,8 @@ class AttributeLongText extends AttributeText */ class AttributeCaseLog extends AttributeLongText { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function GetNullValue() { return ''; @@ -3403,6 +3457,8 @@ class AttributeIPAddress extends AttributeString */ class AttributeOQL extends AttributeText { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function GetEditClass() {return "OQLExpression";} } @@ -3413,6 +3469,7 @@ class AttributeOQL extends AttributeText */ class AttributeTemplateString extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; } /** @@ -3422,6 +3479,7 @@ class AttributeTemplateString extends AttributeString */ class AttributeTemplateText extends AttributeText { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; } /** @@ -3431,6 +3489,8 @@ class AttributeTemplateText extends AttributeText */ class AttributeTemplateHTML extends AttributeText { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function GetSQLColumns($bFullSpec = false) { $aColumns = array(); @@ -3465,6 +3525,8 @@ class AttributeTemplateHTML extends AttributeText */ class AttributeEnum extends AttributeString { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; + static public function ListExpectedParams() { return parent::ListExpectedParams(); @@ -3892,6 +3954,8 @@ class AttributeMetaEnum extends AttributeEnum */ class AttributeDateTime extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME; + static $oFormat = null; static public function GetFormat() @@ -4418,6 +4482,8 @@ class AttributeDuration extends AttributeInteger */ class AttributeDate extends AttributeDateTime { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE; + static $oDateFormat = null; static public function GetFormat() @@ -4562,6 +4628,8 @@ class AttributeDeadline extends AttributeDateTime */ class AttributeExternalKey extends AttributeDBFieldVoid { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete")); @@ -4767,6 +4835,8 @@ class AttributeExternalKey extends AttributeDBFieldVoid */ class AttributeHierarchicalKey extends AttributeExternalKey { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; + protected $m_sTargetClass; static public function ListExpectedParams() @@ -4914,6 +4984,35 @@ class AttributeHierarchicalKey extends AttributeExternalKey */ class AttributeExternalField extends AttributeDefinition { + /** + * Return the search widget type corresponding to this attribute + * + * @return string + */ + public function GetSearchType() + { + // Not necessary the external key is already present + if ($this->IsFriendlyName()) + { + return self::SEARCH_WIDGET_TYPE_RAW; + } + + try + { + $oRemoteAtt = $this->GetExtAttDef(); + if ($oRemoteAtt instanceof AttributeString) + { + return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD; + } + } + catch (CoreException $e) + { + } + + return self::SEARCH_WIDGET_TYPE_RAW; + } + + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode")); @@ -4965,6 +5064,23 @@ class AttributeExternalField extends AttributeDefinition } return $sLabel; } + + public function GetLabelForSearchField() + { + $sLabel = parent::GetLabel(''); + if (strlen($sLabel) == 0) + { + $sKeyAttCode = $this->Get("extkey_attcode"); + $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); + $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); + + $oRemoteAtt = $this->GetExtAttDef(); + $sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode); + } + + return $sLabel; + } + public function GetDescription($sDefault = null) { $sLabel = parent::GetDescription(''); @@ -5285,6 +5401,8 @@ class AttributeURL extends AttributeString */ class AttributeBlob extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("depends_on")); @@ -5612,6 +5730,8 @@ class AttributeImage extends AttributeBlob */ class AttributeStopWatch extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { // The list of thresholds must be an array of iPercent => array of 'option' => value @@ -6280,6 +6400,8 @@ class AttributeStopWatch extends AttributeDefinition */ class AttributeSubItem extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code')); @@ -6446,6 +6568,8 @@ class AttributeSubItem extends AttributeDefinition */ class AttributeOneWayPassword extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("depends_on")); @@ -6609,6 +6733,8 @@ class AttributeOneWayPassword extends AttributeDefinition // Indexed array having two dimensions class AttributeTable extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + public function GetEditClass() {return "Table";} protected function GetSQLCol($bFullSpec = false) @@ -6838,6 +6964,8 @@ class AttributePropertySet extends AttributeTable */ class AttributeFriendlyName extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + public function __construct($sCode) { $this->m_sCode = $sCode; @@ -7004,6 +7132,8 @@ class AttributeFriendlyName extends AttributeDefinition */ class AttributeRedundancySettings extends AttributeDBField { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + static public function ListExpectedParams() { return array('sql', 'relation_code', 'from_class', 'neighbour_id', 'enabled', 'enabled_mode', 'min_up', 'min_up_type', 'min_up_mode'); @@ -7394,6 +7524,8 @@ class AttributeRedundancySettings extends AttributeDBField */ class AttributeCustomFields extends AttributeDefinition { + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + static public function ListExpectedParams() { return array_merge(parent::ListExpectedParams(), array("handler_class")); diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index a7f8d5e00..2d85f41e2 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -241,8 +241,7 @@ class CMDBSource } catch(mysqli_sql_exception $e) { - throw new MySQLException('Could not connect to the DB server', - array('host' => $sServer, 'user' => $sUser), $e); + throw new MySQLException('Could not connect to the DB server', array('host' => $sServer, 'user' => $sUser), $e); } if ($bCheckTlsAfterConnection diff --git a/core/dict.class.inc.php b/core/dict.class.inc.php index 6d68757a5..b792660e8 100644 --- a/core/dict.class.inc.php +++ b/core/dict.class.inc.php @@ -20,7 +20,7 @@ * Class Dict * Management of localizable strings * - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2018 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -65,6 +65,11 @@ class Dict protected static $m_aData = array(); protected static $m_sApplicationPrefix = null; + /** + * @param $sLanguageCode + * + * @throws \DictExceptionUnknownLanguage + */ public static function SetDefaultLanguage($sLanguageCode) { if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) @@ -74,6 +79,11 @@ class Dict self::$m_sDefaultLanguage = $sLanguageCode; } + /** + * @param $sLanguageCode + * + * @throws \DictExceptionUnknownLanguage + */ public static function SetUserLanguage($sLanguageCode) { if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) @@ -113,7 +123,6 @@ class Dict * @param string $sDefault Default value if there is no match in the dictionary * @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise * - * @throws DictExceptionMissingString * @return string */ public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false) @@ -124,7 +133,7 @@ class Dict if (!array_key_exists(self::GetUserLanguage(), self::$m_aData)) { - // It may happen, when something happens before the dictionnaries get loaded + // It may happen, when something happens before the dictionaries get loaded return $sStringCode; } $aCurrentDictionary = self::$m_aData[self::GetUserLanguage()]; @@ -155,25 +164,12 @@ class Dict } // Could not find the string... // - switch (self::$m_iErrorMode) + if (is_null($sDefault)) { - case DICT_ERR_STRING: - if (is_null($sDefault)) - { - return $sStringCode; - } - else - { - return $sDefault; - } - break; - - case DICT_ERR_EXCEPTION: - default: - throw new DictExceptionMissingString(self::$m_sCurrentLanguage, $sStringCode); - break; + return $sStringCode; } - return 'bug!'; + + return $sDefault; } @@ -285,6 +281,9 @@ class Dict /** * Clone a string in every language (if it exists in that language) + * + * @param $sSourceCode + * @param $sDestCode */ public static function CloneString($sSourceCode, $sDestCode) { @@ -357,5 +356,38 @@ class Dict // No need to actually load the strings since it's only used to know the list of languages // at setup time !! } + + /** + * Export all the dictionary entries - of the given language - whose code matches the given prefix + * missing entries in the current language will be replaced by entries in the default language + * @param string $sStartingWith + * @return string[] + */ + public static function ExportEntries($sStartingWith) + { + self::InitLangIfNeeded(self::GetUserLanguage()); + self::InitLangIfNeeded(self::$m_sDefaultLanguage); + $aEntries = array(); + $iLength = strlen($sStartingWith); + + // First prefill the array with entries from the default language + foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry) + { + if (substr($sCode, 0, $iLength) == $sStartingWith) + { + $aEntries[$sCode] = $sEntry; + } + } + + // Now put (overwrite) the entries for the user language + foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry) + { + if (substr($sCode, 0, $iLength) == $sStartingWith) + { + $aEntries[$sCode] = $sEntry; + } + } + return $aEntries; + } } ?> diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php index 1d09070f6..ab1a31e6e 100644 --- a/core/displayablegraph.class.inc.php +++ b/core/displayablegraph.class.inc.php @@ -1439,7 +1439,7 @@ class DisplayableGraph extends SimpleGraph $aExcludedByClass[get_class($oObj)][] = $oObj->GetKey(); } $oP->add("
\n"); - $oP->add("
\n"); + $oP->add("
\n"); if (!$oP->IsPrintableVersion()) { $oP->add_ready_script( diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 80db27c07..0a36c106a 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -1,5 +1,5 @@ Render($aArgs); + } + + public function GetAttDef($aClasses = array()) + { + return null; + } + /** * Recursively browse the expression tree * @param Closure $callback @@ -46,7 +72,7 @@ abstract class Expression abstract public function Browse(Closure $callback); abstract public function ApplyParameters($aArgs); - + // recursively builds an array of class => fieldname abstract public function ListRequiredFields(); @@ -54,10 +80,10 @@ abstract class Expression abstract public function CollectUsedParents(&$aTable); abstract public function IsTrue(); - + // recursively builds an array of [classAlias][fieldName] => value abstract public function ListConstantFields(); - + public function RequiresField($sClass, $sFieldName) { // #@# todo - optimize : this is called quite often when building a single query ! @@ -71,6 +97,12 @@ abstract class Expression return base64_encode($this->Render()); } + /** + * @param $sValue + * + * @return Expression + * @throws OQLException + */ static public function unserialize($sValue) { return self::FromOQL(base64_decode($sValue)); @@ -119,22 +151,57 @@ abstract class Expression { return new BinaryExpression($this, 'OR', $oExpr); } - + abstract public function RenameParam($sOldName, $sNewName); abstract public function RenameAlias($sOldName, $sNewName); /** * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * * @return The label - */ + */ public function MakeValueLabel($oFilter, $sValue, $sDefault) { return $sDefault; } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + return array( + 'widget' => AttributeDefinition::SEARCH_WIDGET_TYPE_RAW, + 'oql' => $this->Render($aArgs, $bRetrofitParams), + 'label' => $this->Display($oSearch, $aArgs, $oAttDef), + 'source' => get_class($this), + ); + } + + /** + * Split binary expression on given operator + * + * @param Expression $oExpr + * @param string $sOperator + * @param array $aAndExpr + * + * @return array of expressions + */ + public static function Split($oExpr, $sOperator = 'AND', &$aAndExpr = array()) + { + if (($oExpr instanceof BinaryExpression) && ($oExpr->GetOperator() == $sOperator)) + { + static::Split($oExpr->GetLeftExpr(), $sOperator, $aAndExpr); + static::Split($oExpr->GetRightExpr(), $sOperator, $aAndExpr); + } + else + { + $aAndExpr[] = $oExpr; + } + + return $aAndExpr; + } } class SQLExpression extends Expression @@ -165,7 +232,7 @@ class SQLExpression extends Expression public function ApplyParameters($aArgs) { } - + public function GetUnresolvedFields($sAlias, &$aUnresolved) { } @@ -183,12 +250,12 @@ class SQLExpression extends Expression public function CollectUsedParents(&$aTable) { } - + public function ListConstantFields() { return array(); } - + public function RenameParam($sOldName, $sNewName) { // Do nothing, since there is nothing to rename @@ -244,7 +311,7 @@ class BinaryExpression extends Expression } return false; } - + public function GetLeftExpr() { return $this->m_oLeftExpr; @@ -295,7 +362,7 @@ class BinaryExpression extends Expression $this->m_oRightExpr->ApplyParameters($aArgs); } } - + public function GetUnresolvedFields($sAlias, &$aUnresolved) { $this->GetLeftExpr()->GetUnresolvedFields($sAlias, $aUnresolved); @@ -321,7 +388,15 @@ class BinaryExpression extends Expression $this->GetLeftExpr()->CollectUsedParents($aTable); $this->GetRightExpr()->CollectUsedParents($aTable); } - + + public function GetAttDef($aClasses = array()) + { + $oAttDef = $this->GetLeftExpr()->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + + return $this->GetRightExpr()->GetAttDef($aClasses); + } + /** * List all constant expression of the form = or = : * Could be extended to support = @@ -355,7 +430,7 @@ class BinaryExpression extends Expression } return $aResult; } - + public function RenameParam($sOldName, $sNewName) { $this->GetLeftExpr()->RenameParam($sOldName, $sNewName); @@ -367,6 +442,131 @@ class BinaryExpression extends Expression $this->GetLeftExpr()->RenameAlias($sOldName, $sNewName); $this->GetRightExpr()->RenameAlias($sOldName, $sNewName); } + + // recursive rendering + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + $bReverseOperator = false; + $oLeftExpr = $this->GetLeftExpr(); + if ($oLeftExpr instanceof FieldExpression) + { + $oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses()); + } + $oRightExpr = $this->GetRightExpr(); + if ($oRightExpr instanceof FieldExpression) + { + $oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses()); + $bReverseOperator = true; + } + + $sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef); + $sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef); + + if ($bReverseOperator) + { + // switch left and right expressions so reverse the operator + // Note that the operation is the same so < becomes > and not >= + switch ($this->GetOperator()) + { + case '>': + $sOperator = '<'; + break; + case '<': + $sOperator = '>'; + break; + case '>=': + $sOperator = '<='; + break; + case '<=': + $sOperator = '>='; + break; + default: + $sOperator = $this->GetOperator(); + break; + } + $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); + + return "({$sRight}{$sOperator}{$sLeft})"; + } + + $sOperator = $this->GetOperator(); + $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); + + return "({$sLeft}{$sOperator}{$sRight})"; + } + + private function OperatorToNaturalLanguage($sOperator, $oAttDef) + { + if ($oAttDef instanceof AttributeDateTime) + { + return Dict::S('Expression:Operator:Date:'.$sOperator, " $sOperator "); + } + + return Dict::S('Expression:Operator:'.$sOperator, " $sOperator "); + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $bReverseOperator = false; + $oLeftExpr = $this->GetLeftExpr(); + $oRightExpr = $this->GetRightExpr(); + + $oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses()); + + if (is_null($oAttDef)) + { + $oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses()); + $bReverseOperator = true; + } + + if (is_null($oAttDef)) + { + return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + } + + $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + + if ($bReverseOperator) + { + $aCriteria = array_merge($aCriteriaRight, $aCriteriaLeft); + // switch left and right expressions so reverse the operator + // Note that the operation is the same so < becomes > and not >= + switch ($this->GetOperator()) + { + case '>': + $aCriteria['operator'] = '<'; + break; + case '<': + $aCriteria['operator'] = '>'; + break; + case '>=': + $aCriteria['operator'] = '<='; + break; + case '<=': + $aCriteria['operator'] = '>='; + break; + default: + $aCriteria['operator'] = $this->GetOperator(); + break; + } + } + else + { + $aCriteria = array_merge($aCriteriaLeft, $aCriteriaRight); + $aCriteria['operator'] = $this->GetOperator(); + } + $aCriteria['oql'] = $this->Render($aArgs, $bRetrofitParams); + $aCriteria['label'] = $this->Display($oSearch, $aArgs, $oAttDef); + + if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] != $aCriteriaRight['ref'])) + { + // Only one Field is supported in the expressions + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + } + + return $aCriteria; + } } @@ -404,7 +604,7 @@ class UnaryExpression extends Expression public function ApplyParameters($aArgs) { } - + public function GetUnresolvedFields($sAlias, &$aUnresolved) { } @@ -427,7 +627,7 @@ class UnaryExpression extends Expression { return array(); } - + public function RenameParam($sOldName, $sNewName) { // Do nothing @@ -451,6 +651,46 @@ class ScalarExpression extends UnaryExpression parent::__construct($value); } + /** + * @param array $oSearch + * @param array $aArgs + * @param AttributeDefinition $oAttDef + * + * @return array|string + * @throws \Exception + */ + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + if (!is_null($oAttDef)) + { + if ($oAttDef->IsExternalKey()) + { + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $this->m_value); + + return $oObj->Get("friendlyname"); + } catch (CoreException $e) + { + } + } + + if (!($oAttDef instanceof AttributeDateTime)) + { + return $oAttDef->GetAsPlainText($this->m_value); + } + } + + if (strpos($this->m_value, '%') === 0) + { + return ''; + } + + return $this->Render($aArgs); + } + // recursive rendering public function Render(&$aArgs = null, $bRetrofitParams = false) { @@ -469,6 +709,54 @@ class ScalarExpression extends UnaryExpression { return clone $this; } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = array(); + switch ((string)($this->m_value)) + { + case '%Y-%m-%d': + $aCriteria['date_type'] = 'd'; + break; + case '%Y-%m': + $aCriteria['date_type'] = 'm'; + break; + case '%w': + $aCriteria['date_type'] = 'w'; + break; + default: + $aValue = array('value' => $this->GetValue()); + if (!is_null($oAttDef)) + { + switch (true) + { + case $oAttDef->IsExternalKey(): + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $this->GetValue()); + + $aValue['label'] = $oObj->Get("friendlyname"); + + } + catch (Exception $e) + { + IssueLog::Error($e->getMessage()); + } + break; + default: + $aValue['label'] = $oAttDef->GetAsPlainText($this->GetValue()); + break; + } + } + $aCriteria['values'] = array($aValue); + break; + } + + return $aCriteria; + } + } class TrueExpression extends ScalarExpression @@ -525,6 +813,44 @@ class FieldExpression extends UnaryExpression $this->m_value = $sParent.'.'.$this->m_sName; } + private function GetClassName($aClasses = array()) + { + if (isset($aClasses[$this->m_sParent])) + { + return $aClasses[$this->m_sParent]; + } + else + { + return $this->m_sParent; + } + } + + /** + * @param DBObjectSearch $oSearch + * @param array $aArgs + * @param AttributeDefinition $oAttDef + * + * @return array|string + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception + */ + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + if (empty($this->m_sParent)) + { + return "`{$this->m_sName}`"; + } + $sClass = $this->GetClassName($oSearch->GetJoinedClasses()); + $sAttName = MetaModel::GetLabel($sClass, $this->m_sName); + if ($sClass != $oSearch->GetClass()) + { + $sAttName = MetaModel::GetName($sClass).':'.$sAttName; + } + + return $sAttName; + } + // recursive rendering public function Render(&$aArgs = null, $bRetrofitParams = false) { @@ -535,6 +861,37 @@ class FieldExpression extends UnaryExpression return "`{$this->m_sParent}`.`{$this->m_sName}`"; } + public function GetAttDef($aClasses = array()) + { + if (!empty($this->m_sParent)) + { + $sClass = $this->GetClassName($aClasses); + $aAttDefs = MetaModel::ListAttributeDefs($sClass); + if (isset($aAttDefs[$this->m_sName])) + { + return $aAttDefs[$this->m_sName]; + } + else + { + if ($this->m_sName == 'id') + { + $aParams = array( + 'default_value' => 0, + 'is_null_allowed' => false, + 'allowed_values' => null, + 'depends_on' => null, + 'sql' => 'id', + ); + + return new AttributeInteger($this->m_sName, $aParams); + } + } + } + + return null; + } + + public function ListRequiredFields() { return array($this->m_sParent.'.'.$this->m_sName); @@ -565,8 +922,9 @@ class FieldExpression extends UnaryExpression if (!array_key_exists($this->m_sParent, $aTranslationData)) { if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData))); + return clone $this; - } + } if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent])) { if (!array_key_exists('*', $aTranslationData[$this->m_sParent])) @@ -595,12 +953,13 @@ class FieldExpression extends UnaryExpression /** * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * * @return The label - */ + */ public function MakeValueLabel($oFilter, $sValue, $sDefault) { $sAttCode = $this->GetName(); @@ -646,6 +1005,47 @@ class FieldExpression extends UnaryExpression $this->m_sParent = $sNewName; } } + + + /** + * @param $oSearch + * @param null $aArgs + * @param bool $bRetrofitParams + * @param AttributeDefinition $oAttDef + * + * @return array + */ + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $oAttDef = $this->GetAttDef($oSearch->GetJoinedClasses()); + if (!is_null($oAttDef)) + { + $sSearchType = $oAttDef->GetSearchType(); + try + { + if ($sSearchType == AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY) + { + // TODO Check the type of external key ? (EXTKEY_ABSOLUTE or EXTKEY_RELATIVE) + if (MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass())) + { + $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; + } + } + } + catch (CoreException $e) + { + } + } + else + { + $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE; + } + return array( + 'widget' => $sSearchType, + 'ref' => $this->GetParent().'.'.$this->GetName(), + 'class_alias' => $this->GetParent(), + ); + } } // Has been resolved into an SQL expression @@ -678,7 +1078,68 @@ class VariableExpression extends UnaryExpression return false; } - public function GetName() {return $this->m_sName;} + public function GetName() + { + return $this->m_sName; + } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + $sValue = $this->m_value; + if (!is_null($aArgs) && (array_key_exists($this->m_sName, $aArgs))) + { + $sValue = $aArgs[$this->m_sName]; + } + elseif (($iPos = strpos($this->m_sName, '->')) !== false) + { + $sParamName = substr($this->m_sName, 0, $iPos); + $oObj = null; + $sAttCode = 'id'; + if (array_key_exists($sParamName.'->object()', $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName.'->object()']; + } + elseif (array_key_exists($sParamName, $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName]; + } + if (!is_null($oObj)) + { + if ($sAttCode == 'id') + { + $sValue = $oObj->Get("friendlyname"); + } + else + { + $sValue = $oObj->Get($sAttCode); + } + + return $sValue; + } + } + if (!is_null($oAttDef)) + { + if ($oAttDef->IsExternalKey()) + { + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $sValue); + + return $oObj->Get("friendlyname"); + } catch (CoreException $e) + { + } + } + + return $oAttDef->GetAsPlainText($sValue); + } + + return $this->Render($aArgs); + } // recursive rendering public function Render(&$aArgs = null, $bRetrofitParams = false) @@ -729,7 +1190,7 @@ class VariableExpression extends UnaryExpression $this->m_sName = $sNewName; } } - + public function GetAsScalar($aArgs) { $oRet = null; @@ -833,7 +1294,7 @@ class ListExpression extends Expression } } } - + public function GetUnresolvedFields($sAlias, &$aUnresolved) { foreach ($this->m_aExpressions as $oExpr) @@ -861,7 +1322,7 @@ class ListExpression extends Expression } return $aRes; } - + public function CollectUsedParents(&$aTable) { foreach ($this->m_aExpressions as $oExpr) @@ -879,7 +1340,7 @@ class ListExpression extends Expression } return $aRes; } - + public function RenameParam($sOldName, $sNewName) { $aRes = array(); @@ -887,7 +1348,7 @@ class ListExpression extends Expression { $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); } - } + } public function RenameAlias($sOldName, $sNewName) { @@ -896,7 +1357,34 @@ class ListExpression extends Expression { $oExpr->RenameAlias($sOldName, $sNewName); } - } + } + + public function GetAttDef($aClasses = array()) + { + foreach($this->m_aExpressions as $oExpression) + { + $oAttDef = $oExpression->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + } + + return null; + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aValues = array(); + + foreach($this->m_aExpressions as $oExpression) + { + $aCrit = $oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + if (array_key_exists('values', $aCrit)) + { + $aValues = array_merge($aValues, $aCrit['values']); + } + } + + return array('values' => $aValues); + } } @@ -990,7 +1478,7 @@ class FunctionExpression extends Expression } return $aRes; } - + public function CollectUsedParents(&$aTable) { foreach ($this->m_aArgs as $oExpr) @@ -1008,7 +1496,7 @@ class FunctionExpression extends Expression } return $aRes; } - + public function RenameParam($sOldName, $sNewName) { foreach ($this->m_aArgs as $key => $oExpr) @@ -1025,14 +1513,26 @@ class FunctionExpression extends Expression } } + public function GetAttDef($aClasses = array()) + { + foreach($this->m_aArgs as $oExpression) + { + $oAttDef = $oExpression->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + } + + return null; + } + /** * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * * @return The label - */ + */ public function MakeValueLabel($oFilter, $sValue, $sDefault) { static $aWeekDayToString = null; @@ -1095,6 +1595,87 @@ class FunctionExpression extends Expression } return $sRes; } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + $sOperation = ''; + $sVerb = ''; + switch ($this->m_sVerb) + { + case 'NOW': + $sVerb = $this->VerbToNaturalLanguage(); + break; + case 'DATE_SUB': + $sVerb = ' -'; + break; + case 'DATE_ADD': + $sVerb = ' +'; + break; + case 'DATE_FORMAT': + break; + default: + return $this->Render($aArgs); + } + + foreach($this->m_aArgs as $oExpression) + { + if ($oExpression instanceof IntervalExpression) + { + $sOperation .= $sVerb; + $sVerb = ''; + } + $sOperation .= $oExpression->Display($oSearch, $aArgs, $oAttDef); + } + + if (!empty($sVerb)) + { + $sOperation .= $sVerb; + } + return $sOperation; + } + + private function VerbToNaturalLanguage() + { + return Dict::S('Expression:Verb:'.$this->m_sVerb, " {$this->m_sVerb} "); + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = array(); + switch ($this->m_sVerb) + { + case 'ISNULL': + $aCriteria['operator'] = $this->m_sVerb; + foreach($this->m_aArgs as $oExpression) + { + $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); + } + $aCriteria['has_undefined'] = true; + break; + + case 'NOW': + $aCriteria = array('widget' => 'date_time'); + $aCriteria['is_relative'] = true; + $aCriteria['verb'] = $this->m_sVerb; + break; + + case 'DATE_ADD': + case 'DATE_SUB': + case 'DATE_FORMAT': + $aCriteria = array('widget' => 'date_time'); + foreach($this->m_aArgs as $oExpression) + { + $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); + } + $aCriteria['verb'] = $this->m_sVerb; + break; + + default: + return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + } + + return $aCriteria; + } } class IntervalExpression extends Expression @@ -1147,7 +1728,7 @@ class IntervalExpression extends Expression $this->m_oValue->ApplyParameters($aArgs); } } - + public function GetUnresolvedFields($sAlias, &$aUnresolved) { $this->m_oValue->GetUnresolvedFields($sAlias, $aUnresolved); @@ -1171,16 +1752,29 @@ class IntervalExpression extends Expression { return array(); } - + public function RenameParam($sOldName, $sNewName) { $this->m_oValue->RenameParam($sOldName, $sNewName); - } + } public function RenameAlias($sOldName, $sNewName) { $this->m_oValue->RenameAlias($sOldName, $sNewName); - } + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = $this->m_oValue->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + $aCriteria['unit'] = $this->m_sUnit; + + return $aCriteria; + } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null) + { + return $this->m_oValue->Render($aArgs).' '.Dict::S('Expression:Unit:Long:'.$this->m_sUnit, $this->m_sUnit); + } } class CharConcatExpression extends Expression @@ -1210,7 +1804,7 @@ class CharConcatExpression extends Expression foreach ($this->m_aExpressions as $oExpr) { $sCol = $oExpr->Render($aArgs, $bRetrofitParams); - // Concat will be globally NULL if one single argument is null ! + // Concat will be globally NULL if one single argument is null ! $aRes[] = "COALESCE($sCol, '')"; } return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)"; @@ -1293,7 +1887,7 @@ class CharConcatExpression extends Expression { $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); } - } + } public function RenameAlias($sOldName, $sNewName) { @@ -1301,7 +1895,7 @@ class CharConcatExpression extends Expression { $oExpr->RenameAlias($sOldName, $sNewName); } - } + } } @@ -1322,7 +1916,7 @@ class CharConcatWSExpression extends CharConcatExpression foreach ($this->m_aExpressions as $oExpr) { $sCol = $oExpr->Render($aArgs, $bRetrofitParams); - // Concat will be globally NULL if one single argument is null ! + // Concat will be globally NULL if one single argument is null ! $aRes[] = "COALESCE($sCol, '')"; } $sSep = CMDBSource::Quote($this->m_separator); @@ -1502,6 +2096,7 @@ class QueryBuilderExpressions { $this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); } + foreach($this->m_aClassIds as $sClass => $oExpression) { $this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); diff --git a/css/css-variables.scss b/css/css-variables.scss index 6e61874f8..975555e61 100644 --- a/css/css-variables.scss +++ b/css/css-variables.scss @@ -1,8 +1,49 @@ -$highlight-color: #E87C1E; +// Base colors +$gray-base: #000 !default; +$gray-darker: lighten($gray-base, 13.5%) !default; // #222 +$gray-dark: #444 !default; +$gray: #777 !default; +$gray-light: #808080 !default; +$gray-lighter: #ddd !default; +$gray-extra-light: #F1F1F1 !default; + +$white: #FFFFFF !default; + +$combodo-orange: #EA7D1E !default; +$combodo-dark-gray: #585653 !default; + +$combodo-orange-dark: darken($combodo-orange, 13.8%) !default; +$combodo-orange-darker: darken($combodo-orange, 18%) !default; +$combodo-dark-gray-dark: darken($combodo-dark-gray, 13.5%) !default; +$combodo-dark-gray-darker: darken($combodo-dark-gray, 18%) !default; + +// Vars +$highlight-color: $combodo-orange; $grey-color: #555555; $complement-color: #1c94c4; $complement-light: #d6e8ef; -$frame-background-color: #F1F1F1; +$frame-background-color: $gray-extra-light; $text-color: #000; +$box-radius: 0px; +$box-shadow-regular: 0 1px 1px rgba(0, 0, 0, 0.15); +// - Boxes +//$search-criteria-box-color: #2D2D2D; +//$search-criteria-box-bg-color: #f0f3f5; +//$search-criteria-box-border-color: #3f7294; +//$search-criteria-box-border: 1px solid $search-criteria-box-border-color; +//$search-criteria-box-radius: 1px; +// +$search-criteria-box-color: #2D2D2D; +$search-criteria-box-picto-color: #E87C1E; +$search-criteria-box-bg-color: #EEEEEE; +$search-criteria-box-hover-color: $white; +$search-criteria-box-border-color: #CCCCCC; +$search-criteria-box-border: 1px solid $search-criteria-box-border-color; +$search-criteria-box-radius: 1px; +// +$search-add-criteria-box-color: $search-criteria-box-color; +$search-add-criteria-box-bg-color: $white; +$search-add-criteria-box-hover-color: $gray-extra-light; + // Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0 -$version: "v2.4.0"; \ No newline at end of file +$version: "v2.4.0"; diff --git a/css/font-combodo/combodo-webfont.ttf b/css/font-combodo/combodo-webfont.ttf index 5be2cbdd4..495c822c0 100644 Binary files a/css/font-combodo/combodo-webfont.ttf and b/css/font-combodo/combodo-webfont.ttf differ diff --git a/css/font-combodo/combodo-webfont.woff b/css/font-combodo/combodo-webfont.woff index cf7212013..f8588d085 100644 Binary files a/css/font-combodo/combodo-webfont.woff and b/css/font-combodo/combodo-webfont.woff differ diff --git a/css/font-combodo/combodo-webfont.woff2 b/css/font-combodo/combodo-webfont.woff2 index 812157161..0bddf7d18 100644 Binary files a/css/font-combodo/combodo-webfont.woff2 and b/css/font-combodo/combodo-webfont.woff2 differ diff --git a/css/font-combodo/combodo.sfd b/css/font-combodo/combodo.sfd index 102234437..ee4bb1e16 100644 --- a/css/font-combodo/combodo.sfd +++ b/css/font-combodo/combodo.sfd @@ -21,7 +21,7 @@ OS2Version: 0 OS2_WeightWidthSlopeOnly: 0 OS2_UseTypoMetrics: 1 CreationTime: 1463745065 -ModificationTime: 1506001058 +ModificationTime: 1522325525 OS2TypoAscent: 0 OS2TypoAOffset: 1 OS2TypoDescent: 0 @@ -35,6 +35,7 @@ HheadAscent: 0 HheadAOffset: 1 HheadDescent: 0 HheadDOffset: 1 +OS2Vendor: 'PfEd' MarkAttachClasses: 1 DEI: 91125 Encoding: ISO8859-1 @@ -46,7 +47,7 @@ FitToEm: 0 WinInfo: 0 31 10 BeginPrivate: 0 EndPrivate -BeginChars: 256 11 +BeginChars: 256 13 StartChar: zero Encoding: 48 48 0 @@ -940,7 +941,135 @@ SplineSet 815.574 32.7998 814.774 33.5996 813.175 33.5996 c 2 810.774 33.5996 l 1 EndSplineSet -Validated: 524321 +Validated: 33 +EndChar + +StartChar: B +Encoding: 66 66 11 +Width: 1024 +VWidth: 0 +HStem: 0 59<210.859 353.111 670.997 813.141> 103 28<483.475 540.525> 326 280<245 398 626 779> +VStem: 87 62<121.402 265.716> 415 60<134.336 199.804> 549 60<134.336 199.804> 875 62<121.402 265.716> +LayerCount: 3 +Fore +SplineSet +376 606 m 0 + 420 606 456 571 456 527 c 0 + 456 514 l 1 + 458 444 l 1 + 566 444 l 1 + 568 514 l 1 + 568 514 568 524 568 527 c 0 + 568 571 604 606 648 606 c 0 + 677 606 704 590 718 564 c 1 + 719 564 l 1 + 754 502 l 1 + 781 491 803 472 817 446 c 1 + 818 446 l 1 + 914 283 l 1 + 929 255 937 224 937 192 c 0 + 937 87 852 0 747 0 c 0 + 666 0 594 54 568 131 c 1 + 555 114 534 103 512 103 c 0 + 490 103 469 114 456 132 c 1 + 431 55 358 0 277 0 c 0 + 172 0 87 87 87 192 c 0 + 87 224 95 255 110 283 c 1 + 206 446 l 1 + 207 446 l 1 + 221 472 243 491 270 502 c 1 + 306 564 l 1 + 320 590 347 606 376 606 c 0 +282 326 m 0 + 208 326 149 266 149 193 c 0 + 149 119 208 59 282 59 c 0 + 355 59 415 119 415 193 c 0 + 415 266 355 326 282 326 c 0 +742 326 m 0 + 669 326 609 266 609 193 c 0 + 609 119 669 59 742 59 c 0 + 816 59 875 119 875 193 c 0 + 875 266 816 326 742 326 c 0 +512 204 m 0 + 492 204 475 188 475 168 c 0 + 475 148 492 131 512 131 c 0 + 532 131 549 148 549 168 c 0 + 549 188 532 204 512 204 c 0 +EndSplineSet +EndChar + +StartChar: b +Encoding: 98 98 12 +Width: 1024 +VWidth: 0 +Flags: H +HStem: -1 35<183.649 347.721 676.937 838.822> 198 25<478.803 548.794> 294 234<470.756 555.897> +VStem: 75 44<83.375 145> 414 42<83.6665 369.146> 432 24<255 473> 569 42<83.6665 369.146> 569 24<255 473> 906 43<84.3258 145> +LayerCount: 3 +Fore +SplineSet +352 616 m 0xf080 + 390 616 423 593 420 563 c 1 + 422 488 l 1 + 452 468 454 441 454 441 c 1 + 455 305 456 159 456 12 c 1 + 453 -62 369 -121 266 -121 c 0 + 161 -121 74 -59 74 17 c 0 + 74 20 76 22 76 25 c 1 + 74 25 l 1 + 106 196 l 1 + 106 196 l 1 + 113 235 143 270 188 292 c 1 + 186 292 l 1 + 206 408 l 1 + 206 408 228 457 274 487 c 1 + 282 570 l 1 + 284 570 l 1 + 287 596 316 616 352 616 c 0xf080 +266 114 m 0 + 185 114 118 69 118 14 c 0 + 118 -41 185 -86 266 -86 c 0 + 347 -86 414 -41 414 14 c 0 + 414 69 347 114 266 114 c 0 +514 408 m 0 + 554 408 589 384 592 353 c 1 + 592 353 l 1 + 592 135 l 1 + 592 135 l 1 + 590 103 555 78 514 78 c 0 + 473 78 436 103 434 135 c 1 + 432 135 l 1 + 432 353 l 1 + 434 353 l 1 + 437 384 474 408 514 408 c 0 +466 138 m 0 + 466 118 489 103 514 103 c 2 + 539 103 560 118 560 138 c 0 + 560 158 539 174 514 174 c 0 + 489 174 466 158 466 138 c 0 +672 616 m 0 + 708 616 737 596 740 570 c 1 + 742 570 l 1 + 750 487 l 1 + 796 457 818 408 818 408 c 1 + 838 292 l 1 + 836 292 l 1 + 881 270 911 235 918 196 c 1 + 950 25 l 1 + 948 25 l 1 + 948 22 948 20 948 17 c 0 + 948 -59 863 -121 758 -121 c 0 + 655 -121 571 -62 568 12 c 1 + 568 159 569 305 570 441 c 1 + 570 441 572 468 602 488 c 1 + 604 563 l 1 + 601 593 634 616 672 616 c 0 +758 114 m 0 + 677 114 610 69 610 14 c 0 + 610 -41 677 -86 758 -86 c 0 + 839 -86 906 -41 906 14 c 0 + 906 69 839 114 758 114 c 0 +EndSplineSet EndChar EndChars EndSplineFont diff --git a/css/font-combodo/font-combodo.css b/css/font-combodo/font-combodo.css index 07a583cda..bf60a2766 100644 --- a/css/font-combodo/font-combodo.css +++ b/css/font-combodo/font-combodo.css @@ -1,8 +1,8 @@ @font-face { font-family: 'CombodoRegular'; - src: url('combodo-webfont.woff2?v=2.0') format('woff2'), - url('combodo-webfont.woff?v=2.0') format('woff'), - url('combodo-webfont.ttf?v=2.0') format('truetype'); + src: url('combodo-webfont.woff2?v=2.1') format('woff2'), + url('combodo-webfont.woff?v=2.1') format('woff'), + url('combodo-webfont.ttf?v=2.1') format('truetype'); font-weight: normal; font-style: normal; @@ -187,6 +187,12 @@ .fc-closed-request:before { content: "3"; } +.fc-binoculars:before { + content: "B"; +} +.fc-binoculars-alt:before { + content: "b"; +} .fc-combodo-icon-o:before { content: "C"; } diff --git a/css/font-combodo/glyphs/B.svg b/css/font-combodo/glyphs/B.svg new file mode 100755 index 000000000..d1a20a22c --- /dev/null +++ b/css/font-combodo/glyphs/B.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/css/font-combodo/glyphs/b-lowercase.svg b/css/font-combodo/glyphs/b-lowercase.svg new file mode 100755 index 000000000..08dac675b --- /dev/null +++ b/css/font-combodo/glyphs/b-lowercase.svg @@ -0,0 +1,280 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/css/light-grey.css b/css/light-grey.css index 1c3555356..001c43ffb 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -58,7 +58,7 @@ label { cursor: pointer; } .hilite, .hilite a, .hilite a:visited { - color: #e87c1e; + color: #ea7d1e; text-decoration: none; } table.datatable { @@ -114,7 +114,7 @@ table.listResults td .view-image img { cursor: pointer; margin-bottom: 3px; padding: 2px; - background-color: #e87c1e; + background-color: #ea7d1e; } .edit-image .edit-buttons .button.disabled { cursor: default; @@ -275,7 +275,7 @@ legend.transparent { } .ui-widget-content td a:hover, p a:hover, td a:hover { text-decoration: underline; - color: #e87c1e; + color: #ea7d1e; } .cke_reset_all *:hover { text-decoration: none; @@ -283,8 +283,8 @@ legend.transparent { } table.cke_dialog_contents a.cke_dialog_ui_button_ok { color: #000; - border-color: #e87c1e; - background: #e87c1e; + border-color: #ea7d1e; + background: #ea7d1e; } .cke_notifications_area { display: none; @@ -309,7 +309,7 @@ td a.mailto, td a.mailto:visited { } td a.mailto:hover { text-decoration: underline; - color: #e87c1e; + color: #ea7d1e; padding-left: 20px; background: url(../images/mail.png?v=v2.4.0) no-repeat left; } @@ -329,7 +329,7 @@ a.small_action { padding-left: 5px; padding-top: 2px; padding-bottom: 2px; - background: #e87c1e url(../images/actions_left.png?v=v2.4.0) no-repeat left; + background: #ea7d1e url(../images/actions_left.png?v=v2.4.0) no-repeat left; } .actions_details span { background: url(../images/actions_right.png?v=v2.4.0) no-repeat right; @@ -424,7 +424,7 @@ div.ui-accordion-content { text-decoration: none; } .ui-accordion-content a:hover { - color: #e87c1e; + color: #ea7d1e; text-decoration: none; } .ui-accordion-content ul { @@ -455,7 +455,7 @@ a.CollapsibleLabel.open, td a.CollapsibleLabel.open { padding: 0px 0pt 0px 16px; font-size: 8pt; text-decoration: none; - color: #e87c1e; + color: #ea7d1e; background: url(../images/mini-arrow-orange-open.gif) no-repeat left; } .page_header { @@ -506,7 +506,7 @@ div.actions_menu > ul { nowidth: 70px; padding-left: 5px; /* Nasty work-around for IE... en attendant mieux */ - background: #e87c1e url(../images/actions_left.png?v=v2.4.0) no-repeat top left; + background: #ea7d1e url(../images/actions_left.png?v=v2.4.0) no-repeat top left; cursor: pointer; margin: 0; } @@ -577,7 +577,7 @@ div.actions_menu > ul > li { position: absolute; display: none; border-top: 1px solid white; - z-index: 999; + z-index: 1500; } .itop_popup li ul li, #logOffBtn li ul li { float: none; @@ -588,7 +588,7 @@ div.actions_menu > ul > li { text-align: left; } .itop_popup li ul li a:hover, #logOffBtn li ul li a:hover { - background: #e87c1e; + background: #ea7d1e; color: #fff; font-weight: bold; } @@ -675,60 +675,6 @@ input.dp-applied { float: left; } /* For search forms */ -.SearchDrawer { - border-top: 5px solid #1c94c4; - border-left: 5px solid #1c94c4; - border-right: 5px solid #1c94c4; - border-bottom: 0; - background: #d6e8ef; - color: #000; - padding: 10px; - margin: 0; - font-size: 12px; -} -.SearchDrawer label { - background: #d6e8ef; - color: #000; - text-align: right; -} -.SearchDrawer h1 { - color: #000; -} -.SearchDrawer .SearchAttribute > .field_input_zone { - display: inline-block; -} -.SearchDrawer .SearchAttribute > .field_input_zone > .field_select_wrapper { - display: inline-block; -} -.DrawerClosed { - display: none; -} -.DrawerHandle { - margin: 0; - padding: 5px; - background: url(../images/drawer-handle.gif) bottom no-repeat transparent; - color: #fff; - cursor: pointer; - text-align: center; - /* center the block */ - width: 100px; - margin-left: auto; - margin-right: auto; - margin-top: 0; - margin-bottom: 0; - display: block; - font-size: 12px; -} -div.HRDrawer { - height: 5px; - width: 100%; - margin: 0; - background-color: #1c94c4; - margin: 0; - padding: 0; - border: 0; - display: block; -} .mini_tabs a { text-decoration: none; font-weight: bold; @@ -753,6 +699,506 @@ div.HRDrawer { nopadding-right: 1em; margin-top: 0; } +/* Search forms v2 */ +.search_box { + box-sizing: border-box; + position: relative; + z-index: 1100; + /* To be over the table block/unblock UI. Not very sure about this. */ + /* Sizing reset */ +} +.search_box * { + box-sizing: border-box; +} +.search_form_handler { + position: relative; + z-index: 10; + font-size: 12px; + border: 1px solid #3f7294; + /* Sizing reset */ + /* Hyperlink reset */ + /* Input reset */ + /* List helpers */ +} +.search_form_handler * { + box-sizing: border-box; +} +.search_form_handler a { + color: inherit; + text-decoration: none; +} +.search_form_handler input[type="text"], .search_form_handler select { + padding: 1px 2px; +} +.search_form_handler.opened .sf_title .sft_toggler { + transform: rotateX(180deg); +} +.search_form_handler.opened .sf_criterion_area { + /*display: inherit;*/ +} +.search_form_handler .sf_title { + padding: 8px 10px; + margin: 0; + color: #fff; + background-color: #3f7294; + cursor: pointer; + /* Pictogram */ +} +.search_form_handler .sf_title .sft_picto { + display: none; + /* TODO: Remove this class and the correspondig DOM element if this option is kept. */ + margin-right: 10px; +} +.search_form_handler .sf_title .sft_refresh, .search_form_handler .sf_title .sft_toggler { + transition: color 0.2s ease-in-out, transform 0.4s ease-in-out; +} +.search_form_handler .sf_title .sft_refresh:hover, .search_form_handler .sf_title .sft_toggler:hover { + color: #f1f1f1; +} +.search_form_handler .sf_title .sft_refresh { + font-size: 10pt; + line-height: 13pt; +} +.search_form_handler .sf_title .sft_toggler { + margin-left: 0.7em; +} +.search_form_handler .sf_message { + display: none; + margin: 8px 8px 0px 8px; + border-radius: 0px; +} +.search_form_handler .sf_criterion_area { + /*display: none;*/ + padding: 8px 8px 3px 8px; + /* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */ + background-color: #fff; + /* Common style between criterion and more criterion */ + /* Criteria tags */ + /* More criterion */ +} +.search_form_handler .sf_criterion_area .search_form_criteria, .search_form_handler .sf_criterion_area .sf_more_criterion { + position: relative; + display: inline-block; + margin-bottom: 5px; + vertical-align: top; +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened, .search_form_handler .sf_criterion_area .sf_more_criterion.opened { + margin-bottom: 0px; + /* To compensate the .sfc/.sfm_header:padding-bottom: 13px */ +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_header, .search_form_handler .sf_criterion_area .sf_more_criterion.opened .sfc_header, .search_form_handler .sf_criterion_area .search_form_criteria.opened .sfm_header, .search_form_handler .sf_criterion_area .sf_more_criterion.opened .sfm_header { + border-bottom: none !important; + box-shadow: none !important; + padding-bottom: 13px; + /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */ +} +.search_form_handler .sf_criterion_area .search_form_criteria > *, .search_form_handler .sf_criterion_area .sf_more_criterion > * { + padding: 7px 8px; + vertical-align: top; + border: 1px solid #ccc; + border-radius: 1px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group, .search_form_handler .sf_criterion_area .sf_more_criterion .sfc_form_group, .search_form_handler .sf_criterion_area .search_form_criteria .sfm_content, .search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content { + position: absolute; + z-index: -1; + min-width: 100%; + left: 0px; + margin-top: -1px; +} +.search_form_handler .sf_criterion_area .search_form_criteria { + margin-right: 30px; + /* Non editable criteria */ + /* Draft criteria (modifications not applied) */ + /* Opened criteria (form group displayed) */ + /* Top left corner icons */ + /* Special criterion processing */ +} +.search_form_handler .sf_criterion_area .search_form_criteria.locked { + background-color: #f1f1f1; +} +.search_form_handler .sf_criterion_area .search_form_criteria.locked .sfc_title { + user-select: none; + cursor: initial; +} +.search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_header, .search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_form_group { + border-style: dashed; +} +.search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_title { + font-style: italic; +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened { + z-index: 1; + /* To be over other criterion */ +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_toggle { + transform: rotateX(-180deg); +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_form_group { + display: block; +} +.search_form_handler .sf_criterion_area .search_form_criteria.opened_left .sfc_form_group { + left: auto; + right: 0px; +} +.search_form_handler .sf_criterion_area .search_form_criteria:not(:last-of-type)::after { + /* TODO: Find an elegant way to do this, without hardcoding the content (could be a EOF - ); - $oPage->add('
'); - $oPage->add('
'); - $oPage->add('

 

'); - $oPage->add('

'.Dict::S('UI:CSVImport:AdvancedMode+').'

'); - $oPage->add('

 

'); - $oPage->add('
'); - $oPage->add('

'.Dict::S('ExcelExport:PreparingExport').'

'); - $oPage->add('
'.Dict::S('ExcelExport:Statistics').'
'); - $oPage->add('
'); - $aLabels = array( - 'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'), - 'cancel_button' => Dict::S('UI:Button:Cancel'), - 'export_button' => Dict::S('ExcelExporter:ExportButton'), - 'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??) - ); - $sJSLabels = json_encode($aLabels); - $sFilter = addslashes($sFilter); - $sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php'); - $oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});"); - break; - + ); + $oPage->add('
'); + $oPage->add('
'); + $oPage->add('

 

'); + $oPage->add('

'.Dict::S('UI:CSVImport:AdvancedMode+').'

'); + $oPage->add('

 

'); + $oPage->add('
'); + $oPage->add('

'.Dict::S('ExcelExport:PreparingExport').'

'); + $oPage->add('
'.Dict::S('ExcelExport:Statistics').'
'); + $oPage->add('
'); + $aLabels = array( + 'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'), + 'cancel_button' => Dict::S('UI:Button:Cancel'), + 'export_button' => Dict::S('ExcelExporter:ExportButton'), + 'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??) + ); + $sJSLabels = json_encode($aLabels); + $sFilter = addslashes($sFilter); + $sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php'); + $oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});"); + break; + case 'xlsx_start': - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $bAdvanced = (utils::ReadParam('advanced', 'false') == 'true'); - $oSearch = DBObjectSearch::unserialize($sFilter); - - $oExcelExporter = new ExcelExporter(); - $oExcelExporter->SetObjectList($oSearch); - //$oExcelExporter->SetChunkSize(10); //Only for testing - $oExcelExporter->SetAdvancedMode($bAdvanced); - $sToken = $oExcelExporter->SaveState(); - $oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken))); - break; - + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $bAdvanced = (utils::ReadParam('advanced', 'false') == 'true'); + $oSearch = DBObjectSearch::unserialize($sFilter); + + $oExcelExporter = new ExcelExporter(); + $oExcelExporter->SetObjectList($oSearch); + //$oExcelExporter->SetChunkSize(10); //Only for testing + $oExcelExporter->SetAdvancedMode($bAdvanced); + $sToken = $oExcelExporter->SaveState(); + $oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken))); + break; + case 'xlsx_run': - $sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit'); - ini_set('memory_limit', $sMemoryLimit); - ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes - - $sToken = utils::ReadParam('token', '', false, 'raw_data'); - $oExcelExporter = new ExcelExporter($sToken); - $aStatus = $oExcelExporter->Run(); - $aResults = array('status' => $aStatus['code'], 'percentage' => $aStatus['percentage'], 'message' => $aStatus['message']); - if ($aStatus['code'] == 'done') - { - $aResults['statistics'] = $oExcelExporter->GetStatistics('html'); - } - $oPage->add(json_encode($aResults)); - break; - + $sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit'); + ini_set('memory_limit', $sMemoryLimit); + ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes + + $sToken = utils::ReadParam('token', '', false, 'raw_data'); + $oExcelExporter = new ExcelExporter($sToken); + $aStatus = $oExcelExporter->Run(); + $aResults = array('status' => $aStatus['code'], 'percentage' => $aStatus['percentage'], 'message' => $aStatus['message']); + if ($aStatus['code'] == 'done') + { + $aResults['statistics'] = $oExcelExporter->GetStatistics('html'); + } + $oPage->add(json_encode($aResults)); + break; + case 'xlsx_download': - $sToken = utils::ReadParam('token', '', false, 'raw_data'); - $oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - $oPage->SetContentDisposition('attachment', 'export.xlsx'); - $sFileContent = ExcelExporter::GetExcelFileFromToken($sToken); - $oPage->add($sFileContent); - ExcelExporter::CleanupFromToken($sToken); - break; - + $sToken = utils::ReadParam('token', '', false, 'raw_data'); + $oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $oPage->SetContentDisposition('attachment', 'export.xlsx'); + $sFileContent = ExcelExporter::GetExcelFileFromToken($sToken); + $oPage->add($sFileContent); + ExcelExporter::CleanupFromToken($sToken); + break; + case 'xlsx_abort': - // Stop & cleanup an export... - $sToken = utils::ReadParam('token', '', false, 'raw_data'); - ExcelExporter::CleanupFromToken($sToken); - break; + // Stop & cleanup an export... + $sToken = utils::ReadParam('token', '', false, 'raw_data'); + ExcelExporter::CleanupFromToken($sToken); + break; case 'relation_pdf': case 'relation_attachment': - require_once(APPROOT.'core/simplegraph.class.inc.php'); - require_once(APPROOT.'core/relationgraph.class.inc.php'); - require_once(APPROOT.'core/displayablegraph.class.inc.php'); - $sRelation = utils::ReadParam('relation', 'impacts'); - $sDirection = utils::ReadParam('direction', 'down'); - - $iGroupingThreshold = utils::ReadParam('g', 5, false, 'integer'); - $sPageFormat = utils::ReadParam('p', 'A4'); - $sPageOrientation = utils::ReadParam('o', 'L'); - $sTitle = utils::ReadParam('title', '', false, 'raw_data'); - $sPositions = utils::ReadParam('positions', null, false, 'raw_data'); - $aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data'); - $bIncludeList = (bool)utils::ReadParam('include_list', false); - $sComments = utils::ReadParam('comments', '', false, 'raw_data'); - $aContexts = utils::ReadParam('contexts', array(), false, 'raw_data'); - $sContextKey = utils::ReadParam('context_key', '', false, 'raw_data'); - $aPositions = null; - if ($sPositions != null) - { - $aPositions = json_decode($sPositions, true); - } - - // Get the list of source objects - $aSources = utils::ReadParam('sources', array(), false, 'raw_data'); - $aSourceObjects = array(); - foreach($aSources as $sClass => $aIDs) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $aIDs, 'IN'); - $oSet = new DBObjectSet($oSearch); - while($oObj = $oSet->Fetch()) - { - $aSourceObjects[] = $oObj; - } - } - $sSourceClass = '*'; - if (count($aSourceObjects) == 1) - { - $sSourceClass = get_class($aSourceObjects[0]); - } - - // Get the list of excluded objects - $aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data'); - $aExcludedObjects = array(); - foreach($aExcluded as $sClass => $aIDs) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $aIDs, 'IN'); - $oSet = new DBObjectSet($oSearch); - while($oObj = $oSet->Fetch()) - { - $aExcludedObjects[] = $oObj; - } - } - - $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); - if ($sDirection == 'up') - { - $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts); - } - else - { - $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts); - } - - // Remove excluded classes from the graph - if (count($aExcludedClasses) > 0) - { - $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); - foreach($oIterator as $oNode) - { - $oObj = $oNode->GetProperty('object'); - if ($oObj && in_array(get_class($oObj), $aExcludedClasses)) - { - $oRelGraph->FilterNode($oNode); - } - } - } - - $oPage = new PDFPage($sTitle, $sPageFormat, $sPageOrientation); - $oPage->SetContentDisposition('attachment', $sTitle.'.pdf'); - - $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); - $oGraph->InitFromGraphviz(); - if ($aPositions != null) - { - $oGraph->UpdatePositions($aPositions); - } + require_once(APPROOT.'core/simplegraph.class.inc.php'); + require_once(APPROOT.'core/relationgraph.class.inc.php'); + require_once(APPROOT.'core/displayablegraph.class.inc.php'); + $sRelation = utils::ReadParam('relation', 'impacts'); + $sDirection = utils::ReadParam('direction', 'down'); - $aGroups = array(); - $oIterator = new RelationTypeIterator($oGraph, 'Node'); - foreach($oIterator as $oNode) - { - if ($oNode instanceof DisplayableGroupNode) + $iGroupingThreshold = utils::ReadParam('g', 5, false, 'integer'); + $sPageFormat = utils::ReadParam('p', 'A4'); + $sPageOrientation = utils::ReadParam('o', 'L'); + $sTitle = utils::ReadParam('title', '', false, 'raw_data'); + $sPositions = utils::ReadParam('positions', null, false, 'raw_data'); + $aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data'); + $bIncludeList = (bool)utils::ReadParam('include_list', false); + $sComments = utils::ReadParam('comments', '', false, 'raw_data'); + $aContexts = utils::ReadParam('contexts', array(), false, 'raw_data'); + $sContextKey = utils::ReadParam('context_key', '', false, 'raw_data'); + $aPositions = null; + if ($sPositions != null) { - $aGroups[$oNode->GetProperty('group_index')] = $oNode->GetObjects(); + $aPositions = json_decode($sPositions, true); } - } - // First page is the graph - $oGraph->RenderAsPDF($oPage, $sComments, $sContextKey); - if ($bIncludeList) - { - // Then the lists of objects (one table per finalclass) - $aResults = array(); - $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); - foreach($oIterator as $oNode) - { - $oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object - if ($oObj) - { - $sObjClass = get_class($oObj); - if (!array_key_exists($sObjClass, $aResults)) - { - $aResults[$sObjClass] = array(); - } - $aResults[$sObjClass][] = $oObj; - } - } - - $oPage->get_tcpdf()->AddPage(); - $oPage->get_tcpdf()->SetFont('dejavusans', '', 10, '', true); // Reset the font size to its default - $oPage->add(''); - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - foreach($aResults as $sListClass => $aObjects) - { - set_time_limit($iLoopTimeLimit*count($aObjects)); - $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); - $oSet->SetShowObsoleteData(utils::ShowObsoleteData()); - $sHtml = "
\n"; - $sHtml .= "
".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sListClass))."
\n"; - $sHtml .= "
\n"; - $oPage->add($sHtml); - cmdbAbstractObject::DisplaySet($oPage, $oSet, array('table_id' => $sSourceClass.'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)); - $oPage->p(''); // Some space - } - - // Then the content of the groups (one table per group) - if (count($aGroups) > 0) - { - $oPage->get_tcpdf()->AddPage(); - $oPage->add(''); - foreach($aGroups as $idx => $aObjects) - { - set_time_limit($iLoopTimeLimit*count($aObjects)); - $sListClass = get_class(current($aObjects)); - $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); - $sHtml = "
\n"; - $sHtml .= "
".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."
\n"; - $sHtml .= "
\n"; - $oPage->add($sHtml); - cmdbAbstractObject::DisplaySet($oPage, $oSet); - $oPage->p(''); // Some space - } - } - } - if ($operation == 'relation_attachment') - { - $sObjClass = utils::ReadParam('obj_class', '', false, 'class'); - $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer'); - - // Save the generated PDF as an attachment - $sPDF = $oPage->get_pdf(); - $oPage = new ajax_page(''); - $oAttachment = new Attachment(); - $oAttachment->Set('item_class', $sObjClass); - $oAttachment->Set('item_id', $iObjKey); - $oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf'); - $oAttachment->Set('contents', $oDoc); - $iAttachmentId = $oAttachment->DBInsert(); - $aRet = array( - 'status' => 'ok', - 'att_id' => $iAttachmentId, - ); - $oPage->add(json_encode($aRet)); - } - break; - - case 'relation_json': - require_once(APPROOT.'core/simplegraph.class.inc.php'); - require_once(APPROOT.'core/relationgraph.class.inc.php'); - require_once(APPROOT.'core/displayablegraph.class.inc.php'); - $sRelation = utils::ReadParam('relation', 'impacts'); - $sDirection = utils::ReadParam('direction', 'down'); - $iGroupingThreshold = utils::ReadParam('g', 5); - $sPositions = utils::ReadParam('positions', null, false, 'raw_data'); - $aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data'); - $aContexts = utils::ReadParam('contexts', array(), false, 'raw_data'); - $sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data'); - $aPositions = null; - if ($sPositions != null) - { - $aPositions = json_decode($sPositions, true); - } - // Get the list of source objects - $aSources = utils::ReadParam('sources', array(), false, 'raw_data'); - $aSourceObjects = array(); - foreach($aSources as $sClass => $aIDs) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $aIDs, 'IN'); - $oSet = new DBObjectSet($oSearch); - while($oObj = $oSet->Fetch()) + $aSources = utils::ReadParam('sources', array(), false, 'raw_data'); + $aSourceObjects = array(); + foreach($aSources as $sClass => $aIDs) { - $aSourceObjects[] = $oObj; - } - } - - // Get the list of excluded objects - $aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data'); - $aExcludedObjects = array(); - foreach($aExcluded as $sClass => $aIDs) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $aIDs, 'IN'); - $oSet = new DBObjectSet($oSearch); - while($oObj = $oSet->Fetch()) - { - $aExcludedObjects[] = $oObj; - } - } - - // Compute the graph - $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); - if ($sDirection == 'up') - { - $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts); - } - else - { - $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts); - } - - // Remove excluded classes from the graph - if (count($aExcludedClasses) > 0) - { - $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); - foreach($oIterator as $oNode) - { - $oObj = $oNode->GetProperty('object'); - if ($oObj && in_array(get_class($oObj), $aExcludedClasses)) + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $aIDs, 'IN'); + $oSet = new DBObjectSet($oSearch); + while ($oObj = $oSet->Fetch()) { - $oRelGraph->FilterNode($oNode); + $aSourceObjects[] = $oObj; } } - } - - $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); - $oGraph->InitFromGraphviz(); - if ($aPositions != null) - { - $oGraph->UpdatePositions($aPositions); - } - $oPage->add($oGraph->GetAsJSON($sContextKey)); - $oPage->SetContentType('application/json'); - break; - - case 'relation_groups': - $aGroups = utils::ReadParam('groups'); - $iBlock = 1; // Zero is not a valid blockid - foreach($aGroups as $idx => $aDefinition) - { - $sListClass = $aDefinition['class']; - $oSearch = new DBObjectSearch($sListClass); - $oSearch->AddCondition('id', $aDefinition['keys'], 'IN'); - $oSearch->SetShowObsoleteData(utils::ShowObsoleteData()); - $oPage->add("

".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."

\n"); - $oPage->add("
\n"); - $oPage->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aDefinition['keys']), Metamodel::GetName($sListClass))."

\n"); - $oPage->add("
\n"); - $oBlock = new DisplayBlock($oSearch, 'list'); - $oBlock->Display($oPage, 'group_'.$iBlock++); - $oPage->p(' '); // Some space ? - } - break; + $sSourceClass = '*'; + if (count($aSourceObjects) == 1) + { + $sSourceClass = get_class($aSourceObjects[0]); + } - case 'relation_lists': - $aLists = utils::ReadParam('lists'); - $iBlock = 1; // Zero is not a valid blockid - foreach($aLists as $sListClass => $aKeys) - { - $oSearch = new DBObjectSearch($sListClass); - $oSearch->AddCondition('id', $aKeys, 'IN'); - $oSearch->SetShowObsoleteData(utils::ShowObsoleteData()); - $oPage->add("
\n"); - $oPage->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aKeys), Metamodel::GetName($sListClass))."

\n"); - $oPage->add("
\n"); - $oBlock = new DisplayBlock($oSearch, 'list'); - $oBlock->Display($oPage, 'list_'.$iBlock++, array('table_id' => 'ImpactAnalysis_'.$sListClass)); - $oPage->p(' '); // Some space ? - } - break; - - case 'ticket_impact': - require_once(APPROOT.'core/simplegraph.class.inc.php'); - require_once(APPROOT.'core/relationgraph.class.inc.php'); - require_once(APPROOT.'core/displayablegraph.class.inc.php'); - $sRelation = utils::ReadParam('relation', 'impacts'); - $sDirection = utils::ReadParam('direction', 'down'); - $iGroupingThreshold = utils::ReadParam('g', 5); - $sClass = utils::ReadParam('class', '', false, 'class'); - $sAttCode = utils::ReadParam('attcode', 'functionalcis_list'); - $sImpactAttCode = utils::ReadParam('impact_attcode', 'impact_code'); - $sImpactAttCodeValue = utils::ReadParam('impact_attcode_value', 'manual'); - $iId = (int)utils::ReadParam('id', 0, false, 'integer'); - - // Get the list of source objects - $oTicket = MetaModel::GetObject($sClass, $iId); - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); - $oExtKeyToRemote = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sExtKeyToRemote); - $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); - $oSet = $oTicket->Get($sAttCode); - $aSourceObjects = array(); - $aExcludedObjects = array(); - while($oLnk = $oSet->Fetch()) - { - if ($oLnk->Get($sImpactAttCode) == 'manual') + // Get the list of excluded objects + $aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data'); + $aExcludedObjects = array(); + foreach($aExcluded as $sClass => $aIDs) { - $aSourceObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote)); - } - if ($oLnk->Get($sImpactAttCode) == 'not_impacted') - { - $aExcludedObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote)); - } - } - - // Compute the graph - $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); - if ($sDirection == 'up') - { - $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth); - } - else - { - $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, $aExcludedObjects); - } - - $aResults = $oRelGraph->GetObjectsByClass(); - $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); - - $sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection; - $oAppContext = new ApplicationContext(); - $oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket)); - break; - - case 'export_build': - register_shutdown_function(function() - { - $aErr = error_get_last(); - if (($aErr !== null) && ($aErr['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR))) - { - ob_end_clean(); - echo json_encode(array('code' => 'error', 'percentage' => 100, 'message' => Dict::Format('UI:Error_Details', $aErr['message']))); - } - }); - try - { - $token = utils::ReadParam('token', null); - $aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Export not found for token: '$token'"); // Fallback error, just in case - $data = ''; - if ($token === null) - { - $sFormat = utils::ReadParam('format', ''); - $sExpression = utils::ReadParam('expression', null, false, 'raw_data'); - $iQueryId = utils::ReadParam('query', null); - if ($sExpression === null) + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $aIDs, 'IN'); + $oSet = new DBObjectSet($oSearch); + while ($oObj = $oSet->Fetch()) { - $oQuerySearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $iQueryId)); - $oQuerySearch->UpdateContextFromUser(); - $oQueries = new DBObjectSet($oQuerySearch); - if ($oQueries->Count() > 0) - { - $oQuery = $oQueries->Fetch(); - $sExpression = $oQuery->Get('oql'); - } - else - { - $aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Invalid query phrasebook identifier: '$iQueryId'"); - } + $aExcludedObjects[] = $oObj; } - if($sExpression !== null) - { - $oSearch = DBObjectSearch::FromOQL($sExpression); - $oSearch->UpdateContextFromUser(); - $oExporter = BulkExport::FindExporter($sFormat, $oSearch); - $oExporter->SetObjectList($oSearch); - $oExporter->SetFormat($sFormat); - $oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE); - $oExporter->ReadParameters(); - } - - // First pass, generate the headers - $data .= $oExporter->GetHeader(); + } + + $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); + if ($sDirection == 'up') + { + $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts); } else { - $oExporter = BulkExport::FindExporterFromToken($token); + $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts); } - - if ($oExporter) + + // Remove excluded classes from the graph + if (count($aExcludedClasses) > 0) { - $data .= $oExporter->GetNextChunk($aResult); - if ($aResult['code'] != 'done') + $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); + foreach($oIterator as $oNode) { - $oExporter->AppendToTmpFile($data); - $aResult['token'] = $oExporter->SaveState(); - } - else - { - // Last pass - $data .= $oExporter->GetFooter(); - $oExporter->AppendToTmpFile($data); - $aResult['token'] = $oExporter->SaveState(); - if (substr($oExporter->GetMimeType(), 0, 5) == 'text/') + $oObj = $oNode->GetProperty('object'); + if ($oObj && in_array(get_class($oObj), $aExcludedClasses)) { - // Result must be encoded in UTF-8 to be passed as part of a JSON structure - $sCharset = $oExporter->GetCharacterSet(); - if (strtoupper($sCharset) != 'UTF-8') + $oRelGraph->FilterNode($oNode); + } + } + } + + $oPage = new PDFPage($sTitle, $sPageFormat, $sPageOrientation); + $oPage->SetContentDisposition('attachment', $sTitle.'.pdf'); + + $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); + $oGraph->InitFromGraphviz(); + if ($aPositions != null) + { + $oGraph->UpdatePositions($aPositions); + } + + $aGroups = array(); + $oIterator = new RelationTypeIterator($oGraph, 'Node'); + foreach($oIterator as $oNode) + { + if ($oNode instanceof DisplayableGroupNode) + { + $aGroups[$oNode->GetProperty('group_index')] = $oNode->GetObjects(); + } + } + // First page is the graph + $oGraph->RenderAsPDF($oPage, $sComments, $sContextKey); + + if ($bIncludeList) + { + // Then the lists of objects (one table per finalclass) + $aResults = array(); + $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); + foreach($oIterator as $oNode) + { + $oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object + if ($oObj) + { + $sObjClass = get_class($oObj); + if (!array_key_exists($sObjClass, $aResults)) { - $aResult['text_result'] = iconv($sCharset, 'UTF-8', file_get_contents($oExporter->GetTmpFilePath())); + $aResults[$sObjClass] = array(); + } + $aResults[$sObjClass][] = $oObj; + } + } + + $oPage->get_tcpdf()->AddPage(); + $oPage->get_tcpdf()->SetFont('dejavusans', '', 10, '', true); // Reset the font size to its default + $oPage->add(''); + $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); + foreach($aResults as $sListClass => $aObjects) + { + set_time_limit($iLoopTimeLimit * count($aObjects)); + $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); + $oSet->SetShowObsoleteData(utils::ShowObsoleteData()); + $sHtml = "
\n"; + $sHtml .= "
".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), + Metamodel::GetName($sListClass))."
\n"; + $sHtml .= "
\n"; + $oPage->add($sHtml); + cmdbAbstractObject::DisplaySet($oPage, $oSet, array('table_id' => $sSourceClass.'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)); + $oPage->p(''); // Some space + } + + // Then the content of the groups (one table per group) + if (count($aGroups) > 0) + { + $oPage->get_tcpdf()->AddPage(); + $oPage->add(''); + foreach($aGroups as $idx => $aObjects) + { + set_time_limit($iLoopTimeLimit * count($aObjects)); + $sListClass = get_class(current($aObjects)); + $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); + $sHtml = "
\n"; + $sHtml .= "
".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:RelationGroupNumber_N', (1 + $idx))."
\n"; + $sHtml .= "
\n"; + $oPage->add($sHtml); + cmdbAbstractObject::DisplaySet($oPage, $oSet); + $oPage->p(''); // Some space + } + } + } + if ($operation == 'relation_attachment') + { + $sObjClass = utils::ReadParam('obj_class', '', false, 'class'); + $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer'); + + // Save the generated PDF as an attachment + $sPDF = $oPage->get_pdf(); + $oPage = new ajax_page(''); + $oAttachment = new Attachment(); + $oAttachment->Set('item_class', $sObjClass); + $oAttachment->Set('item_id', $iObjKey); + $oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf'); + $oAttachment->Set('contents', $oDoc); + $iAttachmentId = $oAttachment->DBInsert(); + $aRet = array( + 'status' => 'ok', + 'att_id' => $iAttachmentId, + ); + $oPage->add(json_encode($aRet)); + } + break; + + case 'relation_json': + require_once(APPROOT.'core/simplegraph.class.inc.php'); + require_once(APPROOT.'core/relationgraph.class.inc.php'); + require_once(APPROOT.'core/displayablegraph.class.inc.php'); + $sRelation = utils::ReadParam('relation', 'impacts'); + $sDirection = utils::ReadParam('direction', 'down'); + $iGroupingThreshold = utils::ReadParam('g', 5); + $sPositions = utils::ReadParam('positions', null, false, 'raw_data'); + $aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data'); + $aContexts = utils::ReadParam('contexts', array(), false, 'raw_data'); + $sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data'); + $aPositions = null; + if ($sPositions != null) + { + $aPositions = json_decode($sPositions, true); + } + + // Get the list of source objects + $aSources = utils::ReadParam('sources', array(), false, 'raw_data'); + $aSourceObjects = array(); + foreach($aSources as $sClass => $aIDs) + { + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $aIDs, 'IN'); + $oSet = new DBObjectSet($oSearch); + while ($oObj = $oSet->Fetch()) + { + $aSourceObjects[] = $oObj; + } + } + + // Get the list of excluded objects + $aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data'); + $aExcludedObjects = array(); + foreach($aExcluded as $sClass => $aIDs) + { + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $aIDs, 'IN'); + $oSet = new DBObjectSet($oSearch); + while ($oObj = $oSet->Fetch()) + { + $aExcludedObjects[] = $oObj; + } + } + + // Compute the graph + $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); + if ($sDirection == 'up') + { + $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts); + } + else + { + $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts); + } + + // Remove excluded classes from the graph + if (count($aExcludedClasses) > 0) + { + $oIterator = new RelationTypeIterator($oRelGraph, 'Node'); + foreach($oIterator as $oNode) + { + $oObj = $oNode->GetProperty('object'); + if ($oObj && in_array(get_class($oObj), $aExcludedClasses)) + { + $oRelGraph->FilterNode($oNode); + } + } + } + + $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); + $oGraph->InitFromGraphviz(); + if ($aPositions != null) + { + $oGraph->UpdatePositions($aPositions); + } + $oPage->add($oGraph->GetAsJSON($sContextKey)); + $oPage->SetContentType('application/json'); + break; + + case 'relation_groups': + $aGroups = utils::ReadParam('groups'); + $iBlock = 1; // Zero is not a valid blockid + foreach($aGroups as $idx => $aDefinition) + { + $sListClass = $aDefinition['class']; + $oSearch = new DBObjectSearch($sListClass); + $oSearch->AddCondition('id', $aDefinition['keys'], 'IN'); + $oSearch->SetShowObsoleteData(utils::ShowObsoleteData()); + $oPage->add("

".Dict::Format('UI:RelationGroupNumber_N', (1 + $idx))."

\n"); + $oPage->add("
\n"); + $oPage->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aDefinition['keys']), Metamodel::GetName($sListClass))."

\n"); + $oPage->add("
\n"); + $oBlock = new DisplayBlock($oSearch, 'list'); + $oBlock->Display($oPage, 'group_'.$iBlock++); + $oPage->p(' '); // Some space ? + } + break; + + case 'relation_lists': + $aLists = utils::ReadParam('lists'); + $iBlock = 1; // Zero is not a valid blockid + foreach($aLists as $sListClass => $aKeys) + { + $oSearch = new DBObjectSearch($sListClass); + $oSearch->AddCondition('id', $aKeys, 'IN'); + $oSearch->SetShowObsoleteData(utils::ShowObsoleteData()); + $oPage->add("
\n"); + $oPage->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aKeys), Metamodel::GetName($sListClass))."

\n"); + $oPage->add("
\n"); + $oBlock = new DisplayBlock($oSearch, 'list'); + $oBlock->Display($oPage, 'list_'.$iBlock++, array('table_id' => 'ImpactAnalysis_'.$sListClass)); + $oPage->p(' '); // Some space ? + } + break; + + case 'ticket_impact': + require_once(APPROOT.'core/simplegraph.class.inc.php'); + require_once(APPROOT.'core/relationgraph.class.inc.php'); + require_once(APPROOT.'core/displayablegraph.class.inc.php'); + $sRelation = utils::ReadParam('relation', 'impacts'); + $sDirection = utils::ReadParam('direction', 'down'); + $iGroupingThreshold = utils::ReadParam('g', 5); + $sClass = utils::ReadParam('class', '', false, 'class'); + $sAttCode = utils::ReadParam('attcode', 'functionalcis_list'); + $sImpactAttCode = utils::ReadParam('impact_attcode', 'impact_code'); + $sImpactAttCodeValue = utils::ReadParam('impact_attcode_value', 'manual'); + $iId = (int)utils::ReadParam('id', 0, false, 'integer'); + + // Get the list of source objects + $oTicket = MetaModel::GetObject($sClass, $iId); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); + $oExtKeyToRemote = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sExtKeyToRemote); + $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); + $oSet = $oTicket->Get($sAttCode); + $aSourceObjects = array(); + $aExcludedObjects = array(); + while ($oLnk = $oSet->Fetch()) + { + if ($oLnk->Get($sImpactAttCode) == 'manual') + { + $aSourceObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote)); + } + if ($oLnk->Get($sImpactAttCode) == 'not_impacted') + { + $aExcludedObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote)); + } + } + + // Compute the graph + $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); + if ($sDirection == 'up') + { + $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth); + } + else + { + $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, $aExcludedObjects); + } + + $aResults = $oRelGraph->GetObjectsByClass(); + $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); + + $sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection; + $oAppContext = new ApplicationContext(); + $oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket)); + break; + + case 'export_build': + register_shutdown_function(function () { + $aErr = error_get_last(); + if (($aErr !== null) && ($aErr['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR))) + { + ob_end_clean(); + echo json_encode(array('code' => 'error', 'percentage' => 100, 'message' => Dict::Format('UI:Error_Details', $aErr['message']))); + } + }); + try + { + $token = utils::ReadParam('token', null); + $aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Export not found for token: '$token'"); // Fallback error, just in case + $data = ''; + if ($token === null) + { + $sFormat = utils::ReadParam('format', ''); + $sExpression = utils::ReadParam('expression', null, false, 'raw_data'); + $iQueryId = utils::ReadParam('query', null); + if ($sExpression === null) + { + $oQuerySearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $iQueryId)); + $oQuerySearch->UpdateContextFromUser(); + $oQueries = new DBObjectSet($oQuerySearch); + if ($oQueries->Count() > 0) + { + $oQuery = $oQueries->Fetch(); + $sExpression = $oQuery->Get('oql'); } else { - $aResult['text_result'] = file_get_contents($oExporter->GetTmpFilePath()); + $aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Invalid query phrasebook identifier: '$iQueryId'"); } - $aResult['mime_type'] = $oExporter->GetMimeType(); } - $aResult['message'] = Dict::Format('Core:BulkExport:ClickHereToDownload_FileName', $oExporter->GetDownloadFileName()); + if ($sExpression !== null) + { + $oSearch = DBObjectSearch::FromOQL($sExpression); + $oSearch->UpdateContextFromUser(); + $oExporter = BulkExport::FindExporter($sFormat, $oSearch); + $oExporter->SetObjectList($oSearch); + $oExporter->SetFormat($sFormat); + $oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE); + $oExporter->ReadParameters(); + } + + // First pass, generate the headers + $data .= $oExporter->GetHeader(); } + else + { + $oExporter = BulkExport::FindExporterFromToken($token); + } + + if ($oExporter) + { + $data .= $oExporter->GetNextChunk($aResult); + if ($aResult['code'] != 'done') + { + $oExporter->AppendToTmpFile($data); + $aResult['token'] = $oExporter->SaveState(); + } + else + { + // Last pass + $data .= $oExporter->GetFooter(); + $oExporter->AppendToTmpFile($data); + $aResult['token'] = $oExporter->SaveState(); + if (substr($oExporter->GetMimeType(), 0, 5) == 'text/') + { + // Result must be encoded in UTF-8 to be passed as part of a JSON structure + $sCharset = $oExporter->GetCharacterSet(); + if (strtoupper($sCharset) != 'UTF-8') + { + $aResult['text_result'] = iconv($sCharset, 'UTF-8', file_get_contents($oExporter->GetTmpFilePath())); + } + else + { + $aResult['text_result'] = file_get_contents($oExporter->GetTmpFilePath()); + } + $aResult['mime_type'] = $oExporter->GetMimeType(); + } + $aResult['message'] = Dict::Format('Core:BulkExport:ClickHereToDownload_FileName', $oExporter->GetDownloadFileName()); + } + } + $oPage->add(json_encode($aResult)); + } catch (BulkExportException $e) + { + $aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->GetLocalizedMessage()); + $oPage->add(json_encode($aResult)); + } catch (Exception $e) + { + $aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->getMessage()); + $oPage->add(json_encode($aResult)); } - $oPage->add(json_encode($aResult)); - } - catch(BulkExportException $e) - { - $aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->GetLocalizedMessage()); - $oPage->add(json_encode($aResult)); - } - catch(Exception $e) - { - $aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->getMessage()); - $oPage->add(json_encode($aResult)); - } - break; - + break; + case 'export_download': - $token = utils::ReadParam('token', null); - if ($token !== null) - { - $oExporter = BulkExport::FindExporterFromToken($token); - if ($oExporter) + $token = utils::ReadParam('token', null); + if ($token !== null) { - $sMimeType = $oExporter->GetMimeType(); - if (substr($sMimeType, 0, 5) == 'text/') + $oExporter = BulkExport::FindExporterFromToken($token); + if ($oExporter) { - $sMimeType .= ';charset='.strtolower($oExporter->GetCharacterSet()); + $sMimeType = $oExporter->GetMimeType(); + if (substr($sMimeType, 0, 5) == 'text/') + { + $sMimeType .= ';charset='.strtolower($oExporter->GetCharacterSet()); + } + $oPage->SetContentType($sMimeType); + $oPage->SetContentDisposition('attachment', $oExporter->GetDownloadFileName()); + $oPage->add(file_get_contents($oExporter->GetTmpFilePath())); } - $oPage->SetContentType($sMimeType); - $oPage->SetContentDisposition('attachment', $oExporter->GetDownloadFileName()); - $oPage->add(file_get_contents($oExporter->GetTmpFilePath())); } - } - break; - + break; + case 'export_cancel': - $token = utils::ReadParam('token', null); - if ($token !== null) - { - $oExporter = BulkExport::FindExporterFromToken($token); - if ($oExporter) + $token = utils::ReadParam('token', null); + if ($token !== null) { - $oExporter->Cleanup(); - } - } - $aResult = array('code' => 'error', 'percentage' => 100, 'message' => Dict::S('Core:BulkExport:ExportCancelledByUser')); - $oPage->add(json_encode($aResult)); - break; - - case 'extend_lock': - $sObjClass = utils::ReadParam('obj_class', '', false, 'class'); - $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer'); - $sToken = utils::ReadParam('token', 0, false, 'raw_data'); - $aResult = iTopOwnershipLock::ExtendLock($sObjClass, $iObjKey, $sToken); - if (!$aResult['status']) - { - if ($aResult['operation'] == 'lost') - { - $sName = $aResult['owner']->GetName(); - if ($aResult['owner']->Get('contactid') != 0) + $oExporter = BulkExport::FindExporterFromToken($token); + if ($oExporter) { - $sName .= ' ('.$aResult['owner']->Get('contactid_friendlyname').')'; + $oExporter->Cleanup(); } - $aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName); - $aResult['popup_message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User_Explanation', $sName); } - else if ($aResult['operation'] == 'expired') + $aResult = array('code' => 'error', 'percentage' => 100, 'message' => Dict::S('Core:BulkExport:ExportCancelledByUser')); + $oPage->add(json_encode($aResult)); + break; + + case 'extend_lock': + $sObjClass = utils::ReadParam('obj_class', '', false, 'class'); + $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer'); + $sToken = utils::ReadParam('token', 0, false, 'raw_data'); + $aResult = iTopOwnershipLock::ExtendLock($sObjClass, $iObjKey, $sToken); + if (!$aResult['status']) { - $aResult['message'] = Dict::S('UI:CurrentObjectLockExpired'); - $aResult['popup_message'] = Dict::S('UI:CurrentObjectLockExpired_Explanation'); + if ($aResult['operation'] == 'lost') + { + $sName = $aResult['owner']->GetName(); + if ($aResult['owner']->Get('contactid') != 0) + { + $sName .= ' ('.$aResult['owner']->Get('contactid_friendlyname').')'; + } + $aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName); + $aResult['popup_message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User_Explanation', $sName); + } + else + { + if ($aResult['operation'] == 'expired') + { + $aResult['message'] = Dict::S('UI:CurrentObjectLockExpired'); + $aResult['popup_message'] = Dict::S('UI:CurrentObjectLockExpired_Explanation'); + } + } } - } - $oPage->add(json_encode($aResult)); - break; - + $oPage->add(json_encode($aResult)); + break; + case 'watchdog': - $oPage->add('ok'); // Better for debugging... - break; - + $oPage->add('ok'); // Better for debugging... + break; + case 'cke_img_upload': // Image uploaded via CKEditor $aResult = array( - 'uploaded' => 0, - 'fileName' => '', - 'url' => '', - 'icon' => '', - 'msg' => '', - 'att_id' => 0, - 'preview' => 'false', + 'uploaded' => 0, + 'fileName' => '', + 'url' => '', + 'icon' => '', + 'msg' => '', + 'att_id' => 0, + 'preview' => 'false', ); - + $sObjClass = stripslashes(utils::ReadParam('obj_class', '', false, 'class')); $sTempId = utils::ReadParam('temp_id', ''); if (empty($sObjClass)) @@ -2333,9 +2369,9 @@ EOF $oAttachment->Set('item_class', $sObjClass); $oAttachment->SetDefaultOrgId(); $oAttachment->Set('contents', $oDoc); - $oAttachment->Set('secret', sprintf ('%06x', mt_rand(0, 0xFFFFFF))); // something not easy to guess + $oAttachment->Set('secret', sprintf('%06x', mt_rand(0, 0xFFFFFF))); // something not easy to guess $iAttId = $oAttachment->DBInsert(); - + $aResult['uploaded'] = 1; $aResult['msg'] = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); $aResult['fileName'] = $oDoc->GetFileName(); @@ -2350,15 +2386,14 @@ EOF { $aResult['error'] = $oDoc->GetFileName().' is not a valid image format.'; } - } - catch (FileUploadException $e) + } catch (FileUploadException $e) { $aResult['error'] = $e->GetMessage(); } } $oPage->add(json_encode($aResult)); break; - + case 'cke_upload_and_browse': $sTempId = utils::ReadParam('temp_id'); $sObjClass = utils::ReadParam('obj_class', '', false, 'class'); @@ -2375,32 +2410,31 @@ EOF $oAttachment->Set('item_class', $sObjClass); $oAttachment->SetDefaultOrgId(); $oAttachment->Set('contents', $oDoc); - $oAttachment->Set('secret', sprintf ('%06x', mt_rand(0, 0xFFFFFF))); // something not easy to guess + $oAttachment->Set('secret', sprintf('%06x', mt_rand(0, 0xFFFFFF))); // something not easy to guess $iAttId = $oAttachment->DBInsert(); - + } - } - catch (FileUploadException $e) + } catch (FileUploadException $e) { // fail silently ?? - } + } // Fall though !! => browse - + case 'cke_browse': $oPage = new NiceWebPage(Dict::S('UI:BrowseInlineImages')); $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/magnific-popup.css'); $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.magnific-popup.min.js'); $sImgUrl = utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL; - + $sTempId = utils::ReadParam('temp_id'); $sClass = utils::ReadParam('obj_class', '', false, 'class'); $iObjectId = utils::ReadParam('obj_key', 0, false, 'integer'); $sCKEditorFuncNum = utils::ReadParam('CKEditorFuncNum', ''); - + $sPostUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?CKEditorFuncNum='.$sCKEditorFuncNum; - + $oPage->add_style( -<<add( -<<
$sUploadLegend @@ -2428,9 +2462,9 @@ EOF
EOF ); - + $oPage->add_script( -<<add_ready_script( -<<'); $('#upload_form').submit(); @@ -2475,14 +2509,14 @@ EOF $sOQL = "SELECT InlineImage WHERE ((temp_id = :temp_id) OR (item_class = :obj_class AND item_id = :obj_id))"; $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array('temp_id' => $sTempId, 'obj_class' => $sClass, 'obj_id' => $iObjectId)); $oPage->add("
$sAvailableImagesLegend"); - + if ($oSet->Count() == 0) { $oPage->add("

$sNoInlineImage

"); } else { - while($oAttachment = $oSet->Fetch()) + while ($oAttachment = $oSet->Fetch()) { $oDoc = $oAttachment->Get('contents'); if ($oDoc->GetMainMimeType() == 'image') @@ -2495,8 +2529,8 @@ EOF } } $oPage->add("
"); - break; - + break; + case 'custom_fields_update': $oPage->SetContentType('application/json'); $sAttCode = utils::ReadParam('attcode', ''); @@ -2518,21 +2552,28 @@ EOF $aRenderRes = $oRenderer->Render($aRequestedFields); $aResult['form']['updated_fields'] = $aRenderRes; - } - catch (Exception $e) + } catch (Exception $e) { $aResult['error'] = $e->getMessage(); } $oPage->add(json_encode($aResult)); break; + case 'dict': + $sSignature = Utils::ReadParam('s', ''); // Sanitization prevents / and .. + $oPage = new ajax_page(""); // New page to cleanup the no_cache done above + $oPage->SetContentType('text/javascript'); + $oPage->add_header('Cache-control: public, max-age=86400'); // Cache for 24 hours + $oPage->add_header("Pragma: cache"); // Reset the value set .... where ? + $oPage->add(file_get_contents(Utils::GetCachePath().$sSignature.'.js')); + break; + default: - $oPage->p("Invalid query."); + $oPage->p("Invalid query."); } $oPage->output(); -} -catch (Exception $e) +} catch (Exception $e) { // note: transform to cope with XSS attacks echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8'); diff --git a/pages/ajax.searchform.php b/pages/ajax.searchform.php new file mode 100644 index 000000000..faa25a183 --- /dev/null +++ b/pages/ajax.searchform.php @@ -0,0 +1,139 @@ + + * + */ + +use Combodo\iTop\Application\Search\AjaxSearchException; +use Combodo\iTop\Application\Search\CriterionParser; + +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); +require_once(APPROOT.'/application/startup.inc.php'); +require_once(APPROOT.'/application/user.preferences.class.inc.php'); +require_once(APPROOT.'/application/loginwebpage.class.inc.php'); +require_once(APPROOT.'/sources/application/search/ajaxsearchexception.class.inc.php'); +require_once(APPROOT.'/sources/application/search/criterionparser.class.inc.php'); +require_once(APPROOT.'/application/wizardhelper.class.inc.php'); + +try +{ + if (LoginWebPage::EXIT_CODE_OK != LoginWebPage::DoLoginEx(null /* any portal */, false, LoginWebPage::EXIT_RETURN)) + { + throw new SecurityException('You must be logged in'); + } + + $sParams = utils::ReadParam('params', '', false, 'raw_data'); + if (!$sParams) + { + throw new AjaxSearchException("Invalid query (empty filter)", 400); + } + + $oPage = new ajax_page(""); + $oPage->no_cache(); + $oPage->SetContentType('text/html'); + + $sListParams = utils::ReadParam('list_params', '{}', false, 'raw_data'); + $aListParams = (array)json_decode($sListParams, true); + + $aParams = json_decode($sParams, true); + if (array_key_exists('hidden_criteria', $aListParams)) + { + $sHiddenCriteria = $aListParams['hidden_criteria']; + } + else + { + $sHiddenCriteria = ''; + } + $oFilter = CriterionParser::Parse($aParams['base_oql'], $aParams['criterion'], $sHiddenCriteria); + + if (isset($aListParams['debug'])) + { + $sOQL = $oFilter->ToOQL(); + $oPage->add("
$sOQL
\n"); + } + + //IssueLog::Info('Search OQL: "'.$oFilter->ToOQL().'"'); + $oDisplayBlock = new DisplayBlock($oFilter, 'list', false); + + foreach($aListParams as $key => $value) + { + $aExtraParams[$key] = $value; + } + + if (array_key_exists('table_inner_id', $aListParams)) + { + $sListId = $aListParams['table_inner_id']; + } + + if (array_key_exists('json', $aListParams)) + { + $aJson = $aListParams['json']; + $sJson = json_encode($aJson); + $oWizardHelper = WizardHelper::FromJSON($sJson); + $oObj = $oWizardHelper->GetTargetObject(); + if (array_key_exists('query_params', $aExtraParams)) + { + $aExtraParams['query_params']['this'] = $oObj; + } + else + { + $aExtraParams['query_params'] = array('this' => $oObj); + } + +// // Current extkey value, so we can display event if it is not available anymore (eg. archived). +// $iCurrentExtKeyId = (is_null($oObj)) ? 0 : $oObj->Get($this->sAttCode); +// $aExtraParams['current_extkey_id'] = $iCurrentExtKeyId; + + } + + $aExtraParams['display_limit'] = true; + $aExtraParams['truncated'] = true; + if (isset($sListId)) + { + $oDisplayBlock->Display($oPage, $sListId, $aExtraParams); + } + else + { + $oDisplayBlock->RenderContent($oPage, $aExtraParams); + } + + + $oPage->output(); + +} catch (AjaxSearchException $e) +{ + http_response_code($e->getCode()); + // note: transform to cope with XSS attacks + echo '
' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '
'; + IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); +} catch (SecurityException $e) +{ + http_response_code(403); + // note: transform to cope with XSS attacks + echo '
' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '
'; + IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); +} catch (Exception $e) +{ + http_response_code(500); + // note: transform to cope with XSS attacks + echo '
' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '
'; + IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); +} \ No newline at end of file diff --git a/sources/application/search/ajaxsearchexception.class.inc.php b/sources/application/search/ajaxsearchexception.class.inc.php new file mode 100644 index 000000000..a3d259674 --- /dev/null +++ b/sources/application/search/ajaxsearchexception.class.inc.php @@ -0,0 +1,35 @@ + + * + */ + +/** + * Created by PhpStorm. + * User: Eric + * Date: 08/03/2018 + * Time: 11:18 + */ + +namespace Combodo\iTop\Application\Search; + + +class AjaxSearchException extends \Exception +{ + +} \ No newline at end of file diff --git a/sources/application/search/criterionconversion/criteriontooql.class.inc.php b/sources/application/search/criterionconversion/criteriontooql.class.inc.php new file mode 100644 index 000000000..140e2cf2c --- /dev/null +++ b/sources/application/search/criterionconversion/criteriontooql.class.inc.php @@ -0,0 +1,356 @@ + + * + */ + + +namespace Combodo\iTop\Application\Search\CriterionConversion; + + +use AttributeDate; +use AttributeDateTime; +use AttributeDefinition; +use AttributeEnum; +use Combodo\iTop\Application\Search\AjaxSearchException; +use Combodo\iTop\Application\Search\CriterionConversionAbstract; +use Combodo\iTop\Application\Search\SearchForm; + +class CriterionToOQL extends CriterionConversionAbstract +{ + + public static function Convert($aCriteria) + { + if (!empty($aCriteria['oql'])) + { + return $aCriteria['oql']; + } + + $aRef = explode('.', $aCriteria['ref']); + for($i = 0; $i < count($aRef); $i++) + { + $aRef[$i] = '`'.$aRef[$i].'`'; + } + $sRef = implode('.', $aRef); + + $sOperator = $aCriteria['operator']; + + $aMappedOperators = array( + self::OP_CONTAINS => 'ContainsToOql', + self::OP_STARTS_WITH => 'StartsWithToOql', + self::OP_ENDS_WITH => 'EndsWithToOql', + self::OP_EMPTY => 'EmptyToOql', + self::OP_NOT_EMPTY => 'NotEmptyToOql', + self::OP_BETWEEN_DATES => 'BetweenDatesToOql', + self::OP_BETWEEN => 'BetweenToOql', + self::OP_IN => 'InToOql', + self::OP_ALL => 'AllToOql', + ); + + if (array_key_exists($sOperator, $aMappedOperators)) + { + $sFct = $aMappedOperators[$sOperator]; + + return self::$sFct($sRef, $aCriteria); + } + + $sValue = self::GetValue(self::GetValues($aCriteria), 0); + + return "({$sRef} {$sOperator} '{$sValue}')"; + } + + private static function GetValues($aCriteria) + { + if (!array_key_exists('values', $aCriteria)) + { + return array(); + } + + return $aCriteria['values']; + } + + private static function GetValue($aValues, $iIndex) + { + if (!array_key_exists($iIndex, $aValues)) + { + return null; + } + if (!array_key_exists('value', $aValues[$iIndex])) + { + return null; + } + + return $aValues[$iIndex]['value']; + } + + protected static function ContainsToOql($sRef, $aCriteria) + { + $aValues = self::GetValues($aCriteria); + $sValue = self::GetValue($aValues, 0); + + return "({$sRef} LIKE '%{$sValue}%')"; + } + + protected static function StartsWithToOql($sRef, $aCriteria) + { + $aValues = self::GetValues($aCriteria); + $sValue = self::GetValue($aValues, 0); + + return "({$sRef} LIKE '{$sValue}%')"; + } + + protected static function EndsWithToOql($sRef, $aCriteria) + { + $aValues = self::GetValues($aCriteria); + $sValue = self::GetValue($aValues, 0); + + return "({$sRef} LIKE '%{$sValue}')"; + } + + protected static function EmptyToOql($sRef, $aCriteria) + { + if (isset($aCriteria['widget'])) + { + switch ($aCriteria['widget']) + { + case AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC: + case AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD: + return "ISNULL({$sRef})"; + } + } + + return "({$sRef} = '')"; + } + + protected static function NotEmptyToOql($sRef, $aCriteria) + { + return "({$sRef} != '')"; + } + + protected static function InToOql($sRef, $aCriteria) + { + $sAttCode = $aCriteria['code']; + $sClass = $aCriteria['class']; + $aValues = self::GetValues($aCriteria); + + if (count($aValues) == 0) + { + // Ignore when nothing is selected + return "1"; + } + + $bFilterOnUndefined = false; + try + { + $aAttributeDefs = \MetaModel::ListAttributeDefs($sClass); + if (array_key_exists($sAttCode, $aAttributeDefs)) + { + $oAttDef = $aAttributeDefs[$sAttCode]; + if ($oAttDef instanceof AttributeEnum) + { + $aAllowedValues = SearchForm::GetFieldAllowedValues($oAttDef); + if (array_key_exists('values', $aAllowedValues)) + { + // Can't invert the test if NULL is allowed + if (!$oAttDef->IsNullAllowed()) + { + $aAllowedValues = $aAllowedValues['values']; + if (count($aValues) == count($aAllowedValues)) + { + // All entries are selected + return "1"; + } + // more selected values than remaining so use NOT IN + else + { + if (count($aValues) > (count($aAllowedValues) / 2)) + { + foreach($aValues as $aValue) + { + unset($aAllowedValues[$aValue['value']]); + } + $sInList = implode("','", array_keys($aAllowedValues)); + + return "({$sRef} NOT IN ('$sInList'))"; + } + } + } + // search for "undefined" + for($i = 0; $i < count($aValues); $i++) + { + $aValue = $aValues[$i]; + if (isset($aValue['value']) && ($aValue['value'] === 'null')) + { + $bFilterOnUndefined = true; + unset($aValues[$i]); + break; + } + } + } + } + } + } catch (\CoreException $e) + { + } + + $aInValues = array(); + foreach($aValues as $aValue) + { + $aInValues[] = $aValue['value']; + } + $sInList = implode("','", $aInValues); + + if ($bFilterOnUndefined) + { + $sFilterOnUndefined = "ISNULL({$sRef})"; + if (count($aValues) === 0) + { + return $sFilterOnUndefined; + } + + if (count($aInValues) == 1) + { + // Add 'AND 1' to group the 'OR' inside an AND list for OQL parsing + return "((({$sRef} = '$sInList') OR {$sFilterOnUndefined}) AND 1)"; + } + + // Add 'AND 1' to group the 'OR' inside an AND list for OQL parsing + return "(({$sRef} IN ('$sInList') OR {$sFilterOnUndefined}) AND 1)"; + } + + if (count($aInValues) == 1) + { + return "({$sRef} = '$sInList')"; + } + + return "({$sRef} IN ('$sInList'))"; + } + + protected static function BetweenDatesToOql($sRef, $aCriteria) + { + $aOQL = array(); + + $aValues = self::GetValues($aCriteria); + if (count($aValues) != 2) + { + return "1"; + } + + $sWidget = $aCriteria['widget']; + if ($sWidget == AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME) + { + $sAttributeClass = AttributeDateTime::class; + } + else + { + $sAttributeClass = AttributeDate::class; + } + $oFormat = $sAttributeClass::GetFormat(); + + $sStartDate = $aValues[0]['value']; + if (!empty($sStartDate)) + { + $oDate = $oFormat->parse($sStartDate); + $sStartDate = $oDate->format($sAttributeClass::GetSQLFormat()); + $aOQL[] = "({$sRef} >= '$sStartDate')"; + } + + $sEndDate = $aValues[1]['value']; + if (!empty($sEndDate)) + { + $oDate = $oFormat->parse($sEndDate); + $sEndDate = $oDate->format($sAttributeClass::GetSQLFormat()); + $aOQL[] = "({$sRef} <= '$sEndDate')"; + } + + $sOQL = implode(' AND ', $aOQL); + + if (empty($sOQL)) + { + $sOQL = "1"; + } + + return $sOQL; + } + + /** + * @param $sRef + * @param $aCriteria + * + * @return string + * @throws \Combodo\iTop\Application\Search\AjaxSearchException + */ + protected static function BetweenToOql($sRef, $aCriteria) + { + $aOQL = array(); + + $aValues = self::GetValues($aCriteria); + if (count($aValues) != 2) + { + return "1"; + } + + if (isset($aValues[0]['value'])) + { + $sStartNum = trim($aValues[0]['value']); + if (is_numeric($sStartNum)) + { + $aOQL[] = "({$sRef} >= '$sStartNum')"; + } + else + { + if (!empty($sStartNum)) + { + throw new AjaxSearchException("'$sStartNum' is not a numeric value", 400); + } + } + } + + if (isset($aValues[1]['value'])) + { + $sEndNum = trim($aValues[1]['value']); + if (is_numeric($sEndNum)) + { + $aOQL[] = "({$sRef} <= '$sEndNum')"; + } + else + { + if (!empty($sEndNum)) + { + throw new AjaxSearchException("'$sEndNum' is not a numeric value", 400); + } + } + } + + $sOQL = implode(' AND ', $aOQL); + + if (empty($sOQL)) + { + $sOQL = "1"; + } + + return $sOQL; + } + + + protected static function AllToOql($sRef, $aCriteria) + { + return "1"; + } + +} \ No newline at end of file diff --git a/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php b/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php new file mode 100644 index 000000000..f85dcf9c3 --- /dev/null +++ b/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php @@ -0,0 +1,644 @@ + + * + */ + +/** + * Convert structures from OQL expressions into structure for the search form + */ +namespace Combodo\iTop\Application\Search\CriterionConversion; + + +use AttributeDate; +use AttributeDateTime; +use AttributeDefinition; +use Combodo\iTop\Application\Search\CriterionConversionAbstract; +use DateInterval; +use DateTime; +use Dict; +use Exception; +use MetaModel; + +class CriterionToSearchForm extends CriterionConversionAbstract +{ + + /** + * @param array $aAndCriterionRaw + * @param array $aFieldsByCategory + * + * @param array $aClasses all the classes of the filter + * + * @param bool $bIsRemovable + * + * @return array + */ + public static function Convert($aAndCriterionRaw, $aFieldsByCategory, $aClasses, $bIsRemovable = true) + { + $aAllFields = array(); + foreach($aFieldsByCategory as $aFields) + { + foreach($aFields as $aField) + { + $sAlias = $aField['class_alias']; + $sCode = $aField['code']; + $aAllFields["$sAlias.$sCode"] = $aField; + } + } + $aAndCriterion = array(); + $aMappingOperatorToFunction = array( + AttributeDefinition::SEARCH_WIDGET_TYPE_STRING => 'TextToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD => 'ExternalFieldToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE => 'DateTimeToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME => 'DateTimeToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC => 'NumericToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY => 'ExternalKeyToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY => 'ExternalKeyToSearchForm', + AttributeDefinition::SEARCH_WIDGET_TYPE_ENUM => 'EnumToSearchForm', + ); + + foreach($aAndCriterionRaw as $aCriteria) + { + if (isset($aCriteria['label'])) + { + $aCriteria['label'] = preg_replace("@\)$@", '', $aCriteria['label']); + $aCriteria['label'] = preg_replace("@^\(@", '', $aCriteria['label']); + } + $aCriteria['is_removable'] = $bIsRemovable; + + $sClass = ''; + if (isset($aCriteria['ref'])) + { + $aRef = explode('.', $aCriteria['ref']); + if (isset($aClasses[$aRef[0]])) + { + $sClass = $aClasses[$aRef[0]]; + $aCriteria['class'] = $sClass; + } + } + + // Check criteria validity + if (!isset($aCriteria['ref']) || !isset($aAllFields[$aCriteria['ref']])) + { + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + $aCriteria['label'] = Dict::S('UI:Search:Criteria:Raw:Filtered'); + if (isset($aCriteria['ref'])) + { + try + { + $aCriteria['label'] = Dict::Format('UI:Search:Criteria:Raw:FilteredOn', MetaModel::GetName($sClass)); + } + catch (Exception $e) + { + } + } + } + if (array_key_exists('widget', $aCriteria)) + { + if (array_key_exists($aCriteria['widget'], $aMappingOperatorToFunction)) + { + $sFct = $aMappingOperatorToFunction[$aCriteria['widget']]; + $aAndCriterion[] = self::$sFct($aCriteria, $aAllFields); + } + else + { + $aAndCriterion[] = $aCriteria; + } + } + } + + // Regroup criterion by variable name (no ref first) + usort($aAndCriterion, function ($a, $b) { + if (array_key_exists('ref', $a) || array_key_exists('ref', $b)) + { + if (array_key_exists('ref', $a) && array_key_exists('ref', $b)) + { + $iRefCmp = strcmp($a['ref'], $b['ref']); + if ($iRefCmp != 0) return $iRefCmp; + + return strcmp($a['operator'], $b['operator']); + } + if (array_key_exists('ref', $a)) + { + return 1; + } + + return -1; + } + if (array_key_exists('oql', $a) && array_key_exists('oql', $b)) + { + return strcmp($a['oql'], $b['oql']); + } + + return 0; + }); + + $aMergeFctByWidget = array( + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE => 'MergeDate', + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME => 'MergeDateTime', + AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC => 'MergeNumeric', + AttributeDefinition::SEARCH_WIDGET_TYPE_ENUM => 'MergeEnumExtKeys', + AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY => 'MergeEnumExtKeys', + ); + + $aPrevCriterion = null; + $aMergedCriterion = array(); + foreach($aAndCriterion as $aCurrCriterion) + { + if (!is_null($aPrevCriterion)) + { + if (array_key_exists('ref', $aPrevCriterion)) + { + // If previous has ref, the current has ref as the array is sorted with all without ref first + if (strcmp($aPrevCriterion['ref'], $aCurrCriterion['ref']) == 0) + { + // Same attribute, try to merge + if (array_key_exists('widget', $aCurrCriterion)) + { + if (array_key_exists($aCurrCriterion['widget'], $aMergeFctByWidget)) + { + $sFct = $aMergeFctByWidget[$aCurrCriterion['widget']]; + $aPrevCriterion = self::$sFct($aPrevCriterion, $aCurrCriterion, $aMergedCriterion); + continue; + } + } + } + } + $aMergedCriterion[] = $aPrevCriterion; + } + + $aPrevCriterion = $aCurrCriterion; + } + if (!is_null($aPrevCriterion)) + { + $aMergedCriterion[] = $aPrevCriterion; + } + + // Sort by label criterion by variable name (no ref first) + usort($aMergedCriterion, function ($a, $b) { + if (($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW) || + ($b['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW)) + { + if (($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW) && + ($b['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW)) + { + return strcmp($a['label'], $b['label']); + } + if ($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW) + { + return -1; + } + + return 1; + } + + return strcmp($a['label'], $b['label']); + }); + + return $aMergedCriterion; + } + + /** + * @param $aPrevCriterion + * @param $aCurrCriterion + * @param $aMergedCriterion + * + * @return Current criteria or null if merged + * @throws \Exception + */ + protected static function MergeDate($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $sPrevOperator = $aPrevCriterion['operator']; + $sCurrOperator = $aCurrCriterion['operator']; + if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>='))) + { + $aMergedCriterion[] = $aPrevCriterion; + + return $aCurrCriterion; + } + + // Merge into 'between' operation. + // The ends of the interval are included + $aCurrCriterion['operator'] = 'between_dates'; + $oFormat = AttributeDate::GetFormat(); + $sLastDate = $aPrevCriterion['values'][0]['value']; + $oDate = new DateTime($sLastDate); + if ($sPrevOperator == '<') + { + // previous day to include ends + $oDate->sub(DateInterval::createFromDateString('1 day')); + } + $sLastDateValue = $oDate->format(AttributeDate::GetSQLFormat()); + $sLastDateLabel = $oFormat->format($oDate); + + $sFirstDate = $aCurrCriterion['values'][0]['value']; + $oDate = new DateTime($sFirstDate); + if ($sCurrOperator == '>') + { + // next day to include ends + $oDate->add(DateInterval::createFromDateString('1 day')); + } + $sFirstDateValue = $oDate->format(AttributeDate::GetSQLFormat()); + $sFirstDateLabel = $oFormat->format($oDate); + + $aCurrCriterion['values'] = array(); + $aCurrCriterion['values'][] = array('value' => $sFirstDateValue, 'label' => $sFirstDateLabel); + $aCurrCriterion['values'][] = array('value' => $sLastDateValue, 'label' => $sLastDateLabel); + + $aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})"; + $aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND', 'AND').' '.$aCurrCriterion['label']; + + $aMergedCriterion[] = $aCurrCriterion; + + return null; + } + + /** + * @param $aPrevCriterion + * @param $aCurrCriterion + * @param $aMergedCriterion + * + * @return Current criteria or null if merged + * @throws \Exception + */ + protected static function MergeDateTime($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $sPrevOperator = $aPrevCriterion['operator']; + $sCurrOperator = $aCurrCriterion['operator']; + if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>='))) + { + $aMergedCriterion[] = $aPrevCriterion; + + return $aCurrCriterion; + } + + // Merge into 'between' operation. + // The ends of the interval are included + $sLastDate = $aPrevCriterion['values'][0]['value']; + $sFirstDate = $aCurrCriterion['values'][0]['value']; + $oDate = new DateTime($sLastDate); + $aCurrCriterion['operator'] = 'between_dates'; + $sInterval = '1 second'; + + if ($sPrevOperator == '<') + { + // previous day/second to include ends + $oDate->sub(DateInterval::createFromDateString($sInterval)); + } + $sLastDateValue = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sLastDateLabel = AttributeDateTime::GetFormat()->Format($sLastDateValue); + + $oDate = new DateTime($sFirstDate); + if ($sCurrOperator == '>') + { + // next day/second to include ends + $oDate->add(DateInterval::createFromDateString($sInterval)); + } + $sFirstDateValue = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sFirstDateLabel = AttributeDateTime::GetFormat()->Format($sFirstDateValue); + + $aCurrCriterion['values'] = array(); + $aCurrCriterion['values'][] = array('value' => $sFirstDateValue, 'label' => $sFirstDateLabel); + $aCurrCriterion['values'][] = array('value' => $sLastDateValue, 'label' => $sLastDateLabel); + + $aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})"; + $aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND', + 'AND').' '.$aCurrCriterion['label']; + + $aMergedCriterion[] = $aCurrCriterion; + + return null; + } + + /** + * @param $aPrevCriterion + * @param $aCurrCriterion + * @param $aMergedCriterion + * + * @return Current criteria or null if merged + * @throws \Exception + */ + protected static function MergeNumeric($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $sPrevOperator = $aPrevCriterion['operator']; + $sCurrOperator = $aCurrCriterion['operator']; + if (($sPrevOperator != '<=') || ($sCurrOperator != '>=')) + { + $aMergedCriterion[] = $aPrevCriterion; + + return $aCurrCriterion; + } + + // Merge into 'between' operation. + $sLastNum = $aPrevCriterion['values'][0]['value']; + $sFirstNum = $aCurrCriterion['values'][0]['value']; + $aCurrCriterion['values'] = array(); + $aCurrCriterion['values'][] = array('value' => $sFirstNum, 'label' => "$sFirstNum"); + $aCurrCriterion['values'][] = array('value' => $sLastNum, 'label' => "$sLastNum"); + + $aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})"; + $aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND', 'AND').' '.$aCurrCriterion['label']; + $aCurrCriterion['operator'] = 'between'; + + $aMergedCriterion[] = $aCurrCriterion; + + return null; + } + + private static function SerializeValues($aValues) + { + $aSerializedValues = array(); + foreach($aValues as $aValue) + { + $aSerializedValues[] = serialize($aValue); + } + + return $aSerializedValues; + } + + protected static function MergeEnumExtKeys($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $aFirstValues = self::SerializeValues($aPrevCriterion['values']); + $aNextValues = self::SerializeValues($aCurrCriterion['values']); + + // Keep only the common values + $aCurrCriterion['values'] = array_map("unserialize", array_intersect($aFirstValues, $aNextValues)); + + $aMergedCriterion[] = $aCurrCriterion; + return null; + } + + protected static function TextToSearchForm($aCriteria, $aFields) + { + $sOperator = $aCriteria['operator']; + $sValue = $aCriteria['values'][0]['value']; + + $bStartWithPercent = substr($sValue, 0, 1) == '%' ? true : false; + $bEndWithPercent = substr($sValue, -1) == '%' ? true : false; + + switch (true) + { + case ('' == $sValue and ($sOperator == '=' or $sOperator == 'LIKE')): + $aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY; + break; + case ('' == $sValue and $sOperator == '!='): + $aCriteria['operator'] = CriterionConversionAbstract::OP_NOT_EMPTY; + break; + case ($sOperator == 'LIKE' && $bStartWithPercent && $bEndWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_CONTAINS; + $sValue = substr($sValue, 1, -1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + case ($sOperator == 'LIKE' && $bStartWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_ENDS_WITH; + $sValue = substr($sValue, 1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + case ($sOperator == 'LIKE' && $bEndWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_STARTS_WITH; + $sValue = substr($sValue, 0, -1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + } + + return $aCriteria; + } + + protected static function ExternalFieldToSearchForm($aCriteria, $aFields) + { + $sOperator = $aCriteria['operator']; + $sValue = $aCriteria['values'][0]['value']; + + $bStartWithPercent = substr($sValue, 0, 1) == '%' ? true : false; + $bEndWithPercent = substr($sValue, -1) == '%' ? true : false; + + switch (true) + { + case ($sOperator == 'ISNULL'): + case ('' == $sValue and ($sOperator == 'LIKE')): + $aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY; + break; + case ('' == $sValue and $sOperator == '!='): + $aCriteria['operator'] = CriterionConversionAbstract::OP_NOT_EMPTY; + break; + case ($sOperator == 'LIKE' && $bStartWithPercent && $bEndWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_CONTAINS; + $sValue = substr($sValue, 1, -1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + case ($sOperator == 'LIKE' && $bStartWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_ENDS_WITH; + $sValue = substr($sValue, 1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + case ($sOperator == 'LIKE' && $bEndWithPercent): + $aCriteria['operator'] = CriterionConversionAbstract::OP_STARTS_WITH; + $sValue = substr($sValue, 0, -1); + $aCriteria['values'][0]['value'] = $sValue; + $aCriteria['values'][0]['label'] = "$sValue"; + break; + } + + return $aCriteria; + } + + protected static function DateTimeToSearchForm($aCriteria, $aFields) + { + if (!array_key_exists('is_relative', $aCriteria) || !$aCriteria['is_relative']) + { + // Convert '=' in 'between' + if (isset($aCriteria['operator']) && ($aCriteria['operator'] === '=')) + { + $aCriteria['operator'] = CriterionConversionAbstract::OP_BETWEEN_DATES; + $sWidget = $aCriteria['widget']; + if ($sWidget == AttributeDefinition::SEARCH_WIDGET_TYPE_DATE) + { + $aCriteria['values'][1] = $aCriteria['values'][0]; + } + else + { + $sDate = $aCriteria['values'][0]['value']; + $oDate = new DateTime($sDate); + + $sFirstDateValue = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sFirstDateLabel = AttributeDateTime::GetFormat()->Format($sFirstDateValue); + $aCriteria['values'][0] = array('value' => $sFirstDateValue, 'label' => "$sFirstDateLabel"); + + + $oDate->add(DateInterval::createFromDateString('1 day')); + $oDate->sub(DateInterval::createFromDateString('1 second')); + + $sLastDateValue = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sLastDateLabel = AttributeDateTime::GetFormat()->Format($sLastDateValue); + $aCriteria['values'][1] = array('value' => $sLastDateValue, 'label' => "$sLastDateLabel"); + } + } + + return $aCriteria; + } + + if (isset($aCriteria['values'][0]['value'])) + { + $sLabel = $aCriteria['values'][0]['value']; + if (isset($aCriteria['verb'])) + { + switch ($aCriteria['verb']) + { + case 'DATE_SUB': + $sLabel = '-'.$sLabel; + break; + case 'DATE_ADD': + $sLabel = '+'.$sLabel; + break; + } + } + if (isset($aCriteria['unit'])) + { + $sLabel .= Dict::S('Expression:Unit:Short:'.$aCriteria['unit'], $aCriteria['unit']); + } + $aCriteria['values'][0]['label'] = "$sLabel"; + } + + // Temporary until the JS widget support relative dates + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + + return $aCriteria; + } + + protected static function NumericToSearchForm($aCriteria, $aFields) + { + if ($aCriteria['operator'] == 'ISNULL') + { + $aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY; + } + + return $aCriteria; + } + + protected static function EnumToSearchForm($aCriteria, $aFields) + { + $sOperator = $aCriteria['operator']; + switch ($sOperator) + { + case '=': + // Same as IN + $aCriteria['operator'] = CriterionConversionAbstract::OP_IN; + break; + case 'NOT IN': + case 'NOTIN': + case '!=': + // Same as NOT IN + $aCriteria = self::RevertValues($aCriteria, $aFields); + break; + case 'IN': + // Nothing special to do + break; + case 'OR': + case 'ISNULL': + // Special case when undefined and/or other values are selected + $aCriteria['operator'] = CriterionConversionAbstract::OP_IN; + if (isset($aCriteria['has_undefined']) && $aCriteria['has_undefined']) + { + if (!isset($aCriteria['values'])) + { + $aCriteria['values'] = array(); + } + // Convention for 'undefined' enums + $aCriteria['values'][] = array('value' => 'null', 'label' => 'null'); + } + break; + default: + // Unknown operator + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + break; + } + + return $aCriteria; + } + + protected static function ExternalKeyToSearchForm($aCriteria, $aFields) + { + $sOperator = $aCriteria['operator']; + switch ($sOperator) + { + case '=': + // Same as IN + $aCriteria['operator'] = CriterionConversionAbstract::OP_IN; + break; + case 'IN': + // Nothing special to do + break; + default: + // Unknown operator + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + break; + } + + return $aCriteria; + } + + /** + * @param $aCriteria + * @param $aFields + * + * @return mixed + */ + protected static function RevertValues($aCriteria, $aFields) + { + $sRef = $aCriteria['ref']; + $aValues = $aCriteria['values']; + if (array_key_exists($sRef, $aFields)) + { + $aField = $aFields[$sRef]; + if (array_key_exists('allowed_values', $aField) && array_key_exists('values', $aField['allowed_values'])) + { + $aAllowedValues = $aField['allowed_values']['values']; + } + else + { + // Can't obtain the list of allowed values, just set as unknown + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + } + } + + if (isset($aAllowedValues)) + { + foreach($aValues as $aValue) + { + $sValue = $aValue['value']; + unset($aAllowedValues[$sValue]); + } + $aCriteria['values'] = array(); + + foreach($aAllowedValues as $sValue => $sLabel) + { + $aValue = array('value' => $sValue, 'label' => "$sLabel"); + $aCriteria['values'][] = $aValue; + } + $aCriteria['operator'] = 'IN'; + } + + return $aCriteria; + } + +} \ No newline at end of file diff --git a/sources/application/search/criterionconversionabstract.class.inc.php b/sources/application/search/criterionconversionabstract.class.inc.php new file mode 100644 index 000000000..041d840e8 --- /dev/null +++ b/sources/application/search/criterionconversionabstract.class.inc.php @@ -0,0 +1,40 @@ + + * + */ + + +namespace Combodo\iTop\Application\Search; + + +abstract class CriterionConversionAbstract +{ + + const OP_CONTAINS = 'contains'; + const OP_STARTS_WITH = 'starts_with'; + const OP_ENDS_WITH = 'ends_with'; + const OP_EMPTY = 'empty'; + const OP_NOT_EMPTY = 'not_empty'; + const OP_IN = 'IN'; + const OP_BETWEEN_DATES = 'between_dates'; + const OP_BETWEEN = 'between'; + const OP_ALL = 'all'; + +} + diff --git a/sources/application/search/criterionparser.class.inc.php b/sources/application/search/criterionparser.class.inc.php new file mode 100644 index 000000000..903bb7b71 --- /dev/null +++ b/sources/application/search/criterionparser.class.inc.php @@ -0,0 +1,103 @@ + + * + */ + +/** + * Created by PhpStorm. + * User: Eric + * Date: 08/03/2018 + * Time: 11:25 + */ + +namespace Combodo\iTop\Application\Search; + + +use Combodo\iTop\Application\Search\CriterionConversion\CriterionToOQL; +use DBObjectSearch; +use Expression; +use IssueLog; +use OQLException; + +class CriterionParser +{ + + /** + * @param $sBaseOql + * @param $aCriterion + * @param $sHiddenCriteria + * + * @return \DBSearch + */ + public static function Parse($sBaseOql, $aCriterion, $sHiddenCriteria = null) + { + $aExpression = array(); + $aOr = $aCriterion['or']; + foreach($aOr as $aAndList) + { + + $sExpression = self::ParseAndList($aAndList['and']); + if (!empty($sExpression)) + { + $aExpression[] = $sExpression; + } + } + + try + { + $oSearch = DBObjectSearch::FromOQL($sBaseOql); + + if (!empty($sHiddenCriteria)) + { + $oHiddenCriteriaExpression = Expression::FromOQL($sHiddenCriteria); + $oSearch->AddConditionExpression($oHiddenCriteriaExpression); + } + + if (empty($aExpression)) + { + return $oSearch; + } + + $oExpression = Expression::FromOQL(implode(" OR ", $aExpression)); + $oSearch->AddConditionExpression($oExpression); + + return $oSearch; + } catch (OQLException $e) + { + IssueLog::Error($e->getMessage()); + } + return null; + } + + private static function ParseAndList($aAnd) + { + $aExpression = array(); + foreach($aAnd as $aCriteria) + { + $aExpression[] = CriterionToOQL::Convert($aCriteria); + } + + if (empty($aExpression)) + { + return ''; + } + + return '('.implode(" AND ", $aExpression).')'; + } +} \ No newline at end of file diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php new file mode 100644 index 000000000..f3a67c541 --- /dev/null +++ b/sources/application/search/searchform.class.inc.php @@ -0,0 +1,483 @@ + + * + */ + + +namespace Combodo\iTop\Application\Search; + + +use ApplicationContext; +use AttributeDefinition; +use CMDBObjectSet; +use Combodo\iTop\Application\Search\CriterionConversion\CriterionToSearchForm; +use CoreException; +use DBObjectSearch; +use DBObjectSet; +use Dict; +use Exception; +use Expression; +use IssueLog; +use MetaModel; +use TrueExpression; +use utils; +use WebPage; + +class SearchForm +{ + + /** + * @param \WebPage $oPage + * @param \CMDBObjectSet $oSet + * @param array $aExtraParams + * + * @return string + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function GetSearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array()) + { + $sHtml = ''; + $oAppContext = new ApplicationContext(); + $sClassName = $oSet->GetFilter()->GetClass(); + $aListParams = array(); + + foreach($aExtraParams as $key => $value) + { + $aListParams[$key] = $value; + } + + // Simple search form + if (isset($aExtraParams['currentId'])) + { + $sSearchFormId = $aExtraParams['currentId']; + } + else + { + $iSearchFormId = $oPage->GetUniqueId(); + $sSearchFormId = 'SimpleSearchForm'.$iSearchFormId; + $sHtml .= "
\n"; + $aListParams['currentId'] = "$iSearchFormId"; + } + // Check if the current class has some sub-classes + if (isset($aExtraParams['baseClass'])) + { + $sRootClass = $aExtraParams['baseClass']; + } + else + { + $sRootClass = $sClassName; + } + + $sJson = utils::ReadParam('json', '', false, 'raw_data'); + if (!empty($sJson)) + { + $aListParams['json'] = json_decode($sJson, true); + } + + if (!isset($aExtraParams['result_list_outer_selector'])) + { + if (isset($aExtraParams['table_id'])) + { + $aExtraParams['result_list_outer_selector'] = $aExtraParams['table_id']; + } + else + { + $aExtraParams['result_list_outer_selector'] = "search_form_result_{$sSearchFormId}"; + } + } + + + $aSubClasses = MetaModel::GetSubclasses($sRootClass); + if (count($aSubClasses) > 0) + { + $aOptions = array(); + $aOptions[MetaModel::GetName($sRootClass)] = "