Compare commits

...

52 Commits

Author SHA1 Message Date
Pierre Goiffon
a42ef8eb90 (Retrofit from trunk) N°1328 Fix CSV import : check if user has rights on imported class (r5597)
SVN:2.2.0[5601]
2018-04-04 07:30:44 +00:00
Romain Quetiez
e3e556e7da (Retrofit from trunk) #1325 Could not declare an ext key to a subclass (view could not be created). This commit is minimalistic and aims at being retrofitted into the various branches of iTop. It will be followed by a second commit, which aims at completing the fix by aligning the internal data structures of iTop... and possibly fix an issue (?)
SVN:2.2.0[4401]
2016-09-14 15:55:50 +00:00
Denis Flaven
960f63d1d0 Enhancement:
- Add more debug traces (if 'synchro_trace' == 'save')
- Show debug traces (if any) at the bottom of the status page
- Protect against time differences between the MySQL server and the PHP server, when running 'synchro_import.php'

SVN:2.2.0[4393]
2016-09-12 12:39:59 +00:00
Denis Flaven
f01e5a5468 (retrofit from trunk) Security: prevent grouping on password fields since it may lead to disclosure of the encrypted version of the password.
SVN:2.2.0[4245]
2016-06-22 13:58:52 +00:00
Denis Flaven
0f0296de53 Properly sanitize the "switch_env" parameter and take it into account only if it contains a valid value.
SVN:2.2.0[4239]
2016-06-22 12:10:58 +00:00
Romain Quetiez
e19f988ff4 Fixed a regression introduced in 2.2.0, and fixed in 2.3.0 (as of [3815], which introduces a new API)
SVN:2.2.0[4185]
2016-06-06 11:49:52 +00:00
Denis Flaven
8f92a38200 Fix: export of null dates (& times) in "spreadsheet" format should not give the current date !
SVN:2.2.0[4167]
2016-06-01 16:28:22 +00:00
Denis Flaven
f84bf4de68 Fix: cannot export an object with a property named "length" !!
SVN:2.2.0[4162]
2016-05-30 09:27:29 +00:00
Romain Quetiez
20fbf151ff #1209 Setup or Backup failing with french error message 'Effacement du fichier ...' Regression introduced in [r3868]. Occurs when a backup fails and prevents users from seeing the mysql error report.
SVN:2.2.0[4015]
2016-04-28 11:21:46 +00:00
Romain Quetiez
9a190e69f6 #1226 Case logs history gets truncated if PHP < 5.4.9 and mbstring is present (only on branch 2.2.0)
SVN:2.2.0[3973]
2016-03-30 15:14:52 +00:00
Romain Quetiez
ae7daa6e56 ResetStopWatch could not be used as a lifecycle action: the symptom is "The action has failed".
SVN:2.2.0[3968]
2016-03-25 10:04:20 +00:00
Denis Flaven
f1456ef058 (retrofit from trunk) Initial feedback while loading the 'list' tab of the impact analysis, useful when this tab is displayed first.
SVN:2.2.0[3950]
2016-03-15 09:41:35 +00:00
Denis Flaven
ab809ce209 (retrofit from trunk) Modified the "List" tab of the Impact Analysis to display only the actually impacted objects. The content of this tab is now refreshed every time the graph is rebuilt to take into account the "context" changes which causes the actual impact to change, or the filtering.
SVN:2.2.0[3942]
2016-03-09 18:07:54 +00:00
Denis Flaven
7f7ff19233 (retrofit from trunk) Optimization/bug (!): Never use the whole object as a placeholder in ApplyParams !!
SVN:2.2.0[3932]
2016-02-29 16:22:23 +00:00
Denis Flaven
1ee8044690 (retrofit from trunk) Optimization: do not load all columns when checking if a CI is part of the "context" of a given ticket.
SVN:2.2.0[3930]
2016-02-29 15:49:19 +00:00
Denis Flaven
61f408514a Use one-way encryption for storing the token used for the "Forgotten password" feature.
SVN:2.2.0[3921]
2016-02-19 18:19:13 +00:00
Denis Flaven
c0537f62ca (retrofit from trunk) #1202: Fix for a security vulnerability in the Configuration Editor.
SVN:2.2.0[3904]
2016-02-11 10:27:52 +00:00
Romain Quetiez
1ca3d46889 Preparing release 2.2.1
SVN:2.2.0[3899]
2016-02-03 16:12:30 +00:00
Romain Quetiez
cc74af4343 #1196 Only administrators can add attachments by the mean of the REST/JSON API -retrofit from trunk
SVN:2.2.0[3897]
2016-02-03 13:05:15 +00:00
Denis Flaven
8041f1a8f3 #1193: When creating new object from tab with <edit_mode>add_only</edit_mode> id of the parent object was not transfered to the form. Fix provided by Vladimir Kunin. Thank you Vladimir.
SVN:2.2.0[3895]
2016-02-02 13:34:27 +00:00
Denis Flaven
c32290e1fc internal: new autoOpen flag.
SVN:2.2.0[3888]
2016-01-27 13:22:05 +00:00
Denis Flaven
6449891f07 #1174: support HTML fields in the bulk modify forms (capability to enable/disable the field live)
SVN:2.2.0[3884]
2016-01-26 14:34:42 +00:00
Denis Flaven
55b166c076 #1183: more refactoring and some robustness enhancements after tests on big datasets.
SVN:2.2.0[3882]
2016-01-26 13:25:07 +00:00
Denis Flaven
c5793ab3b2 #1153: preserve leading zeroes (in "numeric" fields) in the Excel export.
SVN:2.2.0[3880]
2016-01-26 09:53:50 +00:00
Denis Flaven
2b315801f1 #1183: grouping threshold is now taken int account for "Depends on..." graphs (i.e. grouping backwards)
SVN:2.2.0[3876]
2016-01-25 14:34:23 +00:00
Denis Flaven
d5ae7722b7 #1176: empty placeholders are represented by an empty string as in previous version.
SVN:2.2.0[3874]
2016-01-25 13:00:41 +00:00
Denis Flaven
f37b9a47d1 IconSelectorField (Design time !) can be read-only.
SVN:2.2.0[3872]
2016-01-21 16:05:56 +00:00
Romain Quetiez
49c5fda280 #1165 backup with errors fills up tmp-directories with lots of backup-files
SVN:2.2.0[3869]
2016-01-21 15:07:58 +00:00
Denis Flaven
4097a7c74d #1150: Spurious message "A restore is running..." - FIXED !
SVN:2.2.0[3865]
2016-01-20 15:59:39 +00:00
Denis Flaven
4808ef74f3 (retrofit) Properly read radio button values inside a form.
SVN:2.2.0[3851]
2015-12-14 13:14:21 +00:00
Denis Flaven
beaaa163bb (retrofit) (internal) Remove _altered_in when exporting the delta.
SVN:2.2.0[3850]
2015-12-14 13:11:58 +00:00
Denis Flaven
81b7fc25b3 (retrofit) Keep track of which module altered which node in the XML.
SVN:2.2.0[3846]
2015-12-09 14:58:59 +00:00
Denis Flaven
f7772570cd (retrofit from trunk) Fixed the computation of the lowest common ancestor.
SVN:2.2.0[3841]
2015-12-02 10:40:29 +00:00
Denis Flaven
ef9deb89c3 (retrofit from trunk) Internal: dehardcoded OqlUnionQuery::GetClass against the metamodel reflection API
SVN:2.2.0[3840]
2015-12-02 10:39:35 +00:00
Denis Flaven
5cdf1765a4 (retrofit from trunk) Added AttributeDef::EnumTemplateVerbs, to generate the documentation about the available attribute formatting placeholders
SVN:2.2.0[3838]
2015-12-02 10:38:18 +00:00
Denis Flaven
00b3b9e1cc (retrofit from trunk) Internal - MFFactory: fixed GetDelta when there is no change at all
SVN:2.2.0[3834]
2015-11-30 14:59:24 +00:00
Denis Flaven
ff3eafc1d2 Added structured error reporting in case of missing dependencies for the modules to install.
SVN:2.2.0[3832]
2015-11-25 16:57:59 +00:00
Denis Flaven
c3b3ee85a2 Properly create DOMNodes with a text content (beware of XML entities inside the text)
SVN:2.2.0[3830]
2015-11-25 16:54:22 +00:00
Denis Flaven
424f08d650 Support validation patterns containing a forward slash
SVN:2.2.0[3828]
2015-11-25 16:50:57 +00:00
Denis Flaven
97809190de Support of derived classes in "add_remove" edition mode for AttributeLinkSet fields (the search form was not refreshing / loading properly when toggling the class to search for).
SVN:2.2.0[3823]
2015-11-20 14:21:42 +00:00
Denis Flaven
e33930b10c Make ReloadSearchForm work properly when the "submit" event handler is declared either with or without a "namespace" portion (e.g. 'submit.itop' vs 'submit')
SVN:2.2.0[3817]
2015-11-09 10:48:01 +00:00
Denis Flaven
41ca454c5d #1164: typo in German localization.
SVN:2.2.0[3812]
2015-10-26 10:59:35 +00:00
Denis Flaven
d17abfafe3 Support the download of "bigger-than-memory" backup files.
SVN:2.2.0[3810]
2015-10-26 10:41:00 +00:00
Romain Quetiez
8025dcbc37 Fixed regressions due to the recent code refactoring [3803]
SVN:2.2.0[3808]
2015-10-12 15:40:44 +00:00
Denis Flaven
cbe8745f3a Do NOT localize finalclass values in REST/JSON.
SVN:2.2.0[3806]
2015-10-12 12:40:58 +00:00
Denis Flaven
87206225d2 Code refactoring to make the OQL parsing self contained in the "oql" subdirectory.
SVN:2.2.0[3804]
2015-10-12 10:05:38 +00:00
Denis Flaven
96b10baeee Added a version number (arbitrary initialized to 2015-08-31 for iTop v2.2.0) to the OQL Lexer/parser.
SVN:2.2.0[3802]
2015-10-12 09:55:52 +00:00
Romain Quetiez
a98e1c838e #1159 Cannot add edge (impact analysis not working depending on the data model customizations)
SVN:2.2.0[3800]
2015-10-09 15:33:17 +00:00
Denis Flaven
e8279ca731 Do not rely on MetaModel::GetRootClass() to check the data model, use the abstraction of ModelReflection instead to keep the code portable.
SVN:2.2.0[3798]
2015-10-08 15:57:21 +00:00
Denis Flaven
2976e919f8 (retrofit from trunk) #1156: properly escape file paths containing spaces
SVN:2.2.0[3796]
2015-10-05 19:54:16 +00:00
Denis Flaven
9c7671e894 Better error reporting when the setup fails to create a directory.
SVN:2.2.0[3794]
2015-09-30 14:09:57 +00:00
Romain Quetiez
6c1e658fe4 Releasing 2.2.0
SVN:2.2.0[3791]
2015-09-23 08:40:56 +00:00
56 changed files with 2519 additions and 2026 deletions

View File

@@ -733,7 +733,8 @@ abstract class DashletGroupBy extends Dashlet
if (is_subclass_of($sAttType, 'AttributeFriendlyName')) continue;
if ($sAttType == 'AttributeExternalField') continue;
if (is_subclass_of($sAttType, 'AttributeExternalField')) continue;
if ($sAttType == 'AttributeOneWayPassword') continue;
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
$aGroupBy[$sAttCode] = $sLabel;

View File

@@ -394,7 +394,7 @@ class DisplayBlock
{
if (isset($aExtraParams['group_by_label']))
{
$oGroupByExp = Expression::FromOQL($aExtraParams['group_by']);
$oGroupByExp = Expression::FromOQL($aExtraParams['group_by']);
$sGroupByLabel = $aExtraParams['group_by_label'];
}
else
@@ -405,6 +405,21 @@ class DisplayBlock
$sGroupByLabel = MetaModel::GetLabel($this->m_oFilter->GetClass(), $aExtraParams['group_by']);
}
// Security filtering
$aFields = $oGroupByExp->ListRequiredFields();
foreach($aFields as $sFieldAlias)
{
if (preg_match('/^([^.]+)\\.([^.]+)$/', $sFieldAlias, $aMatches))
{
$sFieldClass = $this->m_oFilter->GetClassName($aMatches[1]);
$oAttDef = MetaModel::GetAttributeDef($sFieldClass, $aMatches[2]);
if ($oAttDef instanceof AttributeOneWayPassword)
{
throw new Exception('Grouping on password fields is not supported.');
}
}
}
$aGroupBy = array();
$aGroupBy['grouped_by_1'] = $oGroupByExp;
$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy, true);

View File

