Bladeren bron

NEW! Archiving data. Archiving is a soft delete. It can be undone. Enter the archive mode to see all the data including archives (everything is read-only in that mode). Archiving must be enabled per class (data model). Archiving is achieved by the mean of the API DBObject::Archive (or Unarchive).

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4692 a333f486-631f-4898-b8df-5754b55c2be0
romainq 8 jaren geleden
bovenliggende
commit
34ab6cd77b
54 gewijzigde bestanden met toevoegingen van 1402 en 443 verwijderingen
  1. 7 3
      application/ajaxwebpage.class.inc.php
  2. 39 12
      application/cmdbabstract.class.inc.php
  3. 77 18
      application/itopwebpage.class.inc.php
  4. 4 2
      application/loginwebpage.class.inc.php
  5. 3 3
      application/portalwebpage.class.inc.php
  6. 17 3
      application/utils.inc.php
  7. 194 17
      core/attributedef.class.inc.php
  8. 205 168
      core/cmdbobject.class.inc.php
  9. 139 8
      core/dbobject.class.php
  10. 32 1
      core/dbobjectsearch.class.php
  11. 23 2
      core/dbsearch.class.php
  12. 119 47
      core/metamodel.class.php
  13. 4 6
      core/oql/expression.class.inc.php
  14. 21 2
      core/querybuildercontext.class.inc.php
  15. 1 1
      core/sqlobjectquery.class.inc.php
  16. 26 4
      core/userrights.class.inc.php
  17. 11 3
      core/valuesetdef.class.inc.php
  18. 4 4
      core/xmlbulkexport.class.inc.php
  19. 1 1
      css/css-variables.scss
  20. 90 35
      css/light-grey.css
  21. 71 8
      css/light-grey.scss
  22. 6 1
      datamodels/2.x/itop-portal-base/portal/web/index.php
  23. 9 2
      dictionaries/cs.dictionary.itop.core.php
  24. 4 2
      dictionaries/cs.dictionary.itop.ui.php
  25. 8 2
      dictionaries/da.dictionary.itop.core.php
  26. 11 2
      dictionaries/da.dictionary.itop.ui.php
  27. 8 2
      dictionaries/de.dictionary.itop.core.php
  28. 11 2
      dictionaries/de.dictionary.itop.ui.php
  29. 9 2
      dictionaries/dictionary.itop.core.php
  30. 11 2
      dictionaries/dictionary.itop.ui.php
  31. 9 2
      dictionaries/es_cr.dictionary.itop.core.php
  32. 12 4
      dictionaries/es_cr.dictionary.itop.ui.php
  33. 8 2
      dictionaries/fr.dictionary.itop.core.php
  34. 11 2
      dictionaries/fr.dictionary.itop.ui.php
  35. 9 4
      dictionaries/hu.dictionary.itop.core.php
  36. 12 4
      dictionaries/hu.dictionary.itop.ui.php
  37. 10 5
      dictionaries/it.dictionary.itop.core.php
  38. 12 4
      dictionaries/it.dictionary.itop.ui.php
  39. 9 4
      dictionaries/ja.dictionary.itop.core.php
  40. 12 4
      dictionaries/ja.dictionary.itop.ui.php
  41. 10 5
      dictionaries/nl.dictionary.itop.core.php
  42. 12 4
      dictionaries/nl.dictionary.itop.ui.php
  43. 10 5
      dictionaries/pt_br.dictionary.itop.core.php
  44. 12 4
      dictionaries/pt_br.dictionary.itop.ui.php
  45. 7 0
      dictionaries/ru.dictionary.itop.core.php
  46. 9 0
      dictionaries/ru.dictionary.itop.ui.php
  47. 10 6
      dictionaries/tr.dictionary.itop.core.php
  48. 12 7
      dictionaries/tr.dictionary.itop.ui.php
  49. 9 6
      dictionaries/zh.dictionary.itop.core.php
  50. 9 0
      dictionaries/zh.dictionary.itop.ui.php
  51. 2 1
      js/breadcrumb.js
  52. 20 3
      pages/UI.php
  53. 5 1
      portal/index.php
  54. 6 1
      setup/compiler.class.inc.php

+ 7 - 3
application/ajaxwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
  * Simple web page with no includes, header or fancy formatting, useful to
  * generate HTML fragments when called by an AJAX method
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -53,7 +53,11 @@ class ajax_page extends WebPage implements iTabbedPage
 		$this->sContentType = 'text/html';
 		$this->sContentDisposition = 'inline';
 		$this->m_sMenu = "";
-    }	
+
+		$bArchiveMode = utils::IsArchiveMode();
+		DBSearch::SetArchiveModeDefault($bArchiveMode);
+		if ($bArchiveMode) MetaModel::DBSetReadOnly();
+    }
 
 	public function AddTabContainer($sTabContainer, $sPrefix = '')
 	{

+ 39 - 12
application/cmdbabstract.class.inc.php

@@ -198,8 +198,6 @@ EOF
 			$oBlock->Display($oPage, -1);
 		}
 
-		$oPage->add("<div class=\"page_header\"><h1>".$this->GetIcon()."&nbsp;\n");
-
 		// Master data sources
 		$bSynchronized = false;
 		$aIcons = array();
@@ -292,16 +290,46 @@ EOF
 					$sTip .= "<p style=\"white-space:nowrap\">".$oDataSource->GetIcon(true, 'style="vertical-align:middle"')."&nbsp;$sLink<br/>";
 					$sTip .= Dict::S('Core:Synchro:LastSynchro') . '<br/>' . $sLastSynchro . "</p>";
 				}
-				$aIcons[] = '&nbsp;<img style="vertical-align:middle;" id="synchro_icon" src="../images/locked.png"/>';
+				$sLabel = htmlentities(Dict::S('Tag:Synchronized'), ENT_QUOTES, 'UTF-8');
+				$sSynchroTagId = 'synchro_icon-'.$this->GetKey();
+				$aIcons[] = "<div class=\"tag\" id=\"$sSynchroTagId\"><span class=\"object-synchronized fa fa-lock fa-1x\">&nbsp;</span>&nbsp;$sLabel</div>";
 				$sTip = addslashes($sTip);
-				$oPage->add_ready_script("$('#synchro_icon').qtip( { content: '$sTip', show: 'mouseover', hide: { fixed: true }, style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
+				$oPage->add_ready_script("$('#$sSynchroTagId').qtip( { content: '$sTip', show: 'mouseover', hide: { fixed: true }, style: { name: 'dark', tip: 'topLeft' }, position: { corner: { target: 'bottomMiddle', tooltip: 'topLeft' }} } );");
 			}
 		}
-	
-		$sIcons = implode(' ', $aIcons);
-		$oPage->add(MetaModel::GetName(get_class($this)).": <span class=\"hilite\">".$this->GetName()."</span>$sIcons</h1>\n");
-		$oPage->add("</div>\n");
-		
+
+		if ($this->IsArchived())
+		{
+			$sLabel = htmlentities(Dict::S('Tag:Archived'), ENT_QUOTES, 'UTF-8');
+			$sTitle = htmlentities(Dict::S('Tag:Archived+'), ENT_QUOTES, 'UTF-8');
+			$aIcons[] = "<div class=\"tag\" title=\"$sTitle\"><span class=\"object-archived fa fa-archive fa-1x\">&nbsp;</span>&nbsp;$sLabel</div>";
+		}
+
+		$sObjectIcon = $this->GetIcon();
+		$sClassName = MetaModel::GetName(get_class($this));
+		$sObjectName = $this->GetName();
+		if (count($aIcons) > 0)
+		{
+			$sTags = '<div class="tags">'.implode('&nbsp;', $aIcons).'</div>';
+		}
+		else
+		{
+			$sTags = '';
+		}
+
+		$oPage->add(
+<<<EOF
+<div class="page_header">
+   <div class="object-details-header">
+      <div class ="object-icon">$sObjectIcon</div>
+      <div class ="object-infos">
+		  <h1 class="object-name">$sClassName: <span class="hilite">$sObjectName</span></h1>
+		  $sTags
+      </div>
+   </div>
+</div>
+EOF
+		);
 	}
 
 	function DisplayBareHistory(WebPage $oPage, $bEditMode = false, $iLimitCount = 0, $iLimitStart = 0)
@@ -558,7 +586,6 @@ EOF
 				
 				foreach($aNotificationClasses as $sNotifClass)
 				{
-					
 					$oPage->p(MetaModel::GetClassIcon($sNotifClass, true).'&nbsp;'.MetaModel::GetName($sNotifClass));
 					$oBlock = new DisplayBlock($aNotifSearches[$sNotifClass], 'list', false);
 					$oBlock->Display($oPage, 'notifications_'.$sNotifClass, array('menu' => false));
@@ -1933,7 +1960,7 @@ EOF
 					$aEventsList[] ='validate';
 					$aEventsList[] ='change';
 
-					$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs);
+					$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs, '', $value);
 					$sFieldName = $sFieldPrefix.$sAttCode.$sNameSuffix;
 					$aExtKeyParams = $aArgs;
 					$aExtKeyParams['iFieldSize'] = $oAttDef->GetMaxSize();
@@ -2526,7 +2553,7 @@ EOF
 					{
 						if ($oAttDef->IsExternalKey())
 						{
-							$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs);
+							$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs, '', $this->Get($sAttCode));
 							if ($oAllowedValues->Count() == 1)
 							{
 								$oRemoteObj = $oAllowedValues->Fetch();

+ 77 - 18
application/itopwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Class iTopWebPage
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -34,7 +34,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 {
 	private $m_sMenu;
 	//	private $m_currentOrganization;
-	private $m_sMessage;
+	private $m_aMessages;
 	private $m_sInitScript;
 	protected $m_oTabs;
 	protected $bBreadCrumbEnabled;
@@ -63,8 +63,12 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 			$this->bBreadCrumbEnabled = false;
 		}
 
+		$bArchiveMode = utils::IsArchiveMode();
+		DBSearch::SetArchiveModeDefault($bArchiveMode);
+		if ($bArchiveMode) MetaModel::DBSetReadOnly();
+
 		$this->m_sMenu = "";
-		$this->m_sMessage = '';
+		$this->m_aMessages = array();
 		$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
 		$this->add_header("Content-type: text/html; charset=utf-8");
 		$this->add_header("Cache-control: no-cache");
@@ -75,6 +79,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 		$this->add_linked_stylesheet("../css/jquery.multiselect.css");
 		$this->add_linked_stylesheet("../css/magnific-popup.css");
 		$this->add_linked_stylesheet("../css/c3.min.css");
+		$this->add_linked_stylesheet("../css/font-awesome/css/font-awesome.min.css");
 
 		$this->add_linked_script('../js/jquery.layout.min.js');
 		$this->add_linked_script('../js/jquery.ba-bbq.min.js');
@@ -121,6 +126,23 @@ function ShowAboutBox()
 	});
 	return false;
 }
+function ArchiveMode(bEnable)
+{
+	var sPrevUrl = StripArchiveArgument(window.location.search);
+	if (bEnable)
+	{
+		window.location.search = sPrevUrl + '&with-archive=1';
+	}
+	else
+	{
+		window.location.search = sPrevUrl + '&with-archive=0';
+	}
+}
+function StripArchiveArgument(sUrl)
+{
+	var res = sUrl.replace(/&with-archive=[01]/g, '');
+	return res;
+}
 EOF
 			);
 		}
@@ -732,7 +754,7 @@ EOF
 
 		if (UserRights::IsAdministrator() && ExecutionKPI::IsEnabled())
 		{
-			$sNorthPane .= '<div id="admin-banner"><span style="padding:5px;">'.ExecutionKPI::GetDescription().'<span></div>';
+			$sNorthPane .= '<div class="app-message"><span style="padding:5px;">'.ExecutionKPI::GetDescription().'<span></div>';
 		}
 		
 		//$sSouthPane = '<p>Peak memory Usage: '.sprintf('%.3f MB', memory_get_peak_usage(true) / (1024*1024)).'</p>';
@@ -982,7 +1004,20 @@ EOF
 
 			$oPrefs = new URLPopupMenuItem('UI:Preferences', Dict::S('UI:Preferences'), utils::GetAbsoluteUrlAppRoot()."pages/preferences.php?".$oAppContext->GetForLink());
 			$aActions[$oPrefs->GetUID()] = $oPrefs->GetMenuItem();
-				
+
+			if (utils::IsArchiveMode())
+			{
+				$oExitArchive = new JSPopupMenuItem('UI:ArchiveModeOff', Dict::S('UI:ArchiveModeOff'), 'return ArchiveMode(false);');
+				$aActions[$oExitArchive->GetUID()] = $oExitArchive->GetMenuItem();
+
+				$sIcon = '<span class="fa fa-lock fa-1x"></span>';
+				$this->AddApplicationMessage(Dict::S('UI:ArchiveMode:Banner'), $sIcon, Dict::S('UI:ArchiveMode:Banner+'));
+			}
+			elseif (UserRights::CanBrowseArchive())
+			{
+				$oBrowseArchive = new JSPopupMenuItem('UI:ArchiveModeOn', Dict::S('UI:ArchiveModeOn'), 'return ArchiveMode(true);');
+				$aActions[$oBrowseArchive->GetUID()] = $oBrowseArchive->GetMenuItem();
+			}
 			if (utils::CanLogOff())
 			{
 				$oLogOff = new URLPopupMenuItem('UI:LogOffMenu', Dict::S('UI:LogOffMenu'), utils::GetAbsoluteUrlAppRoot().'pages/logoff.php?operation=do_logoff');
@@ -1014,26 +1049,34 @@ EOF
 				$sRestrictions = Dict::S('UI:AccessRO-Users');
 			}
 