@@ -339,7 +339,7 @@ EOF
return '</tr>';
}
public function RenderAsDialog($oPage, $sDialogId, $sDialogTitle, $iDialogWidth, $sOkButtonLabel, $sIntroduction = null)
public function RenderAsDialog($oPage, $sDialogId, $sDialogTitle, $iDialogWidth, $sOkButtonLabel, $sIntroduction = null, $bAutoOpen = true)
{
$this->SetPrefix('dlg_'); // To make sure that the controls have different IDs that the property sheet which may be displayed at the same time
@@ -355,12 +355,14 @@ EOF
$this->Render($oPage);
$oPage->add('</div>');
$sAutoOpen = $bAutoOpen ? 'true' : 'false';
$oPage->add_ready_script(
<<<EOF
$('#$sDialogId').dialog({
height: 'auto',
width: $iDialogWidth,
modal: true,
autoOpen: $sAutoOpen,
title: '$sDialogTitle',
buttons: [
{ text: "$sOkButtonLabel", click: function() {
@@ -937,8 +939,8 @@ EOF
public function ReadParam(&$aValues)
{
parent::ReadParam($aValues);
if (($this->sValidationPattern != '') &&(!preg_match('/'.$this->sValidationPattern.'/', $aValues[$this->sCode])) )
$sPattern = '/'.str_replace('/', '\/', $this->sValidationPattern).'/'; // Escape the forward slashes since they are used as delimiters for preg_match
if (($this->sValidationPattern != '') && (!preg_match($sPattern, $aValues[$this->sCode])) )
{
$aValues[$this->sCode] = $this->defaultValue;
}
@@ -1336,7 +1338,7 @@ EOF
}
else
{
$sValue = '<img src="'.$this->MakeFileUrl($this->defaultValue).'" />';
$sValue = '<span style="display:inline-block;line-height:48px;height:48px;"><span><img style="vertical-align:middle" src="'.$this->aAllowedValues[$idx]['icon'].'" />&nbsp;'.htmlentities($this->aAllowedValues[$idx]['label'], ENT_QUOTES, 'UTF-8').'</span></span>';
}
$sReadOnly = $this->IsReadOnly() ? 'disabled' : '';
return array('label' => $this->sLabel, 'value' => $sValue);

View File

@@ -306,16 +306,20 @@ class LoginWebPage extends NiceWebPage
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
}
elseif ($oUser->Get('reset_pwd_token') != $sToken)
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."</p>\n");
$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
$this->add_script(
$oEncryptedToken = $oUser->Get('reset_pwd_token');
if (!$oEncryptedToken->CheckPassword($sToken))
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."</p>\n");
$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
$this->add_script(
<<<EOF
function DoCheckPwd()
{
@@ -327,18 +331,19 @@ function DoCheckPwd()
return true;
}
EOF
);
$this->add("<form method=\"post\">\n");
$this->add("<table>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"new_pwd\">".Dict::S('UI:Login:NewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"new_pwd\" name=\"new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"retype_new_pwd\">".Dict::S('UI:Login:RetypeNewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"retype_new_pwd\" name=\"retype_new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></span></td></tr>\n");
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"do_reset_pwd\" />\n");
$this->add("<input type=\"hidden\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("<input type=\"hidden\" name=\"token\" value=\"".htmlentities($sToken, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("</form>\n");
$this->add("</div\n");
);
$this->add("<form method=\"post\">\n");
$this->add("<table>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"new_pwd\">".Dict::S('UI:Login:NewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"new_pwd\" name=\"new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"retype_new_pwd\">".Dict::S('UI:Login:RetypeNewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"retype_new_pwd\" name=\"retype_new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></span></td></tr>\n");
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"do_reset_pwd\" />\n");
$this->add("<input type=\"hidden\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("<input type=\"hidden\" name=\"token\" value=\"".htmlentities($sToken, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("</form>\n");
$this->add("</div\n");
}
}
}
@@ -358,21 +363,25 @@ EOF
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
}
elseif ($oUser->Get('reset_pwd_token') != $sToken)
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
// Trash the token and change the password
$oUser->Set('reset_pwd_token', '');
$oUser->SetPassword($sNewPwd); // Does record the change into the DB
$this->add("<p>".Dict::S('UI:ResetPwd-Ready')."</p>");
$sUrl = utils::GetAbsoluteUrlAppRoot();
$this->add("<p><a href=\"$sUrl\">".Dict::S('UI:ResetPwd-Login')."</a></p>");
$oEncryptedPassword = $oUser->Get('reset_pwd_token');
if (!$oEncryptedPassword->CheckPassword($sToken))
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
// Trash the token and change the password
$oUser->Set('reset_pwd_token', '');
$oUser->SetPassword($sNewPwd); // Does record the change into the DB
$this->add("<p>".Dict::S('UI:ResetPwd-Ready')."</p>");
$sUrl = utils::GetAbsoluteUrlAppRoot();
$this->add("<p><a href=\"$sUrl\">".Dict::S('UI:ResetPwd-Login')."</a></p>");
}
$this->add("</div\n");
}
$this->add("</div\n");
}
public function DisplayChangePwdForm($bFailedLogin = false)

View File

@@ -28,10 +28,11 @@ require_once(APPROOT.'/core/cmdbobject.class.inc.php');
require_once(APPROOT.'/application/utils.inc.php');
session_name('itop-'.md5(APPROOT));
session_start();
if (isset($_REQUEST['switch_env']))
$sSwitchEnv = utils::ReadParam('switch_env', null);
if (($sSwitchEnv != null) && (file_exists(APPCONF.$sSwitchEnv.'/'.ITOP_CONFIG_FILE)))
{
$sEnv = $_REQUEST['switch_env'];
$_SESSION['itop_env'] = $sEnv;
$_SESSION['itop_env'] = $sSwitchEnv;
$sEnv = $sSwitchEnv;
// TODO: reset the credentials as well ??
}
else if (isset($_SESSION['itop_env']))

View File

@@ -99,7 +99,7 @@ class UIHTMLEditorWidget
// Could also be bound to 'instanceReady.ckeditor'
$oPage->add_ready_script("$('#$iId').bind('validate', function(evt, sFormId) { return ValidateCKEditField('$iId', '', {$this->m_sMandatory}, sFormId, '') } );\n");
$oPage->add_ready_script("$('#$iId').bind('update', function() { BlockField('cke_$iId', $('#$iId').attr('disabled')); } );\n");
$oPage->add_ready_script("$('#$iId').bind('update', function() { BlockField('cke_$iId', $('#$iId').attr('disabled')); $(this).data('ckeditorInstance').setReadOnly($(this).prop('disabled')); } );\n");
return $sHtmlValue;
}

View File