-			$sApplicationBanner = '';
 			if (strlen($sRestrictions) > 0)
 			{
+				$sIcon =
+<<<EOF
+<span class="fa-stack fa-sm">
+  <i class="fa fa-pencil fa-flip-horizontal fa-stack-1x"></i>
+  <i class="fa fa-ban fa-stack-2x text-danger"></i>
+</span>
+EOF;
+
 				$sAdminMessage = trim(MetaModel::GetConfig()->Get('access_message'));
-				$sApplicationBanner .= '<div id="admin-banner">';
-				$sApplicationBanner .= '<img src="../images/locked.png" style="vertical-align:middle;">';
-				$sApplicationBanner .= '&nbsp;<b>'.$sRestrictions.'</b>';
 				if (strlen($sAdminMessage) > 0)
 				{
-					$sApplicationBanner .= '&nbsp;<b>'.$sAdminMessage.'</b>';
+					$sRestrictions .= '&nbsp;'.$sAdminMessage;
 				}
-				$sApplicationBanner .= '</div>';
+				$this->AddApplicationMessage($sRestrictions, $sIcon);
 			}
 
-			if(strlen($this->m_sMessage))
+			$sApplicationMessages = '';
+			foreach ($this->m_aMessages as $aMessage)
 			{
-				$sApplicationBanner .= '<div id="admin-banner"><span style="padding:5px;">'.$this->m_sMessage.'<span></div>';
+				$sHtmlIcon = $aMessage['icon'] ? $aMessage['icon'] : '';
+				$sHtmlMessage = $aMessage['message'];
+				$sTitleAttr = $aMessage['tip'] ? 'title="'.htmlentities($aMessage['tip'], ENT_QUOTES, 'UTF-8').'"' : '';
+				$sApplicationMessages .= '<div class="app-message" '.$sTitleAttr.'><span class="app-message-icon">'.$sHtmlIcon.'</span><span class="app-message-body">'.$sHtmlMessage.'</div></span>';
 			}
 
-			$sApplicationBanner .= $sBannerExtraHtml;
+			$sApplicationBanner = "<div class=\"app-banner ui-helper-clearfix\">$sApplicationMessages$sBannerExtraHtml</div>";
 			
 			if (!empty($sNorthPane))
 			{
@@ -1333,10 +1376,26 @@ EOF
 	}
 
 	/**
-	 * Set the message to be displayed in the 'admin-banner' section at the top of the page
+	 * Set the message to be displayed in the 'app-banner' section at the top of the page
 	 */
-	public function SetMessage($sMessage)
+	public function SetMessage($sHtmlMessage)
 	{
-			$this->m_sMessage = $sMessage;
+		$sHtmlIcon = '<span class="fa fa-comment fa-1x"></span>';
+		$this->AddApplicationMessage($sHtmlMessage, $sHtmlIcon);
+	}
+
+	/**
+	 * Add message to be displayed in the 'app-banner' section at the top of the page
+	 */
+	public function AddApplicationMessage($sHtmlMessage, $sHtmlIcon = null, $sTip = null)
+	{
+		if (strlen($sHtmlMessage))
+		{
+			$this->m_aMessages[] = array(
+				'icon' => $sHtmlIcon,
+				'message' => $sHtmlMessage,
+				'tip' => $sTip
+			);
+		}
 	}
 }

+ 4 - 2
application/loginwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Class LoginWebPage
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -430,6 +430,8 @@ EOF
 		// Unset all of the session variables.
 		unset($_SESSION['auth_user']);
 		unset($_SESSION['login_mode']);
+		unset($_SESSION['archive_mode']);
+		unset($_SESSION['archive_allowed']);
 		UserRights::_ResetSessionCache();
 		// If it's desired to kill the session, also delete the session cookie.
 		// Note: This will destroy the session, and not just the session data!

+ 3 - 3
application/portalwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Class PortalWebPage
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -387,7 +387,7 @@ EOF
 		{
 			$sReadOnly = Dict::S('UI:AccessRO-Users');
 			$sAdminMessage = trim(MetaModel::GetConfig()->Get('access_message'));
-			$sApplicationBanner .= '<div id="admin-banner">';
+			$sApplicationBanner .= '<div class="app-message">';
 			$sApplicationBanner .= '<img src="../images/locked.png" style="vertical-align:middle;">';
 			$sApplicationBanner .= '&nbsp;<b>'.$sReadOnly.'</b>';
 			if (strlen($sAdminMessage) > 0)

+ 17 - 3
application/utils.inc.php

@@ -1,7 +1,7 @@
 <?php
 use Html2Text\Html2Text;
 use Leafo\ScssPhp\Compiler;
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -22,7 +22,7 @@ use Leafo\ScssPhp\Compiler;
 /**
  * Static class utils
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -140,6 +140,20 @@ class utils
 		}
 	}
 
+	public static function IsArchiveMode()
+	{
+		if (isset($_SESSION['archive_mode']))
+		{
+			$iDefault = $_SESSION['archive_mode'];
+		}
+		else
+		{
+			$iDefault = 0;
+		}
+		$iCurrent = self::ReadParam('with-archive', $iDefault, true);
+		$_SESSION['archive_mode'] = $iCurrent;
+		return ($iCurrent == 1);
+	}
 
 	public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false, $sSanitizationFilter = 'parameter')
 	{
@@ -700,7 +714,7 @@ class utils
 		}
 		return $bResult;
 	}
-	
+
 	/**
 	 * Initializes the CAS client
 	 */

+ 194 - 17
core/attributedef.class.inc.php

@@ -232,7 +232,8 @@ abstract class AttributeDefinition
 	public function IsExternalKey($iType = EXTKEY_RELATIVE) {return false;} 
 	public function IsHierarchicalKey() {return false;}
 	public function IsExternalField() {return false;} 
-	public function IsWritable() {return false;} 
+	public function IsWritable() {return false;}
+	public function IsMagic() {return $this->GetOptional('magic', false);}
 	public function LoadInObject() {return true;}
 	public function LoadFromDB() {return true;}
 	public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);}
@@ -399,6 +400,7 @@ abstract class AttributeDefinition
 	public function GetSQLColumns($bFullSpec = false) {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
 	public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
 	public function RequiresIndex() {return false;}
+	public function CopyOnAllTables() {return false;}
 
 	public function GetOrderBySQLExpressions($sClassAlias)
 	{
@@ -1416,7 +1418,7 @@ class AttributeDBFieldVoid extends AttributeDefinition
 
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
-	public function IsWritable() {return true;} 
+	public function IsWritable() {return !$this->IsMagic();}
 	public function GetSQLExpr()
 	{
 		return $this->Get("sql");
@@ -1799,7 +1801,7 @@ class AttributeBoolean extends AttributeInteger
 
 	public function GetEditClass() {return "Integer";}
 	protected function GetSQLCol($bFullSpec = false) {return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : '');}
-	
+
 	public function MakeRealValue($proposedValue, $oHostObj)
 	{
 		if (is_null($proposedValue)) return null;
@@ -1814,25 +1816,169 @@ class AttributeBoolean extends AttributeInteger
 		return 0;
 	}
 
-	public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
+	public function GetValueLabel($bValue)
 	{
-		return $sValue ? '1' : '0';
+		if (is_null($bValue))
+		{
+			$sLabel = Dict::S('Core:'.get_class($this).'/Value:null');
+		}
+		else
+		{
+			$sValue = $bValue ? 'yes' : 'no';
+			$sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue);
+			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/);
+		}
+		return $sLabel;
 	}
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false)
+
+	public function GetValueDescription($bValue)
+	{
+		if (is_null($bValue))
+		{
+			$sDescription = Dict::S('Core:'.get_class($this).'/Value:null+');
+		}
+		else
+		{
+			$sValue = $bValue ? 'yes' : 'no';
+			$sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+');
+			$sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault, true /*user lang*/);
+		}
+		return $sDescription;
+	}
+
+	public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true)
 	{
-		return $sValue ? '1' : '0';
+		if (is_null($bValue))
+		{
+			$sRes = '';
+		}
+		elseif ($bLocalize)
+		{
+			$sLabel = $this->GetValueLabel($bValue);
+			$sDescription = $this->GetValueDescription($bValue);
+			// later, we could imagine a detailed description in the title
+			$sRes = "<span title=\"$sDescription\">".parent::GetAsHtml($sLabel)."</span>";
+		}
+		else
+		{
+			$sRes = $bValue ? 'yes' : 'no';
+		}
+		return $sRes;
 	}
-	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+
+	public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true)
+	{
+		if (is_null($bValue))
+		{
+			$sFinalValue = '';
+		}
+		elseif ($bLocalize)
+		{
+			$sFinalValue = $this->GetValueLabel($bValue);
+		}
+		else
+		{
+			$sFinalValue = $bValue ? 'yes' : 'no';
+		}
+		$sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize);
+		return $sRes;
+	}
+
+	public function GetAsCSV($bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false)
+	{
+		if (is_null($bValue))
+		{
+			$sFinalValue = '';
+		}
+		elseif ($bLocalize)
+		{
+			$sFinalValue = $this->GetValueLabel($bValue);
+		}
+		else
+		{
+			$sFinalValue = $bValue ? 'yes' : 'no';
+		}
+		$sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize);
+		return $sRes;
+	}
+
+	static public function GetFormFieldClass()
+	{
+		return '\\Combodo\\iTop\\Form\\Field\\SelectField';
+	}
+
+	public function MakeFormField(DBObject $oObject, $oFormField = null)
+	{
+		if ($oFormField === null)
+		{
+			$sFormFieldClass = static::GetFormFieldClass();
+			$oFormField = new $sFormFieldClass($this->GetCode());
+		}
+
+		$oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false)));
+		parent::MakeFormField($oObject, $oFormField);
+
+		return $oFormField;
+	}
+
+	public function GetEditValue($value, $oHostObj = null)
 	{
-		return $sValue ? '1' : '0';
+		if (is_null($value))
+		{
+			return '';
+		}
+		else
+		{
+			return $this->GetValueLabel($value);
+		}
 	}
+
 	/**
 	 * Helper to get a value that will be JSON encoded
-	 * The operation is the opposite to FromJSONToValue	 
-	 */	 	
+	 * The operation is the opposite to FromJSONToValue
+	 */
 	public function GetForJSON($value)
 	{
-		return $value ? '1' : '0';
+		return (bool)$value;
+	}
+
+	public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
+	{
+		$sInput = strtolower(trim($sProposedValue));
+		if ($bLocalizedValue)
+		{
+			switch ($sInput)
+			{
+				case '1': // backward compatibility
+				case $this->GetValueLabel(true):
+					$value = true;
+					break;
+				case '0': // backward compatibility
+				case 'no':
+				case $this->GetValueLabel(false):
+					$value = false;
+					break;
+				default:
+					$value = null;
+			}
+		}
+		else
+		{
+			switch ($sInput)
+			{
+				case '1': // backward compatibility
+				case 'yes':
+					$value = true;
+					break;
+				case '0': // backward compatibility
+				case 'no':
+					$value = false;
+					break;
+				default:
+					$value = null;
+			}
+		}
+		return $value;
 	}
 }
 
@@ -2088,6 +2234,10 @@ class AttributeFinalClass extends AttributeString
 	{
 		return false;
 	}
+	public function IsMagic()
+	{
+		return true;
+	}
 
 	public function RequiresIndex()
 	{
@@ -4312,10 +4462,10 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 		}
 	}
 
-	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '')
+	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
 	{
 		$oValSetDef = $this->GetValuesDef();
-		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains);
+		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
 		return $oSet;
 	}
 
@@ -4535,7 +4685,7 @@ class AttributeHierarchicalKey extends AttributeExternalKey
 		}
 	}
 
-	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '')
+	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
 	{
 		$oValSetDef = $this->GetValuesDef();
 		if (array_key_exists('this', $aArgs))
@@ -4551,7 +4701,7 @@ class AttributeHierarchicalKey extends AttributeExternalKey
 				$oValSetDef->AddCondition($oFilter);
 			}
 		}
-		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains);
+		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
 		return $oSet;
 	}
 
@@ -5265,7 +5415,7 @@ class AttributeStopWatch extends AttributeDefinition
 	
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
-	public function IsWritable() {return false;} 
+	public function IsWritable() {return true;}
 	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();}
 
 	public function GetEditValue($value, $oHostObj = null)
@@ -6630,6 +6780,10 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 	{
 		return false;
 	}
+	public function IsMagic()
+	{
+		return true;
+	}
 
 	public function IsDirectField()
 	{
@@ -7440,3 +7594,26 @@ class AttributeCustomFields extends AttributeDefinition
 	}
 }
 
+class AttributeArchiveFlag extends AttributeBoolean
+{
+	public function __construct($sCode)
+	{
+		parent::__construct($sCode, array("allowed_values" => null, "sql" => $sCode, "default_value" => false, "is_null_allowed" => false, "depends_on" => array()));
+	}
+	public function RequiresIndex()
+	{
+		return true;
+	}
+	public function CopyOnAllTables()
+	{
+		return true;
+	}
+	public function IsWritable()
+	{
+		return false;
+	}
+	public function IsMagic()
+	{
+		return true;
+	}
+}

+ 205 - 168
core/cmdbobject.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Class cmdbObject
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -231,206 +231,219 @@ abstract class CMDBObject extends DBObject
 		$iId = $oMyChangeOp->DBInsertNoReload();
 	}
 
-	protected function RecordAttChanges(array $aValues, array $aOrigValues)
+	/**
+	 * @param $sAttCode
+	 * @param $original Original value
+	 * @param $value Current value
+	 */
+	protected function RecordAttChange($sAttCode, $original, $value)
 	{
-		parent::RecordAttChanges($aValues, $aOrigValues);
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+		if ($oAttDef->IsExternalField()) return;
+		if ($oAttDef->IsLinkSet()) return;
+		if ($oAttDef->GetTrackingLevel() == ATTRIBUTE_TRACKING_NONE) return;
 
-		// $aValues is an array of $sAttCode => $value
-		//
-		foreach ($aValues as $sAttCode=> $value)
+		if ($oAttDef instanceOf AttributeOneWayPassword)
 		{
-			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-			if ($oAttDef->IsExternalField()) continue;
-			if ($oAttDef->IsLinkSet()) continue;
-			if ($oAttDef->GetTrackingLevel() == ATTRIBUTE_TRACKING_NONE) continue;
+			// One Way encrypted passwords' history is stored -one way- encrypted
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
 
-			if (array_key_exists($sAttCode, $aOrigValues))
+			if (is_null($original))
 			{
-				$original = $aOrigValues[$sAttCode];
+				$original = '';
 			}
-			else
+			$oMyChangeOp->Set("prev_pwd", $original);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeEncryptedString)
+		{
+			// Encrypted string history is stored encrypted
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+
+			if (is_null($original))
 			{
-				$original = null;
+				$original = '';
 			}
+			$oMyChangeOp->Set("prevstring", $original);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeBlob)
+		{
+			// Data blobs
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
 
-			if ($oAttDef instanceOf AttributeOneWayPassword)
+			if (is_null($original))
 			{
-				// One Way encrypted passwords' history is stored -one way- encrypted
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-
-				if (is_null($original))
-				{
-					$original = '';
-				}
-				$oMyChangeOp->Set("prev_pwd", $original);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$original = new ormDocument();
 			}
-			elseif ($oAttDef instanceOf AttributeEncryptedString)
+			$oMyChangeOp->Set("prevdata", $original);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeStopWatch)
+		{
+			// Stop watches - record changes for sub items only (they are visible, the rest is not visible)
+			//
+			foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef)
 			{
-				// Encrypted string history is stored encrypted
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
+				$item_value = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $value, $this);
+				$item_original = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $original, $this);
 
-				if (is_null($original))
+				if ($item_value != $item_original)
 				{
-					$original = '';
+					$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+					$oMyChangeOp->Set("objclass", get_class($this));
+					$oMyChangeOp->Set("objkey", $this->GetKey());
+					$oMyChangeOp->Set("attcode", $sSubItemAttCode);
+
+					$oMyChangeOp->Set("oldvalue", $item_original);
+					$oMyChangeOp->Set("newvalue", $item_value);
+					$iId = $oMyChangeOp->DBInsertNoReload();
 				}
-				$oMyChangeOp->Set("prevstring", $original);
-				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
-			elseif ($oAttDef instanceOf AttributeBlob)
-			{
-				// Data blobs
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
+		}
+		elseif ($oAttDef instanceOf AttributeCaseLog)
+		{
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
 
-				if (is_null($original))
-				{
-					$original = new ormDocument();
-				}
-				$oMyChangeOp->Set("prevdata", $original);
-				$iId = $oMyChangeOp->DBInsertNoReload();
-			}
-			elseif ($oAttDef instanceOf AttributeStopWatch)
+			$oMyChangeOp->Set("lastentry", $value->GetLatestEntryIndex());
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeLongText)
+		{
+			// Data blobs
+			if ($oAttDef->GetFormat() == 'html')
 			{
-				// Stop watches - record changes for sub items only (they are visible, the rest is not visible)
-				//
-				foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef)
-				{
-					$item_value = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $value, $this);
-					$item_original = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $original, $this);
-
-					if ($item_value != $item_original)
-					{
-						$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-						$oMyChangeOp->Set("objclass", get_class($this));
-						$oMyChangeOp->Set("objkey", $this->GetKey());
-						$oMyChangeOp->Set("attcode", $sSubItemAttCode);
-		
-						$oMyChangeOp->Set("oldvalue", $item_original);
-						$oMyChangeOp->Set("newvalue", $item_value);
-						$iId = $oMyChangeOp->DBInsertNoReload();
-					}
-				}
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML");
 			}
-			elseif ($oAttDef instanceOf AttributeCaseLog)
+			else
 			{
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-
-				$oMyChangeOp->Set("lastentry", $value->GetLatestEntryIndex());
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeLongText");
 			}
-			elseif ($oAttDef instanceOf AttributeLongText)
-			{
-				// Data blobs
-				if ($oAttDef->GetFormat() == 'html')
-				{
-					$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML");
-				}
-				else
-				{
-					$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeLongText");
-				}
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
 
-				if (!is_null($original) && ($original instanceof ormCaseLog))
-				{
-					$original = $original->GetText();
-				}
-				$oMyChangeOp->Set("prevdata", $original);
-				$iId = $oMyChangeOp->DBInsertNoReload();
-			}
-			elseif ($oAttDef instanceOf AttributeText)
+			if (!is_null($original) && ($original instanceof ormCaseLog))
 			{
-				// Data blobs
-				if ($oAttDef->GetFormat() == 'html')
-				{
-					$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML");
-				}
-				else
-				{
-					$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText");
-				}
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-
-				if (!is_null($original) && ($original instanceof ormCaseLog))
-				{
-					$original = $original->GetText();
-				}
-				$oMyChangeOp->Set("prevdata", $original);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$original = $original->GetText();
 			}
-			elseif ($oAttDef instanceOf AttributeBoolean)
+			$oMyChangeOp->Set("prevdata", $original);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeText)
+		{
+			// Data blobs
+			if ($oAttDef->GetFormat() == 'html')
 			{
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-				$oMyChangeOp->Set("oldvalue", $original ? 1 : 0);
-				$oMyChangeOp->Set("newvalue", $value ? 1 : 0);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML");
 			}
-			elseif ($oAttDef instanceOf AttributeHierarchicalKey)
+			else
 			{
-				// Hierarchical keys
-				//
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-				$oMyChangeOp->Set("oldvalue", $original);
-				$oMyChangeOp->Set("newvalue", $value[$sAttCode]);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText");
 			}
-			elseif ($oAttDef instanceOf AttributeCustomFields)
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+
+			if (!is_null($original) && ($original instanceof ormCaseLog))
 			{
-				// Custom fields
-				//
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCustomFields");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-				$oMyChangeOp->Set("prevdata", json_encode($original->GetValues()));
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$original = $original->GetText();
 			}
-			elseif ($oAttDef instanceOf AttributeURL)
+			$oMyChangeOp->Set("prevdata", $original);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeBoolean)
+		{
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("oldvalue", $original ? 1 : 0);
+			$oMyChangeOp->Set("newvalue", $value ? 1 : 0);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeHierarchicalKey)
+		{
+			// Hierarchical keys
+			//
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("oldvalue", $original);
+			$oMyChangeOp->Set("newvalue", $value[$sAttCode]);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeCustomFields)
+		{
+			// Custom fields
+			//
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCustomFields");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("prevdata", json_encode($original->GetValues()));
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		elseif ($oAttDef instanceOf AttributeURL)
+		{
+			// URLs
+			//
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeURL");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("oldvalue", $original);
+			$oMyChangeOp->Set("newvalue", $value);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+		else
+		{
+			// Scalars
+			//
+			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+			$oMyChangeOp->Set("objclass", get_class($this));
+			$oMyChangeOp->Set("objkey", $this->GetKey());
+			$oMyChangeOp->Set("attcode", $sAttCode);
+			$oMyChangeOp->Set("oldvalue", $original);
+			$oMyChangeOp->Set("newvalue", $value);
+			$iId = $oMyChangeOp->DBInsertNoReload();
+		}
+	}
+
+	/**
+	 * @param array $aValues
+	 * @param array $aOrigValues
+	 */
+	protected function RecordAttChanges(array $aValues, array $aOrigValues)
+	{
+		parent::RecordAttChanges($aValues, $aOrigValues);
+
+		// $aValues is an array of $sAttCode => $value
+		//
+		foreach ($aValues as $sAttCode=> $value)
+		{
+			if (array_key_exists($sAttCode, $aOrigValues))
 			{
-				// URLs
-				//
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeURL");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-				$oMyChangeOp->Set("oldvalue", $original);
-				$oMyChangeOp->Set("newvalue", $value);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$original = $aOrigValues[$sAttCode];
 			}
 			else
 			{
-				// Scalars
-				//
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-				$oMyChangeOp->Set("oldvalue", $original);
-				$oMyChangeOp->Set("newvalue", $value);
-				$iId = $oMyChangeOp->DBInsertNoReload();
+				$original = null;
 			}
+			$this->RecordAttChange($sAttCode, $original, $value);
 		}
 	}
 
@@ -596,6 +609,30 @@ abstract class CMDBObject extends DBObject
 		}
 		return $ret;
 	}
+
+	public function DBArchive()
+	{
+		// Note: do the job anyway, so as to repair any DB discrepancy
+		$bOriginal = $this->Get('archive_flag');
+		parent::DBArchive();
+
+		if (!$bOriginal)
+		{
+			$this->RecordAttChange('archive_flag', false, true);
+		}
+	}
+
+	public function DBUnarchive()
+	{
+		// Note: do the job anyway, so as to repair any DB discrepancy
+		$bOriginal = $this->Get('archive_flag');
+		parent::DBUnarchive();
+
+		if ($bOriginal)
+		{
+			$this->RecordAttChange('archive_flag', true, false);
+		}
+	}
 }
 
 
@@ -649,5 +686,5 @@ class CMDBObjectSet extends DBObjectSet
 			$oRetSet->AddObjectExtended($aObjectsByClassAlias);
 		}
 		return $oRetSet;
-	} 
+	}
 }

+ 139 - 8
core/dbobject.class.php

@@ -226,6 +226,18 @@ abstract class DBObject implements iDisplay
 
 			$oLinkSearch = new DBObjectSearch($sLinkClass);
 			$oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe);
+			if ($oAttDef->IsIndirect())
+			{
+				// Join the remote class so that the archive flag will be taken into account
+				$sExtKeyToRemote = $oAttDef->GetExtKeyToRemote();
+				$oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote);
+				$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+				if (MetaModel::IsArchivable($sRemoteClass))
+				{
+					$oRemoteSearch = new DBObjectSearch($sRemoteClass);
+					$oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $oAttDef->GetExtKeyToRemote());
+				}
+			}
 			$oLinks = new DBObjectSet($oLinkSearch);
 
 			$this->m_aCurrValues[$sAttCode] = $oLinks;
@@ -360,8 +372,15 @@ abstract class DBObject implements iDisplay
 			// Ignore it - this attribute is set upon object creation and that's it
 			return false;
 		}
-		
+
 		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+
+		if (!$oAttDef->IsWritable())
+		{
+			$sClass = get_class($this);
+			throw new Exception("Attempting to set the value on the read-only attribute $sClass::$sAttCode");
+		}
+
 		if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty)
 		{
 			// First time Set is called... ensure that the object gets fully loaded
@@ -742,7 +761,8 @@ abstract class DBObject implements iDisplay
 			else
 			{
 				$sHtmlLabel = htmlentities($this->Get($sAttCode.'_friendlyname'), ENT_QUOTES, 'UTF-8');
-				return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel);
+				$bArchived = $this->IsArchived($sAttCode);
+				return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel, null, true, $bArchived);
 			}
 		}
 
@@ -821,10 +841,11 @@ abstract class DBObject implements iDisplay
 	 * @param string $sHtmlLabel Label with HTML entities escaped (< escaped as &lt;)
 	 * @param null $sUrlMakerClass
 	 * @param bool|true $bWithNavigationContext
+	 * @param bool|false $bArchived
 	 * @return string
 	 * @throws DictExceptionMissingString
 	 */
-	public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true)
+	public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true, $bArchived = false)
 	{
 		if ($sObjKey <= 0) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs
 
@@ -847,19 +868,51 @@ abstract class DBObject implements iDisplay
 		}
 		$sHint = MetaModel::GetName($sObjClass)."::$sObjKey";
 		$sUrl = ApplicationContext::MakeObjectUrl($sObjClass, $sObjKey, $sUrlMakerClass, $bWithNavigationContext);
-		if (strlen($sUrl) > 0)
+
+		$bClickable = !$bArchived || utils::IsArchiveMode();
+		if ($bArchived)
 		{
-			return "<a href=\"$sUrl\" title=\"$sHint\">$sHtmlLabel</a>";
+			$sSpanClass = 'archived';
+			$sFA = 'fa-archive object-archived';
+			$sHint = Dict::S('ObjectRef:Archived');
 		}
 		else
 		{
-			return $sHtmlLabel;
+			$sSpanClass = '';
+			$sFA = '';
+		}
+		if ($sFA == '')
+		{
+			$sIcon = '';
 		}
+		else
+		{
+			if ($bClickable)
+			{
+				$sIcon = "<span class=\"object-ref-icon fa $sFA fa-1x fa-fw\"></span>";
+			}
+			else
+			{
+				$sIcon = "<span class=\"object-ref-icon-disabled fa $sFA fa-1x fa-fw\"></span>";
+			}
+		}
+
+		if ($bClickable && (strlen($sUrl) > 0))
+		{
+			$sHLink = "<a class=\"object-ref-link\" href=\"$sUrl\">$sIcon$sHtmlLabel</a>";
+		}
+		else
+		{
+			$sHLink = $sIcon.$sHtmlLabel;
+		}
+		$sRet = "<span class=\"object-ref $sSpanClass\" title=\"$sHint\">$sHLink</span>";
+		return $sRet;
 	}
 
 	public function GetHyperlink($sUrlMakerClass = null, $bWithNavigationContext = true)
 	{
-		return self::MakeHyperLink(get_class($this), $this->GetKey(), $this->GetName(), $sUrlMakerClass, $bWithNavigationContext);
+		$bArchived = $this->IsArchived();
+		return self::MakeHyperLink(get_class($this), $this->GetKey(), $this->GetName(), $sUrlMakerClass, $bWithNavigationContext, $bArchived);
 	}
 	
 	public static function ComputeStandardUIPage($sClass)
@@ -1232,7 +1285,7 @@ abstract class DBObject implements iDisplay
 
 		$aChanges = $this->ListChanges();
 