@@ -89,7 +89,7 @@ class UILinksWidgetDirect
$sDefault = "default[$sExtKeyToMe]=".$oCurrentObj->GetKey();
$oAppContext = new ApplicationContext();
$sParams = $oAppContext->GetForLink();
$oPage->p("<a target=\"_blank\" href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=new&class=$sTargetClass&$sParams{$sDefault}\">".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sTargetClass))."</a>\n");
$oPage->p("<a target=\"_blank\" href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=new&class=$sTargetClass&$sParams&{$sDefault}\">".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sTargetClass))."</a>\n");
}
$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, false /* bDisplayMenu*/);
break;
@@ -294,7 +294,7 @@ class UILinksWidgetDirect
$valuesDef = $oLinksetDef->GetValuesDef();
if ($valuesDef === null)
{
$oFilter = new DBObjectSearch($this->sLinkedClass);
$oFilter = new DBObjectSearch($sRemoteClass);
}
else
{

View File

@@ -154,7 +154,7 @@ Class XLSXWriter
$cell = self::xlsCell($row_number, $column_number);
$s = isset($styles[$cell_format]) ? $styles[$cell_format] : '0';
if (is_numeric($value)) {
if (is_int($value) || is_float($value)) {
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.($value*1).'</v></c>');//int,float, etc
} else if ($cell_format=='date') {
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');

View File

@@ -489,6 +489,19 @@ abstract class AttributeDefinition
return $this->GetAsHTML($sValue, $oHostObject, $bLocalize);
}
/**
* List the available verbs for 'GetForTemplate'
*/
public static function EnumTemplateVerbs()
{
return array(
'' => 'Plain text (unlocalized) representation',
'html' => 'HTML representation',
'label' => 'Localized representation',
);
}
/**
* Get various representations of the value, for insertion into a template (e.g. in Notifications)
* @param $value mixed The current value of the field
@@ -818,6 +831,17 @@ class AttributeLinkedSet extends AttributeDefinition
return $sRes;
}
/**
* List the available verbs for 'GetForTemplate'
*/
public static function EnumTemplateVerbs()
{
return array(
'' => 'Plain text (unlocalized) representation',
'html' => 'HTML representation (unordered list)',
);
}
/**
* Get various representations of the value, for insertion into a template (e.g. in Notifications)
* @param $value mixed The current value of the field
@@ -1938,7 +1962,16 @@ class AttributeFinalClass extends AttributeString
if (empty($sValue)) return '';
return MetaModel::GetName($sValue);
}
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
// JSON values are NOT localized
return $value;
}
public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true)
{
if ($bLocalize && $value != '')
@@ -2493,6 +2526,18 @@ class AttributeCaseLog extends AttributeLongText
}
}
/**
* List the available verbs for 'GetForTemplate'
*/
public static function EnumTemplateVerbs()
{
return array(
'' => 'Plain text representation of all the log entries',
'head' => 'Plain text representation of the latest entry',
'head_html' => 'HTML representation of the latest entry',
'html' => 'HTML representation of all the log entries',
);
}
/**
* Get various representations of the value, for insertion into a template (e.g. in Notifications)

View File

@@ -628,8 +628,9 @@ class CMDBChangeOpSetAttributeCaseLog extends CMDBChangeOpSetAttribute
if (function_exists('mb_strcut'))
{
// Safe with multi-byte strings
$sBefore = $this->ToHtml(mb_strcut($sTextEntry, 0, $iMaxVisibleLength, 'UTF-8'));
$sAfter = $this->ToHtml(mb_strcut($sTextEntry, $iMaxVisibleLength, null, 'UTF-8'));
mb_internal_encoding('UTF-8'); // Do not use the form mb_strcut(str, start, null, encoding) which does not work if PHP < 5.4.9 (null => 0)
$sBefore = $this->ToHtml(mb_strcut($sTextEntry, 0, $iMaxVisibleLength));
$sAfter = $this->ToHtml(mb_strcut($sTextEntry, $iMaxVisibleLength));
}
else
{

View File

@@ -46,18 +46,18 @@ require_once('stimulus.class.inc.php');
require_once('valuesetdef.class.inc.php');
require_once('MyHelpers.class.inc.php');
require_once('expression.class.inc.php');
require_once('cmdbsource.class.inc.php');
require_once('sqlquery.class.inc.php');
require_once('sqlobjectquery.class.inc.php');
require_once('sqlunionquery.class.inc.php');
require_once('oql/expression.class.inc.php');
require_once('oql/oqlquery.class.inc.php');
require_once('oql/oqlexception.class.inc.php');
require_once('oql/oql-parser.php');
require_once('oql/oql-lexer.php');
require_once('oql/oqlinterpreter.class.inc.php');
require_once('cmdbsource.class.inc.php');
require_once('sqlquery.class.inc.php');
require_once('sqlobjectquery.class.inc.php');
require_once('sqlunionquery.class.inc.php');
require_once('dbobject.class.php');
require_once('dbsearch.class.php');
require_once('dbobjectset.class.php');

View File

@@ -2205,7 +2205,8 @@ abstract class DBObject implements iDisplay
$oSW = $this->Get($sAttCode);
$oSW->Reset($this, $oAttDef);
$this->Set($sAttCode, $oSW);
}
return true;
}
/**
* Lifecycle action: Recover the default value (aka when an object is being created)
@@ -2415,7 +2416,10 @@ abstract class DBObject implements iDisplay
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this);
}
if ($ret === null)
{
$ret = '';
}
}
return $ret;
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2015 Combodo SARL
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -20,7 +20,7 @@
/**
* Define filters for a given class of objects (formerly named "filter")
*
* @copyright Copyright (C) 2010-2015 Combodo SARL
* @copyright Copyright (C) 2010-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -565,7 +565,7 @@ class DBObjectSearch extends DBSearch
// NO: $oFilter = $oFilter->DeepClone();
// See also: Trac #639, and self::AddCondition_PointingTo()
$aAliasTranslation = array();
$res = $this->AddCondition_ReferencedBy_InNameSpace(DBObjectSearch, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
$res = $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
$this->TransferConditionExpression($oFilter, $aAliasTranslation);
return $res;
}
@@ -906,7 +906,8 @@ class DBObjectSearch extends DBSearch
public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery)
{
$sClass = $oOqlQuery->GetClass();
$oModelReflection = new ModelReflectionRuntime();
$sClass = $oOqlQuery->GetClass($oModelReflection);
$sClassAlias = $oOqlQuery->GetClassAlias();
$aAliases = array($sClassAlias => $sClass);

View File

@@ -20,17 +20,6 @@
require_once('dbobjectsearch.class.php');
require_once('dbunionsearch.class.php');
define('TREE_OPERATOR_EQUALS', 0);
define('TREE_OPERATOR_BELOW', 1);
define('TREE_OPERATOR_BELOW_STRICT', 2);
define('TREE_OPERATOR_NOT_BELOW', 3);
define('TREE_OPERATOR_NOT_BELOW_STRICT', 4);
define('TREE_OPERATOR_ABOVE', 5);
define('TREE_OPERATOR_ABOVE_STRICT', 6);
define('TREE_OPERATOR_NOT_ABOVE', 7);
define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8);
/**
* An object search
*

View File

@@ -35,7 +35,7 @@ class DisplayableNode extends GraphNode
* @param number $x Horizontal position
* @param number $y Vertical position
*/
public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0)
public function __construct(SimpleGraph $oGraph, $sId, $x = null, $y = null)
{
parent::__construct($oGraph, $sId);
$this->x = $x;
@@ -236,6 +236,143 @@ class DisplayableNode extends GraphNode
return is_object($this->GetProperty('object', null)) ? get_class($this->GetProperty('object', null)) : null;
}
protected function AddToStats($oNode, &$aNodesPerClass)
{
$sClass = $oNode->GetObjectClass();
if (!array_key_exists($sClass, $aNodesPerClass))
{
$aNodesPerClass[$sClass] = array(
'reached' => array(
'count' => 0,
'nodes' => array(),
'icon_url' => $oNode->GetProperty('icon_url'),
),
'not_reached' => array(
'count' => 0,
'nodes' => array(),
'icon_url' => $oNode->GetProperty('icon_url'),
)
);
}
$sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached';
if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes']))
{
$aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode;
$aNodesPerClass[$sClass][$sKey]['count'] += $oNode->GetObjectCount();
}
}
/**
* Retrieves the list of neighbour nodes, in the given direction: 'up' or 'down'
* @param bool $bDirectionDown
* @return multitype:NULL
*/
protected function GetNextNodes($bDirectionDown = true)
{
$aNextNodes = array();
if ($bDirectionDown)
{
foreach($this->GetOutgoingEdges() as $oEdge)
{
$aNextNodes[] = $oEdge->GetSinkNode();
}
}
else
{
foreach($this->GetIncomingEdges() as $oEdge)
{
$aNextNodes[] = $oEdge->GetSourceNode();
}
}
return $aNextNodes;
}
/**
* Replaces the next neighbour node (in the given direction: 'up' or 'down') by the supplied group node
* preserving the connectivity of the graph
* @param DisplayableGraph $oGraph
* @param DisplayableNode $oNextNode
* @param DisplayableGroupNode $oNewNode
* @param bool $bDirectionDown
*/
protected function ReplaceNextNodeBy(DisplayableGraph $oGraph, DisplayableNode $oNextNode, DisplayableGroupNode $oNewNode, $bDirectionDown = true)
{
$sClass = $oNewNode->GetProperty('class');
if ($bDirectionDown)
{
foreach($oNextNode->GetIncomingEdges() as $oEdge)
{
if ($oEdge->GetSourceNode()->GetId() !== $this->GetId())
{
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
}
catch(Exception $e)
{
// ignore this edge
}
}
}
foreach($oNextNode->GetOutgoingEdges() as $oEdge)
{
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
}
catch(Exception $e)
{
// ignore this edge
}
}
}
else
{
foreach($oNextNode->GetOutgoingEdges() as $oEdge)
{
if ($oEdge->GetSinkNode()->GetId() !== $this->GetId())
{
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
}
catch(Exception $e)
{
// ignore this edge
}
}
}
foreach($oNextNode->GetIncomingEdges() as $oEdge)
{
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
}
catch(Exception $e)
{
// ignore this edge
}
}
}
if ($oGraph->GetNode($oNextNode->GetId()))
{
$oGraph->_RemoveNode($oNextNode);
if ($oNextNode instanceof DisplayableGroupNode)
{
// Copy all the objects of the previous group into the new group
foreach($oNextNode->GetObjects() as $oObj)
{
$oNewNode->AddObject($oObj);
}
}
else
{
$oNewNode->AddObject($oNextNode->GetProperty('object'));
}
}
}
/**
* Group together (as a special kind of nodes) all the similar neighbours of the current node
* @param DisplayableGraph $oGraph
@@ -247,123 +384,65 @@ class DisplayableNode extends GraphNode
{
if ($this->GetProperty('grouped') === true) return;
$this->SetProperty('grouped', true);
if ($bDirectionDown)
$aNodesPerClass = array();
foreach($this->GetNextNodes($bDirectionDown) as $oNode)
{
$aNodesPerClass = array();
foreach($this->GetOutgoingEdges() as $oEdge)
$sClass = $oNode->GetObjectClass();
if ($sClass !== null)
{
$oNode = $oEdge->GetSinkNode();
$sClass = $oNode->GetObjectClass();
if ($sClass !== null)
{
if (!array_key_exists($sClass, $aNodesPerClass))
{
$aNodesPerClass[$sClass] = array(
'reached' => array(
'count' => 0,
'nodes' => array(),
'icon_url' => $oNode->GetProperty('icon_url'),
),
'not_reached' => array(
'count' => 0,
'nodes' => array(),
'icon_url' => $oNode->GetProperty('icon_url'),
)
);
}
$sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached';
if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes']))
{
$aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode;
$aNodesPerClass[$sClass][$sKey]['count'] += $oNode->GetObjectCount();
}
}
else
{
$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
$this->AddToStats($oNode, $aNodesPerClass);
}
foreach($aNodesPerClass as $sClass => $aDefs)
else
{
foreach($aDefs as $sStatus => $aGroupProps)
$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
}
foreach($aNodesPerClass as $sClass => $aDefs)
{
foreach($aDefs as $sStatus => $aGroupProps)
{
if (count($aGroupProps['nodes']) >= $iThresholdCount)
{
if (count($aGroupProps['nodes']) >= $iThresholdCount)
$sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': '');
$oNewNode = $oGraph->GetNode($sNewId);
if ($oNewNode == null)
{
$sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': '');
$oNewNode = $oGraph->GetNode($sNewId);
if ($oNewNode == null)
{
$oNewNode = new DisplayableGroupNode($oGraph, $sNewId);
$oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
$oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
$oNewNode->SetProperty('class', $sClass);
$oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
$oNewNode->SetProperty('count', $aGroupProps['count']);
}
try
$oNewNode = new DisplayableGroupNode($oGraph, $sNewId);
$oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
$oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
$oNewNode->SetProperty('class', $sClass);
$oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
$oNewNode->SetProperty('count', $aGroupProps['count']);
}
try
{
if ($bDirectionDown)
{
$oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode);
}
catch(Exception $e)
else
{
// Ignore this redundant egde
}
foreach($aGroupProps['nodes'] as $oNode)
{
foreach($oNode->GetIncomingEdges() as $oEdge)
{
if ($oEdge->GetSourceNode()->GetId() !== $this->GetId())
{
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
}
catch(Exception $e)
{
// ignore this edge
}
}
}
foreach($oNode->GetOutgoingEdges() as $oEdge)
{
$aOutgoing[] = $oEdge->GetSinkNode();
try
{
$oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
}
catch(Exception $e)
{
// ignore this edge
}
}
if ($oGraph->GetNode($oNode->GetId()))
{
$oGraph->_RemoveNode($oNode);
if ($oNode instanceof DisplayableGroupNode)
{
// Copy all the objects of the previous group into the new group
foreach($oNode->GetObjects() as $oObj)
{
$oNewNode->AddObject($oObj);
}
}
else
{
$oNewNode->AddObject($oNode->GetProperty('object'));
}
}
$oOutgoingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $oNewNode, $this);
}
$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
else
catch(Exception $e)
{
foreach($aGroupProps['nodes'] as $oNode)
{
$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
// Ignore this redundant egde
}
foreach($aGroupProps['nodes'] as $oNextNode)
{
$this->ReplaceNextNodeBy($oGraph, $oNextNode, $oNewNode, $bDirectionDown);
}
$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
else
{
foreach($aGroupProps['nodes'] as $oNode)
{
$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
}
}
@@ -486,64 +565,67 @@ class DisplayableRedundancyNode extends DisplayableNode
public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
{
parent::GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
if ($bDirectionUp)
{
$aNodesPerClass = array();
foreach($this->GetIncomingEdges() as $oEdge)
{
$oNode = $oEdge->GetSourceNode();
if (($oNode->GetObjectClass() !== null) && (!$oNode->GetProperty('is_reached')))
{
$sClass = $oNode->GetObjectClass();
if (!array_key_exists($sClass, $aNodesPerClass))
{
$aNodesPerClass[$sClass] = array('reached' => array(), 'not_reached' => array());
}
$aNodesPerClass[$sClass][$oNode->GetProperty('is_reached') ? 'reached' : 'not_reached'][] = $oNode;
{
$this->AddToStats($oNode, $aNodesPerClass);
}
else
{
//$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
}
}
foreach($aNodesPerClass as $sClass => $aDefs)
{
foreach($aDefs as $sStatus => $aNodes)
foreach($aDefs as $sStatus => $aGroupProps)
{
if (count($aNodes) >= $iThresholdCount)
if (count($aGroupProps['nodes']) >= $iThresholdCount)
{
$oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus);
$oNewNode->SetProperty('label', 'x'.count($aNodes));
$oNewNode->SetProperty('icon_url', $aNodes[0]->GetProperty('icon_url'));
$oNewNode->SetProperty('is_reached', $aNodes[0]->GetProperty('is_reached'));
$oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
foreach($aNodes as $oNode)
$oNewNode->SetProperty('label', 'x'.count($aGroupProps['nodes']));
$oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
$oNewNode->SetProperty('is_reached', ($sStatus == 'is_reached'));
$oNewNode->SetProperty('class', $sClass);
$oNewNode->SetProperty('count', count($aGroupProps['nodes']));
$sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': '');
$oNewNode = $oGraph->GetNode($sNewId);
if ($oNewNode == null)
{
foreach($oNode->GetIncomingEdges() as $oEdge)
{
$oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
}
foreach($oNode->GetOutgoingEdges() as $oEdge)
{
if ($oEdge->GetSinkNode()->GetId() !== $this->GetId())
{
$aOutgoing[] = $oEdge->GetSinkNode();
$oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass.'/'.$sStatus, $oNewNode, $oEdge->GetSinkNode());
}
}
$oGraph->_RemoveNode($oNode);
$oNewNode->AddObject($oNode->GetProperty('object'));
$oNewNode = new DisplayableGroupNode($oGraph, $sNewId);
$oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
$oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
$oNewNode->SetProperty('class', $sClass);
$oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
$oNewNode->SetProperty('count', $aGroupProps['count']);
}
try
{
$oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
}
catch(Exception $e)
{
// Ignore this redundant egde
}
foreach($aGroupProps['nodes'] as $oNextNode)
{
$this->ReplaceNextNodeBy($oGraph, $oNextNode, $oNewNode, !$bDirectionUp);
}
//$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
else
{
foreach($aNodes as $oNode)
foreach($aGroupProps['nodes'] as $oNode)
{
//$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
}
@@ -580,10 +662,21 @@ class DisplayableEdge extends GraphEdge
{
public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
{
$xStart = $this->GetSourceNode()->x * $fScale;
$yStart = $this->GetSourceNode()->y * $fScale;
$xEnd = $this->GetSinkNode()->x * $fScale;
$yEnd = $this->GetSinkNode()->y * $fScale;
$oSourceNode = $this->GetSourceNode();
if (($oSourceNode->x == null) || ($oSourceNode->y == null))
{
return;
}
$xStart = $oSourceNode->x * $fScale;
$yStart = $oSourceNode->y * $fScale;
$oSinkNode = $this->GetSinkNode();
if (($oSinkNode->x == null) || ($oSinkNode->y == null))
{
return;
}
$xEnd = $oSinkNode->x * $fScale;
$yEnd = $oSinkNode->y * $fScale;
$bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached'));
@@ -627,14 +720,17 @@ class DisplayableGroupNode extends DisplayableNode
$this->aObjects = array();
}
public function AddObject(DBObject $oObj)
public function AddObject(DBObject $oObj = null)
{
$sPrevClass = $this->GetObjectClass();
if (($sPrevClass !== null) && (get_class($oObj) !== $sPrevClass))
if (is_object($oObj))
{
throw new Exception("Error: adding an object of class '".get_class($oObj)."' to a group of '$sPrevClass' objects.");
$sPrevClass = $this->GetObjectClass();
if (($sPrevClass !== null) && (get_class($oObj) !== $sPrevClass))
{
throw new Exception("Error: adding an object of class '".get_class($oObj)."' to a group of '$sPrevClass' objects.");
}
$this->aObjects[$oObj->GetKey()] = $oObj;
}
$this->aObjects[$oObj->GetKey()] = $oObj;
}
public function GetObjects()
@@ -863,9 +959,13 @@ class DisplayableGraph extends SimpleGraph
foreach($oNodesIter as $oNode)
{
set_time_limit($iLoopTimeLimit);
if ($oNode->GetProperty('source'))
if ($bDirectionDown && $oNode->GetProperty('source'))
{
$oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true);
$oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown);
}
else if (!$bDirectionDown && $oNode->GetProperty('sink'))
{
$oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown);
}
}
// Groups numbering
@@ -878,7 +978,7 @@ class DisplayableGraph extends SimpleGraph
{
if ($oNode->GetObjectCount() == 0)
{
// Remove emtpry groups
// Remove empty groups
$oNewGraph->_RemoveNode($oNode);
}
else
@@ -945,8 +1045,15 @@ class DisplayableGraph extends SimpleGraph
$yPos = $aMatches[3];
$oNode = $this->GetNode($sId);
$oNode->x = (float)$xPos;
$oNode->y = (float)$yPos;
if ($oNode !== null)
{
$oNode->x = (float)$xPos;
$oNode->y = (float)$yPos;
}
else
{
IssueLog::Warning("??? Position of the non-existing node '$sId', x=$xPos, y=$yPos");
}
}
}
}
@@ -1009,7 +1116,7 @@ class DisplayableGraph extends SimpleGraph
{
$aContextDefs = static::GetContextDefinitions($sContextKey, false);
$aData = array('nodes' => array(), 'edges' => array(), 'groups' => array());
$aData = array('nodes' => array(), 'edges' => array(), 'groups' => array(), 'lists' => array());
$iGroupIdx = 0;
$oIterator = new RelationTypeIterator($this, 'Node');
foreach($oIterator as $sId => $oNode)
@@ -1032,10 +1139,35 @@ class DisplayableGraph extends SimpleGraph
$aData['groups'][$iGroupIdx] = array('class' => $sClass, 'keys' => $aKeys);
$oNode->SetProperty('group_index', $iGroupIdx);
$iGroupIdx++;
if ($oNode->GetProperty('is_reached'))
{
// Also add the objects from this group into the 'list' tab
if (!array_key_exists($sClass, $aData['lists']))
{
$aData['lists'][$sClass] = $aKeys;
}
else
{
$aData['lists'][$sClass] = array_merge($aData['lists'][$sClass], $aKeys);
}
}
}
if (($oNode instanceof DisplayableNode) && $oNode->GetProperty('is_reached') && is_object($oNode->GetProperty('object')))
{
$sObjClass = get_class($oNode->GetProperty('object'));
if (!array_key_exists($sObjClass, $aData['lists']))
{
$aData['lists'][$sObjClass] = array();
}
$aData['lists'][$sObjClass][] = $oNode->GetProperty('object')->GetKey();
}
$aData['nodes'][] = $oNode->GetForRaphael($aContextDefs);
}
uksort($aData['lists'], array(get_class($this), 'SortOnClassLabel')); // sort on the localized names of the classes to provide a consistent and stable order
$oIterator = new RelationTypeIterator($this, 'Edge');
foreach($oIterator as $sId => $oEdge)
{
@@ -1051,6 +1183,17 @@ class DisplayableGraph extends SimpleGraph
return json_encode($aData);
}
/**
* Sort class "codes" based on their localized name
* @param string $sClass1
* @param string $sClass2
* @return number -1, 0 or 1
*/
public static function SortOnClassLabel($sClass1, $sClass2)
{
return strcasecmp(MetaModel::GetName($sClass1), MetaModel::GetName($sClass2));
}
/**
* Renders the graph in a PDF document: centered in the current page
* @param PDFPage $oPage The PDFPage representing the PDF document to draw into
@@ -1124,7 +1267,7 @@ class DisplayableGraph extends SimpleGraph
set_time_limit($iLoopTimeLimit);
$oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs);
}
$oIterator = new RelationTypeIterator($this, 'Node');
$oPdf->SetAutoPageBreak(true, $fBreakMargin);
$oPdf->SetAlpha(1);
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,18 +40,6 @@ require_once(APPROOT.'core/relationgraph.class.inc.php');
// const VERSION = '1.0.0';
// }
/**
* add some description here...
*
* @package iTopORM
*/
define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1);
/**
* add some description here...
*
* @package iTopORM
*/
define('ENUM_CHILD_CLASSES_ALL', 2);
/**
* add some description here...
*
@@ -1790,7 +1778,7 @@ abstract class MetaModel
$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode, $sAttCode);
$oFriendlyName->SetHostClass($sClass);
self::$m_aAttribDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyName;
self::$m_aAttribOrigins[$sClass][$sFriendlyNameAttCode] = $sRemoteClass;
self::$m_aAttribOrigins[$sClass][$sFriendlyNameAttCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
$oFriendlyNameFlt = new FilterFromAttribute($oFriendlyName);
self::$m_aFilterDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyNameFlt;
self::$m_aFilterOrigins[$sClass][$sFriendlyNameAttCode] = $sRemoteClass;
@@ -4830,9 +4818,11 @@ abstract class MetaModel
continue; // Ignore this non-scalar value
}
}
$aSearches[] = '$'.$sSearch.'$';
$aReplacements[] = (string) $replace;
else
{
$aSearches[] = '$'.$sSearch.'$';
$aReplacements[] = (string) $replace;
}
}
return str_replace($aSearches, $aReplacements, $sInput);
}

View File

@@ -23,7 +23,21 @@
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Exclude the parent class from the list
*
* @package iTopORM
*/
define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1);
/**
* Include the parent class in the list
*
* @package iTopORM
*/
define('ENUM_CHILD_CLASSES_ALL', 2);
abstract class ModelReflection
{
abstract public function GetClassIcon($sClass, $bImgTag = true);
@@ -62,6 +76,9 @@ abstract class ModelReflection
}
abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
abstract public function GetRootClass($sClass);
abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP);
}
abstract class QueryReflection
@@ -234,6 +251,16 @@ class ModelReflectionRuntime extends ModelReflection
{
return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
}
public function GetRootClass($sClass)
{
return MetaModel::GetRootClass($sClass);
}
public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP)
{
return MetaModel::EnumChildClasses($sClass, $iOption);
}
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2013 Combodo SARL
// Copyright (C) 2013-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -24,7 +24,7 @@
* Relies on MySQL locks because the API sem_get is not always present in the
* installed PHP.
*
* @copyright Copyright (C) 2013 Combodo SARL
* @copyright Copyright (C) 2013-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class iTopMutex
@@ -139,6 +139,36 @@ class iTopMutex
}
return ($res !== '0');
}
/**
* Check if the mutex is locked WITHOUT TRYING TO ACQUIRE IT
* @returns bool True if the mutex is in use, false otherwise
*/
public function IsLocked()
{
if ($this->bLocked)
{
return true; // Already acquired
}
if (self::$aAcquiredLocks[$this->sName] > 0)
{
return true;
}
$res = $this->QueryToScalar("SELECT IS_FREE_LOCK('".$this->sName."')"); // IS_FREE_LOCK detects some error cases that IS_USED_LOCK do not detect
if (is_null($res))
{
$sMsg = "MySQL Error, IS_FREE_LOCK('".$this->sName."') returned null. Error (".mysqli_errno($this->hDBLink).") = '".mysqli_error($this->hDBLink)."'";
IssueLog::Error($sMsg);
throw new Exception($sMsg);
}
else if ($res == '1')
{
// Lock is free
return false;
}
return true;
}
/**
* Release the mutex

View File

@@ -1,4 +1,11 @@
#!/bin/bash
php PHP/LexerGenerator/cli.php oql-lexer.plex
php PHP/ParserGenerator/cli.php oql-parser.y
#
# Rebuild the iTop Lexer / Parser
# PEAR is required to build (really?)
# Launch this batch from the core/oql/build directory
# with ./build.bash
#
php PHP/LexerGenerator/cli.php ../oql-lexer.plex
php PHP/ParserGenerator/cli.php ../oql-parser.y
php -r "echo date('Y-m-d');" > ../version.txt

View File

@@ -2,4 +2,5 @@ rem must be run with current directory = the directory of the batch
rem PEAR is required to build
php -d include_path=".;C:\iTop\PHP\PEAR" ".\PHP\LexerGenerator\cli.php" ..\oql-lexer.plex
php ".\PHP\ParserGenerator\cli.php" ..\oql-parser.y
php -r "echo date('Y-m-d');" > ..\version.txt
pause

50
core/oql/check_oql.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
/**
* Minimal file (with all the needed includes) to check the validity of an OQL by verifying:
* - The syntax (of the OQL query string)
* - The consistency with a given data model (represented by an instance of ModelReflection)
*
* Usage:
*
* require_once(APPROOT.'core/oql/check_oql.php');
*
* $sOQL = "SELECT Zerver WHERE status = 'production'";
* $oModelReflection = new ModelReflectionRuntime();
* $aResults = CheckOQL($sOQL, $oModelReflection);
* if ($aResults['status'] == 'error')
* {
* echo "The query '$sOQL' is not a valid query. Reason: {$aResults['message']}";
* }
* else
* {
* echo "Ok, '$sOQL' is a valid query";
* }
*/
class CoreException extends Exception
{
}
require_once(__DIR__.'/expression.class.inc.php');
require_once(__DIR__.'/oqlquery.class.inc.php');
require_once(__DIR__.'/oqlexception.class.inc.php');
require_once(__DIR__.'/oql-parser.php');
require_once(__DIR__.'/oql-lexer.php');
require_once(__DIR__.'/oqlinterpreter.class.inc.php');
function CheckOQL($sOQL, ModelReflection $oModelReflection)
{
$aRes = array('status' => 'ok', 'message' => '');
try
{
$oOql = new OqlInterpreter($sOQL);
$oOqlQuery = $oOql->ParseQuery(); // Exceptions thrown in case of issue
$oOqlQuery->Check($oModelReflection,$sOQL); // Exceptions thrown in case of issue
}
catch(Exception $e)
{
$aRes['status'] = 'error';
$aRes['message'] = $e->getMessage();
}
return $aRes;
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,16 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
define('TREE_OPERATOR_EQUALS', 0);
define('TREE_OPERATOR_BELOW', 1);
define('TREE_OPERATOR_BELOW_STRICT', 2);
define('TREE_OPERATOR_NOT_BELOW', 3);
define('TREE_OPERATOR_NOT_BELOW_STRICT', 4);
define('TREE_OPERATOR_ABOVE', 5);
define('TREE_OPERATOR_ABOVE_STRICT', 6);
define('TREE_OPERATOR_NOT_ABOVE', 7);
define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8);
// Position a string within an OQL query
// This is a must if we want to be able to pinpoint an error at any stage of the query interpretation
// In particular, the normalization phase requires this
@@ -278,6 +288,23 @@ abstract class OqlQuery
* @throws OqlNormalizeException
*/
abstract public function Check(ModelReflection $oModelReflection, $sSourceQuery);
/**
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @return string
* @throws Exception
*/
abstract public function GetClass(ModelReflection $oModelReflection);
/**
* Determine the class alias
*
* @return string
* @throws Exception
*/
abstract public function GetClassAlias();
}
class OqlObjectQuery extends OqlQuery
@@ -303,10 +330,25 @@ class OqlObjectQuery extends OqlQuery
{
return $this->m_aSelect;
}
public function GetClass()
/**
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @return string
* @throws Exception
*/
public function GetClass(ModelReflection $oModelReflection)
{
return $this->m_oClass->GetValue();
}
/**
* Determine the class alias
*
* @return string
* @throws Exception
*/
public function GetClassAlias()
{
return $this->m_oClassAlias->GetValue();
@@ -339,7 +381,7 @@ class OqlObjectQuery extends OqlQuery
*/
public function Check(ModelReflection $oModelReflection, $sSourceQuery)
{
$sClass = $this->GetClass();
$sClass = $this->GetClass($oModelReflection);
$sClassAlias = $this->GetClassAlias();
if (!$oModelReflection->IsValidClass($sClass))
@@ -490,7 +532,7 @@ class OqlObjectQuery extends OqlQuery
*/
public function ToDBSearch($sQuery)
{
$sClass = $this->GetClass();
$sClass = $this->GetClass(new ModelReflectionRuntime());
$sClassAlias = $this->GetClassAlias();
$oSearch = new DBObjectSearch($sClass, $sClassAlias);
@@ -538,7 +580,7 @@ class OqlUnionQuery extends OqlQuery
{
$oQuery->Check($oModelReflection, $sSourceQuery);
$aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass());
$aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass($oModelReflection));
$aJoinSpecs = $oQuery->GetJoins();
if (is_array($aJoinSpecs))
{
@@ -580,13 +622,13 @@ class OqlUnionQuery extends OqlQuery
if ($iQuery == 0)
{
// Establish the reference
$sRootClass = MetaModel::GetRootClass($aData['class']);
$sRootClass = $oModelReflection->GetRootClass($aData['class']);
}
else
{
if (MetaModel::GetRootClass($aData['class']) != $sRootClass)
if ($oModelReflection->GetRootClass($aData['class']) != $sRootClass)
{
$aSubclasses = MetaModel::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL);
$aSubclasses = $oModelReflection->EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL);
throw new OqlNormalizeException('Incompatible classes: could not find a common ancestor', $sSourceQuery, $aData['class_name'], $aSubclasses);
}
}
@@ -594,6 +636,100 @@ class OqlUnionQuery extends OqlQuery
}
}
/**
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @return string
* @throws Exception
*/
public function GetClass(ModelReflection $oModelReflection)
{
$aFirstColClasses = array();
foreach ($this->aQueries as $iQuery => $oQuery)
{
$aFirstColClasses[] = $oQuery->GetClass($oModelReflection);
}
$sClass = self::GetLowestCommonAncestor($oModelReflection, $aFirstColClasses);
if (is_null($sClass))
{
throw new Exception('Could not determine the class of the union query. This issue should have been detected earlier by calling OqlQuery::Check()');
}
return $sClass;
}
/**
* Determine the class alias
*
* @return string
* @throws Exception
*/
public function GetClassAlias()
{
$sAlias = $this->aQueries[0]->GetClassAlias();
return $sAlias;
}
/**
* Check the validity of the expression with regard to the data model
* and the query in which it is used
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @param array $aClasses Flat list of classes
* @return string the lowest common ancestor amongst classes, null if none has been found
* @throws Exception
*/
public static function GetLowestCommonAncestor(ModelReflection $oModelReflection, $aClasses)
{
$sAncestor = null;
foreach($aClasses as $sClass)
{
if (is_null($sAncestor))
{
// first loop
$sAncestor = $sClass;
}
elseif ($sClass == $sAncestor)
{
// remains the same
}
elseif ($oModelReflection->GetRootClass($sClass) != $oModelReflection->GetRootClass($sAncestor))
{
$sAncestor = null;
break;
}
else
{
$sAncestor = self::LowestCommonAncestor($oModelReflection, $sAncestor, $sClass);
}
}
return $sAncestor;
}
/**
* Note: assumes that class A and B have a common ancestor
*/
protected static function LowestCommonAncestor(ModelReflection $oModelReflection, $sClassA, $sClassB)
{
if ($sClassA == $sClassB)
{
$sRet = $sClassA;
}
elseif (in_array($sClassA, $oModelReflection->EnumChildClasses($sClassB)))
{
$sRet = $sClassB;
}
elseif (in_array($sClassB, $oModelReflection->EnumChildClasses($sClassA)))
{
$sRet = $sClassA;
}
else
{
// Recurse
$sRet = self::LowestCommonAncestor($oModelReflection, $sClassA, $oModelReflection->GetParentClass($sClassB));
}
return $sRet;
}
/**
* Make the relevant DBSearch instance (FromOQL)
*/