-		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
+		foreach($aChanges as $sAttCode => $value)
 		{
 			$res = $this->CheckValue($sAttCode);
 			if ($res !== true)
@@ -3544,5 +3597,83 @@ abstract class DBObject implements iDisplay
 				throw new Exception("Invalid verb");
 		}
 	}
+
+	public function IsArchived($sKeyAttCode = null)
+	{
+		$bRet = false;
+		$sFlagAttCode = is_null($sKeyAttCode) ? 'archive_flag' : $sKeyAttCode.'_archive_flag';
+		if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode))
+		{
+			$bRet = true;
+		}
+		return $bRet;
+	}
+
+	/**
+	 * @param $bArchive
+	 * @throws Exception
+	 */
+	protected function DBWriteArchiveFlag($bArchive)
+	{
+		if (!MetaModel::IsArchivable(get_class($this)))
+		{
+			throw new Exception(get_class($this).' is not an archivable class');
+		}
+
+		$iFlag = $bArchive ? 1 : 0;
+		$sDate = $bArchive ? '"'.date(AttributeDate::GetSQLFormat()).'"' : 'null';
+
+		$sClass = get_class($this);
+		$sArchiveRoot = MetaModel::GetAttributeOrigin($sClass, 'archive_flag');
+		$sRootTable = MetaModel::DBGetTable($sArchiveRoot);
+		$sRootKey = MetaModel::DBGetKey($sArchiveRoot);
+		$aJoins = array("`$sRootTable`");
+		$aUpdates = array();
+		foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass)
+		{
+			if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag')) continue;
+
+			$sTable = MetaModel::DBGetTable($sParentClass);
+			$aUpdates[] = "`$sTable`.`archive_flag` = $iFlag";
+			if ($sParentClass == $sArchiveRoot)
+			{
+				if (!$bArchive || $this->Get('archive_date') == '')
+				{
+					// Erase or set the date (do not change it)
+					$aUpdates[] = "`$sTable`.`archive_date` = $sDate";
+				}
+			}
+			else
+			{
+				$sKey = MetaModel::DBGetKey($sParentClass);
+				$aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`";
+			}
+		}
+		$sJoins = implode(' INNER JOIN ', $aJoins);
+		$sValues = implode(', ', $aUpdates);
+		$sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` = ".$this->GetKey();
+		CMDBSource::Query($sUpdateQuery);
+	}
+
+	/**
+	 * Can be called to repair the database (tables consistency)
+	 * The archive_date will be preserved
+	 * @throws Exception
+	 */
+	public function DBArchive()
+	{
+		$this->DBWriteArchiveFlag(true);
+		$this->m_aCurrValues['archive_flag'] = true;
+		$this->m_aOrigValues['archive_flag'] = true;
+	}
+
+	public function DBUnarchive()
+	{
+		$this->DBWriteArchiveFlag(false);
+		$this->m_aCurrValues['archive_flag'] = false;
+		$this->m_aOrigValues['archive_flag'] = false;
+		$this->m_aCurrValues['archive_date'] = null;
+		$this->m_aOrigValues['archive_date'] = null;
+	}
 }
 

+ 32 - 1
core/dbobjectsearch.class.php

@@ -1467,6 +1467,7 @@ class DBObjectSearch extends DBSearch
 			{
 				$sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns
 			}
+			$sRawId .= $oSearch->GetArchiveMode() ? '--arch' : '';
 			$sOqlId = md5($sRawId);
 		}
 		else
@@ -1552,6 +1553,7 @@ class DBObjectSearch extends DBSearch
 			$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
 		}
 
+		$aMandatoryTables = null;
 		if (self::$m_bOptimizeQueries)
 		{
 			if ($bGetCount)
@@ -1562,6 +1564,17 @@ class DBObjectSearch extends DBSearch
 			$oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables);
 			$oSQLQuery->OptimizeJoins($aMandatoryTables);
 		}
+		// Filter tables as late as possible: do not interfere with the optimization process
+		foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions)
+		{
+			if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables))
+			{
+				foreach ($aConditions as $oCondition)
+				{
+					$oSQLQuery->AddCondition($oCondition);
+				}
+			}
+		}
 
 		return $oSQLQuery;
 	}
@@ -1869,7 +1882,7 @@ class DBObjectSearch extends DBSearch
 		$oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts);
 		
 		$bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
-		
+
 		self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
 
 		// 1 - SELECT and UPDATE
@@ -2098,6 +2111,24 @@ class DBObjectSearch extends DBSearch
 		$oBuild->m_oQBExpressions->Translate($aTranslation, false);
 //echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
 
+		// Filter out archived records
+		//
+		if (MetaModel::IsArchivable($sTableClass))
+		{
+			if (!$oBuild->GetRootFilter()->GetArchiveMode())
+			{
+				$bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses());
+				//$bIsOnJoinedClass = true;
+				if ($bIsOnJoinedClass)
+				{
+					if (MetaModel::IsParentClass($sTableClass, $sTargetClass))
+					{
+						$oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0));
+						$oBuild->AddFilteredTable($sTableAlias, $oNotArchived);
+					}
+				}
+			}
+		}
 		//MyHelpers::var_dump_html($oSelectBase->RenderSelect());
 		return $oSelectBase;
 	}

+ 23 - 2
core/dbsearch.class.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2015-2016 Combodo SARL
+// Copyright (C) 2015-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -33,7 +33,7 @@ require_once('dbunionsearch.class.php');
  *    - do not provide a type-hint for function parameters defined in the modules
  *    - leave the statements DBObjectSearch::FromOQL in the modules, though DBSearch is more relevant 
  *
- * @copyright   Copyright (C) 2015-2016 Combodo SARL
+ * @copyright   Copyright (C) 2015-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  
@@ -44,9 +44,11 @@ abstract class DBSearch
 
 	protected $m_bNoContextParameters = false;
 	protected $m_aModifierProperties = array();
+	protected $m_bArchiveMode = false;
 
 	public function __construct()
 	{
+		$this->m_bArchiveMode = self::GetArchiveModeDefault();
 	}
 
 	/**
@@ -60,6 +62,25 @@ abstract class DBSearch
 	abstract public function AllowAllData();
 	abstract public function IsAllDataAllowed();
 
+	static $bArchiveModeDefault = false;
+	static public function SetArchiveModeDefault($bEnable)
+	{
+		self::$bArchiveModeDefault = $bEnable;
+	}
+	static public function GetArchiveModeDefault()
+	{
+		return self::$bArchiveModeDefault;
+	}
+
+	public function SetArchiveMode($bEnable)
+	{
+		$this->m_bArchiveMode = $bEnable;
+	}
+	public function GetArchiveMode()
+	{
+		return $this->m_bArchiveMode;
+	}
+
 	public function NoContextParameters() {$this->m_bNoContextParameters = true;}
 	public function HasContextParameters() {return $this->m_bNoContextParameters;}
 

+ 119 - 47
core/metamodel.class.php

@@ -319,6 +319,11 @@ abstract class MetaModel
 		self::_check_subclass($sClass);	
 		return (self::$m_aClassParams[$sClass]["key_type"] == "autoincrement");
 	}
+	final static public function IsArchivable($sClass)
+	{
+		self::_check_subclass($sClass);
+		return self::$m_aClassParams[$sClass]["archive"];
+	}
 	final static public function GetNameSpec($sClass)
 	{
 		self::_check_subclass($sClass);
@@ -559,12 +564,13 @@ abstract class MetaModel
 		self::_check_subclass($sClass);
 		if (isset(self::$m_aClassParams[$sClass]['indexes']))
 		{
-			return self::$m_aClassParams[$sClass]['indexes'];
+			$aRet = self::$m_aClassParams[$sClass]['indexes'];
 		}
 		else
 		{
-			return array();
+			$aRet = array();
 		}
+		return $aRet;
 	}
 
 	final static public function DBGetKey($sClass)
@@ -1639,10 +1645,10 @@ abstract class MetaModel
 		return $oFltDef->GetAllowedValues($aArgs, $sContains);
 	}
 
-	public static function GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs = array(), $sContains = '')
+	public static function GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs = array(), $sContains = '', $iAdditionalValue = null)
 	{
 		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
-		return $oAttDef->GetAllowedValuesAsObjectSet($aArgs, $sContains);
+		return $oAttDef->GetAllowedValuesAsObjectSet($aArgs, $sContains, $iAdditionalValue);
 	}
 	//
 	// Businezz model declaration verbs (should be static)
@@ -1672,6 +1678,24 @@ abstract class MetaModel
 		self::$m_aRelationInfos[$sRelCode] = $sRelCode;
 	}
 
+	/*
+	 * Helper to correctly add a magic attribute (called from InitClasses)
+	 */
+	private static function AddMagicAttribute(AttributeDefinition $oAttribute, $sTargetClass, $sOriginClass = null)
+	{
+		$sCode = $oAttribute->GetCode();
+		if (is_null($sOriginClass))
+		{
+			$sOriginClass = $sTargetClass;
+		}
+		$oAttribute->SetHostClass($sTargetClass);
+		self::$m_aAttribDefs[$sTargetClass][$sCode] = $oAttribute;
+		self::$m_aAttribOrigins[$sTargetClass][$sCode] = $sOriginClass;
+
+		$oFlt = new FilterFromAttribute($oAttribute);
+		self::$m_aFilterDefs[$sTargetClass][$sCode] = $oFlt;
+		self::$m_aFilterOrigins[$sTargetClass][$sCode] = $sOriginClass;
+	}
 	// Must be called once and only once...
 	public static function InitClasses($sTablePrefix)
 	{
@@ -1726,6 +1750,23 @@ abstract class MetaModel
 					if ($oMethod->getDeclaringClass()->name == $sPHPClass)
 					{
 						call_user_func(array($sPHPClass, 'Init'));
+
+						// Inherit archive flag
+						$bParentArchivable = isset(self::$m_aClassParams[$sParent]['archive']) ? self::$m_aClassParams[$sParent]['archive'] : false;
+						$bArchivable = isset(self::$m_aClassParams[$sPHPClass]['archive']) ? self::$m_aClassParams[$sPHPClass]['archive'] : null;
+						if ($bParentArchivable && ($bArchivable === false))
+						{
+							throw new Exception("$sPHPClass must be archivable (consistency throughout the whole class tree is a must)");
+						}
+						$bReallyArchivable = $bParentArchivable || $bArchivable;
+						self::$m_aClassParams[$sPHPClass]['archive'] = $bReallyArchivable;
+						$bArchiveRoot = $bReallyArchivable && !$bParentArchivable;
+						self::$m_aClassParams[$sPHPClass]['archive_root'] = $bArchiveRoot;
+						if ($bReallyArchivable)
+						{
+							self::$m_aClassParams[$sPHPClass]['archive_root_class'] = $bArchiveRoot ? $sPHPClass : self::$m_aClassParams[$sParent]['archive_root_class'];
+						}
+
 						foreach (MetaModel::EnumPlugins('iOnClassInitialization') as $sPluginClass => $oClassInit)
 						{
 							$oClassInit->OnAfterClassInitialization($sPHPClass);
@@ -1757,13 +1798,7 @@ abstract class MetaModel
 					"is_null_allowed"=>false,
 					"depends_on"=>array()
 			));
-			$oClassAtt->SetHostClass($sRootClass);
-			self::$m_aAttribDefs[$sRootClass]['finalclass'] = $oClassAtt;
-			self::$m_aAttribOrigins[$sRootClass]['finalclass'] = $sRootClass;
-
-			$oClassFlt = new FilterFromAttribute($oClassAtt);
-			self::$m_aFilterDefs[$sRootClass]['finalclass'] = $oClassFlt;
-			self::$m_aFilterOrigins[$sRootClass]['finalclass'] = $sRootClass;
+			self::AddMagicAttribute($oClassAtt, $sRootClass);
 
 			foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass)
 			{
@@ -1776,14 +1811,8 @@ abstract class MetaModel
 					throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as a filter code");
 				}
 				$oCloned = clone $oClassAtt;
-				$oCloned->SetHostClass($sChildClass);
 				$oCloned->SetFixedValue($sChildClass);
-				self::$m_aAttribDefs[$sChildClass]['finalclass'] = $oCloned;
-				self::$m_aAttribOrigins[$sChildClass]['finalclass'] = $sRootClass;
-
-				$oClassFlt = new FilterFromAttribute($oClassAtt);
-				self::$m_aFilterDefs[$sChildClass]['finalclass'] = $oClassFlt;
-				self::$m_aFilterOrigins[$sChildClass]['finalclass'] = self::GetRootClass($sChildClass);
+				self::AddMagicAttribute($oCloned, $sChildClass, $sRootClass);
 			}
 		}
 
@@ -1795,13 +1824,30 @@ abstract class MetaModel
 			// Create the friendly name attribute
 			$sFriendlyNameAttCode = 'friendlyname'; 
 			$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode, 'id');
-			$oFriendlyName->SetHostClass($sClass);
-			self::$m_aAttribDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyName;
-			self::$m_aAttribOrigins[$sClass][$sFriendlyNameAttCode] = $sClass;
-			$oFriendlyNameFlt = new FilterFromAttribute($oFriendlyName);
-			self::$m_aFilterDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyNameFlt;
-			self::$m_aFilterOrigins[$sClass][$sFriendlyNameAttCode] = $sClass;
+			self::AddMagicAttribute($oFriendlyName, $sClass);
+
+			if (self::$m_aClassParams[$sClass]["archive_root"])
+			{
+				// Create archive attributes on top the archivable hierarchy
+				$oArchiveFlag = new AttributeArchiveFlag('archive_flag');
+				self::AddMagicAttribute($oArchiveFlag, $sClass);
 
+				$oArchiveDate = new AttributeDate('archive_date', array('magic' => true, "allowed_values"=>null, "sql"=>'archive_date', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()));
+				self::AddMagicAttribute($oArchiveDate, $sClass);
+			}
+			elseif (self::$m_aClassParams[$sClass]["archive"])
+			{
+				$sArchiveRoot = self::$m_aClassParams[$sClass]['archive_root_class'];
+				// Inherit archive attributes
+				$oArchiveFlag = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_flag'];
+				$oArchiveFlag->SetHostClass($sArchiveRoot);
+				self::$m_aAttribDefs[$sClass]['archive_flag'] = $oArchiveFlag;
+				self::$m_aAttribOrigins[$sClass]['archive_flag'] = $sArchiveRoot;
+				$oArchiveDate = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_date'];
+				$oArchiveDate->SetHostClass($sArchiveRoot);
+				self::$m_aAttribDefs[$sClass]['archive_date'] = $oArchiveDate;
+				self::$m_aAttribOrigins[$sClass]['archive_date'] = $sArchiveRoot;
+			}
 			self::$m_aExtKeyFriends[$sClass] = array();
 			foreach (self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef)
 			{
@@ -1840,26 +1886,15 @@ abstract class MetaModel
 						$sKeyAttCode = $oAttDef->GetKeyAttCode();
 						$sRemoteAttCode = $oAttDef->GetExtAttCode()."_friendlyname";
 						$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
-						// propagate "is_null_allowed" ? 
 						$oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array("allowed_values"=>null, "extkey_attcode"=>$sKeyAttCode, "target_attcode"=>$sRemoteAttCode, "depends_on"=>array()));
-						$oFriendlyName->SetHostClass($sClass);
-						self::$m_aAttribDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyName;
-						self::$m_aAttribOrigins[$sClass][$sFriendlyNameAttCode] = self::$m_aAttribOrigins[$sClass][$sKeyAttCode];
-						$oFriendlyNameFlt = new FilterFromAttribute($oFriendlyName);
-						self::$m_aFilterDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyNameFlt;
-						self::$m_aFilterOrigins[$sClass][$sFriendlyNameAttCode] = self::$m_aFilterOrigins[$sClass][$sKeyAttCode];
+						self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]);
 					}
 					else
 					{
 						// Create the friendly name attribute
 						$sFriendlyNameAttCode = $sAttCode.'_friendlyname'; 
 						$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode, $sAttCode);
-						$oFriendlyName->SetHostClass($sClass);
-						self::$m_aAttribDefs[$sClass][$sFriendlyNameAttCode] = $oFriendlyName;
-						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] = self::$m_aFilterOrigins[$sClass][$sAttCode];
+						self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
 
 						if (self::HasChildrenClasses($sRemoteClass))
 						{
@@ -1872,13 +1907,7 @@ abstract class MetaModel
 									"is_null_allowed"=>true,
 									"depends_on"=>array()
 							));
-							$oClassRecall->SetHostClass($sClass);
-							self::$m_aAttribDefs[$sClass][$sClassRecallAttCode] = $oClassRecall;
-							self::$m_aAttribOrigins[$sClass][$sClassRecallAttCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
-
-							$oClassFlt = new FilterFromAttribute($oClassRecall);
-							self::$m_aFilterDefs[$sClass][$sClassRecallAttCode] = $oClassFlt;
-							self::$m_aFilterOrigins[$sClass][$sClassRecallAttCode] = self::$m_aFilterOrigins[$sClass][$sAttCode];
+							self::AddMagicAttribute($oClassRecall, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
 
 							// Add it to the ZLists where the external key is present
 							//foreach(self::$m_aListData[$sClass] as $sListCode => $aAttributes)
@@ -1924,6 +1953,13 @@ abstract class MetaModel
 							self::$m_aExtKeyFriends[$sClass][$sAttCode][$oExtField->GetCode()] = $oExtField;
 						}
 					}
+
+					if (self::IsArchivable($sRemoteClass))
+					{
+						$sArchiveRemote = $sAttCode.'_archive_flag';
+						$oArchiveRemote = new AttributeExternalField($sArchiveRemote, array("allowed_values"=>null, "extkey_attcode"=>$sAttCode, "target_attcode"=>'archive_flag', "depends_on"=>array()));
+						self::AddMagicAttribute($oArchiveRemote, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
+					}
 				}
 				if ($oAttDef instanceof AttributeMetaEnum)
 				{
@@ -2467,6 +2503,19 @@ abstract class MetaModel
 		}
 		return $aRes;
 	}
+	public static function EnumArchivableClasses()
+	{
+		$aRes = array();
+		foreach (self::GetClasses() as $sClass)
+		{
+			if (self::IsArchivable($sClass))
+			{
+				$aRes[] = $sClass;
+			}
+		}
+		return $aRes;
+	}
+
 	public static function HasChildrenClasses($sClass)
 	{
 		return (count(self::$m_aChildClasses[$sClass]) > 0);
@@ -3401,6 +3450,12 @@ abstract class MetaModel
 		return $aDataDump;
 	}
 
+	protected static $m_bReadOnlyMode = false;
+	public static function DBSetReadOnly()
+	{
+		self::$m_bReadOnlyMode = true;
+	}
+
 	/*
 	* Determines wether the target DB is frozen or not
 	*/		
@@ -3408,6 +3463,10 @@ abstract class MetaModel
 	{
 		// Improvement: check the mySQL variable -> Read-only
 
+		if (self::$m_bReadOnlyMode)
+		{
+			return true;
+		}
 		if (UserRights::IsAdministrator())
 		{
 			return (!self::DBHasAccess(ACCESS_ADMIN_WRITE));
@@ -3620,8 +3679,11 @@ abstract class MetaModel
 			$aTableInfo['Fields'][$sKeyField]['used'] = true;
 			foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
 			{
-				// Skip this attribute if not originaly defined in this class
-				if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue;
+				if (!$oAttDef->CopyOnAllTables())
+				{
+					// Skip this attribute if not originaly defined in this class
+					if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue;
+				}
 				foreach($oAttDef->GetSQLColumns(true) as $sField => $sDBFieldSpec)
 				{
 					// Keep track of columns used by iTop
@@ -3725,7 +3787,7 @@ abstract class MetaModel
 					}
 				}
 			}
-			
+
 			// Find out unused columns
 			//
 			foreach($aTableInfo['Fields'] as $sField => $aFieldData)
@@ -4538,6 +4600,7 @@ abstract class MetaModel
 			$sModifierProperties = json_encode($aModifierProperties);
 			$sQuerySign .= '_all_'.md5($sModifierProperties);
 		}
+		$sQuerySign .= DBSearch::GetArchiveModeDefault() ? '_arch_' : '';
 
 		if (!array_key_exists($sQuerySign, self::$aQueryCacheGetObject))
 		{
@@ -4628,6 +4691,15 @@ abstract class MetaModel
 		return self::GetObjectByRow($sClass, $aRow);
 	}
 
+	public static function GetObjectWithArchive($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
+	{
+		$bPreviousMode = DBSearch::GetArchiveModeDefault();
+		DBSearch::SetArchiveModeDefault(true);
+		$oObject = static::GetObject($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
+		DBSearch::SetArchiveModeDefault($bPreviousMode);
+		return $oObject;
+	}
+
 	public static function GetObjectByName($sClass, $sName, $bMustBeFound = true)
 	{
 		self::_check_subclass($sClass);	

+ 4 - 6
core/oql/expression.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * General definition of an expression tree (could be OQL, SQL or whatever) 
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -579,7 +579,7 @@ class FieldExpression extends UnaryExpression
 			$iObjKey = (int)$sValue;
 			if ($iObjKey > 0)
 			{
-				$oObject = MetaModel::GetObject($sObjClass, $iObjKey);
+				$oObject = MetaModel::GetObjectWithArchive($sObjClass, $iObjKey);
 				$sRes = $oObject->GetHyperlink();
 			}
 			else
@@ -1403,6 +1403,4 @@ class QueryBuilderExpressions
 			$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);
 		}
 	}
-}
-
-?>
+}

+ 21 - 2
core/querybuildercontext.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Associated with the metamodel -> MakeQuery/MakeQuerySingleTable
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -30,6 +30,7 @@ class QueryBuilderContext
 	protected $m_aTableAliases;
 	protected $m_aModifierProperties;
 	protected $m_aSelectedClasses;
+	protected $m_aFilteredTables;
 
 	public $m_oQBExpressions;
 
@@ -40,6 +41,7 @@ class QueryBuilderContext
 
 		$this->m_aClassAliases = $oFilter->GetJoinedClasses();
 		$this->m_aTableAliases = array();
+		$this->m_aFilteredTables = array();
 
 		$this->m_aModifierProperties = $aModifierProperties;
 		if (is_null($aSelectedClasses))
@@ -84,4 +86,21 @@ class QueryBuilderContext
 	{
 		return $this->m_aSelectedClasses[$sAlias];
 	}
+
+	public function AddFilteredTable($sTableAlias, $oCondition)
+	{
+		if (array_key_exists($sTableAlias, $this->m_aFilteredTables))
+		{
+			$this->m_aFilteredTables[$sTableAlias][] = $oCondition;
+		}
+		else
+		{
+			$this->m_aFilteredTables[$sTableAlias] = array($oCondition);
+		}
+	}
+
+	public function GetFilteredTables()
+	{
+		return $this->m_aFilteredTables;
+	}
 }

+ 1 - 1
core/sqlobjectquery.class.inc.php

@@ -166,7 +166,7 @@ class SQLObjectQuery extends SQLQuery
 		}
 		else
 		{
-			$this->m_oConditionExpr->LogAnd($oConditionExpr);
+			$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oConditionExpr);
 		}
 	}
 

+ 26 - 4
core/userrights.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * User rights management API
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -641,6 +641,29 @@ class UserRights
 		}
 	}
 
+	/**
+	 * Tells whether or not the archive mode is allowed to the current user
+	 * @return boolean
+	 */
+	static function CanBrowseArchive()
+	{
+		if (is_null(self::$m_oUser))
+		{
+			$bRet = false;
+		}
+		elseif (isset($_SESSION['archive_allowed']))
+		{
+			$bRet = $_SESSION['archive_allowed'];
+		}
+		else
+		{
+			// As of now, anybody can swith to the archive mode
+			$bRet = true;
+			$_SESSION['archive_allowed'] = $bRet;
+		}
+		return $bRet;
+	}
+
 	public static function CanChangePassword()
 	{
 		if (MetaModel::DBIsReadOnly())
@@ -1675,5 +1698,4 @@ class CAS_SelfRegister implements iSelfRegister
 }
 
 // By default enable the 'CAS_SelfRegister' defined above
-UserRights::SelectSelfRegister('CAS_SelfRegister');
-?>
+UserRights::SelectSelfRegister('CAS_SelfRegister');

+ 11 - 3
core/valuesetdef.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Value set definitions (from a fixed list or from a query, etc.)
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -124,7 +124,7 @@ class ValueSetObjects extends ValueSetDefinition
 		$this->m_aExtraConditions[] = $oFilter;		
 	}
 
-	public function ToObjectSet($aArgs = array(), $sContains = '')
+	public function ToObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
 	{
 		if ($this->m_bAllowAllData)
 		{
@@ -145,6 +145,14 @@ class ValueSetObjects extends ValueSetDefinition
 				$oFilter->SetModifierProperty($sPluginClass, $sProperty, $value);
 			}
 		}
+		if ($iAdditionalValue > 0)
+		{
+			$oSearchAdditionalValue = new DBObjectSearch($oFilter->GetClass());
+			$oSearchAdditionalValue->AddCondition('id', $iAdditionalValue);
+			$oSearchAdditionalValue->AllowAllData();
+			$oSearchAdditionalValue->SetArchiveMode(true);
+			$oFilter = new DBUnionSearch(array($oFilter, $oSearchAdditionalValue));
+		}
 
 		return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs);
 	}

+ 4 - 4
core/xmlbulkexport.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2015 Combodo SARL
+// Copyright (C) 2015-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Bulk export: XML export
  *
- * @copyright   Copyright (C) 2015 Combodo SARL
+ * @copyright   Copyright (C) 2015-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -118,7 +118,7 @@ class XMLBulkExport extends BulkExport
 					{
 						continue;
 					}
-					if (!$oAttDef->IsWritable())
+					if ($oAttDef->IsExternalField())
 					{
 						continue;
 					}
@@ -138,7 +138,7 @@ class XMLBulkExport extends BulkExport
 				$aClass2Attributes[$sAlias] = $aAttributes;
 			}
 		}
-		
+
 		$iPreviousTimeLimit = ini_get('max_execution_time');
 		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
 		

+ 1 - 1
css/css-variables.scss

@@ -5,4 +5,4 @@ $complement-light: #d6e8ef;
 $frame-background-color: #F1F1F1;
 $text-color: #000;
 // Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0
-$version: "v2.3.0";
+$version: "v2.4.0-alpha";

+ 90 - 35
css/light-grey.css