1
core/oql/version.txt Normal file
View File

@@ -0,0 +1 @@
2015-08-31

View File

@@ -145,10 +145,10 @@ class RelationRedundancyNode extends GraphNode
*/
class RelationEdge extends GraphEdge
{
public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode)
public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode, $bMustBeUnique = false)
{
$sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId();
parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode);
parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode, $bMustBeUnique);
}
}
@@ -250,6 +250,7 @@ class RelationGraph extends SimpleGraph
$aAliasNames = array_keys($aAliases);
$sRootCauseAlias = $aAliasNames[1]; // 1st column (=0) = object, second column = root cause
$oSet = new DBObjectSet($aContextQuery['search'], array(), array('id' => $oObj->GetKey()));
$oSet->OptimizeColumnLoad(array($aAliasNames[0] => array(), $aAliasNames[1] => array())); // Do not load any column... better do a reload than many joins
while($aRow = $oSet->FetchAssoc())
{
if (!is_null($aRow[$sRootCauseAlias]))
@@ -426,7 +427,7 @@ class RelationGraph extends SimpleGraph
if (!$oRedundancyNode)
{
// Direct link (otherwise handled by ComputeRedundancy)
$oEdge = new RelationEdge($this, $oSourceNode, $oSinkNode);
new RelationEdge($this, $oSourceNode, $oSinkNode);
}
// Recurse
$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);

View File