@@ -253,8 +253,6 @@ legend.transparent {
 .ui-widget-content td a, p a, p a:visited, td a, td a:visited {
   text-decoration: none;
   color: #1c94c4;
-  padding-left: 14px;
-  background: url(../images/mini-arrow-orange.gif) no-repeat left;
 }
 .ui-widget-content td a.cke_button, .ui-widget-content td a.cke_toolbox_collapser, .ui-widget-content td a.cke_combo_button, cke_dialog a {
   padding-left: 0;
@@ -292,13 +290,13 @@ td a.mailto, td a.mailto:visited {
   text-decoration: none;
   color: #000;
   padding-left: 20px;
-  background: url(../images/mail.png?v=v2.3.0) no-repeat left;
+  background: url(../images/mail.png?v=v2.4.0-alpha) no-repeat left;
 }
 td a.mailto:hover {
   text-decoration: underline;
   color: #e87c1e;
   padding-left: 20px;
-  background: url(../images/mail.png?v=v2.3.0) no-repeat left;
+  background: url(../images/mail.png?v=v2.4.0-alpha) no-repeat left;
 }
 a.small_action {
   font-family: Tahoma, Verdana, Arial, Helvetica;
@@ -316,10 +314,10 @@ a.small_action {
   padding-left: 5px;
   padding-top: 2px;
   padding-bottom: 2px;
-  background: #e87c1e url(../images/actions_left.png?v=v2.3.0) no-repeat left;
+  background: #e87c1e url(../images/actions_left.png?v=v2.4.0-alpha) no-repeat left;
 }
 .actions_details span {
-  background: url(../images/actions_right.png?v=v2.3.0) no-repeat right;
+  background: url(../images/actions_right.png?v=v2.4.0-alpha) no-repeat right;
   color: #fff;
   font-weight: bold;
   padding-top: 2px;
@@ -493,7 +491,7 @@ div.actions_menu > ul {
   nowidth: 70px;
   padding-left: 5px;
   /* Nasty work-around for IE... en attendant mieux */
-  background: #e87c1e url(../images/actions_left.png?v=v2.3.0) no-repeat top left;
+  background: #e87c1e url(../images/actions_left.png?v=v2.4.0-alpha) no-repeat top left;
   cursor: pointer;
   margin: 0;
 }
@@ -505,7 +503,7 @@ div.actions_menu > ul > li {
   height: 17px;
   padding-right: 16px;
   padding-left: 4px;
-  background: url(../images/actions_right.png?v=v2.3.0) no-repeat top right transparent;
+  background: url(../images/actions_right.png?v=v2.4.0-alpha) no-repeat top right transparent;
   font-weight: bold;
   color: #fff;
   vertical-align: middle;
@@ -648,7 +646,7 @@ td a.dp-choose-date, a.dp-choose-date, td a.dp-choose-date:hover, a.dp-choose-da
   display: block;
   text-indent: -2000px;
   overflow: hidden;
-  background: url(../images/calendar.png?v=v2.3.0) no-repeat;
+  background: url(../images/calendar.png?v=v2.4.0-alpha) no-repeat;
 }
 td a.dp-choose-date.dp-disabled, a.dp-choose-date.dp-disabled {
   background-position: 0 -20px;
@@ -739,19 +737,19 @@ div.HRDrawer {
 }
 /* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */
 table.listResults tr.odd td.truncated, table.listResults tr td.truncated, .wizContainer table.listResults tr.odd td.truncated, .wizContainer table.listResults tr td.truncated {
-  background: url(../images/truncated.png?v=v2.3.0) bottom repeat-x;
+  background: url(../images/truncated.png?v=v2.4.0-alpha) bottom repeat-x;
 }
 /* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */
 table.listResults tr.even td.truncated, .wizContainer table.listResults tr.even td.truncated {
-  background: #f9f9f1 url(../images/truncated.png?v=v2.3.0) bottom repeat-x;
+  background: #f9f9f1 url(../images/truncated.png?v=v2.4.0-alpha) bottom repeat-x;
 }
 /* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */
 table.listResults tr.even td.hover.truncated, .wizContainer table.listResults tr.even td.hover.truncated {
-  background: #fdf5d0 url(../images/truncated.png?v=v2.3.0) bottom repeat-x;
+  background: #fdf5d0 url(../images/truncated.png?v=v2.4.0-alpha) bottom repeat-x;
 }
 /* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */
 table.listResults tr.odd td.hover.truncated, table.listResults tr td.hover.truncated, .wizContainer table.listResults tr.odd td.hover.truncated, .wizContainer table.listResults tr td.hover.truncated {
-  background: #fdf5d0 url(../images/truncated.png?v=v2.3.0) bottom repeat-x;
+  background: #fdf5d0 url(../images/truncated.png?v=v2.4.0-alpha) bottom repeat-x;
 }
 table.listResults.truncated {
   border-bottom: 0;
@@ -859,7 +857,7 @@ div#logo {
 div#logo div {
   height: 88px;
   width: 244px;
-  background: url(../images/itop-logo-2.png?v=v2.3.0) left no-repeat;
+  background: url(../images/itop-logo-2.png?v=v2.4.0-alpha) left no-repeat;
 }
 #left-pane .ui-layout-north {
   overflow: hidden;
@@ -868,13 +866,56 @@ div#logo div {
   background: #f1f1f1;
   text-align: right;
 }
-#admin-banner {
+.app-message {
   float: left;
   margin-top: 2px;
-  padding: 8px;
-  border: 1px solid #c33;
-  background-color: #fee;
-  -moz-border-radius: 0.5em;
+  margin-right: 4px;
+  padding: 6px 9px;
+  background-color: #e87c1e;
+  color: white;
+  border-radius: 6px;
+  text-align: left;
+}
+.app-message-icon {
+  margin-right: 5px;
+}
+.fa-sm {
+  font-size: 0.66em;
+}
+.object-details-header {
+  margin-top: 7px;
+  margin-bottom: 7px;
+}
+.object-icon {
+  display: table-cell;
+  vertical-align: middle;
+  margin-left: 10px;
+  margin-right: 10px;
+}
+.object-infos {
+  display: table-cell;
+  vertical-align: middle;
+}
+.object-name {
+  margin-top: 0px;
+  margin-bottom: 0px;
+}
+.tags {
+  margin-top: 5px;
+}
+.tag {
+  font-size: 10px;
+  font-weight: initial;
+  display: inline-block;
+  color: white;
+  background-color: #555;
+  padding: 3px 6px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.text-danger {
+  color: red;
 }
 #global-search {
   height: 55px;
@@ -908,7 +949,7 @@ div#logo div {
 }
 #global-search-image {
   vertical-align: middle;
-  background: url(../images/search.png?v=v2.3.0) center center no-repeat;
+  background: url(../images/search.png?v=v2.4.0-alpha) center center no-repeat;
   display: inline-block;
   width: 28px;
   height: 30px;
@@ -937,7 +978,7 @@ span.ui-icon {
   margin: 0 2px;
 }
 .ui-layout-button-pin-down {
-  background: url(../images/splitter-bkg.png?v=v2.3.0) transparent;
+  background: url(../images/splitter-bkg.png?v=v2.4.0-alpha) transparent;
   width: 16px;
   background-position: -144px -144px;
 }
@@ -1148,7 +1189,7 @@ img.prev, img.first, img.next, img.last {
 }
 div.actions_button {
   float: right;
-  background: #e87c1e url("../images/actions_left.png?v=v2.3.0") no-repeat scroll left top;
+  background: #e87c1e url("../images/actions_left.png?v=v2.4.0-alpha") no-repeat scroll left top;
   padding-left: 5px;
   margin-top: 0;
   margin-right: 10px;
@@ -1156,7 +1197,7 @@ div.actions_button {
   vertical-align: middle;
 }
 div.actions_button a, .actions_button a:hover, .actions_button a:visited {
-  background: #e87c1e url(../images/actions_bkg.png?v=v2.3.0) no-repeat scroll right top;
+  background: #e87c1e url(../images/actions_bkg.png?v=v2.4.0-alpha) no-repeat scroll right top;
   color: #fff;
   padding-right: 8px;
   cursor: pointer;
@@ -1180,10 +1221,10 @@ select#org_id {
   cursor: not-allowed;
 }
 .dragHover {
-  background: url(./ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=v2.3.0);
+  background: url(./ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=v2.4.0-alpha);
 }
 .edit_mode .dashlet {
-  background: url(./ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=v2.3.0);
+  background: url(./ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=v2.4.0-alpha);
   padding: 5px;
   margin: 0;
   position: relative;
@@ -1215,7 +1256,7 @@ table.prop_table {
   top: 0;
   right: 0;
   z-index: 10;
-  background: transparent url(../images/delete.png?v=v2.3.0) no-repeat center;
+  background: transparent url(../images/delete.png?v=v2.4.0-alpha) no-repeat center;
 }
 td.prop_value {
   text-align: left;
@@ -1409,17 +1450,17 @@ a.summary, a.summary:hover {
 }
 .message_info {
   border: 1px solid #993;
-  background: url(../images/info-mini.png?v=v2.3.0) 1em 1em no-repeat #ffc;
+  background: url(../images/info-mini.png?v=v2.4.0-alpha) 1em 1em no-repeat #ffc;
   padding-left: 3em;
 }
 .message_ok {
   border: 1px solid #393;
-  background: url(../images/ok.png?v=v2.3.0) 1em 1em no-repeat #cfc;
+  background: url(../images/ok.png?v=v2.4.0-alpha) 1em 1em no-repeat #cfc;
   padding-left: 3em;
 }
 .message_error {
   border: 1px solid #933;
-  background: url(../images/error.png?v=v2.3.0) 1em 1em no-repeat #fcc;
+  background: url(../images/error.png?v=v2.4.0-alpha) 1em 1em no-repeat #fcc;
   padding-left: 3em;
 }
 .fg-menu a img {
@@ -1547,18 +1588,18 @@ div.explain-printable {
 }
 #hiddeable_chapters .ui-tabs .ui-tabs-nav li.hideable-chapter span {
   padding-left: 20px;
-  background: url(../images/eye-open-555.png?v=v2.3.0) 2px center no-repeat;
+  background: url(../images/eye-open-555.png?v=v2.4.0-alpha) 2px center no-repeat;
 }
 #hiddeable_chapters .ui-tabs .ui-tabs-nav li.hideable-chapter.strikethrough span {
   text-decoration: line-through;
-  background: url(../images/eye-closed-555.png?v=v2.3.0) 2px center no-repeat;
+  background: url(../images/eye-closed-555.png?v=v2.4.0-alpha) 2px center no-repeat;
 }
 .printable-version legend {
   padding-left: 26px;
-  background: #1c94c4 url(../images/eye-open-fff.png?v=v2.3.0) 8px center no-repeat;
+  background: #1c94c4 url(../images/eye-open-fff.png?v=v2.4.0-alpha) 8px center no-repeat;
 }
 .printable-version .strikethrough legend {
-  background: #1c94c4 url(../images/eye-closed-fff.png?v=v2.3.0) 8px center no-repeat;
+  background: #1c94c4 url(../images/eye-closed-fff.png?v=v2.4.0-alpha) 8px center no-repeat;
 }
 .printable-version fieldset.strikethrough span {
   display: none;
@@ -1577,7 +1618,7 @@ span.refresh-button {
   width: 21px;
   height: 18px;
   cursor: pointer;
-  background: transparent url(../images/refresh-fff.png?v=v2.3.0) left center no-repeat;
+  background: transparent url(../images/refresh-fff.png?v=v2.4.0-alpha) left center no-repeat;
 }
 .case-log-history-entry {
   display: block;
@@ -1705,7 +1746,7 @@ span.refresh-button {
 #itop-breadcrumb .breadcrumb-item a::after {
   content: '';
   position: absolute;
-  background-image: url(../images/breadcrumb-separator.png?v=v2.3.0);
+  background-image: url(../images/breadcrumb-separator.png?v=v2.4.0-alpha);
   background-repeat: no-repeat;
   width: 8px;
   height: 16px;
@@ -1763,3 +1804,17 @@ span.refresh-button {
   margin-top: 3px;
   margin-bottom: 1px;
 }
+.object-ref-icon.fa {
+  color: #e87c1e;
+  font-size: smaller;
+  vertical-align: 1px;
+  margin-right: 1px;
+}
+.object-ref-icon-disabled.fa {
+  color: #555;
+  font-size: smaller;
+  margin-right: 1px;
+}
+.object-ref-link {
+  background: none;
+}

+ 71 - 8
css/light-grey.scss

@@ -302,8 +302,6 @@ legend.transparent {
 .ui-widget-content td a, p a, p a:visited, td a, td a:visited {
 	text-decoration:none;
 	color: $complement-color;
-	padding-left:14px;
-	background: url(../images/mini-arrow-orange.gif) no-repeat left;
 }
 .ui-widget-content td a.cke_button, .ui-widget-content td a.cke_toolbox_collapser, .ui-widget-content td a.cke_combo_button, cke_dialog a {
 	padding-left: 0;
@@ -956,13 +954,62 @@ div#logo div {
 	background: $frame-background-color;
 	text-align: right;
 }
-#admin-banner {
+.app-banner {
+
+}
+.app-message {
 	float: left;
 	margin-top: 2px;
-	padding: 8px;
-	border: 1px solid #c33;
-	background-color: #fee;
-	-moz-border-radius: 0.5em;
+	margin-right: 4px;
+	padding: 6px 9px;
+	background-color: $highlight-color;
+	color: white;
+	border-radius: 6px;
+	text-align: left;
+}
+.app-message-icon {
+	margin-right: 5px;
+}
+.app-message-body {
+}
+.fa-sm {
+	font-size: 0.66em;
+}
+.object-details-header {
+	margin-top: 7px;
+	margin-bottom: 7px;
+
+}
+.object-icon {
+	display: table-cell;
+	vertical-align: middle;
+	margin-left: 10px;
+	margin-right: 10px;
+}
+.object-infos {
+	display: table-cell;
+	vertical-align: middle;
+}
+.object-name {
+	margin-top: 0px;
+	margin-bottom: 0px;
+}
+.tags {
+	margin-top: 5px;
+}
+.tag {
+	font-size: 10px;
+	font-weight: initial;
+	display: inline-block;
+	color:white;
+	background-color:#555;
+	padding: 3px 6px;
+	-webkit-border-radius:4px;
+	-moz-border-radius:4px;
+	border-radius:4px;
+}
+.text-danger {
+	color: red;
 }
 #global-search {
 	height: 55px;
@@ -1888,4 +1935,20 @@ span.refresh-button {
 	font-size: smaller;
 	margin-top: 3px;
 	margin-bottom: 1px;
-}
+}
+.object-ref.archived {
+}
+.object-ref-icon.fa {
+	color: $highlight-color;
+	font-size: smaller;
+	vertical-align: 1px;
+	margin-right: 1px;
+}
+.object-ref-icon-disabled.fa {
+	color: $grey-color;
+	font-size: smaller;
+	margin-right: 1px;
+}
+.object-ref-link {
+	background: none;
+}

+ 6 - 1
datamodels/2.x/itop-portal-base/portal/web/index.php

@@ -1,6 +1,6 @@
 <?php
 
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -86,6 +86,11 @@ $oApp->before(function(Symfony\Component\HttpFoundation\Request $oRequest, Silex
         $oApp->abort(500, Dict::S('Portal:ErrorNoContactForThisUser'));
     }
 
+	// Enable archived data
+	$bArchiveMode = utils::IsArchiveMode();
+	DBSearch::SetArchiveModeDefault($bArchiveMode);
+	if ($bArchiveMode) MetaModel::DBSetReadOnly();
+
     // Enabling datalocalizer if needed
     if (!defined('DISABLE_DATA_LOCALIZER_PORTAL'))
     {

+ 9 - 2
dictionaries/cs.dictionary.itop.core.php

@@ -1,6 +1,6 @@
 <?php
 
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -23,7 +23,7 @@
  *
  * @author      Lukáš Dvořák <lukas.dvorak@itopportal.cz>
  * @author      Daniel Rokos <daniel.rokos@itopportal.cz>
- * @copyright   Copyright (C) 2010-2014 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 Dict::Add('CS CZ', 'Czech', 'Čeština', array(
@@ -47,6 +47,13 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
 
     'Core:AttributeBoolean' => 'Boolean',
     'Core:AttributeBoolean+' => 'Boolean',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
     'Core:AttributeString' => 'Řetězec (string)',
     'Core:AttributeString+' => 'Alfanumerický řetězec',

+ 4 - 2
dictionaries/cs.dictionary.itop.ui.php

@@ -1,6 +1,6 @@
 <?php
 
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -23,7 +23,7 @@
  *
  * @author      Lukáš Dvořák <lukas.dvorak@itopportal.cz>
  * @author      Daniel Rokos <daniel.rokos@itopportal.cz>
- * @copyright   Copyright (C) 2010-2014 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -1070,6 +1070,8 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
     'UI:Pagination:All' => 'Vše',
     'UI:HierarchyOf_Class' => 'Hierarchie %1$s',
     'UI:Preferences' => 'Předvolby',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
     'UI:FavoriteOrganizations' => 'Oblíbené organizace',
     'UI:FavoriteOrganizations+' => 'Zaškrtněte, které organizace chcete vidět v rozbalovacím menu pro rychlý přístup. Mějte na paměti, že toto není bezpečnostní opatření. Objekty všech organizací jsou pořád viditelné a přístupné vybráním "Všechny organizace" z rozbalovacího menu.',
     'UI:FavoriteLanguage' => 'Jazyk uživatelského rozhraní',

+ 8 - 2
dictionaries/da.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * @author Erik Bøg <erik@boegmoeller.dk>
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -1397,6 +1397,12 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
 	'Core:AttributeDecimal+' => '',
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => '',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => '',
 	'Core:AttributeClass' => 'Class',

+ 11 - 2
dictionaries/da.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * @author Erik Bøg <erik@boegmoeller.dk>
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -608,6 +608,11 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s detaljer',
 	'UI:ErrorPageTitle' => 'iTop - Fejl',
 	'UI:ObjectDoesNotExist' => 'Beklager, dette objekt eksisterer ikke (eller du har ikke tilladelse til at se det).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Søge Resultater',
 	'UI:SearchResultsTitle' => 'Søge Resultater',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -857,6 +862,10 @@ Ved tilknytningen til en trigger, bliver hver handling tildelt et "rækkefølge"
 	'UI:Pagination:All' => 'Alle',
 	'UI:HierarchyOf_Class' => 'Hierarchy af %1$s',
 	'UI:Preferences' => 'Indstillinger...',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favorit Organisationer',
 	'UI:FavoriteOrganizations+' => '',
 	'UI:FavoriteLanguage' => 'Sprog i brugergrænseflade',

+ 8 - 2
dictionaries/de.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * @author	Stephan Rosenke <stephan.rosenke@itomig.de>
 
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -323,6 +323,12 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'Core:AttributeDecimal+' => 'Dezimaler Wert (kann negativ sein)',
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => 'Boolscher Wert',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => 'Alphanumerischer String',
 	'Core:AttributeClass' => 'Class',

+ 11 - 2
dictionaries/de.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * @author	Stephan Rosenke <stephan.rosenke@itomig.de>
 
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -609,6 +609,11 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s Details',
 	'UI:ErrorPageTitle' => 'iTop - Fehler',
 	'UI:ObjectDoesNotExist' => 'Leider existiert dieses Objekt nicht oder Sie sind nicht berechtigt es einzusehen.',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Suchergebnisse',
 	'UI:SearchResultsTitle' => 'Suchergebnisse',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -882,6 +887,10 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
 	'UI:Pagination:All' => 'Alles',
 	'UI:HierarchyOf_Class' => 'Hierarchie von %1$s',
 	'UI:Preferences' => 'Einstellungen...',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Bevorzugte Organisationen',
 	'UI:FavoriteOrganizations+' => '',
 	'UI:FavoriteLanguage' => 'Sprache des Benutzerinterfaces',

+ 9 - 2
dictionaries/dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -45,6 +45,13 @@ Dict::Add('EN US', 'English', 'English', array(
 
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => 'Boolean',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes',
+	'Core:AttributeBoolean/Value:no' => 'No',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode',
+	'Core:AttributeArchiveFlag/Value:no' => 'No',
 
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => 'Alphanumeric string',

+ 11 - 2
dictionaries/dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -770,6 +770,11 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s details',
 	'UI:ErrorPageTitle' => 'iTop - Error',
 	'UI:ObjectDoesNotExist' => 'Sorry, this object does not exist (or you are not allowed to view it).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.',
+	'Tag:Archived' => 'Archived',
+	'Tag:Archived+' => 'Can be accessed only in archive mode',
+	'Tag:Synchronized' => 'Synchronized',
+	'ObjectRef:Archived' => 'Archived',
 	'UI:SearchResultsPageTitle' => 'iTop - Search Results',
 	'UI:SearchResultsTitle' => 'Search Results',
 	'UI:SearchResultsTitle+' => 'Full-text search results',
@@ -1074,6 +1079,10 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:Pagination:All' => 'All',
 	'UI:HierarchyOf_Class' => 'Hierarchy of %1$s',
 	'UI:Preferences' => 'Preferences...',
+	'UI:ArchiveModeOn' => 'Activate archive mode',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode',
+	'UI:ArchiveMode:Banner' => 'Archive mode',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed',
 	'UI:FavoriteOrganizations' => 'Favorite Organizations',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. '.
 								   'Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting "All Organizations" in the drop-down list.',

+ 9 - 2
dictionaries/es_cr.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -45,6 +45,13 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 
 	'Core:AttributeBoolean' => 'Booleano',
 	'Core:AttributeBoolean+' => 'Booleano',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
 	'Core:AttributeString' => 'Cadena de caracteres',
 	'Core:AttributeString+' => 'Cadena de caracteres alfanumerico',

+ 12 - 4
dictionaries/es_cr.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -760,6 +760,11 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - Detalles %2$s',
 	'UI:ErrorPageTitle' => 'iTop - Error',
 	'UI:ObjectDoesNotExist' => 'Disculpe, este objeto no existe (o no está autorizado para verlo).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Resultados de la Búsqueda',
 	'UI:SearchResultsTitle' => 'Resultados de la Búsqueda',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1034,6 +1039,10 @@ Cuando se asocien con un disparador, cada acción recibe un número de "orden",
 	'UI:Pagination:All' => 'Todos',
 	'UI:HierarchyOf_Class' => 'Jerarquía de %1$s',
 	'UI:Preferences' => 'Preferencias',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Mi Organización Favorita',
 	'UI:FavoriteOrganizations+' => 'Verifique en la siguiente lista de Organizaciones, la que necesite ver en los menues para un rápido acceso. '.
 								   'Nota, esto no es una configuración de seguridad, elementos de cualquier Organización son visibles y pueden ser accesados mediante la selección de "Todas las Organizaciones" en la lista del menú.',
@@ -1430,5 +1439,4 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
 	'UI:Menu:PrintableVersion' => 'Versión imprimible',
-));
-?>
+));

+ 8 - 2
dictionaries/fr.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -18,7 +18,7 @@
 
 
 /**
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -432,6 +432,12 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Core:AttributeDecimal+' => 'Valeur numérique décimale',
 	'Core:AttributeBoolean' => 'Booléen',
 	'Core:AttributeBoolean+' => 'Booléen',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Oui',
+	'Core:AttributeBoolean/Value:no' => 'Non',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Oui',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'Cet object n\'est visible que dans le mode Archive',
+	'Core:AttributeArchiveFlag/Value:no' => 'Non',
 	'Core:AttributeString' => 'Chaîne de caractères',
 	'Core:AttributeString+' => 'Chaîne de caractères (limitée à une ligne)',
 	'Core:AttributeClass' => 'Classe',

+ 11 - 2
dictionaries/fr.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -17,7 +17,7 @@
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
 
 /**
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -660,6 +660,11 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'UI:DetailsPageTitle' => 'iTop - %2$s - Détails de %1$s',
 	'UI:ErrorPageTitle' => 'iTop - Erreur',
 	'UI:ObjectDoesNotExist' => 'Désolé cet objet n\'existe pas (où vous n\'êtes pas autorisé à l\'afficher).',
+	'UI:ObjectArchived' => 'Cet objet a été archivé. Veuillez activer le mode Archive, on contactez votre administrateur.',
+	'Tag:Archived' => 'Archivé',
+	'Tag:Archived+' => 'Accessible seulement dans le mode Archive',
+	'Tag:Synchronized' => 'Synchronisé',
+	'ObjectRef:Archived' => 'Archivé',
 	'UI:SearchResultsPageTitle' => 'iTop - Résultats de la recherche',
 	'UI:SearchResultsTitle' => 'Recherche globale',
 	'UI:SearchResultsTitle+' => 'Résultat de recherche globale',
@@ -938,6 +943,10 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:Pagination:All' => 'Tous',
 	'UI:HierarchyOf_Class' => 'Hiérarchie de type %1$s',
 	'UI:Preferences' => 'Préférences...',
+	'UI:ArchiveModeOn' => 'Activer le mode Archive',
+	'UI:ArchiveModeOff' => 'Désactiver le mode Archive',
+	'UI:ArchiveMode:Banner' => 'Mode Archive',
+	'UI:ArchiveMode:Banner+' => 'Les objets archivés sont visibles, et aucune modification n\'est possible',
 	'UI:FavoriteOrganizations' => 'Organisations Favorites',
 	'UI:FavoriteOrganizations+' => 'Cochez dans la liste ci-dessous les organisations que vous voulez voir listées dans le menu principal. '.
 								   'Ceci n\'est pas un réglage de sécurité. Les objets de toutes les organisations sont toujours visibles en choisissant "Toutes les Organisations" dans le menu.',

+ 9 - 4
dictionaries/hu.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -17,7 +17,7 @@
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
 
 /**
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -306,6 +306,12 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
 	'Core:AttributeDecimal+' => '',
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => '',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => '',
 	'Core:AttributeClass' => 'Class',
@@ -609,5 +615,4 @@ Operators:<br/>
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-?>
+));

+ 12 - 4
dictionaries/hu.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -17,7 +17,7 @@
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
 
 /**
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -539,6 +539,11 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s részletek',
 	'UI:ErrorPageTitle' => 'iTop - Hiba',
 	'UI:ObjectDoesNotExist' => 'Sajnálom ez az objektum nem létezik (vagy a megtekintése nem engedélyezett a felhasználó számára).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Keresés eredményei',
 	'UI:SearchResultsTitle' => 'Keresés eredményei',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -906,6 +911,10 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
 	'UI:Pagination:All' => 'All~~',
 	'UI:HierarchyOf_Class' => 'Hierarchy of %1$s~~',
 	'UI:Preferences' => 'Preferences...~~',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favorite Organizations~~',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting \"All Organizations\" in the drop-down list.~~',
 	'UI:FavoriteLanguage' => 'Language of the User Interface~~',
@@ -1099,5 +1108,4 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-?>
+));

+ 10 - 5
dictionaries/it.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -39,6 +39,13 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
 
 	'Core:AttributeBoolean' => 'Booleano',
 	'Core:AttributeBoolean+' => 'Booleano',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
 	'Core:AttributeString' => 'Stringa',
 	'Core:AttributeString+' => 'Stringa alfanumerica',
@@ -855,6 +862,4 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-
-?>
+));

+ 12 - 4
dictionaries/it.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -671,6 +671,11 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s dettagli',
 	'UI:ErrorPageTitle' => 'iTop - Errore',
 	'UI:ObjectDoesNotExist' => 'Spiacenti, questo oggetto non esiste (o non si è autorizzati per vederlo).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Risultati della ricerca',
 	'UI:SearchResultsTitle' => 'Risultati della ricerca',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1031,6 +1036,10 @@ Quando è associata a un trigger, ad ogni azione è assegnato un numero "ordine"
 	'UI:Pagination:All' => 'All~~',
 	'UI:HierarchyOf_Class' => 'Hierarchy of %1$s~~',
 	'UI:Preferences' => 'Preferences...~~',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favorite Organizations~~',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting \"All Organizations\" in the drop-down list.~~',
 	'UI:FavoriteLanguage' => 'Language of the User Interface~~',
@@ -1224,5 +1233,4 @@ Quando è associata a un trigger, ad ogni azione è assegnato un numero "ordine"
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-?>
+));

+ 9 - 4
dictionaries/ja.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -17,7 +17,7 @@
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
 
 /**
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -325,6 +325,12 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
 	'Core:AttributeDecimal+' => 'Decimal値 (負数あり)',
 	'Core:AttributeBoolean' => 'ブール型',
 	'Core:AttributeBoolean+' => 'Bool値',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => '文字列',
 	'Core:AttributeString+' => '文字列',
 	'Core:AttributeClass' => 'クラス',
@@ -631,5 +637,4 @@ Operators:<br/>
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-?>
+));

+ 12 - 4
dictionaries/ja.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -17,7 +17,7 @@
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
 
 /**
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -608,6 +608,11 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$sの詳細',
 	'UI:ErrorPageTitle' => 'iTop - エラー',
 	'UI:ObjectDoesNotExist' => '申し訳ございません。このオブジェクトは既に存在しません。(あるいは参照する権限がありません。)',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - 検索結果',
 	'UI:SearchResultsTitle' => '検索結果',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -856,6 +861,10 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
 	'UI:Pagination:All' => '全',
 	'UI:HierarchyOf_Class' => '%1$s の階層',
 	'UI:Preferences' => 'プリファレンス...',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'クイックアクセス組織',
 	'UI:FavoriteOrganizations+' => '迅速なアクセスのためのドロップダウンメニューに表示したい組織は、以下のリストで確認してください。セキュリティ設定ではないことに注意してください。全ての組織のオブジェクトは、表示可能です。ドロップダウンリストで「すべての組織(All Organizations)」を選択することでアクセスすることができます。',
 	'UI:FavoriteLanguage' => 'ユーザインターフェースの言語',
@@ -1103,5 +1112,4 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-?>
+));

+ 10 - 5
dictionaries/nl.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -23,7 +23,7 @@
  * Linux & Open Source Professionals
  * http://www.linprofs.com
  * 
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -50,6 +50,13 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => 'Boolean',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => 'Alphanumerieke string',
@@ -861,6 +868,4 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-
-?>
+));

+ 12 - 4
dictionaries/nl.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php 
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -23,7 +23,7 @@
  * Linux & Open Source Professionals
  * http://www.linprofs.com
  * 
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @licence	http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -768,6 +768,11 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s details',
 	'UI:ErrorPageTitle' => 'iTop - Error',
 	'UI:ObjectDoesNotExist' => 'Sorry, dit object bestaat niet (of u bent niet gemachtigd het te bekijken).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Zoekresultaten',
 	'UI:SearchResultsTitle' => 'Zoekresultaten',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1042,6 +1047,10 @@ Indien gekoppeld aan een Trigger, wordt aan elke actie een "orde" nummer gegeven
 	'UI:Pagination:All' => 'Alles',
 	'UI:HierarchyOf_Class' => 'Hierarchie van %1$s',
 	'UI:Preferences' => 'Voorkeuren...',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favoriete Organizaties',
 	'UI:FavoriteOrganizations+' => 'Bekijk de organisaties die u wilt zijn in het drop-down menu voor een snelle toegang in de onderstaande lijst. '.
 								   'Merk op dat dit geen security instelling is, objecten van elke organisatie zijn nog steed zichtbaar en toegankelijk door "All Organizations" te selecteren in de drop-down list.',
@@ -1292,5 +1301,4 @@ Indien gekoppeld aan een Trigger, wordt aan elke actie een "orde" nummer gegeven
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-?>
+));

+ 10 - 5
dictionaries/pt_br.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -45,6 +45,13 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 
 	'Core:AttributeBoolean' => 'Boolean',
 	'Core:AttributeBoolean+' => 'Boolean',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
 	'Core:AttributeString' => 'String',
 	'Core:AttributeString+' => 'Seqüência alfanumérica',
@@ -854,6 +861,4 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-
-?>
+));

+ 12 - 4
dictionaries/pt_br.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Localized data
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -760,6 +760,11 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 	'UI:DetailsPageTitle' => '%1$s - %2$s detalhes',
 	'UI:ErrorPageTitle' => 'Erro',
 	'UI:ObjectDoesNotExist' => 'Desculpe, este objeto não existe (ou você não tem permissão para vê-lo).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'Resultado da pesquisa',
 	'UI:SearchResultsTitle' => 'Resultado da pesquisa',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1033,6 +1038,10 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:Pagination:All' => 'Todos',
 	'UI:HierarchyOf_Class' => 'Hierarquia de %1$s',
 	'UI:Preferences' => 'Preferências...',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Organizações favoritas',
 	'UI:FavoriteOrganizations+' => 'Confira na lista abaixo as organizações que você deseja ver no menu drop-down para um acesso rápido.'.
 'Note-se que esta não é uma configuração de segurança, objetos de qualquer organização ainda são visíveis e podem ser acessadas ao selecionar \"Todos Organizações\" na lista drop-down.',
@@ -1283,5 +1292,4 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-?>
+));

+ 7 - 0
dictionaries/ru.dictionary.itop.core.php

@@ -31,6 +31,13 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 
 	'Core:AttributeBoolean' => 'Логич.',
 	'Core:AttributeBoolean+' => 'Boolean',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 
 	'Core:AttributeString' => 'Строка',
 	'Core:AttributeString+' => 'Alphanumeric string',

+ 9 - 0
dictionaries/ru.dictionary.itop.ui.php

@@ -748,6 +748,11 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s подробности',
 	'UI:ErrorPageTitle' => 'iTop - Ошибка',
 	'UI:ObjectDoesNotExist' => 'Извните, этот объект не существует (или вы не можете его видеть).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Результаты поиска',
 	'UI:SearchResultsTitle' => 'Результаты поиска',
 	'UI:SearchResultsTitle+' => 'Результаты полнотекстового поиска',
@@ -1054,6 +1059,10 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 	'UI:Pagination:All' => 'Все',
 	'UI:HierarchyOf_Class' => 'Иерархия по: %1$s~~',
 	'UI:Preferences' => 'Дополнительно...~~',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Избранные организации',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting "All Organizations" in the drop-down list.',
 	'UI:FavoriteLanguage' => 'Язык пользовательского интерфейса',

+ 10 - 6
dictionaries/tr.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -21,7 +21,7 @@
  * Localized data
  *
  * @author      Izzet Sirin <izzet.sirin@htr.com.tr>
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -413,6 +413,13 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
 	'Core:AttributeDecimal+' => 'Decimal value (could be negative)~~',
 	'Core:AttributeBoolean' => 'Boolean~~',
 	'Core:AttributeBoolean+' => 'Boolean~~',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => 'String~~',
 	'Core:AttributeString+' => 'Alphanumeric string~~',
 	'Core:AttributeClass' => 'Class~~',
@@ -781,7 +788,4 @@ Operators:<br/>
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-
-
-?>
+));

+ 12 - 7
dictionaries/tr.dictionary.itop.ui.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -21,7 +21,7 @@
  * Localized data
  *
  * @author      Izzet Sirin <izzet.sirin@htr.com.tr>
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -645,6 +645,11 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s detayları',
 	'UI:ErrorPageTitle' => 'iTop - Hata',
 	'UI:ObjectDoesNotExist' => 'Nesne mevcut değil veya yetkiniz yok.',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - Arama Sonuçları',
 	'UI:SearchResultsTitle' => 'Arama Sonuçları',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1058,6 +1063,10 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe
 	'UI:Pagination:All' => 'All~~',
 	'UI:HierarchyOf_Class' => 'Hierarchy of %1$s~~',
 	'UI:Preferences' => 'Preferences...~~',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favorite Organizations~~',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting \"All Organizations\" in the drop-down list.~~',
 	'UI:FavoriteLanguage' => 'Language of the User Interface~~',
@@ -1251,8 +1260,4 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe
 	'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
 	'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
 	'UI:Menu:ExportPDF' => 'Export as PDF...~~',
-));
-
-
-
-?>
+));

+ 9 - 6
dictionaries/zh.dictionary.itop.core.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -21,7 +21,7 @@
  * Localized data
  *
  * @author      Robert Deng <denglx@gmail.com>
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -412,6 +412,12 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'Core:AttributeDecimal+' => 'Decimal value (could be negative)~~',
 	'Core:AttributeBoolean' => 'Boolean~~',
 	'Core:AttributeBoolean+' => 'Boolean~~',
+	'Core:AttributeBoolean/Value:null' => '',
+	'Core:AttributeBoolean/Value:yes' => 'Yes~~',
+	'Core:AttributeBoolean/Value:no' => 'No~~',
+	'Core:AttributeArchiveFlag/Value:yes' => 'Yes~~',
+	'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode~~',
+	'Core:AttributeArchiveFlag/Value:no' => 'No~~',
 	'Core:AttributeString' => 'String~~',
 	'Core:AttributeString+' => 'Alphanumeric string~~',
 	'Core:AttributeClass' => 'Class~~',
@@ -780,7 +786,4 @@ Operators:<br/>
 	'Core:Validator:Mandatory' => 'Please, fill this field~~',
 	'Core:Validator:MustBeInteger' => 'Must be an integer~~',
 	'Core:Validator:MustSelectOne' => 'Please, select one~~',
-));
-
-
-?>
+));

+ 9 - 0
dictionaries/zh.dictionary.itop.ui.php

@@ -644,6 +644,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'UI:DetailsPageTitle' => 'iTop - %1$s - %2$s 详细内容',
 	'UI:ErrorPageTitle' => 'iTop - 错误',
 	'UI:ObjectDoesNotExist' => '抱歉, 该对象不存在 (或您未被允许浏览该对象).',
+	'UI:ObjectArchived' => 'This object has been archived. Please enable the archive mode or contact your administrator.~~',
+	'Tag:Archived' => 'Archived~~',
+	'Tag:Archived+' => 'Can be accessed only in archive mode~~',
+	'Tag:Synchronized' => 'Synchronized~~',
+	'ObjectRef:Archived' => 'Archived~~',
 	'UI:SearchResultsPageTitle' => 'iTop - 搜索结果',
 	'UI:SearchResultsTitle' => '搜索结果',
 	'UI:SearchResultsTitle+' => 'Full-text search results~~',
@@ -1057,6 +1062,10 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'UI:Pagination:All' => 'All~~',
 	'UI:HierarchyOf_Class' => 'Hierarchy of %1$s~~',
 	'UI:Preferences' => 'Preferences...~~',
+	'UI:ArchiveModeOn' => 'Activate archive mode~~',
+	'UI:ArchiveModeOff' => 'Deactivate archive mode~~',
+	'UI:ArchiveMode:Banner' => 'Archive mode~~',
+	'UI:ArchiveMode:Banner+' => 'Archived objects are visible, and no modification is allowed~~',
 	'UI:FavoriteOrganizations' => 'Favorite Organizations~~',
 	'UI:FavoriteOrganizations+' => 'Check in the list below the organizations that you want to see in the drop-down menu for a quick access. Note that this is not a security setting, objects from any organization are still visible and can be accessed by selecting \"All Organizations\" in the drop-down list.~~',
 	'UI:FavoriteLanguage' => 'Language of the User Interface~~',

+ 2 - 1
js/breadcrumb.js

@@ -75,7 +75,8 @@ $(function()
 						}
 						else
 						{
-							sBreadCrumbHtml += '<div class="breadcrumb-item"><a class="breadcrumb-link" breadcrumb-entry="'+iEntry+'" href="'+oEntry['url']+'" title="'+sTitle+'">'+sIconSpec+'<span class="truncate">'+oEntry['label']+'</span></a></div>';
+							var sSanitizedUrl = StripArchiveArgument(oEntry['url']);
+							sBreadCrumbHtml += '<div class="breadcrumb-item"><a class="breadcrumb-link" breadcrumb-entry="'+iEntry+'" href="'+sSanitizedUrl+'" title="'+sTitle+'">'+sIconSpec+'<span class="truncate">'+oEntry['label']+'</span></a></div>';
 						}
 					}
 				}

+ 20 - 3
pages/UI.php

@@ -377,7 +377,25 @@ try
 				$sMessageKey = $sClass.'::'.$id;
 				DisplayMessages($sMessageKey, $oP);
 				$oP->set_title(Dict::S('UI:ErrorPageTitle'));
-				$oP->P(Dict::S('UI:ObjectDoesNotExist'));
+
+				// Attempt to load the object in archive mode
+				DBSearch::SetArchiveModeDefault(true);
+				if (is_numeric($id))
+				{
+					$oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */);
+				}
+				else
+				{
+					$oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */);
+				}
+				if (is_null($oObj))
+				{
+					$oP->P(Dict::S('UI:ObjectDoesNotExist'));
+				}
+				else
+				{
+					$oP->P(Dict::S('UI:ObjectArchived'));
+				}
 			}
 			else
 			{
@@ -1745,5 +1763,4 @@ catch(Exception $e)
 
 		IssueLog::Error($e->getMessage());
 	}
-}
-?>
+}

+ 5 - 1
portal/index.php

@@ -1310,7 +1310,11 @@ try
 	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
 	LoginWebPage::DoLogin(false /* bMustBeAdmin */, true /* IsAllowedToPortalUsers */); // Check user rights and prompt if needed
 
-   ApplicationContext::SetUrlMakerClass('MyPortalURLMaker');
+	ApplicationContext::SetUrlMakerClass('MyPortalURLMaker');
+
+	$bArchiveMode = utils::IsArchiveMode();
+	DBSearch::SetArchiveModeDefault($bArchiveMode);
+	if ($bArchiveMode) MetaModel::DBSetReadOnly();
 
 	$aClasses = explode(',', MetaModel::GetConfig()->Get('portal_tickets'));
 	$sMainClass = trim(reset($aClasses));

+ 6 - 1
setup/compiler.class.inc.php

@@ -725,7 +725,7 @@ EOF
 				$val = $sDefault;
 			}
 		}
-		return "'".$val."'";
+		return "'".str_replace("'", "\\'", $val)."'";
 	}
 	
 	protected function GetMandatoryPropString($oNode, $sTag)
@@ -989,6 +989,11 @@ EOF
 			$aClassParams['indexes'] = var_export($aIndexes, true);
 		}
 
+		if ($oArchive = $oProperties->GetOptionalElement('archive'))
+		{
+			$bEnabled = $this->GetPropBoolean($oArchive, 'enabled', false);
+			$aClassParams['archive'] = $bEnabled;
+		}
 
 		// Finalize class params declaration
 		//