@@ -234,22 +234,24 @@ class GraphEdge extends GraphElement
{
protected $oSourceNode;
protected $oSinkNode;
/**
* Create a new directed edge inside the given graph
* @param SimpleGraph $oGraph
* @param string $sId The unique identifier of this edge in the graph
* @param GraphNode $oSourceNode
* @param GraphNode $oSinkNode
* @param bool $bMustBeUnique
* @throws SimpleGraphException
*/
public function __construct(SimpleGraph $oGraph, $sId, GraphNode $oSourceNode, GraphNode $oSinkNode)
public function __construct(SimpleGraph $oGraph, $sId, GraphNode $oSourceNode, GraphNode $oSinkNode, $bMustBeUnique = false)
{
parent::__construct($sId);
$this->oSourceNode = $oSourceNode;
$this->oSinkNode = $oSinkNode;
$oGraph->_AddEdge($this);
$oGraph->_AddEdge($this, $bMustBeUnique);
}
/**
* Get the "source" node for this edge
* @return GraphNode
@@ -403,11 +405,22 @@ class SimpleGraph
/**
* INTERNAL USE ONLY
* @param GraphEdge $oEdge
* @param bool $bMustBeUnique
* @throws SimpleGraphException
*/
public function _AddEdge(GraphEdge $oEdge)
public function _AddEdge(GraphEdge $oEdge, $bMustBeUnique = false)
{
if (array_key_exists($oEdge->GetId(), $this->aEdges)) throw new SimpleGraphException('Cannot add edge (id='.$oEdge->GetId().') to the graph. An edge with the same id already exists in the graph.');
if (array_key_exists($oEdge->GetId(), $this->aEdges))
{
if ($bMustBeUnique)
{
throw new SimpleGraphException('Cannot add edge (id=' . $oEdge->GetId() . ') to the graph. An edge with the same id already exists in the graph.');
}
else
{
return;
}
}
$this->aEdges[$oEdge->GetId()] = $oEdge;
$oEdge->GetSourceNode()->_AddOutgoingEdge($oEdge);
@@ -516,7 +529,7 @@ EOF
@fwrite($rFile, $sDotDescription);
@fclose($rFile);
$aOutput = array();
$CommandLine = "\"$sDotExecutable\" -v -Tpng < $sDotFilePath -o$sImageFilePath 2>&1";
$CommandLine = "\"$sDotExecutable\" -v -Tpng < \"$sDotFilePath\" -o\"$sImageFilePath\" 2>&1";
exec($CommandLine, $aOutput, $iRetCode);
if ($iRetCode != 0)
@@ -572,7 +585,7 @@ EOF
@fwrite($rFile, $sDotDescription);
@fclose($rFile);
$aOutput = array();
$CommandLine = "\"$sDotExecutable\" -v -Tdot < $sDotFilePath -o$sXdotFilePath 2>&1";
$CommandLine = "\"$sDotExecutable\" -v -Tdot < \"$sDotFilePath\" -o\"$sXdotFilePath\" 2>&1";
exec($CommandLine, $aOutput, $iRetCode);
if ($iRetCode != 0)
@@ -692,7 +705,7 @@ EOF
}
/**
* Merge back to subgraphs into one
* Merge back two subgraphs into one
* @param SimpleGraph $oGraph
*/
public function Merge(SimpleGraph $oGraph)

View File

@@ -199,10 +199,18 @@ class SpreadsheetBulkExport extends TabularBulkExport
$oFinalAttDef = $oAttDef->GetFinalAttDef();
if (get_class($oFinalAttDef) == 'AttributeDateTime')
{
$iDate = AttributeDateTime::GetAsUnixSeconds($oObj->Get($sAttCode));
$sData .= '<td>'.date('Y-m-d', $iDate).'</td>'; // Add the first column directly
$sField = date('H:i:s', $iDate); // Will add the second column below
$sData .= "<td>$sField</td>";
$value = $oObj->Get($sAttCode);
if (($value === null) || ($value === '0000-00-00') || ($value === '0000-00-00 00:00:00') )
{
$sData .= '<td></td><td></td>';
}
else
{
$iDate = AttributeDateTime::GetAsUnixSeconds($oObj->Get($sAttCode));
$sData .= '<td>'.date('Y-m-d', $iDate).'</td>'; // Add the first column directly
$sField = date('H:i:s', $iDate); // Will add the second column below
$sData .= "<td>$sField</td>";
}
}
else if($oAttDef instanceof AttributeCaseLog)
{

View File

@@ -400,7 +400,7 @@ abstract class UserInternal extends User
MetaModel::Init_InheritAttributes();
// When set, this token allows for password reset
MetaModel::Init_AddAttribute(new AttributeString("reset_pwd_token", array("allowed_values"=>null, "sql"=>"reset_pwd_token", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeOneWayPassword("reset_pwd_token", array("allowed_values"=>null, "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('contactid', 'first_name', 'email', 'login', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details

View File

@@ -280,6 +280,11 @@ legend.transparent {
}
.ui-widget-content td a.cke_toolbox_collapser {
padding-left: 0;
}
p a:hover, td a:hover {
text-decoration: underline;
color: #e87c1e;

View File

@@ -224,6 +224,9 @@ legend.transparent {
padding-left:14px;
background: url(../images/mini-arrow-orange.gif) no-repeat left;
}
.ui-widget-content td a.cke_toolbox_collapser {
padding-left: 0;
}
p a:hover, td a:hover {
text-decoration:underline;
color:$highlight-color;

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2013-2015 Combodo SARL
// Copyright (C) 2013-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -19,7 +19,7 @@
/**
* Backup from an interactive session
*
* @copyright Copyright (C) 2013-215 Combodo SARL
* @copyright Copyright (C) 2013-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -77,9 +77,8 @@ try
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
if ($oRestoreMutex->TryLock())
if (!$oRestoreMutex->IsLocked())
{
$oRestoreMutex->Unlock();
$sFile = utils::ReadParam('file', '', false, 'raw_data');
$sToken = str_replace(' ', '', (string)microtime());
$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2014-2015 Combodo SARL
// Copyright (C) 2014-2016 Combodo SARL
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -28,24 +28,8 @@ class BackupHandler extends ModuleHandlerAPI
{
try
{
$oBackupMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
if ($oBackupMutex->TryLock())
{
$oBackupMutex->Unlock();
}
else
{
// Not needed: the DB dump is done in a single transaction
//MetaModel::GetConfig()->Set('access_mode', ACCESS_READONLY, 'itop-backup');
//MetaModel::GetConfig()->Set('access_message', ' - '.dict::S('bkp-backup-running'), 'itop-backup');
}
$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
if ($oRestoreMutex->TryLock())
{
$oRestoreMutex->Unlock();
}
else
if ($oRestoreMutex->IsLocked())
{
IssueLog::Info(__class__.'::'.__function__.' A user is trying to use iTop while a restore is running. The requested page is in read-only mode.');
MetaModel::GetConfig()->Set('access_mode', ACCESS_READONLY, 'itop-backup');

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2014 Combodo SARL
// Copyright (C) 2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -20,7 +20,7 @@
/**
* Monitor the backup
*
* @copyright Copyright (C) 2013 Combodo SARL
* @copyright Copyright (C) 2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -169,14 +169,13 @@ try
}
$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
if ($oRestoreMutex->TryLock())
if ($oRestoreMutex->IsLocked())
{
$oRestoreMutex->Unlock();
$sDisableRestore = '';
$sDisableRestore = 'disabled="disabled"';
}
else
{
$sDisableRestore = 'disabled="disabled"';
$sDisableRestore = '';
}
// 1st table: list the backups made in the background
@@ -271,20 +270,12 @@ try
// Ongoing operation ?
//
$oBackupMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
if ($oBackupMutex->TryLock())
{
$oBackupMutex->Unlock();
}
else
if ($oBackupMutex->IsLocked())
{
$oP->p(Dict::S('bkp-backup-running'));
}
$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
if ($oRestoreMutex->TryLock())
{
$oRestoreMutex->Unlock();
}
else
if ($oRestoreMutex->IsLocked())
{
$oP->p(Dict::S('bkp-restore-running'));
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2014 Combodo SARL
// Copyright (C) 2014-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -105,6 +105,10 @@ try
{
$oP->add("<div class=\"header_message message_info\">Sorry, iTop is in <b>demonstration mode</b>: the configuration file cannot be edited.</div>");
}
if (MetaModel::GetModuleSetting('itop-config', 'config_editor', '') == 'disabled')
{
$oP->add("<div class=\"header_message message_info\">iTop interactive edition of the configuration as been disabled. See <tt>'config_editor' => 'disabled'</tt> in the configuration file.</div>");
}
else
{
$oP->add_style(
@@ -129,27 +133,50 @@ EOF
if ($sOperation == 'save')
{
$sConfig = utils::ReadParam('new_config', '', false, 'raw_data');
$sTransactionId = utils::ReadParam('transaction_id', '');
$sOrginalConfig = utils::ReadParam('prev_config', '', false, 'raw_data');
if ($sConfig == $sOrginalConfig)
if (!utils::IsTransactionValid($sTransactionId, true))
{
$oP->add('<div id="save_result" class="header_message">'.Dict::S('config-no-change').'</div>');
$oP->add("<div class=\"header_message message_info\">Error: invalid Transaction ID. The configuration was <b>NOT</b> modified.</div>");
}
else
{
try
if ($sConfig == $sOrginalConfig)
{
TestConfig($sConfig, $oP); // throws exceptions
@chmod($sConfigFile, 0770); // Allow overwriting the file
file_put_contents($sConfigFile, $sConfig);
@chmod($sConfigFile, 0444); // Read-only
$oP->p('<div id="save_result" class="header_message message_ok">'.Dict::S('Successfully recorded.').'</div>');
$sOrginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
$oP->add('<div id="save_result" class="header_message">'.Dict::S('config-no-change').'</div>');
}
catch (Exception $e)
else
{
$oP->p('<div id="save_result" class="header_message message_error">'.$e->getMessage().'</div>');
try
{
TestConfig($sConfig, $oP); // throws exceptions
@chmod($sConfigFile, 0770); // Allow overwriting the file
$sTmpFile = tempnam(SetupUtils::GetTmpDir(), 'itop-cfg-');
// Don't write the file as-is since it would allow to inject any kind of PHP code.
// Instead write the interpreted version of the file
// Note:
// The actual raw PHP code will anyhow be interpreted exactly twice: once in TestConfig() above
// and a second time during the load of the Config object below.
// If you are really concerned about an iTop administrator crafting some malicious
// PHP code inside the config file, then turn off the interactive configuration
// editor by adding the configuration parameter:
// 'itop-config' => array(
// 'config_editor' => 'disabled',
// )
file_put_contents($sTmpFile, $sConfig);
$oTempConfig = new Config($sTmpFile, true);
$oTempConfig->WriteToFile($sConfigFile);
@unlink($sTmpFile);
@chmod($sConfigFile, 0444); // Read-only
$oP->p('<div id="save_result" class="header_message message_ok">'.Dict::S('Successfully recorded.').'</div>');
$sOrginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
}
catch (Exception $e)
{
$oP->p('<div id="save_result" class="header_message message_error">'.$e->getMessage().'</div>');
}
}
}
}
@@ -164,6 +191,7 @@ EOF
$oP->p(Dict::S('config-edit-intro'));
$oP->add("<form method=\"POST\">");
$oP->add("<input type=\"hidden\" name=\"operation\" value=\"save\">");
$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">");
$oP->add("<input type=\"submit\" value=\"".Dict::S('config-apply')."\"><button onclick=\"ResetConfig(); return false;\">".Dict::S('config-cancel')."</button>");
$oP->add("<span class=\"current_line\">".Dict::Format('config-current-line', "<span class=\"line_number\"></span>")."</span>");
$oP->add("<input type=\"hidden\" id=\"prev_config\" name=\"prev_config\" value=\"$sOriginalConfigEscaped\">");

View File

@@ -79,6 +79,7 @@
<class id="lnkFunctionalCIToTicket"/>
<class id="lnkContactToTicket"/>
<class id="WorkOrder"/>
<class id="Attachment"/>
</classes>
</group>
<group id="NormalChange" _delta="define">

View File

@@ -1069,7 +1069,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
'Class:ShortcutOQL' => 'Suchergebnis-Shortcut',
'Class:ShortcutOQL/Attribute:oql' => 'Query',
'Class:ShortcutOQL/Attribute:oql+' => 'OQL-Query, der die zu Suchenden Objekte beschreibt',
'Class:ShortcutOQL/Attribute:auto_reload' => 'Rautomatischer Reload',
'Class:ShortcutOQL/Attribute:auto_reload' => 'Automatischer Reload',
'Class:ShortcutOQL/Attribute:auto_reload/Value:none' => 'Deaktiviert',
'Class:ShortcutOQL/Attribute:auto_reload/Value:custom' => 'Eigene Einstellung',
'Class:ShortcutOQL/Attribute:auto_reload_sec' => 'Intervall für automatischen Reload (Sekunden)',

View File

@@ -233,25 +233,32 @@ function ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue)
var bValid;
var sTextContent;
// Get the contents without the tags
var oFormattedContents = $("#cke_"+sFieldId+" iframe");
if (oFormattedContents.length == 0)
if ($('#'+sFieldId).attr('disabled'))
{
var oSourceContents = $("#cke_"+sFieldId+" textarea.cke_source");
sTextContent = oSourceContents.val();
bValid = true; // disabled fields are not checked
}
else
{
sTextContent = oFormattedContents.contents().find("body").text();
}
if (bMandatory && (sTextContent == ''))
{
bValid = false;
}
else
{
bValid = true;
// Get the contents without the tags
var oFormattedContents = $("#cke_"+sFieldId+" iframe");
if (oFormattedContents.length == 0)
{
var oSourceContents = $("#cke_"+sFieldId+" textarea.cke_source");
sTextContent = oSourceContents.val();
}
else
{
sTextContent = oFormattedContents.contents().find("body").text();
}
if (bMandatory && (sTextContent == ''))
{
bValid = false;
}
else
{
bValid = true;
}
}
ReportFieldValidationStatus(sFieldId, sFormId, bValid, '');

View File

@@ -238,8 +238,12 @@ $(function()
}
);
oParams.operation = 'searchObjectsToAdd2';
oParams['class'] = this.options.class_name;
oParams.real_class = '';
if ((oParams['class'] != undefined) && (oParams['class'] != ''))
{
oParams.real_class = oParams['class'];
}
oParams['class'] = this.options.class_name;
oParams.att_code = this.options.att_code;
oParams.iInputId = this.id;
if (this.options.oWizardHelper)

View File

@@ -634,10 +634,18 @@ function ReadFormParams(sFormId)
{
oMap[sName] = ($(this).attr('checked') == 'checked');
}
else if (this.type == 'radio')
{
if ($(this).prop('checked'))
{
oMap[sName] = $(this).val();
}
}
else
{
oMap[sName] = $(this).val();
}
}
}
});

View File

@@ -720,6 +720,10 @@ $(function()
{
this.refresh_groups(oData.groups);
}
if (oData.lists)
{
this.refresh_lists(oData.lists);
}
if (this.element.is(':visible'))
{
this._updateBBox();
@@ -754,6 +758,25 @@ $(function()
}
}
},
refresh_lists: function(aLists)
{
if ($('#impacted_objects_lists').length > 0)
{
// The "Lists" tab is present, refresh it
if (aLists.length == 0)
{
$('#impacted_objects_lists').html('');
}
else
{
$('#impacted_objects_lists').html('<img src="../images/indicator.gif">');
var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
$.post(sUrl, { operation: 'relation_lists', lists: aLists }, function(data) {
$('#impacted_objects_lists').html(data);
});
}
}
},
_reset_pan_and_zoom: function()
{
this.xPan = 0;

View File

@@ -384,7 +384,7 @@ $(function()
_flatten_fields: function(aFields)
{
// Update the "flattened" via of the fields
this.aFieldsByCode = [];
this.aFieldsByCode = {}; // Must be an object since indexes are non-numeric
for(var k in aFields)
{
for(var i in aFields[k])
@@ -515,4 +515,4 @@ $(function()
});
}
});
});
});

View File

@@ -227,13 +227,18 @@ function ReloadSearchForm(divId, sClassName, sBaseClass, sContext)
for(var index = 0; index < aSubmit.length; index++)
{
// Restore the previously bound submit handlers
var sEventName = 'submit';
if ((aSubmit[index].namespace != undefined) && (aSubmit[index].namespace != ''))
{
sEventName += '.'+aSubmit[index].namespace;
}
if (aSubmit[index].data != undefined)
{
oForm.bind('submit.'+aSubmit[index].namespace, aSubmit[index].data, aSubmit[index].handler)
oForm.bind(sEventName, aSubmit[index].data, aSubmit[index].handler)
}
else
{
oForm.bind('submit.'+aSubmit[index].namespace, aSubmit[index].handler)
oForm.bind(sEventName, aSubmit[index].handler)
}
}
}

View File

@@ -230,6 +230,12 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj)
$sOldRelation = 'depends on';
}
$oP->add("<h1>".MetaModel::GetRelationDescription($sOldRelation).' '.$oObj->GetName()."</h1>\n");
$oP->add("<div id=\"impacted_objects_lists\">");
$oP->add('<img src="../images/indicator.gif">');
/*
* Content is rendered asynchronously via pages/ajax.render.php?operation=relation_lists
*/
/*
$iBlock = 1; // Zero is not a valid blockid
foreach($aResults as $sListClass => $aObjects)
{
@@ -241,6 +247,8 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj)
$oBlock->Display($oP, $iBlock++, array('table_id' => get_class($oObj).'_'.$sRelation.'_'.$sDirection.'_'.$sListClass));
$oP->P('&nbsp;'); // Some space ?
}
*/
$oP->add("</div>");
$oP->add("</div>");
}
@@ -248,6 +256,7 @@ function DisplayNavigatorGroupTab($oP)
{
$oP->SetCurrentTab(Dict::S('UI:RelationGroups'));
$oP->add("<div id=\"impacted_groups\" style=\"width:100%;background-color:#fff;padding:10px;\">");
$oP->add('<img src="../images/indicator.gif">');
/*
* Content is rendered asynchronously via pages/ajax.render.php?operation=relation_groups
*/
@@ -1518,6 +1527,10 @@ EOF
}
}
}
$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
// Display the tabs
if ($sFirstTab == 'list')
{

View File

@@ -2010,7 +2010,24 @@ EOF
$oPage->p('&nbsp;'); // 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');
$oPage->add("<div class=\"page_header\">\n");
$oPage->add("<h2>".MetaModel::GetClassIcon($sListClass)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aKeys), Metamodel::GetName($sListClass))."</h2>\n");
$oPage->add("</div>\n");
$oBlock = new DisplayBlock($oSearch, 'list');
$oBlock->Display($oPage, 'list_'.$iBlock++, array('table_id' => 'ImpactAnalysis_'.$sListClass));
$oPage->p('&nbsp;'); // Some space ?
}
break;
case 'ticket_impact':
require_once(APPROOT.'core/simplegraph.class.inc.php');
require_once(APPROOT.'core/relationgraph.class.inc.php');

View File

@@ -191,6 +191,13 @@ try
*/
function ProcessCSVData(WebPage $oPage, $bSimulate = true)
{
$sClassName = utils::ReadParam('class_name', '', false, 'class');
// Class access right check for the import
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_MODIFY) == UR_ALLOWED_NO)
{
throw new CoreException(Dict::S('UI:ActionNotAllowed'));
}
$aResult = array();
$sCSVData = utils::ReadParam('csvdata', '', false, 'raw_data');
$sCSVDataTruncated = utils::ReadParam('csvdata_truncated', '', false, 'raw_data');
@@ -202,7 +209,6 @@ try
{
$iSkippedLines = utils::ReadParam('nb_skipped_lines', '0');
}
$sClassName = utils::ReadParam('class_name', '', false, 'class');
$aFieldsMapping = utils::ReadParam('field', array(), false, 'raw_data');
$aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name');
$iCurrentStep = $bSimulate ? 4 : 5;

View File

@@ -1,4 +1,4 @@
iTop - version 2.2.0 - 23-September-2015
iTop - version 2.2.1 - 04-February-2016
Readme file
1. ABOUT THIS RELEASE
@@ -6,17 +6,17 @@ Readme file
2.1. Requirements
2.2. Install procedure
2.3. CRON
2.4. Upgrading from 2.0.x
2.4. Upgrading from 2.x.x
2.5. Migration from 1.x versions
3. FEATURES
3.1. Changes since 2.1.0
3.1. Changes since 2.2.0
3.2. Known limitations
3.3. Known issues
1. ABOUT THIS RELEASE
==================
Thank you for downloading the 22nd packaged release of iTop.
This version is a major release, with quite a few bug fixes and significative enhancements.
Thank you for downloading the 23rd packaged release of iTop.
This version is a maintenance release, with quite a few bug fixes.
The documentation about iTop is available as a Wiki: https://wiki.openitop.org/
@@ -25,30 +25,13 @@ The source code of iTop can be found on SourceForge: https://sourceforge.net/p/i
1.1 What's new?
---------------------------
This version brings a number of expected enhancements, namely:
Nothing is really new: this release aims at fixing bugs.
- An new engine to compute and display impact analysis (requires Graphviz on the server, but no longer depends on Flash)
- A complete rework of the exports
- A "printer friendly" version of the details of an object
- A few performance optimizations (APC/APCu required on the server to benefit from them)
- Enhancements to customizations that can be performed in XML
- A lock (not enabled by default) to prevent the concurrent modification of the same object by different agents
- The Czech translation of iTop, thanks to Lukáš Dvořák
... and about 50 bug fixes
1.2 Should I upgrade to 2.2.0?
1.2 Should I upgrade to 2.2.1?
--------------------------
This version is a production quality version and, as such, is suitable for running in production.
iTop 2.2.0 is fully backward compatible with iTop 2.1.0. The new version brings quite a number of
bug fixes and enhancements and this is why we encourage you to upgrade your iTop.
Anyhow, prior to making that decision, we encourage you to have a look at the migration notes:
https://wiki.openitop.org/doku.php?id=2_1_0:admin:210_to_220_migration_notes
Warning:
If you upgrade from the 2.2.0-beta, make sure that the value 'query_cache_enabled' is not set to 'false'
in the iTop configuration file. If so, please either change the value to 'true' or remove the line from
the configuration file. Letting the value set to false causes a severe slow down of the application.
iTop 2.2.1 exhibits the same behavior as 2.2.0.
1.3 Special Thanks To:
@@ -204,212 +187,43 @@ That's it.
3. FEATURES
========
3.1. Changes since 2.1.0
3.1. Changes since 2.2.0
-------------------
This release fixes a few issues:
Modernizations
--------------------
New look: a little bit "flatter" and more modern, but still quite similar to
previous versions of iTop for a smooth migration.
The 'zip' extension is now mandatory to install iTop, since the code relies on
the ZipArchive class for the Excel export and the scheduled backup.
iTop now requires PHP 5.3.0 or higher (instead of PHP 5.2.0).
#1174: support HTML fields in the bulk modify forms (capability to enable/disable the field live)
#1183: more refactoring and some robustness enhancements after tests on big datasets.
#1153: preserve leading zeroes (in "numeric" fields) in the Excel export.
#1183: grouping threshold is now taken into account for "Depends on..." graphs (i.e. grouping backwards)
#1176: empty placeholders are represented by an empty string as in previous version.
#1165 backup with errors fills up tmp-directories with lots of backup-files
#1150: Spurious message "A restore is running..." - FIXED !
Support of derived classes in "add_remove" edition mode for AttributeLinkSet fields (the search form was not refreshing / loading properly when toggling the class to search for).
Make ReloadSearchForm work properly when the "submit" event handler is declared either with or without a "namespace" portion (e.g. 'submit.itop' vs 'submit')
#1164: typo in German localization.
Support the download of "bigger-than-memory" backup files.
Do NOT localize finalclass values in REST/JSON.
#1159 Cannot add edge (impact analysis not working depending on the data model customizations)
#1156: properly escape file paths containing spaces
#1196: Only adminitrator accounts can be used to create attachments by the mean of the REST/JSON API
Better error reporting when the setup fails to create a directory.
Internals:
IconSelectorField (Design time !) can be read-only.
Properly read radio button values inside a form.
Remove _altered_in when exporting the delta.
Keep track of which module altered which node in the XML.
Fixed the computation of the lowest common ancestor.
Dehardcoded OqlUnionQuery::GetClass against the metamodel reflection API
Added AttributeDef::EnumTemplateVerbs, to generate the documentation about the available attribute formatting placeholders
MFFactory: fixed GetDelta when there is no change at all
Added structured error reporting in case of missing dependencies for the modules to install.
Properly create DOMNodes with a text content (beware of XML entities inside the text)
Support validation patterns containing a forward slash
Code refactoring to make the OQL parsing self contained in the "oql" subdirectory.
Added a version number (arbitrary initialized to 2015-08-31 for iTop v2.2.0) to the OQL Lexer/parser.
Do not rely on MetaModel::GetRootClass() to check the data model, use the abstraction of ModelReflection instead to keep the code portable.
Impact analysis
-----------------
The computation of the impact takes the redundancy into account (On
"Power Sources" and on "Farms"), which requires also to take into account
the active tickets.
An new "Impact analysis" tab is now available on tickets, to show the exact
impact of a given ticket (can be exported in PDF and attached to the ticket).
The graphical view no longer depends on Flash (browser-side), but now depends
on Graphviz (server-side) which gives a better disposition of the nodes.
The view can be resized and is exportable in PDF.
The display has been improved and better supports high volumes of data by
automatically grouping similar objects.
The impact analysis can now be customized in XML, but remains backwards
compatible with legacy definitions made by the mean of PHP methods.
Exports
-------
The bulk export has been completely redesigned:
- interactive choice of the columns to export (and their order) as well as all the format specific options
- support for high volumes of data for the interactive export
- the same "export engine" is used for interactive or scripted exports
- new PDF format
- a fields specification can now be an extended attribute code (e.g. location_id->org_id->parent_id->code)
- for full backward compatibility the "old" export.php page still exists, the new export is 'export-v2.php"
- bulk export is now only allowed to users having the "bulk read" privilege on the specified class of objects
Since the new export requires the specification of the exact list of fields to be exported, if the attribute 'fields'
is left empty on a Query Phrase Book item, then the iTop user interface proposes the hyperlink to the legacy export and
displays a message explaining the limitations
The following enhancements/bugs were addressed:
#1120 Export V2 not working when using aliases (ex: SELECT Person AS p)
#1071 Bulk Read access rights
#1034 List of fields for Excel export
#772 Some attributes not exportedvia export.php
Printer friendly version of the details
---------------------------------------
#576 Printable view for object details.
From the detail page of an object, a new action "Printer friendly version" has been
added in the "toolkit" pull-down menu. This action displays in a new page a printer
optimized representation of the details. It is also possible to adjust the output
by interactively hiding/showing certain sections of the page before printing it.
Locking
-------------
Note: The locking mechanism is disabled by default. To enable it, set the configuration
parameter: 'concurrent_lock_enabled' to true in the iTop configuration file.
The new locking mechanism has been introduced to prevent the concurrent interactive
modification of the same object (for example a User Request ticket)by two agents
(or by the same agent in two different tabs of her/his browser). In case of troubles
(e.g. a locked session from an inactive user), an administrator can bypass this lock.
OQL syntax
--------------------
1) The OQL language now supports UNION statements:
SELECT Server WHERE cpu = '...' UNION SELECT PC
Unions support polymorphism: you can use UNION on as many OQL queries as needed as
long as the selected classes have a common ancestor.
Unions can be used anywhere in the application where an OQL query is expected.
2) JOIN ... ON objkey = id
Allow JOIN on a objclass/objkey pair of attributes
Enables queries on the synchronized objects (SynchroReplica::dest_id was changed into
an attribute of type AttributeObjectKey), or with change tracking logs.
Scalability / Performance
-------------------------
Optimization: improvement to the OQL cache:
- take benefit of the APC cache (if present)
- memory indexation may have failed in case of long queries (query id based on a md5)
- added a kpi measure on the OQL parsing
Optimization: when displaying an object details, do not check data synchro for each and every attribute (the cache did exist but was inoperant)
Performance optimization: cache the result of the disk scan looking for icons for dashboards (speeds up the welcome page !)
Optimization of DisplayBlock::FromObjectSet, load only the needed column(s)!
Usability enhancements
----------------------
#714 Localization of the date picker calendar. Get rid of the old jquery.datepicker.js file since iTop now relies on the built-in jQuery UI date picker widget.
#257 Dashlet label hardcoded to "Search for objects of type Server"
#759 Ticket lists in CI: show only active tickets (exclude tickets in states rejected/resolved/closed) and display one list per leaf class so that the status column will be visible. It it not possible anymore to edit the ticket list from the CI.
#788 Whenever a timeout is detected by an ajax request, a popup dialog warns the user to log-in again.
#1092 Caller not preset when creating a ticket from a contact
#1082 Dashlet badge: do not display search results everytime.
#1083 HTML export: show a scroll bar when needed.
Better display of the "Attachments" (addition/removal) in the history, incliding a preview of images.
History display enhancement: whenever a new case log entry is added, display its content in the history.
The display is truncated at a configurable max length. The user can expand/collapse the truncated text, entry per entry.
Usability enhancement: Autocomplete: do NOT clear the typed text when the value does not match one of the possible values,
but clear the actual underlying value so that the input field gets marked as "invalid" if it is mandatory.
More "compact" (but vertically aligned) search forms so that it's easier to find a field and it still works on medium screens.
#1087: the sort order on "group by" dashlets inside a dashboard is now saved as a user preference.
Miscellaneous fixes
-------------------
Log REST/JSON calls (config: 'log_rest_service' => true ; stored as EventRestService)
REST/JSON services. Take the user rights into account. Something was already done for core/create and core/delete, but the symptoms were not clear. The other verbs (update, apply_stimulus, get and get_related) had no protection at all.
#1123/#1133 The optimization on loaded columns in SQL queries was inoperant for some queries, resulting in a stopper issue if such queries were added to a union query (2.2.0 beta)
#1062 bumped the version number of the REST/JSON API to 1.3 to be aligned with the documentation !
#963 For security reasons, "Portal users" are no longer allowed to use the REST/JSON API.
#1078 Properly record the history of LinkedSet(Indirect)
#1079 DBWriteLinks deleting related objects
Bug fix: don't accept attachments (like images) via Chrome's copy/paste since it may duplicate the text content of a normal copy/paste and moreover causes troubles because there is no file name associated with the pasted content.
Small enhancement to the display of the meta model: in the list of transitions, display the code of the event as a tooltip.
JSON/REST: When specifying a case log entry (or the whole), it was not possible to set the user name without knowing a valid user id
Bug fix: prevent a crash of the web services when trying to log a non scalar paramater value...
#1088 Support of HTMLEditor in the PortalWebPage, for example if the description of a ticket is in HTML.
Bug fix: properly compute the URLs/URIs for the soap server (and its extensions)
#1059 fix for the Spanish localization first_name and last_name were swaped.
#1054 increase max_execution_time during the setup.
#1052 Fix for the German localization.
#1050 Properly support the 'list' display style for external keys - as stated in the documentation!
#1047 Fix for the FindTab method.
#1045 Fix in the German localization.
#594 Properly display attachments inside "properties" by closing the span and the fieldset in non-edit mode.
#384: Triggers should not be in the "bizmodel" category. User rights do not apply to such objects...
#1106, #1122: Added a new option 'start_tls' (false by default) and improved debugging capabilities for troubleshooting when something goes wrong with LDAP. Thanks to Karl (karkoff1212) for the hint.
#1148: Fixed dashboards upload: use the more modern fileupload component, since we now hook the ajax call in iTopWebPage and removed references to the old component ajax.fileupload from (almost) everywhere...
#1049: CSV import (and edition) of n:n links. The Differences() function is NOT commutative: the original value (i.e. the one from the database) must the the first argument.
#1144 Audit category having no rule -> PHP notices when showing the report + improved the behavior when the OQL of a rule is wrong.
#1143 Records any change (add/remove/modify) for link sets that can be considered as one of the characteristics of a class (currently those having edit mode = in place)
#1142 Dashboard editor: protects from unwanted "exit" without saving the modifications:
- mark the dashboard as modified when a dashlet was added / moved / deleted
- prevent clicking on the hyperlinks inside the preview of the dashboard
#1091 CAS memberships broken (parameter "cas_memberof" NOT given as a regular expression, bugged since iTop 2.0 or earlier)
#1134 Query returning a "null row": just make sure that the row gets displayed (still surprising... see ticket #1138 to follow up on the suppression of those ghost rows)
#1140 UNION queries not working -in fact, loss of the optimization on column load when filtering on org hierarchies (retrofit possible but the fix will be located in MetaModel)
#564 Prompt for an update in a case log on a lifecycle transition.
#1111 Could not attach a UserRequest to a Problem (1-N links). Could not detach either! This fix requires attention: it is assumed that an item of a link set, if it is "modified" then its key to the current object has already been set.
#1074 Portal: errors when selecting Impact/Urgency, and if the user has access to his organization only.
#1130 CAS authentication security leak when cas_memberof is left empty (already committed into branch 2.1.0)
Secure the server: prevent the users from browsing/getting files from the data and log directories. With Apache, it is still a must to enable htaccess with the spec "AllowOverride All". The index.php files are here to prevent from browsing whatever the HTTP server config.
#1095 Object creation form and bulk modify (final step) not working when using apache-proxy
#1118: fixed strange display of synchro data sources status.
#1121: Regression: "filters" on Triggers had no effect. The regression was caused by the new way of computing placeholders "on the fly" (#803).
#1116 (and #1117): default values for ENUMs must always be expressed as strings.
Fixed a potential XSS vulnerability.
Bug fix: typo causing the generation of invalid SQL queries (in some rare cases).
#1099 and #1014: integration of some German translations.
Extending the data model
------------------------
#1081 Customizations: adjust the dimensions of the HTML Editor (CKEditor). Also fixed an issue when specifying width/height with unit (e.g. "30em") for AttributeText/AttributeLongText
Customizations/XML: clearer error reporting when encountering a duplicate value for an AttributeEnum
#1137: the new XML configuration for the "portal as an extension" was too limited. Now one "allow" profile is enough to allow access to a given portal.
New lifecycle action SetCurrentPerson. Also improved the existing lifecycle action SetCurrentUser to prevent from calling it on an external key that is not pointing to users (!= contact), and if the target attribute is a string, then store the friendlyname there.
#1069 Fix to add a new hierarchical key when there are already some records in the DB
Modules implementing a lifecycle written in PHP (and having actions executed on transitions) do not work until 2.1.0. The compatibility patch had been implemented but it was not working.
XML Enhancement: support injection of new modules treated as data.
XML 1.2: handle the XML transformation. Added APIs to report the functionality loss when downgrading (snippets, portal, module parameters, relations and object key)
XML Enhancement: PHP snippets inside the XML.
XML Enhancement: the default value for a module's parameter can now be specified (and altered) via the XML and will no longer reside in the configuration file.
API Enhancement: allow the API to create Case Log entries with a specified user_login.
Modularization of the portal. The entry points for portals is now defined in XML, and thus can be altered by an extension.
#1053 XML comments breaking the setup with message "Notice: Undefined property: DOMComment::$wholeText in ...modelfactory.class.inc.php on line 1280". Now, the XML comments are allowed.
Internals
----------------------
Make the 'curl' options overridable when calling utils::DoPostRequest()
Allow to stop a stop watch at a specified time (case exchange)
Code cleanup: deprecated the unused (and empty) class CMDBSearchFilter, replaced by DBSearch or DBObjectSearch depending on the usage.
Added an alternate implementation for storing "transaction" identifiers on disk instead of inside the $_SESSION variable.
Mutex instrumentation for troubleshooting...
Make sure that the SQL mutexes are specific to the current iTop instance, but still preserving the capability for the setup to detect an already running cron job with or without a valid config file.
Integrated the lexer/parser build tools (Lexer=0.4.0, Parser=0.1.7)
Implemented GetForJSON and FromJSONToValue for AttributeLinkedSet (though this is not used for the Rest/JSON services which are doing much more) -retrofit from branch 2.1.0
Make it possible to overload RestUtils (static methods called with static:: instead of self::) - iTop NOW REQUIRES PHP 5.3: we have verified, there are very installations of iTop made on PHP 5.2. It is worth to note that PHP 5.3 is already end of life (5.4 will become end of life in 8 months)
Improved the symptom when an error occurs in the "apply stimulus form". The symptom used to be: Object could not be written; unknown error. Now it will give the error message (e.g. Missing query arguments) so as to help in determining what's going on.
ormStopWatch::GetElapsedTime not working in case of queries containing :this-> parameters (the prototype of GetElapsedTime has changed and is NOT compatible with the previous one)
Fixed a typo on the default document mimetype: application/x-octet-stream
Meta information on lifecycle actions arguments: added type restrictions, and added the method ResetStopWatch
Additional markup for JQuery scripts...
Forms Enhancement: do not retrieve disabled fields.
Forms : Support several sets of forbidden values (with a specific "reason" message) per field.
- Read-only "long text" fields no longer appear as editable
- Combo and FormSelector fields are now sorted by default (but sorting can be disabled if needed)
Protect against JS errors when the form is in read-only mode.
Properly handle property_sheets with nested selector fields...
#803 template placeholders are now built on demand.
#1060 Internal: improved the symptoms when calling MetaModel::GetAttributeDef with an invalid attribute code (feedback on the class name and no more FATAL errors)
Internal: fixed the caching of DBObject::ToArgs()
1) Wasn't reset when the object was written the DB (thus having its ID set)
2) Wasn't taking the argument name into account (the list of placeholders was defined by the first caller)
Change of the QueryReflection API to support DesignTime.
ModelFactory: Re-creating a class into another location in the class hierarchy it equivalent to moving that class => the delta must be a "redefine" for the class (improved the comment from the previous commit)
ModelFactory: Re-creating a class into another location in the class hierarchy it equivalent to moving that class => the delta must be a "redefine" for the class
Protects the setup against renaming of non-existing classes. Useful for heavily customized models where some very basic classes have been deleted.

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -296,6 +296,12 @@ class DBBackup
}
if ($iRetCode != 0)
{
// Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...)
if (file_exists($sBackupFileName))
{
unlink($sBackupFileName);
}
$this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode");
foreach($aOutput as $sLine)
{
@@ -367,11 +373,14 @@ class DBBackup
*/
public function DownloadBackup($sFile)
{
$oP = new ajax_page('backup');
$oP->SetContentType("multipart/x-zip");
$oP->SetContentDisposition('inline', basename($sFile));
$oP->add(file_get_contents($sFile));
$oP->output();
header('Content-Description: File Transfer');
header('Content-Type: multipart/x-zip');
header('Content-Disposition: inline; filename="'.basename($sFile).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: '.filesize($sFile));
readfile($sFile);
}
/**

View File

@@ -425,6 +425,7 @@ class ModelFactory
$oModuleNode->setAttribute('id', $oModule->GetId());
$oModuleNode->AppendChild($this->oDOMDocument->CreateElement('root_dir', $oModule->GetRootDir()));
$oModuleNode->AppendChild($this->oDOMDocument->CreateElement('label', $oModule->GetLabel()));
$this->oModules->AppendChild($oModuleNode);
foreach($aDataModels as $sXmlFile)
@@ -474,6 +475,15 @@ class ModelFactory
}
}
$oAlteredNodes = $oXPath->query('/itop_design//*[@_delta]');
if ($oAlteredNodes->length > 0)
{
foreach($oAlteredNodes as $oAlteredNode)
{
$oAlteredNode->SetAttribute('_altered_in', $sModuleName);
}
}
$oFormat = new iTopDesignFormat($oDocument);
if (!$oFormat->Convert())
{
@@ -1081,11 +1091,20 @@ EOF
}
}
}
$oNodesToClean = $oDelta->GetNodes('/itop_design//*[@_altered_in]');
foreach($oNodesToClean as $oNode)
{
$oNode->removeAttribute('_altered_in');
}
if ($aAttributes != null)
{
foreach ($aAttributes as $sAttribute => $value)
{
$oDelta->documentElement->setAttribute($sAttribute, $value);
if ($oDelta->documentElement) // yes, this may happen when still no change has been performed (and a module has been selected for installation)
{
$oDelta->documentElement->setAttribute($sAttribute, $value);
}
}
}
return $oDelta;
@@ -1584,7 +1603,24 @@ class MFElement extends DOMElement
return false;
}
/**
* Check if the given node is (a child of a node) altered by one of the supplied modules
* @param array $aModules The list of module codes to consider
* @return boolean
*/
public function IsAlteredByModule($aModules)
{
// Iterate through the parents: reset the flag if any of them has a flag set
for($oParent = $this ; $oParent instanceof MFElement ; $oParent = $oParent->parentNode)
{
if (in_array($oParent->getAttribute('_altered_in'), $aModules))
{
return true;
}
}
return false;
}
static $aTraceAttributes = null;
/**
* Enable/disable the trace on changed nodes
@@ -1986,6 +2022,23 @@ class MFDocument extends DOMDocument
}
return parent::saveXML();
}
/**
* Overload createElement to make sure (via new DOMText) that the XML entities are
* always properly escaped
* (non-PHPdoc)
* @see DOMDocument::createElement()
*/
function createElement($sName, $value = null, $namespaceURI = null)
{
$oElement = $this->importNode(new MFElement($sName, null, $namespaceURI));
if (!empty($value))
{
$oElement->appendChild(new DOMText($value));
}
return $oElement;
}
/**
* For debugging purposes
*/

View File

@@ -25,6 +25,7 @@
class MissingDependencyException extends Exception
{
public $aModulesInfo;
}
class ModuleDiscovery
@@ -165,14 +166,18 @@ class ModuleDiscovery
}
if ($bAbortOnMissingDependency && count($aDependencies) > 0)
{
$aModulesInfo = array();
$aModuleDeps = array();
foreach($aDependencies as $sId => $aDeps)
{
$aModule = $aModules[$sId];
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on ".implode(' + ', $aDeps);
$aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDeps);
}
$sMessage = "The following modules have unmet dependencies: ".implode(', ', $aModuleDeps);
throw new MissingDependencyException($sMessage);
$oException = new MissingDependencyException($sMessage);
$oException->aModulesInfo = $aModulesInfo;
throw $oException;
}
// Return the ordered list, so that the dependencies are met...
$aResult = array();

View File

@@ -75,7 +75,7 @@ class RunTimeEnvironment
require_once(APPROOT.'/core/filterdef.class.inc.php');
require_once(APPROOT.'/core/stimulus.class.inc.php');
require_once(APPROOT.'/core/MyHelpers.class.inc.php');
require_once(APPROOT.'/core/expression.class.inc.php');
require_once(APPROOT.'/core/oql/expression.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/core/sqlquery.class.inc.php');
require_once(APPROOT.'/core/sqlobjectquery.class.inc.php');
@@ -695,7 +695,7 @@ class RunTimeEnvironment
{
if (!@mkdir($sDir))
{
throw new Exception("Failed to create directory '$sTargetPath', please check the rights of the web server");
throw new Exception("Failed to create directory '$sDir', please check that the web server process has enough rights to create the directory.");
}
@chmod($sDir, 0770); // RWX for owner and group, nothing for others
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -18,7 +18,7 @@
/**
* All the steps of the iTop installation wizard
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -624,11 +624,7 @@ EOF
$this->oWizard->GetParameter('db_user', ''),
$this->oWizard->GetParameter('db_pwd', '')
);
if ($oMutex->TryLock())
{
$oMutex->Unlock();
}
else
if ($oMutex->IsLocked())
{
$oPage->p("<img src=\"../images/error.png\"/>&nbsp;An iTop CRON process is being executed on the target database. It is highly recommended to stop any iTop CRON process prior to running the setup program.");
}

View File

@@ -322,7 +322,10 @@ try
$sSep = "\t";
}
$oLoadStartDate = new DateTime(); // Now
// In case there is a difference between the web server time and the DB server time,
// use the DB server time as a reference since this date/time will be compared with the "status_last_seen"
// column, which is populated by MySQL triggers (and so based on the DB server time)
$oLoadStartDate = new DateTime(CMDBSource::QueryToScalar('SELECT NOW()')); // Now... but as read from the database
// Note about date formatting: These MySQL settings are read-only... and in fact unused :-(
// SET SESSION date_format = '%d/%m/%Y';

View File

@@ -340,6 +340,7 @@ class SynchroDataSource extends cmdbAbstractObject
ToggleSynoptics('#cw_obj_new_unchanged_warnings', aValues['obj_new_unchanged_warnings'] > 0);
ToggleSynoptics('#cw_obj_updated_warnings', aValues['obj_updated_warnings'] > 0);
ToggleSynoptics('#cw_obj_unchanged_warnings', aValues['obj_unchanged_warnings'] > 0);
$('#status_traces').html(aValues['traces']);
}
EOF
;
@@ -391,6 +392,7 @@ EOF
$oPage->add($this->HtmlBox('obj_new_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica.php?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"new_errors_link\">Show</a>"));
$oPage->add("</tr>\n</table>\n");
$oPage->add('</td></tr></table>');
$oPage->add('<div id="status_traces" style="overflow-x:auto"></div>');
$oPage->add_ready_script("UpdateSynoptics('$iLastLog')");
}
else
@@ -445,6 +447,14 @@ EOF
$aData['repl_ignored'] = $iIgnored;
$aData['nb_obj_total'] = $iNew + $iExisting + $iDisappeared;
$aData['nb_replica_total'] = $aData['nb_obj_total'] + $iIgnored;
if(strlen($oLastLog->Get('traces')) > 0)
{
$aData['traces'] = '<fieldset><legend>Debug traces</legend><pre>'.htmlentities($oLastLog->Get('traces'), ENT_QUOTES, 'UTF-8').'</pre></fieldset>';
}
else
{
$aData['traces'] = '';
}
return $aData;
}
@@ -1639,6 +1649,7 @@ class SynchroReplica extends DBObject implements iDisplay
public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange, &$oStatLog)
{
$oStatLog->AddTrace(">>> Beginning of SyncroReplica::Synchro, replica status is '".$this->Get('status')."'.", $this);
$this->ResetWarnings();
switch($this->Get('status'))
{
@@ -1671,11 +1682,14 @@ class SynchroReplica extends DBObject implements iDisplay
$oStatLog->AddTrace("Could not reconcile on null value for attribute '$sFilterCode'", $this);
$this->SetLastError("Could not reconcile on null value for attribute '$sFilterCode'");
$oStatLog->Inc('stats_nb_replica_reconciled_errors');
$oStatLog->AddTrace("<<< End of SyncroReplica::Synchro (error could not reconcile on null value for attribute '$sFilterCode').", $this);
return;
}
}
$oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues);
$iCount = $oDestSet->Count();
$sDebugOQL = $oDestSet->GetFilter()->ToOQL(true);
$oStatLog->AddTrace("Reconciliation query: '$sDebugOQL' returned $iCount object(s).", $this);
$aConditions = array();
foreach($aFilterValues as $sCode => $sValue)
{
@@ -1689,6 +1703,7 @@ class SynchroReplica extends DBObject implements iDisplay
$oStatLog->AddTrace("Nothing found on: $sConditionDesc", $this);
if ($oDataSource->Get('action_on_zero') == 'create')
{
$oStatLog->AddTrace("Calling CreateObjectFromReplica", $this);
$bCreated = $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog);
if ($bCreated)
{
@@ -1717,6 +1732,7 @@ class SynchroReplica extends DBObject implements iDisplay
if ($oDataSource->Get('action_on_one') == 'update')
{
$oDestObj = $oDestSet->Fetch();
$oStatLog->AddTrace("Calling UpdateObjectFromReplica(".(get_class($oDestObj).'::'.$oDestObj->GetKey()).")", $this);
$bModified = $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, 'stats_nb_obj_new', 'stats_nb_replica_reconciled_errors');
$this->Set('dest_id', $oDestObj->GetKey());
$this->Set('dest_class', get_class($oDestObj));
@@ -1819,6 +1835,7 @@ class SynchroReplica extends DBObject implements iDisplay
default: // Do nothing in all other cases
}
$oStatLog->AddTrace("<<< End of SyncroReplica::Synchro.", $this);
}
/**
@@ -2281,6 +2298,11 @@ class SynchroExecution
$this->m_oStatLog->Set('stats_nb_replica_total', $this->m_iCountAllReplicas);
$this->m_oStatLog->DBInsertTracked($this->m_oChange);
$sLastFullLoad = is_object($this->m_oLastFullLoadStartDate) ? $this->m_oLastFullLoadStartDate->format('Y-m-d H:i:s') : 'not specified';
$this->m_oStatLog->AddTrace("###### STARTING SYNCHRONIZATION ##### Total: {$this->m_iCountAllReplicas} replica(s). Last full load: '$sLastFullLoad' ");
$sSql = 'SELECT NOW();';
$sDBNow = CMDBSource::QueryToScalar($sSql);
$this->m_oStatLog->AddTrace("Database server current date/time is '$sDBNow', web server current date/time is: '".date('Y-m-d H:i:s')."'");
}
/**
@@ -2616,6 +2638,7 @@ class SynchroExecution
*/
protected function DoJob1($iMaxReplica = null, $iCurrPos = -1)
{
$this->m_oStatLog->AddTrace(">>> Beginning of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)");
$sLimitDate = $this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s');
// Get all the replicas that were not seen in the last import and mark them as obsolete
@@ -2625,6 +2648,8 @@ class SynchroExecution
$sSelectToObsolete = "SELECT SynchroReplica WHERE id > :curr_pos AND sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import";
$oSetScope = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos));
$iCountScope = $oSetScope->Count();
$sDebugOql = $oSetScope->GetFilter()->ToOQL(true);
$this->m_oStatLog->AddTrace("Searching for replicas to mark as obsolete using query: '$sDebugOql', returned $iCountScope replica(s).");
if (($this->m_iCountAllReplicas > 10) && ($this->m_iCountAllReplicas == $iCountScope) && MetaModel::GetConfig()->Get('synchro_prevent_delete_all'))
{
throw new SynchroExceptionNotStarted(Dict::S('Core:SyncTooManyMissingReplicas'));
@@ -2693,6 +2718,7 @@ class SynchroExecution
{
// Continue with this job!
$this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed);
$this->m_oStatLog->AddTrace("<<< End of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)");
return true;
}
}
@@ -2707,6 +2733,7 @@ class SynchroExecution
// Job complete!
$this->m_oStatLog->Set('status_curr_job', 2);
$this->m_oStatLog->Set('status_curr_pos', -1);
$this->m_oStatLog->AddTrace("<<< End of DoJob1(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)");
return false;
}
@@ -2718,13 +2745,17 @@ class SynchroExecution
*/
protected function DoJob2($iMaxReplica = null, $iCurrPos = -1)
{
$this->m_oStatLog->AddTrace(">>> Beginning of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)");
$sLimitDate = $this->m_oLastFullLoadStartDate->Format('Y-m-d H:i:s');
$this->m_oStatLog->AddTrace("\$sLimitDate = '$sLimitDate'");
// Get all the replicas that are 'new' or modified or synchronized with a warning
//
$sSelectToSync = "SELECT SynchroReplica WHERE id > :curr_pos AND (status = 'new' OR status = 'modified' OR (status = 'synchronized' AND status_last_warning != '')) AND sync_source_id = :source_id AND status_last_seen >= :last_import";
$oSetScope = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->m_oDataSource->GetKey(), 'last_import' => $sLimitDate, 'curr_pos' => $iCurrPos), $this->m_aExtDataSpec);
$iCountScope = $oSetScope->Count();
$sDebugOQL = $oSetScope->GetFilter()->ToOQL(true);
$this->m_oStatLog->AddTrace("Looking for - new, modified or synchonized with a warning - replicas using the OQL query: '$sDebugOQL', returned $iCountScope replicas.");
if ($iMaxReplica)
{
@@ -2742,7 +2773,9 @@ class SynchroExecution
while($oReplica = $oSetToProcess->Fetch())
{
$iLastReplicaProcessed = $oReplica->GetKey();
$this->m_oStatLog->AddTrace("Synchronizing replica id=$iLastReplicaProcessed.");
$oReplica->Synchro($this->m_oDataSource, $this->m_aReconciliationKeys, $this->m_aAttributes, $this->m_oChange, $this->m_oStatLog);
$this->m_oStatLog->AddTrace("Updating replica id=$iLastReplicaProcessed.");
$oReplica->DBUpdateTracked($this->m_oChange);
}
@@ -2752,6 +2785,7 @@ class SynchroExecution
{
// Continue with this job!
$this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed);
$this->m_oStatLog->AddTrace("<<< End of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)");
return true;
}
}
@@ -2759,6 +2793,7 @@ class SynchroExecution
// Job complete!
$this->m_oStatLog->Set('status_curr_job', 3);
$this->m_oStatLog->Set('status_curr_pos', -1);
$this->m_oStatLog->AddTrace("<<< End of DoJob2(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)");
return false;
}
@@ -2770,12 +2805,15 @@ class SynchroExecution
*/
protected function DoJob3($iMaxReplica = null, $iCurrPos = -1)
{
$this->m_oStatLog->AddTrace(">>> Beginning of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos)");
$sDeletePolicy = $this->m_oDataSource->Get('delete_policy');
if ($sDeletePolicy != 'update_then_delete')
{
$this->m_oStatLog->AddTrace("\$sDeletePoliciy = $sDeletePolicy != 'update_then_delete', nothing to do!");
// Job complete!
$this->m_oStatLog->Set('status_curr_job', 0);
$this->m_oStatLog->Set('status_curr_pos', -1);
$this->m_oStatLog->AddTrace("<<< End of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)");
return false;
}
@@ -2825,12 +2863,14 @@ class SynchroExecution
{
// Continue with this job!
$this->m_oStatLog->Set('status_curr_pos', $iLastReplicaProcessed);
$this->m_oStatLog->AddTrace("<<< End of DoJob3\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (returning true => more replicas to process)");
return true;
}
}
// Job complete!
$this->m_oStatLog->Set('status_curr_job', 0);
$this->m_oStatLog->Set('status_curr_pos', -1);
$this->m_oStatLog->AddTrace("<<< End of DoJob3(\$iMaxReplica = $iMaxReplica, \$iCurrPos = $iCurrPos) (completed)");
return false;
}
}

View File

@@ -30,7 +30,7 @@ require_once(APPROOT.'/core/filterdef.class.inc.php');
require_once(APPROOT.'/core/stimulus.class.inc.php');
require_once(APPROOT.'/core/MyHelpers.class.inc.php');
require_once(APPROOT.'/core/expression.class.inc.php');
require_once(APPROOT.'/core/oql/expression.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/core/sqlquery.class.inc.php');
require_once(APPROOT.'/core/sqlobjectquery.class.inc.php');

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2013 Combodo SARL
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
@@ -19,7 +19,7 @@
/**
* Heart beat of the application (process asynchron tasks such as broadcasting email)
*
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @copyright Copyright (C) 2010-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -71,49 +71,49 @@ function UsageAndExit($oP)
function RunTask($oProcess, BackgroundTask $oTask, $oStartDate, $iTimeLimit)
{
$oNow = new DateTime();
$fStart = microtime(true);
try
{
$oNow = new DateTime();
$fStart = microtime(true);
$sMessage = $oProcess->Process($iTimeLimit);
$fDuration = microtime(true) - $fStart;
if ($oTask->Get('total_exec_count') == 0)
{
// First execution
$oTask->Set('first_run_date', $oNow->format('Y-m-d H:i:s'));
}
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
$oTask->Set('latest_run_date', $oNow->format('Y-m-d H:i:s'));
$oRefClass = new ReflectionClass(get_class($oProcess));
if ($oRefClass->implementsInterface('iScheduledProcess'))
{
// Schedules process do repeat at specific moments
$oPlannedStart = $oProcess->GetNextOccurrence();
}
else
{
// Background processes do repeat periodically
$oPlannedStart = new DateTime($oTask->Get('latest_run_date'));
// Let's assume that the task was started exactly when planned so that the schedule does no shift each time
// this allows to schedule a task everyday "around" 11:30PM for example
$oPlannedStart->modify('+'.$oProcess->GetPeriodicity().' seconds');
$oEnd = new DateTime();
if ($oPlannedStart->format('U') < $oEnd->format('U'))
{
// Huh, next planned start is already in the past, shift it of the periodicity !
$oPlannedStart = $oEnd->modify('+'.$oProcess->GetPeriodicity().' seconds');
}
}
$oTask->Set('next_run_date', $oPlannedStart->format('Y-m-d H:i:s'));
$oTask->DBUpdate();
}
catch(Exception $e)
{
$sMessage = 'Processing failed, the following exception occured: '.$e->getMessage();
$sMessage = 'Processing failed with message: '.$e->getMessage();
}
return $sMessage;
$fDuration = microtime(true) - $fStart;
if ($oTask->Get('total_exec_count') == 0)
{
// First execution
$oTask->Set('first_run_date', $oNow->format('Y-m-d H:i:s'));
}
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
$oTask->Set('latest_run_date', $oNow->format('Y-m-d H:i:s'));
$oRefClass = new ReflectionClass(get_class($oProcess));
if ($oRefClass->implementsInterface('iScheduledProcess'))
{
// Schedules process do repeat at specific moments
$oPlannedStart = $oProcess->GetNextOccurrence();
}
else
{
// Background processes do repeat periodically
$oPlannedStart = new DateTime($oTask->Get('latest_run_date'));
// Let's assume that the task was started exactly when planned so that the schedule does no shift each time
// this allows to schedule a task everyday "around" 11:30PM for example
$oPlannedStart->modify('+'.$oProcess->GetPeriodicity().' seconds');
$oEnd = new DateTime();
if ($oPlannedStart->format('U') < $oEnd->format('U'))
{
// Huh, next planned start is already in the past, shift it of the periodicity !
$oPlannedStart = $oEnd->modify('+'.$oProcess->GetPeriodicity().' seconds');
}
}
$oTask->Set('next_run_date', $oPlannedStart->format('Y-m-d H:i:s'));
$oTask->DBUpdate();
return $sMessage;
}
function CronExec($oP, $aProcesses, $bVerbose)