Bladeren bron

Bulk Export redesign, addressing the tickets:
#1071 Bulk Read access rights
#1034 List of fields for Excel export
#772 Some attributes not exportedvia export.php
Main features:
- list and order of the fields taken into account
- interactive mode to specify all the parameters interactively (including the list and the order of fields)
- same behavior for all the formats: html, CSV, spreadsheet, XML
- new PDF export

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3606 a333f486-631f-4898-b8df-5754b55c2be0

dflaven 10 jaren geleden
bovenliggende
commit
47e596aa0b
38 gewijzigde bestanden met toevoegingen van 6505 en 2316 verwijderingen
  1. 2 0
      application/itopwebpage.class.inc.php
  2. 1 1
      application/query.class.inc.php
  3. 30 20
      application/utils.inc.php
  4. 5 2
      application/xlsxwriter.class.php
  5. 410 0
      core/bulkexport.class.inc.php
  6. 1 0
      core/config.class.inc.php
  7. 352 0
      core/csvbulkexport.class.inc.php
  8. 309 0
      core/excelbulkexport.class.inc.php
  9. 243 0
      core/htmlbulkexport.class.inc.php
  10. 149 0
      core/pdfbulkexport.class.inc.php
  11. 316 0
      core/spreadsheetbulkexport.class.inc.php
  12. 323 0
      core/tabularbulkexport.class.inc.php
  13. 196 0
      core/xmlbulkexport.class.inc.php
  14. 40 0
      css/dragtable.css
  15. 25 0
      css/light-grey.css
  16. 17 0
      css/light-grey.scss
  17. 1 1
      dictionaries/da.dictionary.itop.ui.php
  18. 1 1
      dictionaries/de.dictionary.itop.ui.php
  19. 45 1
      dictionaries/dictionary.itop.core.php
  20. 1 1
      dictionaries/dictionary.itop.ui.php
  21. 1 1
      dictionaries/es_cr.dictionary.itop.ui.php
  22. 44 1
      dictionaries/fr.dictionary.itop.core.php
  23. 1 1
      dictionaries/fr.dictionary.itop.ui.php
  24. 1 1
      dictionaries/hu.dictionary.itop.ui.php
  25. 1 1
      dictionaries/it.dictionary.itop.ui.php
  26. 1 1
      dictionaries/ja.dictionary.itop.ui.php
  27. 1 1
      dictionaries/nl.dictionary.itop.ui.php
  28. 1 1
      dictionaries/pt_br.dictionary.itop.ui.php
  29. 1 1
      dictionaries/ru.dictionary.itop.ui.php
  30. 1 1
      dictionaries/tr.dictionary.itop.ui.php
  31. 1 1
      dictionaries/zh.dictionary.itop.ui.php
  32. 401 0
      js/jquery.dragtable.js
  33. 513 0
      js/tabularfieldsselector.js
  34. 194 10
      js/utils.js
  35. 0 185
      js/xlsx-export.js
  36. 4 0
      pages/UI.php
  37. 2194 2083
      pages/ajax.render.php
  38. 678 0
      webservices/export-v2.php

+ 2 - 0
application/itopwebpage.class.inc.php

@@ -79,6 +79,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 		$sSearchAny = addslashes(Dict::S('UI:SearchValue:Any'));
 		$sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected'));
 		$this->add_dict_entry('UI:FillAllMandatoryFields');
+		$this->add_dict_entry('UI:Button:Cancel');
+		$this->add_dict_entry('UI:Button:Done');
 		
 		$bForceMenuPane = utils::ReadParam('force_menu_pane', null);
 		$sInitClosed = '';

+ 1 - 1
application/query.class.inc.php

@@ -92,7 +92,7 @@ class QueryOQL extends Query
 		
 		if (!$bEditMode)
 		{
-			$sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey();
+			$sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey();
 			$sOql = $this->Get('oql');
 			$sMessage = null;
 			try

+ 30 - 20
application/utils.inc.php

@@ -487,19 +487,23 @@ class utils
      */                   
 	static public function GetAbsoluteUrlAppRoot()
 	{
-		$sUrl = self::GetConfig()->Get('app_root_url');
-		if (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1)
+		static $sUrl = null;
+		if ($sUrl === null)
 		{
-			if (isset($_SERVER['SERVER_NAME']))
+			$sUrl = self::GetConfig()->Get('app_root_url');
+			if (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1)
 			{
-				$sServerName = $_SERVER['SERVER_NAME'];
-			}
-			else
-			{
-				// CLI mode ?
-				$sServerName = php_uname('n');
+				if (isset($_SERVER['SERVER_NAME']))
+				{
+					$sServerName = $_SERVER['SERVER_NAME'];
+				}
+				else
+				{
+					// CLI mode ?
+					$sServerName = php_uname('n');
+				}
+				$sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl);
 			}
-			$sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl);
 		}
 		return $sUrl;
 	}
@@ -783,16 +787,16 @@ class utils
 			$sOQL = addslashes($param->GetFilter()->ToOQL(true));
 			$sFilter = urlencode($param->GetFilter()->serialize());
 			$sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}";
-			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
-			$sXlsxFilter = $param->GetFilter()->serialize();
-			$sXlsxJSFilter = addslashes($sXlsxFilter);
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
+			$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
 			
 			$aResult = array(
 				new SeparatorPopupMenuItem(),
 				// Static menus: Email this page, CSV Export & Add to Dashboard
 				new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
-				new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), $sUrl."&format=csv"),
-				new JSPopupMenuItem('xlsx-export', Dict::S('ExcelExporter:ExportMenu'), "XlsxExportDialog('$sXlsxJSFilter');", array()),
+				new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '$sDataTableId', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"),
+				new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '$sDataTableId', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"),
 				new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL')"),
 				new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"),
 			);
@@ -801,20 +805,26 @@ class utils
 			case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS:
 			// $param is a DBObject
 			$oObj = $param;
-			$oFilter = DBobjectSearch::FromOQL("SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey());
+			$sOQL = "SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey();
+			$oFilter = DBObjectSearch::FromOQL($sOQL);
 			$sFilter = $oFilter->serialize();
 			$sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey());
 			$sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj));
 			$oAppContext = new ApplicationContext();
 			$sContext = $oAppContext->GetForLink();
-			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
-			$sXlsxJSFilter = addslashes($sFilter);
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
+			$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
+			$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
+			
 			$aResult = array(
 				new SeparatorPopupMenuItem(),
 				// Static menus: Email this page & CSV Export
 				new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
-				new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."&format=csv&{$sContext}"),
-				new JSPopupMenuItem('xlsx-export', Dict::S('ExcelExporter:ExportMenu'), "XlsxExportDialog('$sXlsxJSFilter');", array()),
+				new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"),
+				new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"),
 			);
 			break;
 

+ 5 - 2
application/xlsxwriter.class.php

@@ -79,7 +79,7 @@ Class XLSXWriter
 	}
 
 	
-	public function writeSheet(array $data, $sheet_name='', array $header_types=array() )
+	public function writeSheet(array $data, $sheet_name='', array $header_types=array(), array $header_row=array() )
 	{
 		$data = empty($data) ? array( array('') ) : $data;
 		
@@ -95,7 +95,10 @@ Class XLSXWriter
 
 		$tabselected = count($this->sheets_meta)==1 ? 'true' : 'false';//only first sheet is selected
 		$cell_formats_arr = empty($header_types) ? array_fill(0, $column_count, 'string') : array_values($header_types);
-		$header_row = empty($header_types) ? array() : array_keys($header_types);
+		if (empty($header_row) && !empty($header_types))
+		{
+			$header_row = empty($header_types) ? array() : array_keys($header_types);
+		}
 
 		$fd = fopen($sheet_filename, "w+");
 		if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }

+ 410 - 0
core/bulkexport.class.inc.php

@@ -0,0 +1,410 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+define('EXPORTER_DEFAULT_CHUNK_SIZE', 1000);
+
+class BulkExportException extends Exception
+{
+	protected $sLocalizedMessage;
+	public function __construct($message, $sLocalizedMessage, $code = null, $previous = null)
+	{
+		parent::__construct($message, $code, $previous);
+		$this->sLocalizedMessage = $sLocalizedMessage;
+	}
+	
+	public function GetLocalizedMessage()
+	{
+		return $this->sLocalizedMessage;
+	}
+}
+class BulkExportMissingParameterException extends BulkExportException
+{
+	public function __construct($sFieldCode)
+	{
+		parent::__construct('Missing parameter: '.$sFieldCode, Dict::Format('Core:BulkExport:MissingParameter_Param', $sFieldCode));
+	}
+		
+}
+
+/**
+ * Class BulkExport
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+class BulkExportResult extends DBObject
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => 'core/cmdb',
+			"key_type" => 'autoincrement',
+			"name_attcode" => array('created'),
+			"state_attcode" => '',
+			"reconc_keys" => array(),
+			"db_table" => 'priv_bulk_export_result',
+			"db_key_field" => 'id',
+			"db_finalclass_field" => '',
+			"display_template" => '',
+		);
+		MetaModel::Init_Params($aParams);
+
+		MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeInteger("user_id", array("allowed_values"=>null, "sql"=>"user_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeInteger("chunk_size", array("allowed_values"=>null, "sql"=>"chunk_size", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("format", array("allowed_values"=>null, "sql"=>"format", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("temp_file_path", array("allowed_values"=>null, "sql"=>"temp_file_path", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeLongText("search", array("allowed_values"=>null, "sql"=>"search", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeLongText("status_info", array("allowed_values"=>null, "sql"=>"status_info", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+	}
+	
+	public function ComputeValues()
+	{
+		$this->Set('user_id', UserRights::GetUserId());
+	}
+}
+
+/**
+ * Garbage collector for cleaning "old" export results from the database and the disk.
+ * This background process runs once per day and deletes the results of all exports which
+ * are older than one day.
+ */
+class BulkExportResultGC implements iBackgroundProcess
+{
+	public function GetPeriodicity()
+	{
+		return 24*3600; // seconds
+	}
+
+	public function Process($iTimeLimit)
+	{
+		$sDateLimit = date('Y-m-d H:i:s', time() - 24*3600); // Every BulkExportResult older than one day will be deleted
+
+		$sOQL = "SELECT BulkExportResult WHERE created < '$sDateLimit'";
+		$iProcessed = 0;
+		while (time() < $iTimeLimit)
+		{
+			// Next one ?
+			$oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */);
+			$oSet->OptimizeColumnLoad(array('temp_file_path'));
+			$oResult = $oSet->Fetch();
+			if (is_null($oResult))
+			{
+				// Nothing to be done
+				break;
+			}
+			$iProcessed++;
+			@unlink($oResult->Get('temp_file_path'));
+			$oResult->DBDelete();
+		}
+		return "Cleaned $iProcessed old export results(s).";
+	}
+}
+
+/**
+ * Class BulkExport
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+abstract class BulkExport
+{
+	protected $oSearch;
+	protected $iChunkSize;
+	protected $sFormatCode;
+	protected $aStatusInfo;
+	protected $oBulkExportResult;
+	protected $sTmpFile;
+	
+	public function __construct()
+	{
+		$this->oSearch = null;
+		$this->iChunkSize = 0;
+		$this->sFormatCode = null;
+		$this->aStatusInfo = array();
+		$this->oBulkExportResult = null;
+		$this->sTmpFile = '';
+	}
+		
+	/**
+	 * Find the first class capable of exporting the data in the given format
+	 * @param string $sFormat The lowercase format (e.g. html, csv, spreadsheet, xlsx, xml, json, pdf...)
+	 * @param DBObjectSearch $oSearch The search/filter defining the set of objects to export or null when listing the supported formats
+	 * @return iBulkExport|NULL
+	 */
+	static public function FindExporter($sFormatCode, $oSearch = null)
+	{
+		foreach(get_declared_classes() as $sPHPClass)
+		{
+			$oRefClass = new ReflectionClass($sPHPClass);
+			if ($oRefClass->isSubclassOf('BulkExport') && !$oRefClass->isAbstract())
+			{
+				$oBulkExporter = new $sPHPClass();
+				if ($oBulkExporter->IsFormatSupported($sFormatCode, $oSearch))
+				{
+					if ($oSearch)
+					{
+						$oBulkExporter->SetObjectList($oSearch);
+					}
+					return $oBulkExporter;
+				}
+			}
+		}
+		return null;
+	}
+	
+	/**
+	 * Find the exporter corresponding to the given persistent token
+	 * @param int $iPersistentToken The identifier of the BulkExportResult object storing the information
+	 * @return iBulkExport|NULL
+	 */
+	static public function FindExporterFromToken($iPersistentToken = null)
+	{
+		$oBulkExporter = null;
+		$oInfo = MetaModel::GetObject('BulkExportResult', $iPersistentToken, false);
+		if ($oInfo && ($oInfo->Get('user_id') == UserRights::GetUserId()))
+		{
+			$sFormatCode = $oInfo->Get('format');
+			$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
+			
+			$oBulkExporter = self::FindExporter($sFormatCode, $oSearch);
+			if ($oBulkExporter)
+			{
+				$oBulkExporter->SetFormat($sFormatCode);
+				$oBulkExporter->SetObjectList($oSearch);
+				$oBulkExporter->SetChunkSize($oInfo->Get('chunk_size'));
+				$oBulkExporter->SetStatusInfo(json_decode($oInfo->Get('status_info'), true));
+				$oBulkExporter->sTmpFile = $oInfo->Get('temp_file_path');
+				$oBulkExporter->oBulkExportResult = $oInfo;
+			}
+		}
+		return $oBulkExporter;
+	}
+	
+	public function AppendToTmpFile($data)
+	{
+		if ($this->sTmpFile == '')
+		{
+			$this->sTmpFile = $this->MakeTmpFile($this->GetFileExtension());
+		}
+		$hFile = fopen($this->sTmpFile, 'ab');
+		if ($hFile !== false)
+		{
+			fwrite($hFile, $data);
+			fclose($hFile);
+		}
+	}
+	
+	public function GetTmpFilePath()
+	{
+		return $this->sTmpFile;
+	}
+	
+	/**
+	 * Lists all possible export formats. The output is a hash array in the form: 'format_code' => 'localized format label'
+	 * @return multitype:string
+	 */
+	static public function FindSupportedFormats()
+	{
+		$aSupportedFormats = array();
+		foreach(get_declared_classes() as $sPHPClass)
+		{
+			$oRefClass = new ReflectionClass($sPHPClass);
+			if ($oRefClass->isSubClassOf('BulkExport') && !$oRefClass->isAbstract())
+			{
+				$oBulkExporter = new $sPHPClass;
+				$aFormats = $oBulkExporter->GetSupportedFormats();
+				$aSupportedFormats = array_merge($aSupportedFormats, $aFormats);
+			}
+		}
+		return $aSupportedFormats;
+	}
+
+	/**
+	 * (non-PHPdoc)
+	 * @see iBulkExport::SetChunkSize()
+	 */
+	public function SetChunkSize($iChunkSize)
+	{
+		$this->iChunkSize = $iChunkSize;
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see iBulkExport::SetObjectList()
+	 */
+	public function SetObjectList(DBObjectSearch $oSearch)
+	{
+		$this->oSearch = $oSearch;
+	}
+	
+	public function SetFormat($sFormatCode)
+	{
+		$this->sFormatCode = $sFormatCode;	
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see iBulkExport::IsFormatSupported()
+	 */
+	public function IsFormatSupported($sFormatCode, $oSearch = null)
+	{
+		return array_key_exists($sFormatCode, $this->GetSupportedFormats());
+	}
+
+	/**
+	 * (non-PHPdoc)
+	 * @see iBulkExport::GetSupportedFormats()
+	 */
+	public function GetSupportedFormats()
+	{
+		return array(); // return array('csv' => Dict::S('UI:ExportFormatCSV'));
+	}
+	
+	public function GetHeader()
+	{
+		
+	}
+	abstract public function GetNextChunk(&$aStatus);
+	public function GetFooter()
+	{
+		
+	}
+	
+	public function SaveState()
+	{
+		if ($this->oBulkExportResult === null)
+		{
+			$this->oBulkExportResult = new BulkExportResult();
+			$this->oBulkExportResult->Set('format', $this->sFormatCode);
+			$this->oBulkExportResult->Set('search', $this->oSearch->serialize());
+			$this->oBulkExportResult->Set('chunk_size', $this->iChunkSize);	
+			$this->oBulkExportResult->Set('temp_file_path', $this->sTmpFile);	
+		}
+		$this->oBulkExportResult->Set('status_info', json_encode($this->GetStatusInfo()));
+		return $this->oBulkExportResult->DBWrite();
+	}
+	
+	public function Cleanup()
+	{
+		if (($this->oBulkExportResult &&  (!$this->oBulkExportResult->IsNew())))
+		{
+			$sFilename = $this->oBulkExportResult->Get('temp_file_path');
+			if ($sFilename != '')
+			{
+				@unlink($sFilename);
+			}
+			$this->oBulkExportResult->DBDelete();
+		}
+	}
+	
+	public function EnumFormParts()
+	{
+		return array();
+	}
+	
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+	}
+	
+	public function DisplayUsage(Page $oP)
+	{
+		
+	}
+	public function ReadParameters()
+	{
+		
+	}
+	
+	public function GetResultAsHtml()
+	{
+		
+	}
+	public function GetRawResult()
+	{
+		
+	}
+	public function GetMimeType()
+	{
+		
+	}
+	public function GetFileExtension()
+	{
+		
+	}
+	
+	public function GetStatistics()
+	{
+		
+	}
+	
+	public function GetDownloadFileName()
+	{
+		return Dict::Format('Core:BulkExportOf_Class', MetaModel::GetName($this->oSearch->GetClass())).'.'.$this->GetFileExtension();
+	}
+
+	public function SetStatusInfo($aStatusInfo)
+	{
+		$this->aStatusInfo = $aStatusInfo;
+	}
+	
+	public function GetStatusInfo()
+	{
+		return $this->aStatusInfo;
+	}
+
+	protected function MakeTmpFile($sExtension)
+	{
+		if(!is_dir(APPROOT."data/bulk_export"))
+		{
+			@mkdir(APPROOT."data/bulk_export", 0777, true /* recursive */);
+			clearstatcache();
+		}
+		if (!is_writable(APPROOT."data/bulk_export"))
+		{
+			throw new Exception('Data directory "'.APPROOT.'data/bulk_export" could not be written.');
+		}
+
+		$iNum = rand();
+		$sFileName = '';
+		do
+		{
+			$iNum++;
+			$sToken = sprintf("%08x", $iNum);
+			$sFileName = APPROOT."data/bulk_export/$sToken.".$sExtension;
+			$hFile = @fopen($sFileName, 'x');
+		}
+		while($hFile === false);
+	
+		fclose($hFile);
+		return $sFileName;
+	}
+}
+
+// The built-in exports
+require_once(APPROOT.'core/tabularbulkexport.class.inc.php');
+require_once(APPROOT.'core/htmlbulkexport.class.inc.php');
+require_once(APPROOT.'core/pdfbulkexport.class.inc.php');
+require_once(APPROOT.'core/csvbulkexport.class.inc.php');
+require_once(APPROOT.'core/excelbulkexport.class.inc.php');
+require_once(APPROOT.'core/spreadsheetbulkexport.class.inc.php');
+require_once(APPROOT.'core/xmlbulkexport.class.inc.php');
+

+ 1 - 0
core/config.class.inc.php

@@ -957,6 +957,7 @@ class Config
 			'core/event.class.inc.php',
 			'core/action.class.inc.php',
 			'core/trigger.class.inc.php',
+			'core/bulkexport.class.inc.php',
 			'synchro/synchrodatasource.class.inc.php',
 			'core/backgroundtask.class.inc.php',
 		);

+ 352 - 0
core/csvbulkexport.class.inc.php

@@ -0,0 +1,352 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: CSV export
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+class CSVBulkExport extends TabularBulkExport
+{
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * csv format options:");
+		$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
+		$oP->p(" *\tseparator: (optional) character to be used as the separator (default is ',').");
+		$oP->p(" *\tcharacter-set: (optional) character set for encoding the result (default is 'UTF-8').");
+		$oP->p(" *\ttext-qualifier: (optional) character to be used around text strings (default is '\"').");
+		$oP->p(" *\tno_localize: set to 1 to retrieve non-localized values (for instance for ENUM values). Default is 0 (= localized values)");
+	}
+
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+		$this->aStatusInfo['separator'] = utils::ReadParam('separator', ',', true, 'raw_data');
+		if (strtolower($this->aStatusInfo['separator']) == 'tab')
+		{
+			$this->aStatusInfo['separator'] = "\t";
+		}
+		else if (strtolower($this->aStatusInfo['separator']) == 'other')
+		{
+			$this->aStatusInfo['separator'] = utils::ReadParam('other-separator', ',', true, 'raw_data');
+		}
+			
+		$this->aStatusInfo['text_qualifier'] = utils::ReadParam('text-qualifier', '"', true, 'raw_data');
+		if (strtolower($this->aStatusInfo['text_qualifier']) == 'other')
+		{
+			$this->aStatusInfo['text_qualifier'] = utils::ReadParam('other-text-qualifier', '"', true, 'raw_data');
+		}
+		$this->aStatusInfo['localize'] = !((bool)utils::ReadParam('no_localize', 0, true, 'integer'));
+		$this->aStatusInfo['charset'] = strtoupper(utils::ReadParam('character-set', 'UTF-8', true, 'raw_data'));
+	}
+
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('csv_options' => array('separator', 'character-set', 'text-qualifier', 'no_localize') ,'interactive_fields_csv' => array('interactive_fields_csv')));
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'interactive_fields_csv':
+				$this->GetInteractiveFieldsWidget($oP, 'interactive_fields_csv');
+				break;
+
+			case 'csv_options':
+				$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:CSVOptions').'</legend>');
+				$oP->add('<table class="export_parameters"><tr><td style="vertical-align:top">');
+				$oP->add('<h3>'.Dict::S('UI:CSVImport:SeparatorCharacter').'</h3>');
+				$sRawSeparator = utils::ReadParam('separator', ',', true, 'raw_data');
+				$aSep = array(
+					';' => Dict::S('UI:CSVImport:SeparatorSemicolon+'),
+					',' => Dict::S('UI:CSVImport:SeparatorComma+'),
+					'tab' => Dict::S('UI:CSVImport:SeparatorTab+'),
+				);
+				$sOtherSeparator = '';
+				if (!array_key_exists($sRawSeparator, $aSep))
+				{
+					$sOtherSeparator = $sRawSeparator;
+					$sRawSeparator = 'other';
+				}
+				$aSep['other'] = Dict::S('UI:CSVImport:SeparatorOther').' <input type="text" size="3" name="other-separator" value="'.htmlentities($sOtherSeparator, ENT_QUOTES, 'UTF-8').'"/>';
+
+				foreach($aSep as $sVal => $sLabel)
+				{
+					$sChecked = ($sVal == $sRawSeparator) ? 'checked' : '';
+					$oP->add('<input type="radio" name="separator" value="'.htmlentities($sVal, ENT_QUOTES, 'UTF-8').'" '.$sChecked.'/>&nbsp;'.$sLabel.'<br/>');
+				}
+					
+				$oP->add('</td><td style="vertical-align:top">');
+					
+				$oP->add('<h3>'.Dict::S('UI:CSVImport:TextQualifierCharacter').'</h3>');
+
+				$sRawQualifier = utils::ReadParam('text-qualifier', '"', true, 'raw_data');
+				$aQualifiers = array(
+					'"' => Dict::S('UI:CSVImport:QualifierDoubleQuote+'),
+					'\'' => Dict::S('UI:CSVImport:QualifierSimpleQuote+'),
+				);
+				$sOtherQualifier = '';
+				if (!array_key_exists($sRawQualifier, $aQualifiers))
+				{
+					$sOtherQualifier = $sRawQualifier;
+					$sRawQualifier = 'other';
+				}
+				$aQualifiers['other'] = Dict::S('UI:CSVImport:QualifierOther').' <input type="text" size="3" name="other-text-qualifier" value="'.htmlentities($sOtherQualifier, ENT_QUOTES, 'UTF-8').'"/>';
+					
+				foreach($aQualifiers as $sVal => $sLabel)
+				{
+					$sChecked = ($sVal == $sRawQualifier) ? 'checked' : '';
+					$oP->add('<input type="radio" name="text-qualifier" value="'.htmlentities($sVal, ENT_QUOTES, 'UTF-8').'" '.$sChecked.'/>&nbsp;'.$sLabel.'<br/>');
+				}
+				
+				$sChecked = (utils::ReadParam('no_localize', 0) == 1) ? ' checked ' : '';
+				$oP->add('</td><td style="vertical-align:top">');
+				$oP->add('<h3>'.Dict::S('Core:BulkExport:CSVLocalization').'</h3>');
+				$oP->add('<input type="checkbox" id="csv_no_localize" name="no_localize" value="1"'.$sChecked.'><label for="csv_no_localize"> '.Dict::S('Core:BulkExport:OptionNoLocalize').'</label>');
+				$oP->add('</td></tr></table>');
+				
+				$oP->add('</fieldset>');
+				break;
+					
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+
+	protected function GetSampleData(DBObject $oObj, $sAttCode)
+	{
+		return trim($oObj->GetAsCSV($sAttCode), '"');
+	}
+
+	public function GetHeader()
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$this->aStatusInfo['status'] = 'running';
+		$this->aStatusInfo['position'] = 0;
+		$this->aStatusInfo['total'] = $oSet->Count();
+
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aAuthorizedClasses = array();
+		foreach($aSelectedClasses as $sAlias => $sClassName)
+		{
+			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
+			{
+				$aAuthorizedClasses[$sAlias] = $sClassName;
+			}
+		}
+		$aData = array();
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aAuthorizedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+			if (!array_key_exists($sAlias, $aAuthorizedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aAuthorizedClasses))."'");
+			}
+			$sClass = $aAuthorizedClasses[$sAlias];
+				
+			if ($this->aStatusInfo['localize'])
+			{
+				switch($sAttCode)
+				{
+					case 'id':
+						if (count($aAuthorizedClasses) > 1)
+						{
+							$aData[] = $sAlias.'.id';
+						}
+						else
+						{
+							$aData[] = 'id';
+						}
+						break;
+							
+					default:
+						$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+						$sLabel = $this->aStatusInfo['localize'] ? $oAttDef->GetLabel() : $sAttCode;
+						if (count($aAuthorizedClasses) > 1)
+						{
+							$aData[] = $sAlias.'.'.$sLabel;
+						}
+						else
+						{
+							$aData[] = $sLabel;
+						}
+				}
+			}
+			else
+			{
+				$aData[] = $sExtendedAttCode;
+			}
+		}
+		$sFrom = array("\r\n", $this->aStatusInfo['text_qualifier']);
+		$sTo = array("\n", $this->aStatusInfo['text_qualifier'].$this->aStatusInfo['text_qualifier']);
+		foreach($aData as $idx => $sData)
+		{
+			// Escape and encode (if needed) the headers
+			$sEscaped = str_replace($sFrom, $sTo, (string)$sData);
+			$aData[$idx] = $this->aStatusInfo['text_qualifier'].$sEscaped.$this->aStatusInfo['text_qualifier'];
+			if ($this->aStatusInfo['charset'] != 'UTF-8')
+			{
+				// Note: due to bugs in the glibc library it's safer to call iconv on the smallest possible string
+				// and thus to convert field by field and not the whole row or file at once (see ticket #991)
+				$aData[$idx] = iconv('UTF-8', $this->aStatusInfo['charset'].'//IGNORE//TRANSLIT', $aData[$idx]);
+			}
+		}
+		$sData = implode($this->aStatusInfo['separator'], $aData)."\n";
+
+		return $sData;
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sRetCode = 'run';
+		$iPercentage = 0;
+
+		$oSet = new DBObjectSet($this->oSearch);
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aAuthorizedClasses = array();
+		foreach($aSelectedClasses as $sAlias => $sClassName)
+		{
+			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
+			{
+				$aAuthorizedClasses[$sAlias] = $sClassName;
+			}
+		}
+		$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
+
+		$aAliasByField = array();
+		$aColumnsToLoad = array();
+
+		// Prepare the list of aliases / columns to load
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aAuthorizedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+				
+			if (!array_key_exists($sAlias, $aAuthorizedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aAuthorizedClasses))."'");
+			}
+				
+			if (!array_key_exists($sAlias, $aColumnsToLoad))
+			{
+				$aColumnsToLoad[$sAlias] = array();
+			}
+			if ($sAttCode != 'id')
+			{
+				// id is not a real attribute code and, moreover, is always loaded
+				$aColumnsToLoad[$sAlias][] = $sAttCode;
+			}
+			$aAliasByField[$sExtendedAttCode] = array('alias' => $sAlias, 'attcode' => $sAttCode);
+		}
+
+		$iCount = 0;
+		$sData = '';
+		$oSet->OptimizeColumnLoad($aColumnsToLoad);
+		$iPreviousTimeLimit = ini_get('max_execution_time');
+		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+		while($aRow = $oSet->FetchAssoc())
+		{
+			set_time_limit($iLoopTimeLimit);
+			$aData = array();
+			foreach($aAliasByField as $aAttCode)
+			{
+				$sField = '';
+				$oObj = $aRow[$aAttCode['alias']];
+				if ($oObj != null)
+				{
+					switch($aAttCode['attcode'])
+					{
+						case 'id':
+							$sField = $oObj->GetKey();
+							break;
+								
+						default:
+							$sField = $oObj->GetAsCSV($aAttCode['attcode'], $this->aStatusInfo['separator'], $this->aStatusInfo['text_qualifier'], $this->aStatusInfo['localize']);
+					}
+					if ($this->aStatusInfo['charset'] != 'UTF-8')
+					{
+						// Note: due to bugs in the glibc library it's safer to call iconv on the smallest possible string
+						// and thus to convert field by field and not the whole row or file at once (see ticket #991)
+						$aData[] = iconv('UTF-8', $this->aStatusInfo['charset'].'//IGNORE//TRANSLIT', $sField);
+					}
+					else
+					{
+						$aData[] = $sField;
+					}
+				}
+			}
+			$sData .= implode($this->aStatusInfo['separator'], $aData)."\n";
+			$iCount++;
+		}
+		set_time_limit($iPreviousTimeLimit);
+		$this->aStatusInfo['position'] += $this->iChunkSize;
+		if ($this->aStatusInfo['total'] == 0)
+		{
+			$iPercentage = 100;
+		}
+		else
+		{
+			$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
+		}
+
+		if ($iCount < $this->iChunkSize)
+		{
+			$sRetCode = 'done';
+		}
+
+		$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
+		return $sData;
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('csv' => Dict::S('Core:BulkExport:CSVFormat'));
+	}
+
+	public function GetMimeType()
+	{
+		return 'text/csv';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'csv';
+	}
+
+}

+ 309 - 0
core/excelbulkexport.class.inc.php

@@ -0,0 +1,309 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: Excel (xlsx) export
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+require_once(APPROOT.'application/xlsxwriter.class.php');
+
+class ExcelBulkExport extends TabularBulkExport
+{
+	protected $sData;
+
+	public function __construct()
+	{
+		parent::__construct();
+		$this->aStatusInfo['status'] = 'not_started';
+		$this->aStatusInfo['position'] = 0;
+	}
+
+	public function Cleanup()
+	{
+		@unlink($this->aStatusInfo['tmp_file']);
+		parent::Cleanup();
+	}
+
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * xlsx format options:");
+		$oP->p(" *\tfields: the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
+	}
+
+
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('interactive_fields_xlsx' => array('interactive_fields_xlsx')));
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'interactive_fields_xlsx':
+				$this->GetInteractiveFieldsWidget($oP, 'interactive_fields_xlsx');
+				break;
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+		$this->aStatusInfo['localize'] = !((bool)utils::ReadParam('no_localize', 0, true, 'integer'));
+	}
+
+
+	protected function SuggestField($aAliases, $sClass, $sAlias, $sAttCode)
+	{
+		switch($sAttCode)
+		{
+			case 'id': // replace 'id' by 'friendlyname'
+				$sAttCode = 'friendlyname';
+				break;
+					
+			default:
+				$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+				if ($oAttDef instanceof AttributeExternalKey)
+				{
+					$sAttCode .= '_friendlyname';
+				}
+		}
+
+		return parent::SuggestField($aAliases, $sClass, $sAlias, $sAttCode);
+	}
+
+	public function GetHeader()
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$this->aStatusInfo['status'] = 'retrieving';
+		$this->aStatusInfo['tmp_file'] = $this->MakeTmpFile('data');
+		$this->aStatusInfo['position'] = 0;
+		$this->aStatusInfo['total'] = $oSet->Count();
+
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aTableHeaders = array();
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+			$sClass = $aSelectedClasses[$sAlias];
+
+			$sFullAlias = '';
+			if (count($aSelectedClasses) > 1)
+			{
+				$sFullAlias = $sAlias.'.';
+			}
+				
+			switch($sAttCode)
+			{
+				case 'id':
+					$aTableHeaders[] = array('label' => $sFullAlias.'id', 'type' => '0');
+
+					break;
+
+				default:
+					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+					$sType = 'string';
+					if($oAttDef instanceof AttributeDateTime)
+					{
+						$sType = 'datetime';
+					}
+					$sLabel = $sAttCode;
+					if ($this->aStatusInfo['localize'])
+					{
+						$sLabel = $oAttDef->GetLabel();
+					}
+						
+					$aTableHeaders[] = array('label' => $sFullAlias.$sLabel, 'type' => $sType);
+			}
+		}
+
+		$sRow = json_encode($aTableHeaders);
+		$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
+		if ($hFile === false)
+		{
+			throw new Exception('ExcelBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
+		}
+		fwrite($hFile, $sRow."\n");
+		fclose($hFile);
+		return '';
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sRetCode = 'run';
+		$iPercentage = 0;
+
+		$hFile = fopen($this->aStatusInfo['tmp_file'], 'ab');
+		$oSet = new DBObjectSet($this->oSearch);
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
+
+		$aAliasByField = array();
+		$aColumnsToLoad = array();
+
+		// Prepare the list of aliases / columns to load
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+
+			if (!array_key_exists($sAlias, $aColumnsToLoad))
+			{
+				$aColumnsToLoad[$sAlias] = array();
+			}
+			if ($sAttCode != 'id')
+			{
+				// id is not a real attribute code and, moreover, is always loaded
+				$aColumnsToLoad[$sAlias][] = $sAttCode;
+			}
+			$aAliasByField[$sExtendedAttCode] = array('alias' => $sAlias, 'attcode' => $sAttCode);
+		}
+
+		$iCount = 0;
+		$oSet->OptimizeColumnLoad($aColumnsToLoad);
+		$iPreviousTimeLimit = ini_get('max_execution_time');
+		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+		while($aRow = $oSet->FetchAssoc())
+		{
+			set_time_limit($iLoopTimeLimit);
+			$aData = array();
+			foreach($aAliasByField as $aAttCode)
+			{
+				$sField = '';
+				switch($aAttCode['attcode'])
+				{
+					case 'id':
+						$sField = $aRow[$aAttCode['alias']]->GetKey();
+						break;
+							
+					default:
+						$sField = $aRow[$aAttCode['alias']]->Get($aAttCode['attcode']);
+				}
+				$aData[] = $sField;
+			}
+			fwrite($hFile, json_encode($aData)."\n");
+			$iCount++;
+		}
+		set_time_limit($iPreviousTimeLimit);
+		$this->aStatusInfo['position'] += $this->iChunkSize;
+		if ($this->aStatusInfo['total'] == 0)
+		{
+			$iPercentage = 100;
+			$sRetCode = 'done';  // Next phase (GetFooter) will be to build the xlsx file
+		}
+		else
+		{
+			$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
+		}
+		if ($iCount < $this->iChunkSize)
+		{
+			$sRetCode = 'done';
+		}
+		$aStatus = array('code' => $sRetCode, 'message' =>  Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
+		return ''; // The actual XLSX file is built in GetFooter();
+	}
+
+	public function GetFooter()
+	{
+		$hFile = @fopen($this->aStatusInfo['tmp_file'], 'rb');
+		if ($hFile === false)
+		{
+			throw new Exception('ExcelBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for reading.');
+		}
+		$sHeaders = fgets($hFile);
+		$aHeaders = json_decode($sHeaders, true);
+
+		$aData = array();
+		while($sLine = fgets($hFile))
+		{
+			$aRow = json_decode($sLine);
+			$aData[] = $aRow;
+		}
+		fclose($hFile);
+			
+		$fStartExcel = microtime(true);
+		$writer = new XLSXWriter();
+		$writer->setAuthor(UserRights::GetUserFriendlyName());
+		$aHeaderTypes = array();
+		$aHeaderNames = array();
+		foreach($aHeaders as $Header)
+		{
+			$aHeaderNames[] = $Header['label'];
+			$aHeaderTypes[] = $Header['type'];
+		}
+		$writer->writeSheet($aData,'Sheet1', $aHeaderTypes, $aHeaderNames);
+		$fExcelTime = microtime(true) - $fStartExcel;
+		//$this->aStatistics['excel_build_duration'] = $fExcelTime;
+
+		$fTime = microtime(true);
+		$data = $writer->writeToString();
+		$fExcelSaveTime = microtime(true) - $fTime;
+		//$this->aStatistics['excel_write_duration'] = $fExcelSaveTime;
+
+		@unlink($this->aStatusInfo['tmp_file']);
+
+		return $data;
+	}
+
+	public function GetMimeType()
+	{
+		return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'xlsx';
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('xlsx' => Dict::S('Core:BulkExport:XLSXFormat'));
+	}
+}

+ 243 - 0
core/htmlbulkexport.class.inc.php

@@ -0,0 +1,243 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: HTML export
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+class HTMLBulkExport extends TabularBulkExport
+{
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * html format options:");
+		$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
+	}
+
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('interactive_fields_html' => array('interactive_fields_html')));
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'interactive_fields_html':
+				$this->GetInteractiveFieldsWidget($oP, 'interactive_fields_html');
+				break;
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+
+	protected function GetSampleData(DBObject $oObj, $sAttCode)
+	{
+		return $oObj->GetAsHTML($sAttCode);
+	}
+
+	public function GetHeader()
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$this->aStatusInfo['status'] = 'running';
+		$this->aStatusInfo['position'] = 0;
+		$this->aStatusInfo['total'] = $oSet->Count();
+
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aData = array();
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+			$sClass = $aSelectedClasses[$sAlias];
+				
+			switch($sAttCode)
+			{
+				case 'id':
+					if (count($aSelectedClasses) > 1)
+					{
+						$aData[] = $sAlias.'.id'; //@@@
+					}
+					else
+					{
+						$aData[] = 'id'; //@@@
+					}
+					break;
+
+				default:
+					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+					if (count($aSelectedClasses) > 1)
+					{
+						$aData[] = $sAlias.'.'.$oAttDef->GetLabel();
+					}
+					else
+					{
+						$aData[] = $oAttDef->GetLabel();
+					}
+			}
+		}
+		$sData = "<table class=\"listResults\">\n";
+		$sData .= "<thead>\n";
+		$sData .= "<tr>\n";
+		foreach($aData as $sLabel)
+		{
+			$sData .= "<th>".$sLabel."</th>\n";
+		}
+		$sData .= "</tr>\n";
+		$sData .= "</thead>\n";
+		$sData .= "<tbody>\n";
+		return $sData;
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sRetCode = 'run';
+		$iPercentage = 0;
+
+		$oSet = new DBObjectSet($this->oSearch);
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
+
+		$aAliasByField = array();
+		$aColumnsToLoad = array();
+
+		// Prepare the list of aliases / columns to load
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+				
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+				
+			if (!array_key_exists($sAlias, $aColumnsToLoad))
+			{
+				$aColumnsToLoad[$sAlias] = array();
+			}
+			if ($sAttCode != 'id')
+			{
+				// id is not a real attribute code and, moreover, is always loaded
+				$aColumnsToLoad[$sAlias][] = $sAttCode;
+			}
+			$aAliasByField[$sExtendedAttCode] = array('alias' => $sAlias, 'attcode' => $sAttCode);
+		}
+
+		$iCount = 0;
+		$sData = '';
+		$oSet->OptimizeColumnLoad($aColumnsToLoad);
+		$iPreviousTimeLimit = ini_get('max_execution_time');
+		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+		while($aRow = $oSet->FetchAssoc())
+		{
+			set_time_limit($iLoopTimeLimit);
+			$sFirstAlias = reset($aSelectedClasses);
+			$sHilightClass = $aRow[$sFirstAlias]->GetHilightClass();
+			if ($sHilightClass != '')
+			{
+				$sData .= "<tr class=\"$sHilightClass\">";
+			}
+			else
+			{
+				$sData .= "<tr>";
+			}
+			foreach($aAliasByField as $aAttCode)
+			{
+				$sField = '';
+				switch($aAttCode['attcode'])
+				{
+					case 'id':
+						$sField = $aRow[$aAttCode['alias']]->GetHyperlink();
+						break;
+							
+					default:
+						$sField = $aRow[$aAttCode['alias']]->GetAsHtml($aAttCode['attcode']);
+				}
+				$sValue = ($sField === '') ? '&nbsp;' : $sField;
+				$sData .= "<td>$sValue</td>";
+			}
+			$sData .= "</tr>";
+			$iCount++;
+		}
+		set_time_limit($iPreviousTimeLimit);
+		$this->aStatusInfo['position'] += $this->iChunkSize;
+		if ($this->aStatusInfo['total'] == 0)
+		{
+			$iPercentage = 100;
+		}
+		else
+		{
+			$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
+		}
+
+		if ($iCount < $this->iChunkSize)
+		{
+			$sRetCode = 'done';
+		}
+
+		$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
+		return $sData;
+	}
+
+	public function GetFooter()
+	{
+		$sData = "</tbody>\n";
+		$sData .= "</table>\n";
+
+		return $sData;
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('html' => Dict::S('Core:BulkExport:HTMLFormat'));
+	}
+
+	public function GetMimeType()
+	{
+		return 'text/html';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'html';
+	}
+}

+ 149 - 0
core/pdfbulkexport.class.inc.php

@@ -0,0 +1,149 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: PDF export, based on the HTML export converted to PDF
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+require_once(APPROOT.'application/pdfpage.class.inc.php');
+
+class PDFBulkExport extends HTMLBulkExport
+{
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * pdf format options:");
+		$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
+		$oP->p(" *\tpage_size: (optional) size of the page. One of A4, A3, Letter (default is 'A4').");
+		$oP->p(" *\tpage_orientation: (optional) the orientation of the page. Either Portrait or Landscape (default is 'Portrait').");
+	}
+
+	public function EnumFormParts()
+	{
+		return array_merge(array('pdf_options' => array('pdf_options')), parent::EnumFormParts());
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'pdf_options':
+				$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:PDFOptions').'</legend>');
+				$oP->add('<table>');
+				$oP->add('<tr>');
+				$oP->add('<td>'.Dict::S('Core:BulkExport:PDFPageSize').'</td>');
+				$oP->add('<td>'.$this->GetSelectCtrl('page_size', array('A3', 'A4', 'Letter'), 'Core:BulkExport:PageSize-', 'A4').'</td>');
+				$oP->add('</tr>');
+				$oP->add('<td>'.Dict::S('Core:BulkExport:PDFPageOrientation').'</td>');
+				$oP->add('<td>'.$this->GetSelectCtrl('page_orientation', array('P', 'L'), 'Core:BulkExport:PageOrientation-', 'L').'</td>');
+				$oP->add('</tr>');
+				$oP->add('</table>');
+
+				$oP->add('</fieldset>');
+				break;
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+
+	protected function GetSelectCtrl($sName, $aValues, $sDictPrefix, $sDefaultValue)
+	{
+		$sCurrentValue = utils::ReadParam($sName, $sDefaultValue, false, 'raw_data');
+		$aLabels = array();
+		foreach($aValues as $sVal)
+		{
+			$aLabels[$sVal] = Dict::S($sDictPrefix.$sVal);
+		}
+		asort($aLabels);
+
+		$sHtml = '<select name="'.$sName.'"/>';
+		foreach($aLabels as $sVal => $sLabel)
+		{
+			$sSelected = ($sVal == $sCurrentValue) ? 'selected' : '';
+			$sHtml .= '<option value="'.$sVal.'" '.$sSelected.'>'.htmlentities($sLabel, ENT_QUOTES, 'UTF-8').'</option>';
+		}
+		$sHtml .= '</select>';
+		return $sHtml;
+	}
+
+
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+		$this->aStatusInfo['page_size'] = utils::ReadParam('page_size', 'A4', true, 'raw_data');
+		$this->aStatusInfo['page_orientation'] = utils::ReadParam('page_orientation', 'L', true);
+	}
+
+	public function GetHeader()
+	{
+		$this->aStatusInfo['tmp_file'] = $this->MakeTmpFile('data');
+		$sData = parent::GetHeader();
+		$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
+		if ($hFile === false)
+		{
+			throw new Exception('PDFBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
+		}
+		fwrite($hFile, $sData."\n");
+		fclose($hFile);
+		return '';
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sData = parent::GetNextChunk($aStatus);
+		$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
+		if ($hFile === false)
+		{
+			throw new Exception('PDFBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
+		}
+		fwrite($hFile, $sData."\n");
+		fclose($hFile);
+		return '';
+	}
+
+	public function GetFooter()
+	{
+		$sData = parent::GetFooter();
+
+		$oPage = new PDFPage(Dict::Format('Core:BulkExportOf_Class', MetaModel::GetName($this->oSearch->GetClass())), $this->aStatusInfo['page_size'], $this->aStatusInfo['page_orientation']);
+		$oPage->add(file_get_contents($this->aStatusInfo['tmp_file']));
+		$oPage->add($sData);
+
+		$sPDF = $oPage->get_pdf();
+
+		return $sPDF;
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('pdf' => Dict::S('Core:BulkExport:PDFFormat'));
+	}
+
+	public function GetMimeType()
+	{
+		return 'application/x-pdf';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'pdf';
+	}
+}

+ 316 - 0
core/spreadsheetbulkexport.class.inc.php

@@ -0,0 +1,316 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: "spreadsheet" export: a simplified HTML export in which the date/time columns are split in two column: date AND time
+*
+* @copyright   Copyright (C) 2015 Combodo SARL
+* @license     http://opensource.org/licenses/AGPL-3.0
+*/
+
+class SpreadsheetBulkExport extends TabularBulkExport
+{
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * spreadsheet format options:");
+		$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
+		$oP->p(" *\tno_localize: (optional) pass 1 to retrieve the raw (untranslated) values for enumerated fields. Default: 0.");
+	}
+
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('spreadsheet_options' => array('no-localize') ,'interactive_fields_spreadsheet' => array('interactive_fields_spreadsheet')));
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'interactive_fields_spreadsheet':
+				$this->GetInteractiveFieldsWidget($oP, 'interactive_fields_spreadsheet');
+				break;
+					
+			case 'spreadsheet_options':
+				$sChecked = (utils::ReadParam('no_localize', 0) == 1) ? ' checked ' : '';
+				$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:SpreadsheetOptions').'</legend>');
+				$oP->add('<table>');
+				$oP->add('<tr>');
+				$oP->add('<td><input type="checkbox" id="spreadsheet_no_localize" name="no_localize" value="1"'.$sChecked.'><label for="spreadsheet_no_localize"> '.Dict::S('Core:BulkExport:OptionNoLocalize').'</label></td>');
+				$oP->add('</tr>');
+				$oP->add('</table>');
+				$oP->add('</fieldset>');
+				break;
+				
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+	
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+	
+		$this->aStatusInfo['localize'] = (utils::ReadParam('no_localize', 0) != 1);
+	}	
+	
+	protected function GetSampleData(DBObject $oObj, $sAttCode)
+	{
+		return $oObj->GetAsHTML($sAttCode);
+	}
+
+	public function GetHeader()
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$this->aStatusInfo['status'] = 'running';
+		$this->aStatusInfo['position'] = 0;
+		$this->aStatusInfo['total'] = $oSet->Count();
+
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aData = array();
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+			$sClass = $aSelectedClasses[$sAlias];
+
+			switch($sAttCode)
+			{
+				case 'id':
+					if (count($aSelectedClasses) > 1)
+					{
+						$aData[] = $sAlias.'.id'; //@@@
+					}
+					else
+					{
+						$aData[] = 'id'; //@@@
+					}
+					break;
+
+				default:
+					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+					$sColLabel = $this->aStatusInfo['localize'] ? MetaModel::GetLabel($sClass, $sAttCode) : $sAttCode;
+					$oFinalAttDef = $oAttDef->GetFinalAttDef();
+					if (get_class($oFinalAttDef) == 'AttributeDateTime')
+					{
+						if (count($aSelectedClasses) > 1)
+						{
+							$aData[] = $sAlias.'.'.$sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
+							$aData[] = $sAlias.'.'.$sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
+						}
+						else
+						{
+							$aData[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
+							$aData[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
+						}
+					}
+					else
+					{
+						if (count($aSelectedClasses) > 1)
+						{
+							$aData[] = $sAlias.'.'.$sColLabel;
+						}
+						else
+						{
+							$aData[] = $sColLabel;
+						}
+					}
+			}
+		}
+		$sData = "<table border=\"1\">\n";
+		$sData .= "<tr>\n";
+		foreach($aData as $sLabel)
+		{
+			$sData .= "<td>".$sLabel."</td>\n";
+		}
+		$sData .= "</tr>\n";
+		return $sData;
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sRetCode = 'run';
+		$iPercentage = 0;
+
+		$oSet = new DBObjectSet($this->oSearch);
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
+
+		$aAliasByField = array();
+		$aColumnsToLoad = array();
+
+		// Prepare the list of aliases / columns to load
+		foreach($this->aStatusInfo['fields'] as $sExtendedAttCode)
+		{
+			if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+			}
+			else
+			{
+				$sAlias = reset($aSelectedClasses);
+				$sAttCode = $sExtendedAttCode;
+			}
+
+			if (!array_key_exists($sAlias, $aSelectedClasses))
+			{
+				throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", array_keys($aSelectedClasses))."'");
+			}
+
+			if (!array_key_exists($sAlias, $aColumnsToLoad))
+			{
+				$aColumnsToLoad[$sAlias] = array();
+			}
+			if ($sAttCode != 'id')
+			{
+				// id is not a real attribute code and, moreover, is always loaded
+				$aColumnsToLoad[$sAlias][] = $sAttCode;
+			}
+			$aAliasByField[$sExtendedAttCode] = array('alias' => $sAlias, 'attcode' => $sAttCode);
+		}
+
+		$iCount = 0;
+		$sData = '';
+		$oSet->OptimizeColumnLoad($aColumnsToLoad);
+		$iPreviousTimeLimit = ini_get('max_execution_time');
+		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+		while($aRow = $oSet->FetchAssoc())
+		{
+			set_time_limit($iLoopTimeLimit);
+
+			$sData .= "<tr>";
+			foreach($aAliasByField as $aAttCode)
+			{
+				$sField = '';
+				$oObj = $aRow[$aAttCode['alias']];
+				if ($oObj == null)
+				{
+					$sData .= "<td x:str>$sField</td>";
+					continue;
+				}
+				
+				switch($aAttCode['attcode'])
+				{
+					case 'id':
+					$sField = $oObj->GetName();
+					$sData .= "<td>$sField</td>";
+					break;
+							
+					default:
+					$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $aAttCode['attcode']);
+					$oFinalAttDef = $oAttDef->GetFinalAttDef();
+					if (get_class($oFinalAttDef) == 'AttributeDateTime')
+					{
+						$iDate = AttributeDateTime::GetAsUnixSeconds($oObj->Get($aAttCode['attcode']));
+						$sData .= '<td>'.date('Y-m-d', $iDate).'</td>'; // Add the first column directly
+						$sField = date('H:i:s', $iDate); // Will add the second column below
+						$sData .= "<td>$sField</td>";
+					}
+					else if($oAttDef instanceof AttributeCaseLog)
+					{
+						$rawValue = $oObj->Get($aAttCode['attcode']);
+						$sField = str_replace("\n", "<br/>", htmlentities($rawValue->__toString(), ENT_QUOTES, 'UTF-8'));
+						// Trick for Excel: treat the content as text even if it begins with an equal sign
+						$sData .= "<td x:str>$sField</td>";
+					}
+					else
+					{
+						$rawValue = $oObj->Get($aAttCode['attcode']);
+						// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
+						// let's fix this and make sure we render an empty string if the key == 0
+						if ($oAttDef instanceof AttributeFriendlyName)
+						{
+							$sKeyAttCode = $oAttDef->GetKeyAttCode();
+							if ($sKeyAttCode != 'id')
+							{
+								if ($oObj->Get($sKeyAttCode) == 0)
+								{
+									$sValue = '';
+								}
+							}
+						}
+						if ($this->aStatusInfo['localize'])
+						{
+							$sField = htmlentities($oFinalAttDef->GetEditValue($rawValue), ENT_QUOTES, 'UTF-8');
+						}
+						else
+						{
+							$sField = htmlentities($rawValue, ENT_QUOTES, 'UTF-8');
+						}
+						$sData .= "<td>$sField</td>";
+					}
+				}
+
+			}
+			$sData .= "</tr>";
+			$iCount++;
+		}
+		set_time_limit($iPreviousTimeLimit);
+		$this->aStatusInfo['position'] += $this->iChunkSize;
+		if ($this->aStatusInfo['total'] == 0)
+		{
+			$iPercentage = 100;
+		}
+		else
+		{
+			$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
+		}
+
+		if ($iCount < $this->iChunkSize)
+		{
+			$sRetCode = 'done';
+		}
+
+		$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
+		return $sData;
+	}
+
+	public function GetFooter()
+	{
+		$sData = "</table>\n";
+
+		return $sData;
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('spreadsheet' => Dict::S('Core:BulkExport:SpreadsheetFormat'));
+	}
+
+	public function GetMimeType()
+	{
+		return 'text/html';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'html';
+	}
+}

+ 323 - 0
core/tabularbulkexport.class.inc.php

@@ -0,0 +1,323 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: Tabular export: abstract base class for all "tabular" exports.
+ * Provides the user interface for selecting the column to be exported
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+abstract class TabularBulkExport extends BulkExport
+{
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('tabular_fields' => array('fields')));
+	}
+
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'tabular_fields':
+				$sFields = utils::ReadParam('fields', '', true, 'raw_data');
+				$sSuggestedFields = utils::ReadParam('suggested_fields', null, true, 'raw_data');
+				if (($sSuggestedFields !== null) && ($sSuggestedFields !== ''))
+				{
+					$aSuggestedFields = explode(',', $sSuggestedFields);
+					$sFields = implode(',', $this->SuggestFields($aSuggestedFields));
+				}
+				$oP->add('<input id="tabular_fields" type="hidden" size="50" name="fields" value="'.htmlentities($sFields, ENT_QUOTES, 'UTF-8').'"></input>');
+				break;
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+
+	protected function SuggestFields($aSuggestedFields)
+	{
+		// By defaults all fields are Ok, nothing gets translated but
+		// you can overload this method if some fields are better exported
+		// (in a given format) by using an alternate field, for example id => friendlyname
+		$aAliases = $this->oSearch->GetSelectedClasses();
+		foreach($aSuggestedFields as $idx => $sField)
+		{
+			if (preg_match('/^([^\\.]+)\\.(.+)$/', $sField, $aMatches))
+			{
+				$sAlias = $aMatches[1];
+				$sAttCode = $aMatches[2];
+				$sClass = $aAliases[$sAlias];
+			}
+			else
+			{
+				$sAlias = '';
+				$sAttCode = $sField;
+				$sClass = reset($aAliases);
+			}
+			$aSuggestedFields[$idx] = $this->SuggestField($aAliases, $sClass, $sAlias, $sAttCode);
+		}
+		return $aSuggestedFields;
+	}
+
+	protected function SuggestField($aAliases, $sClass, $sAlias, $sAttCode)
+	{
+		// Remove the aliases (if any) from the field names to make them compatible
+		// with the 'short' notation used in this case by the widget
+		if (count($aAliases) == 1)
+		{
+			return $sAttCode;
+		}
+		return $sAlias.'.'.$sAttCode;
+	}
+
+	protected function IsSubAttribute($sClass, $sAttCode, $oAttDef)
+	{
+		return (($oAttDef instanceof AttributeFriendlyName) || ($oAttDef instanceof AttributeExternalField) || ($oAttDef instanceof AttributeSubItem));
+	}
+
+	protected function GetSubAttributes($sClass, $sAttCode, $oAttDef)
+	{
+		$aResult = array();
+		switch(get_class($oAttDef))
+		{
+			case 'AttributeExternalKey':
+			case 'AttributeHierarchicalKey':
+				$aResult[] = array('code' => $sAttCode, 'unique_label' => $oAttDef->GetLabel(), 'label' => Dict::S('Core:BulkExport:Identifier'), 'attdef' => $oAttDef);
+				$aResult[] = array('code' =>  $sAttCode.'_friendlyname', 'unique_label' => $oAttDef->GetLabel().'->'.Dict::S('Core:BulkExport:Friendlyname'), 'label' => Dict::S('Core:BulkExport:Friendlyname'), 'attdef' => MetaModel::GetAttributeDef($sClass, $sAttCode.'_friendlyname'));
+
+				foreach(MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef)
+				{
+					if ($oSubAttDef instanceof AttributeExternalField)
+					{
+						if ($oSubAttDef->GetKeyAttCode() == $sAttCode)
+						{
+							$aResult[] = array('code' => $sSubAttCode, 'unique_label' => $oAttDef->GetLabel().'->'.$oSubAttDef->GetExtAttDef()->GetLabel(), 'label' => $oSubAttDef->GetExtAttDef()->GetLabel(), 'attdef' => $oSubAttDef);
+						}
+					}
+				}
+				break;
+					
+			case 'AttributeStopWatch':
+				foreach(MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef)
+				{
+					if ($oSubAttDef instanceof AttributeSubItem)
+					{
+						if ($oSubAttDef->GetParentAttCode() == $sAttCode)
+						{
+							$aResult[] = array('code' => $sSubAttCode, 'unique_label' => $oSubAttDef->GetLabel(), 'label' => $oSubAttDef->GetLabel(), 'attdef' => $oSubAttDef);
+						}
+					}
+				}
+				break;
+					
+		}
+		return $aResult;
+	}
+
+	protected function GetInteractiveFieldsWidget(WebPage $oP, $sWidgetId)
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$aSelectedClasses = $this->oSearch->GetSelectedClasses();
+		$aAuthorizedClasses = array();
+		foreach($aSelectedClasses as $sAlias => $sClassName)
+		{
+			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
+			{
+				$aAuthorizedClasses[$sAlias] = $sClassName;
+			}
+		}
+		$aAllFieldsByAlias = array();
+		foreach($aAuthorizedClasses as $sAlias => $sClass)
+		{
+			$aAllFields = array();
+			if (count($aAuthorizedClasses) > 1 )
+			{
+				$sShortAlias = $sAlias.'.';
+			}
+			else
+			{
+				$sShortAlias = '';
+			}
+			if ($this->IsValidField($sClass, 'id'))
+			{
+				$aAllFields[] = array('code' =>  $sShortAlias.'id', 'unique_label' => $sShortAlias.Dict::S('Core:BulkExport:Identifier'), 'label' => $sShortAlias.'id', 'subattr' => array(
+					array('code' =>  $sShortAlias.'id', 'unique_label' => $sShortAlias.Dict::S('Core:BulkExport:Identifier'), 'label' => $sShortAlias.'id'),
+					array('code' =>  $sShortAlias.'friendlyname', 'unique_label' => $sShortAlias.Dict::S('Core:BulkExport:Friendlyname'), 'label' => $sShortAlias.Dict::S('Core:BulkExport:Friendlyname')),
+				));
+			}
+			foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+			{
+				if($this->IsSubAttribute($sClass, $sAttCode, $oAttDef)) continue;
+
+				if ($this->IsValidField($sClass, $sAttCode, $oAttDef))
+				{
+					$sShortLabel = $oAttDef->GetLabel();
+					$sLabel = $sShortAlias.$oAttDef->GetLabel();
+					$aSubAttr = $this->GetSubAttributes($sClass, $sAttCode, $oAttDef);
+					$aValidSubAttr = array();
+					foreach($aSubAttr as $aSubAttDef)
+					{
+						if ($this->IsValidField($sClass, $aSubAttDef['code'], $aSubAttDef['attdef']))
+						{
+							$aValidSubAttr[] = array('code' => $sShortAlias.$aSubAttDef['code'], 'label' => $aSubAttDef['label'], 'unique_label' => $aSubAttDef['unique_label']);
+						}
+					}
+					$aAllFields[] = array('code' => $sShortAlias.$sAttCode, 'label' => $sShortLabel, 'unique_label' => $sLabel, 'subattr' => $aValidSubAttr);
+				}
+			}
+			usort($aAllFields,  array(get_class($this), 'SortOnLabel'));
+			if (count($aAuthorizedClasses) > 1)
+			{
+				$sKey = MetaModel::GetName($sClass).' ('.$sAlias.')';
+			}
+			else
+			{
+				$sKey = MetaModel::GetName($sClass);
+			}
+			$aAllFieldsByAlias[$sKey] = $aAllFields;
+		}
+
+		$oP->add('<div id="'.$sWidgetId.'"></div>');
+		$JSAllFields = json_encode($aAllFieldsByAlias);
+		$oSet = new DBObjectSet($this->oSearch);
+		$iCount = $oSet->Count();
+		$iPreviewLimit = 3;
+		$oSet->SetLimit($iPreviewLimit);
+		$aSampleData = array();
+		while($aRow = $oSet->FetchAssoc())
+		{
+			$aSampleRow = array();
+			foreach($aAuthorizedClasses as $sAlias => $sClass)
+			{
+				if (count($aAuthorizedClasses) > 1 )
+				{
+					$sShortAlias = $sAlias.'.';
+				}
+				else
+				{
+					$sShortAlias = '';
+				}
+
+				if ($this->IsValidField($sClass, 'id'))
+				{
+					$aSampleRow[$sShortAlias.'id'] = $this->GetSampleKey($aRow[$sAlias]);
+				}
+				foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+				{
+					if ($this->IsValidField($sClass, $sAttCode, $oAttDef))
+					{
+						$aSampleRow[$sShortAlias.$sAttCode] = $this->GetSampleData($aRow[$sAlias], $sAttCode);
+					}
+				}
+			}
+			$aSampleData[] = $aSampleRow;
+		}
+		$sJSSampleData = json_encode($aSampleData);
+		$aLabels = array(
+			'preview_header' => Dict::S('Core:BulkExport:DragAndDropHelp'),
+			'empty_preview' => Dict::S('Core:BulkExport:EmptyPreview'),
+			'columns_order' => Dict::S('Core:BulkExport:ColumnsOrder'),
+			'columns_selection' => Dict::S('Core:BulkExport:AvailableColumnsFrom_Class'),
+			'check_all' => Dict::S('Core:BulkExport:CheckAll'),
+			'uncheck_all' => Dict::S('Core:BulkExport:UncheckAll'),
+			'no_field_selected' => Dict::S('Core:BulkExport:NoFieldSelected'),
+		);
+		$sJSLabels = json_encode($aLabels);
+		$oP->add_ready_script(
+<<<EOF
+$('#$sWidgetId').tabularfieldsselector({fields: $JSAllFields, value_holder: '#tabular_fields', advanced_holder: '#tabular_advanced', sample_data: $sJSSampleData, total_count: $iCount, preview_limit: $iPreviewLimit, labels: $sJSLabels });
+EOF
+		);
+	}
+
+	static public function SortOnLabel($aItem1, $aItem2)
+	{
+		return strcmp($aItem1['label'], $aItem2['label']);
+	}
+
+	/**
+	 * Tells if the specified field can be exported
+	 * @param unknown $sClass
+	 * @param unknown $sAttCode
+	 * @param AttributeDefinition $oAttDef Can be null when $sAttCode == 'id'
+	 * @return boolean
+	 */
+	protected function IsValidField($sClass, $sAttCode, $oAttDef = null)
+	{
+		if ($sAttCode == 'id') return true;
+		if ($oAttDef instanceof AttributeLinkedSet) return false;
+		return true; //$oAttDef->IsScalar();
+	}
+
+	/**
+	 * Tells if the specified field is part of the "advanced" fields
+	 * @param unknown $sClass
+	 * @param unknown $sAttCode
+	 * @param AttributeDefinition $oAttDef Can be null when $sAttCode == 'id'
+	 * @return boolean
+	 */
+	protected function IsAdvancedValidField($sClass, $sAttCode, $oAttDef = null)
+	{
+		return (($sAttCode == 'id') || ($oAttDef instanceof AttributeExternalKey));
+	}
+
+	protected function GetSampleData(DBObject $oObj, $sAttCode)
+	{
+		if ($oObj == null) return '';
+		return $oObj->GetEditValue($sAttCode);
+	}
+
+	protected function GetSampleKey(DBObject $oObj)
+	{
+		if ($oObj == null) return '';
+		return $oObj->GetKey();
+	}
+
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+		$sQueryId = utils::ReadParam('query', null, true);
+		$sFields = utils::ReadParam('fields', null, true, 'raw_data');
+		if ((($sFields === null) || ($sFields === '')) && ($sQueryId === null))
+		{
+			throw new BulkExportMissingParameterException('fields');
+		}
+		else if(($sQueryId !== null) && ($sQueryId !== null))
+		{
+			$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId));
+			$oQueries = new DBObjectSet($oSearch);
+			if ($oQueries->Count() > 0)
+			{
+				$oQuery = $oQueries->Fetch();
+				$sFields = trim($oQuery->Get('fields'));
+				if ($sFields === '')
+				{
+					throw new BulkExportMissingParameterException('fields');
+				}
+			}
+			else
+			{
+				throw BulkExportException('Invalid value for the parameter: query. There is no Query Phrasebook with id = '.$sQueryId, Dict::Format('Core:BulkExport:InvalidParameter_Query', $sQueryId));
+			}
+		}
+
+		$this->aStatusInfo['fields'] = explode(',', $sFields);
+	}
+}

+ 196 - 0
core/xmlbulkexport.class.inc.php

@@ -0,0 +1,196 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Bulk export: XML export
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+class XMLBulkExport extends BulkExport
+{
+	public function DisplayUsage(Page $oP)
+	{
+		$oP->p(" * xml format options:");
+		$oP->p(" *\tThere are no options for the XML format.");
+	}
+
+	public function EnumFormParts()
+	{
+		return array_merge(parent::EnumFormParts(), array('xml_options' => array('xml_no_options')));
+	}
+	
+	public function DisplayFormPart(WebPage $oP, $sPartId)
+	{
+		switch($sPartId)
+		{
+			case 'xml_options':
+				$sChecked = (utils::ReadParam('no_localize', 0) == 1) ? ' checked ' : '';
+				$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:XMLOptions').'</legend>');
+				$oP->add('<table>');
+				$oP->add('<tr>');
+				$oP->add('<td><input type="checkbox" id="xml_no_localize" name="no_localize" value="1"'.$sChecked.'><label for="xml_no_localize"> '.Dict::S('Core:BulkExport:XMLNoLocalize').'</label></td>');
+				$oP->add('</tr>');
+				$oP->add('</table>');
+				$oP->add('</fieldset>');
+				break;
+					
+			default:
+				return parent:: DisplayFormPart($oP, $sPartId);
+		}
+	}
+	
+	public function ReadParameters()
+	{
+		parent::ReadParameters();
+	
+		$this->aStatusInfo['localize'] = (utils::ReadParam('no_localize', 0) != 1);
+	}
+	
+	protected function GetSampleData(DBObject $oObj, $sAttCode)
+	{
+		return $oObj->GetAsXML($sAttCode);
+	}
+
+	public function GetHeader()
+	{
+		$oSet = new DBObjectSet($this->oSearch);
+		$this->aStatusInfo['position'] = 0;
+		$this->aStatusInfo['total'] = $oSet->Count();
+		$sData = "<"."?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Set>\n";
+		return $sData;
+	}
+
+	public function GetNextChunk(&$aStatus)
+	{
+		$sRetCode = 'run';
+		$iPercentage = 0;
+
+		$iCount = 0;
+		$sData = '';
+		
+		$oSet = new DBObjectSet($this->oSearch);
+		$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
+		
+		$bLocalize = $this->aStatusInfo['localize'];
+		
+		$aClasses = $this->oSearch->GetSelectedClasses();
+		$aAuthorizedClasses = array();
+		foreach($aClasses as $sAlias => $sClassName)
+		{
+			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
+			{
+				$aAuthorizedClasses[$sAlias] = $sClassName;
+			}
+		}
+		$aAttribs = array();
+		$aList = array();
+		$aList[$sAlias] = MetaModel::GetZListItems($sClassName, 'details');
+		
+		$iPreviousTimeLimit = ini_get('max_execution_time');
+		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+		
+		while ($aObjects = $oSet->FetchAssoc())
+		{
+			set_time_limit($iLoopTimeLimit);
+			if (count($aAuthorizedClasses) > 1)
+			{
+				$sData .= "<Row>\n";
+			}
+			foreach($aAuthorizedClasses as $sAlias => $sClassName)
+			{
+				$oObj = $aObjects[$sAlias];
+				if (is_null($oObj))
+				{
+					$sData .= "<$sClassName alias=\"$sAlias\" id=\"null\">\n";
+				}
+				else
+				{
+					$sClassName = get_class($oObj);
+					$sData .= "<$sClassName alias=\"$sAlias\" id=\"".$oObj->GetKey()."\">\n";
+				}
+				foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode=>$oAttDef)
+				{
+					if (is_null($oObj))
+					{
+						$sData .= "<$sAttCode>null</$sAttCode>\n";
+					}
+					else
+					{
+						if ($oAttDef->IsWritable())
+						{
+							if (!$oAttDef->IsLinkSet())
+							{
+								$sValue = $oObj->GetAsXML($sAttCode, $bLocalize);
+								$sData .= "<$sAttCode>$sValue</$sAttCode>\n";
+							}
+						}
+					}
+				}
+				$sData .= "</$sClassName>\n";
+			}
+			if (count($aAuthorizedClasses) > 1)
+			{
+				$sData .= "</Row>\n";
+			}
+			$iCount++;
+		}
+		
+		set_time_limit($iPreviousTimeLimit);
+		$this->aStatusInfo['position'] += $this->iChunkSize;
+		if ($this->aStatusInfo['total'] == 0)
+		{
+			$iPercentage = 100;
+		}
+		else
+		{
+			$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
+		}
+
+		if ($iCount < $this->iChunkSize)
+		{
+			$sRetCode = 'done';
+		}
+
+		$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
+		return $sData;
+	}
+
+	public function GetFooter()
+	{
+		$sData = "</Set>\n";
+
+		return $sData;
+	}
+
+	public function GetSupportedFormats()
+	{
+		return array('xml' => Dict::S('Core:BulkExport:XMLFormat'));
+	}
+
+	public function GetMimeType()
+	{
+		return 'text/xml';
+	}
+
+	public function GetFileExtension()
+	{
+		return 'xml';
+	}
+}

+ 40 - 0
css/dragtable.css

@@ -0,0 +1,40 @@
+/* 
+ * dragtable
+ *
+ * @Version 2.0.14
+ *
+ * default css
+ *
+ */
+/*##### the dragtable stuff #####*/
+.dragtable-sortable { 
+    list-style-type: none; margin: 0; padding: 0; -moz-user-select: none;
+}
+.dragtable-sortable li {
+    margin: 0; padding: 0; float: left; font-size: 1em; background: white; 
+}
+
+.dragtable-sortable th, .dragtable-sortable td{
+    border-left: 0px;
+}
+
+.dragtable-sortable li:first-child th, .dragtable-sortable li:first-child td {
+    border-left: 1px solid #CCC; 
+}
+
+.ui-sortable-helper {
+    opacity: 0.7;filter: alpha(opacity=70);
+}
+.ui-sortable-placeholder { 
+    -moz-box-shadow: 4px 5px 4px #C6C6C6 inset;
+    -webkit-box-shadow: 4px 5px 4px #C6C6C6 inset;
+    box-shadow: 4px 5px 4px #C6C6C6 inset;
+    border-bottom: 1px solid #CCCCCC;
+    border-top: 1px solid #CCCCCC;
+    visibility: visible !important;
+    background: #EFEFEF !important; 
+    visibility: visible !important;
+}
+.ui-sortable-placeholder * { 
+    opacity: 0.0; visibility: hidden; 
+}

+ 25 - 0
css/light-grey.css

@@ -1943,3 +1943,28 @@ div.ui-dialog-header {
 }
 
 
+table.export_parameters td {
+  padding-right: 2em;
+}
+
+
+.table_preview > table {
+  border-collapse: collapse;
+}
+
+
+.table_preview > table > tbody > tr > td, .table_preview > table > thead > tr > th {
+  border: 1px #555555 solid;
+  min-height: 1em;
+  padding-left: 0.25em;
+  padding-right: 0.25em;
+  font-size: 10pt;
+}
+
+
+.table_preview .nodragtable-sortable li {
+  border: 1px #555555 solid;
+  font-size: 10pt;
+}
+
+

+ 17 - 0
css/light-grey.scss

@@ -1436,4 +1436,21 @@ div.ui-dialog-header {
 .arrow.top:after {
 	bottom: -20px;
 	top: auto;
+}
+table.export_parameters td {
+	padding-right: 2em;
+}
+.table_preview > table {
+	border-collapse: collapse;
+}
+.table_preview > table > thead > tr > th, .table_preview > table > tbody > tr > td {
+	border: 1px $grey-color solid;
+	min-height: 1em;
+	padding-left: 0.25em;
+	padding-right: 0.25em;
+	font-size: 10pt;
+}
+.table_preview .nodragtable-sortable li {
+	border: 1px $grey-color solid;
+	font-size: 10pt;
 }

+ 1 - 1
dictionaries/da.dictionary.itop.ui.php

@@ -328,7 +328,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
 	'UI:Menu:Add' => 'Tilføj...',
 	'UI:Menu:Manage' => 'Administrer...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV Eksport',
+	'UI:Menu:CSVExport' => 'CSV Eksport...',
 	'UI:Menu:Modify' => 'Modificer...',
 	'UI:Menu:Delete' => 'Slet...',
 	'UI:Menu:BulkDelete' => 'Slet...',

+ 1 - 1
dictionaries/de.dictionary.itop.ui.php

@@ -325,7 +325,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'UI:Menu:Add' => 'Hinzufügen...',
 	'UI:Menu:Manage' => 'Verwalten...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV-Export',
+	'UI:Menu:CSVExport' => 'CSV-Export...',
 	'UI:Menu:Modify' => 'Modifizieren...',
 	'UI:Menu:Delete' => 'Löschen...',
 	'UI:Menu:BulkDelete' => 'Löschen...',

+ 45 - 1
dictionaries/dictionary.itop.core.php

@@ -787,5 +787,49 @@ Dict::Add('EN US', 'English', 'English', array(
 	// Explain working time computing
 	'Core:ExplainWTC:ElapsedTime' => 'Time elapsed (stored as "%1$s")',
 	'Core:ExplainWTC:StopWatch-TimeSpent' => 'Time spent for "%1$s"',
-	'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%'
+	'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%',
+	
+	// Bulk export
+	'Core:BulkExport:MissingParameter_Param' => 'Missing parameter "%1$s"',
+	'Core:BulkExport:InvalidParameter_Query' => 'Invalid value for the parameter "query". There is no Query Phrasebook corresponding to the id: "%1$s".',
+	'Core:BulkExport:ExportFormatPrompt' => 'Export format:',
+	'Core:BulkExport:Identifier' => 'Identifier',
+	'Core:BulkExport:Friendlyname' => 'Full name',
+	'Core:BulkExportOf_Class' => '%1$s Export',
+	'Core:BulkExport:ClickHereToDownload_FileName' => 'Click here to download %1$s',
+	'Core:BulkExport:ExportResult' => 'Result of the export:',
+	'Core:BulkExport:RetrievingData' => 'Retrieving data...',
+	'Core:BulkExport:HTMLFormat' => 'Web Page (*.html)',
+	'Core:BulkExport:CSVFormat' => 'Comma Separated Values (*.csv)',
+	'Core:BulkExport:XLSXFormat' => 'Excel 2007 or newer (*.xlsx)',
+	'Core:BulkExport:PDFFormat' => 'PDF Document (*.pdf)',
+	'Core:BulkExport:DragAndDropHelp' => 'Drag and drop the columns\' headers to arrange the columns. Preview of %1$s lines. Total number of lines to export: %2$s.',
+	'Core:BulkExport:EmptyPreview' => 'Select the columns to be exported from the list above',
+	'Core:BulkExport:ColumnsOrder' => 'Columns order',
+	'Core:BulkExport:AvailableColumnsFrom_Class' => 'Available columns from %1$s',
+	'Core:BulkExport:NoFieldSelected' => 'Select at least one column to be exported',
+	'Core:BulkExport:CheckAll' => 'Check All',
+	'Core:BulkExport:UncheckAll' => 'Uncheck All',
+	'Core:BulkExport:ExportCancelledByUser' => 'Export cancelled by the user',
+	'Core:BulkExport:CSVOptions' => 'CSV Options',
+	'Core:BulkExport:CSVLocalization' => 'Localization',
+	'Core:BulkExport:PDFOptions' => 'PDF Options',
+	'Core:BulkExport:PDFPageSize' => 'Page Size:',
+	'Core:BulkExport:PageSize-A4' => 'A4',
+	'Core:BulkExport:PageSize-A3' => 'A3',
+	'Core:BulkExport:PageSize-Letter' => 'Letter',
+	'Core:BulkExport:PDFPageOrientation' => 'Page Orientation:',
+	'Core:BulkExport:PageOrientation-L' => 'Landscape',
+	'Core:BulkExport:PageOrientation-P' => 'Portrait',
+	'Core:BulkExport:XMLFormat' => 'XML file (*.xml)',
+	'Core:BulkExport:XMLOptions' => 'XML Options',
+	'Core:BulkExport:SpreadsheetFormat' => 'Spreadsheet HTML format (*.html)',
+	'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options',
+	'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)',
+	'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export',
+	'Core:BulkExportLabelOQLExpression' => 'OQL Query:',
+	'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:',
+	'Core:BulkExportMessageEmptyOQL' => 'Please enter a valid OQL query.',
+	'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Please select a valid phrasebook entry.',
+
 ));

+ 1 - 1
dictionaries/dictionary.itop.ui.php

@@ -450,7 +450,7 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:Menu:Add' => 'Add...',
 	'UI:Menu:Manage' => 'Manage...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV Export',
+	'UI:Menu:CSVExport' => 'CSV Export...',
 	'UI:Menu:Modify' => 'Modify...',
 	'UI:Menu:Delete' => 'Delete...',
 	'UI:Menu:Manage' => 'Manage...',

+ 1 - 1
dictionaries/es_cr.dictionary.itop.ui.php

@@ -447,7 +447,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 	'UI:Menu:Add' => 'Agregar',
 	'UI:Menu:Manage' => 'Administrar',
 	'UI:Menu:EMail' => 'Enviar por Correo Electrónico',
-	'UI:Menu:CSVExport' => 'Exportar a CSV',
+	'UI:Menu:CSVExport' => 'Exportar a CSV...',
 	'UI:Menu:Modify' => 'Modificar',
 	'UI:Menu:Delete' => 'Borrar',
 	'UI:Menu:Manage' => 'Administrar',

+ 44 - 1
dictionaries/fr.dictionary.itop.core.php

@@ -648,5 +648,48 @@ Opérateurs :<br/>
 	'Core:Duration_Days_Hours_Minutes_Seconds' => '%1$sj %2$dh %3$dmin %4$ds',
 	'Core:ExplainWTC:ElapsedTime' => 'Temps écoulé (enregistré dans "%1$s")',
 	'Core:ExplainWTC:StopWatch-TimeSpent' => 'Temps écoulé pour "%1$s"',
-	'Core:ExplainWTC:StopWatch-Deadline' => 'Date/heure de butée pour "%1$s" à %2$d%%'
+	'Core:ExplainWTC:StopWatch-Deadline' => 'Date/heure de butée pour "%1$s" à %2$d%%',
+	
+	'Core:BulkExport:MissingParameter_Param' => 'Il manque le paramètre "%1$s"',
+	'Core:BulkExport:InvalidParameter_Query' => 'Valeur incorrecte pour le paramètre "query". Il n\'existe aucune entrée dans le livre des requêtes pour l\'identifiant: "%1$s"',
+	'Core:BulkExport:ExportFormatPrompt' => 'Format d\'export:',
+	'Core:BulkExport:Identifier' => 'Identifiant',
+	'Core:BulkExport:Friendlyname' => 'Nom complet',
+	'Core:BulkExportOf_Class' => 'Export de: %1$s',
+	'Core:BulkExport:ClickHereToDownload_FileName' => 'Cliquez ici pour télécharger %1$s',
+	'Core:BulkExport:ExportResult' => 'Résultat de l\'export:',
+	'Core:BulkExport:RetrievingData' => 'Récupération des données...',
+	'Core:BulkExport:HTMLFormat' => 'Page Web (*.html)',
+	'Core:BulkExport:CSVFormat' => 'Fichier CSV (*.csv)',
+	'Core:BulkExport:XLSXFormat' => 'Excel 2007 ou plus récent (*.xlsx)',
+	'Core:BulkExport:PDFFormat' => 'Document PDF (*.pdf)',
+	'Core:BulkExport:DragAndDropHelp' => 'Faîtes glisser les en-têtes des colonnes pour modifier leur ordre. Aperçu de %1$s lignes sur un total de %2$s lignes à exporter.',
+	'Core:BulkExport:EmptyPreview' => 'Selectionnez les colonnes à exporter dans la liste ci-dessus...',
+	'Core:BulkExport:ColumnsOrder' => 'Ordre des colonnes',
+	'Core:BulkExport:AvailableColumnsFrom_Class' => 'Colonnes de la classe %1$s',
+	'Core:BulkExport:NoFieldSelected' => 'Veuillez sélectionner au moins une colonne à exporter',
+	'Core:BulkExport:CheckAll' => 'Tout cocher',
+	'Core:BulkExport:UncheckAll' => 'Tout décocher',
+	'Core:BulkExport:ExportCancelledByUser' => 'Export annulé par l\'utilisateur',
+	
+	'Core:BulkExport:CSVOptions' => 'Options du format CSV',
+	'Core:BulkExport:CSVLocalization' => 'Traduction',
+	'Core:BulkExport:PDFOptions' => 'Options du format PDF',
+	'Core:BulkExport:PDFPageSize' => 'Format de page:',
+	'Core:BulkExport:PageSize-A4' => 'A4',
+	'Core:BulkExport:PageSize-A3' => 'A3',
+	'Core:BulkExport:PageSize-Letter' => 'Lettre (US)',
+	'Core:BulkExport:PDFPageOrientation' => 'Orientation de la page:',
+	'Core:BulkExport:PageOrientation-L' => 'Paysage',
+	'Core:BulkExport:PageOrientation-P' => 'Portrait',
+	'Core:BulkExport:XMLFormat' => 'Fichier XML (*.xml)',
+	'Core:BulkExport:XMLOptions' => 'Options XML',
+	'Core:BulkExport:SpreadsheetFormat' => 'Format HTML pour Excel (*.html)',
+	'Core:BulkExport:SpreadsheetOptions' => 'Options du format HTML pour Excel',
+	'Core:BulkExport:OptionNoLocalize' => 'Ne pas traduire les valeurs (pour les champs de type "Enum")',
+	'Core:BulkExport:ScopeDefinition' => 'Définition des objets à exporter',
+	'Core:BulkExportLabelOQLExpression' => 'Requête OQL:',
+	'Core:BulkExportLabelPhrasebookEntry' => 'Entrée dans le livre des requêtes:',
+	'Core:BulkExportMessageEmptyOQL' => 'Veuillez saisir une requête OQL valide.',
+	'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Veuillez sélectionner une entrée dans le livre des requêtes.',
 ));

+ 1 - 1
dictionaries/fr.dictionary.itop.ui.php

@@ -326,7 +326,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'UI:Menu:Add' => 'Ajouter...',
 	'UI:Menu:Manage' => 'Gérer...',
 	'UI:Menu:EMail' => 'Envoyer par eMail',
-	'UI:Menu:CSVExport' => 'Exporter en CSV',
+	'UI:Menu:CSVExport' => 'Exporter en CSV...',
 	'UI:Menu:Modify' => 'Modifier...',
 	'UI:Menu:Delete' => 'Supprimer...',
 	'UI:Menu:BulkDelete' => 'Supprimer...',

+ 1 - 1
dictionaries/hu.dictionary.itop.ui.php

@@ -312,7 +312,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
 	'UI:Menu:Add' => 'Hozzáad...',
 	'UI:Menu:Manage' => 'Kezel...',
 	'UI:Menu:EMail' => 'e-mail',
-	'UI:Menu:CSVExport' => 'CSV export',
+	'UI:Menu:CSVExport' => 'CSV export...',
 	'UI:Menu:Modify' => 'Módosít...',
 	'UI:Menu:Delete' => 'Töröl...',
 	'UI:Menu:BulkDelete' => 'Töröl...',

+ 1 - 1
dictionaries/it.dictionary.itop.ui.php

@@ -443,7 +443,7 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
 	'UI:Menu:Add' => 'Aggiungi...',
 	'UI:Menu:Manage' => 'Gestischi...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV Export',
+	'UI:Menu:CSVExport' => 'CSV Export...',
 	'UI:Menu:Modify' => 'Modifica...',
 	'UI:Menu:Delete' => 'Cancella...',
 	'UI:Menu:Manage' => 'Gestisci...',

+ 1 - 1
dictionaries/ja.dictionary.itop.ui.php

@@ -328,7 +328,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
 	'UI:Menu:Add' => '追加...',
 	'UI:Menu:Manage' => '管理...',
 	'UI:Menu:EMail' => 'Eメール',
-	'UI:Menu:CSVExport' => 'CSVエクスポート',
+	'UI:Menu:CSVExport' => 'CSVエクスポート...',
 	'UI:Menu:Modify' => '修正...',
 	'UI:Menu:Delete' => '削除...',
 	'UI:Menu:BulkDelete' => '削除...',

+ 1 - 1
dictionaries/nl.dictionary.itop.ui.php

@@ -453,7 +453,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 	'UI:Menu:Add' => 'Voeg toe...',
 	'UI:Menu:Manage' => 'Manage...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV Export',
+	'UI:Menu:CSVExport' => 'CSV Export...',
 	'UI:Menu:Modify' => 'Bewerk...',
 	'UI:Menu:Delete' => 'Verwijder...',
 	'UI:Menu:Manage' => 'Manage...',

+ 1 - 1
dictionaries/pt_br.dictionary.itop.ui.php

@@ -446,7 +446,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 	'UI:Menu:Add' => 'Adicionar...',
 	'UI:Menu:Manage' => 'Gerenciar...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'Exportar CSV',
+	'UI:Menu:CSVExport' => 'Exportar CSV...',
 	'UI:Menu:Modify' => 'Modificar...',
 	'UI:Menu:Delete' => 'Excluir...',
 	'UI:Menu:Manage' => 'Gerenciar...',

+ 1 - 1
dictionaries/ru.dictionary.itop.ui.php

@@ -442,7 +442,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 	'UI:Menu:Add' => 'Добавить...',
 	'UI:Menu:Manage' => 'Управление...',
 	'UI:Menu:EMail' => 'Отправить ссылку по email',
-	'UI:Menu:CSVExport' => 'Экспорт в CSV',
+	'UI:Menu:CSVExport' => 'Экспорт в CSV...',
 	'UI:Menu:Modify' => 'Изменить...',
 	'UI:Menu:Delete' => 'Удалить...',
 	'UI:Menu:Manage' => 'Управление...',

+ 1 - 1
dictionaries/tr.dictionary.itop.ui.php

@@ -416,7 +416,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
 	'UI:Menu:Add' => 'Ekle...',
 	'UI:Menu:Manage' => 'Yönet...',
 	'UI:Menu:EMail' => 'e-posta',
-	'UI:Menu:CSVExport' => 'CSV olarak dışarı ver',
+	'UI:Menu:CSVExport' => 'CSV olarak dışarı ver...',
 	'UI:Menu:Modify' => 'Düzenle...',
 	'UI:Menu:Delete' => 'Sil...',
 	'UI:Menu:Manage' => 'Yönet...',

+ 1 - 1
dictionaries/zh.dictionary.itop.ui.php

@@ -415,7 +415,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'UI:Menu:Add' => '添加...',
 	'UI:Menu:Manage' => '管理...',
 	'UI:Menu:EMail' => 'eMail',
-	'UI:Menu:CSVExport' => 'CSV 导出',
+	'UI:Menu:CSVExport' => 'CSV 导出...',
 	'UI:Menu:Modify' => '修改...',
 	'UI:Menu:Delete' => '删除...',
 	'UI:Menu:Manage' => '管理...',

+ 401 - 0
js/jquery.dragtable.js

@@ -0,0 +1,401 @@
+/*!
+ * dragtable
+ *
+ * @Version 2.0.14
+ *
+ * Copyright (c) 2010-2013, Andres akottr@gmail.com
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * Inspired by the the dragtable from Dan Vanderkam (danvk.org/dragtable/)
+ * Thanks to the jquery and jqueryui comitters
+ *
+ * Any comment, bug report, feature-request is welcome
+ * Feel free to contact me.
+ */
+
+/* TOKNOW:
+ * For IE7 you need this css rule:
+ * table {
+ *   border-collapse: collapse;
+ * }
+ * Or take a clean reset.css (see http://meyerweb.com/eric/tools/css/reset/)
+ */
+
+/* TODO: investigate
+ * Does not work properly with css rule:
+ * html {
+ *      overflow: -moz-scrollbars-vertical;
+ *  }
+ * Workaround:
+ * Fixing Firefox issues by scrolling down the page
+ * http://stackoverflow.com/questions/2451528/jquery-ui-sortable-scroll-helper-element-offset-firefox-issue
+ *
+ * var start = $.noop;
+ * var beforeStop = $.noop;
+ * if($.browser.mozilla) {
+ * var start = function (event, ui) {
+ *               if( ui.helper !== undefined )
+ *                 ui.helper.css('position','absolute').css('margin-top', $(window).scrollTop() );
+ *               }
+ * var beforeStop = function (event, ui) {
+ *              if( ui.offset !== undefined )
+ *                ui.helper.css('margin-top', 0);
+ *              }
+ * }
+ *
+ * and pass this as start and stop function to the sortable initialisation
+ * start: start,
+ * beforeStop: beforeStop
+ */
+/*
+ * Special thx to all pull requests comitters
+ */
+
+(function($) {
+  $.widget("akottr.dragtable", {
+    options: {
+      revert: false,               // smooth revert
+      dragHandle: '.table-handle', // handle for moving cols, if not exists the whole 'th' is the handle
+      maxMovingRows: 40,           // 1 -> only header. 40 row should be enough, the rest is usually not in the viewport
+      excludeFooter: false,        // excludes the footer row(s) while moving other columns. Make sense if there is a footer with a colspan. */
+      onlyHeaderThreshold: 100,    // TODO:  not implemented yet, switch automatically between entire col moving / only header moving
+      dragaccept: null,            // draggable cols -> default all
+      persistState: null,          // url or function -> plug in your custom persistState function right here. function call is persistState(originalTable)
+      restoreState: null,          // JSON-Object or function:  some kind of experimental aka Quick-Hack TODO: do it better
+      exact: true,                 // removes pixels, so that the overlay table width fits exactly the original table width
+      clickDelay: 10,              // ms to wait before rendering sortable list and delegating click event
+      containment: null,           // @see http://api.jqueryui.com/sortable/#option-containment, use it if you want to move in 2 dimesnions (together with axis: null)
+      cursor: 'move',              // @see http://api.jqueryui.com/sortable/#option-cursor
+      cursorAt: false,             // @see http://api.jqueryui.com/sortable/#option-cursorAt
+      distance: 0,                 // @see http://api.jqueryui.com/sortable/#option-distance, for immediate feedback use "0"
+      tolerance: 'pointer',        // @see http://api.jqueryui.com/sortable/#option-tolerance
+      axis: 'x',                   // @see http://api.jqueryui.com/sortable/#option-axis, Only vertical moving is allowed. Use 'x' or null. Use this in conjunction with the 'containment' setting
+      beforeStart: $.noop,         // returning FALSE will stop the execution chain.
+      beforeMoving: $.noop,
+      beforeReorganize: $.noop,
+      beforeStop: $.noop
+    },
+    originalTable: {
+      el: null,
+      selectedHandle: null,
+      sortOrder: null,
+      startIndex: 0,
+      endIndex: 0
+    },
+    sortableTable: {
+      el: $(),
+      selectedHandle: $(),
+      movingRow: $()
+    },
+    persistState: function() {
+      var _this = this;
+      this.originalTable.el.find('th').each(function(i) {
+        if (this.id !== '') {
+          _this.originalTable.sortOrder[this.id] = i;
+        }
+      });
+      $.ajax({
+        url: this.options.persistState,
+        data: this.originalTable.sortOrder
+      });
+    },
+    /*
+     * persistObj looks like
+     * {'id1':'2','id3':'3','id2':'1'}
+     * table looks like
+     * |   id2  |   id1   |   id3   |
+     */
+    _restoreState: function(persistObj) {
+      for (var n in persistObj) {
+        this.originalTable.startIndex = $('#' + n).closest('th').prevAll().size() + 1;
+        this.originalTable.endIndex = parseInt(persistObj[n], 10) + 1;
+        this._bubbleCols();
+      }
+    },
+    // bubble the moved col left or right
+    _bubbleCols: function() {
+      var i, j, col1, col2;
+      var from = this.originalTable.startIndex;
+      var to = this.originalTable.endIndex;
+      /* Find children thead and tbody.
+       * Only to process the immediate tr-children. Bugfix for inner tables
+       */
+      var thtb = this.originalTable.el.children();
+      if (this.options.excludeFooter) {
+        thtb = thtb.not('tfoot');
+      }
+      if (from < to) {
+        for (i = from; i < to; i++) {
+          col1 = thtb.find('> tr > td:nth-child(' + i + ')')
+            .add(thtb.find('> tr > th:nth-child(' + i + ')'));
+          col2 = thtb.find('> tr > td:nth-child(' + (i + 1) + ')')
+            .add(thtb.find('> tr > th:nth-child(' + (i + 1) + ')'));
+          for (j = 0; j < col1.length; j++) {
+            swapNodes(col1[j], col2[j]);
+          }
+        }
+      } else {
+        for (i = from; i > to; i--) {
+          col1 = thtb.find('> tr > td:nth-child(' + i + ')')
+            .add(thtb.find('> tr > th:nth-child(' + i + ')'));
+          col2 = thtb.find('> tr > td:nth-child(' + (i - 1) + ')')
+            .add(thtb.find('> tr > th:nth-child(' + (i - 1) + ')'));
+          for (j = 0; j < col1.length; j++) {
+            swapNodes(col1[j], col2[j]);
+          }
+        }
+      }
+    },
+    _rearrangeTableBackroundProcessing: function() {
+      var _this = this;
+      return function() {
+        _this._bubbleCols();
+        _this.options.beforeStop(_this.originalTable);
+        _this.sortableTable.el.remove();
+        restoreTextSelection();
+        // persist state if necessary
+        if (_this.options.persistState !== null) {
+          $.isFunction(_this.options.persistState) ? _this.options.persistState(_this.originalTable) : _this.persistState();
+        }
+      };
+    },
+    _rearrangeTable: function() {
+      var _this = this;
+      return function() {
+        // remove handler-class -> handler is now finished
+        _this.originalTable.selectedHandle.removeClass('dragtable-handle-selected');
+        // add disabled class -> reorgorganisation starts soon
+        _this.sortableTable.el.sortable("disable");
+        _this.sortableTable.el.addClass('dragtable-disabled');
+        _this.options.beforeReorganize(_this.originalTable, _this.sortableTable);
+        // do reorganisation asynchronous
+        // for chrome a little bit more than 1 ms because we want to force a rerender
+        _this.originalTable.endIndex = _this.sortableTable.movingRow.prevAll().size() + 1;
+        setTimeout(_this._rearrangeTableBackroundProcessing(), 50);
+      };
+    },
+    /*
+     * Disrupts the table. The original table stays the same.
+     * But on a layer above the original table we are constructing a list (ul > li)
+     * each li with a separate table representig a single col of the original table.
+     */
+    _generateSortable: function(e) {
+      !e.cancelBubble && (e.cancelBubble = true);
+      var _this = this;
+      // table attributes
+      var attrs = this.originalTable.el[0].attributes;
+      var attrsString = '';
+      for (var i = 0; i < attrs.length; i++) {
+        if (attrs[i].nodeValue && attrs[i].nodeName != 'id' && attrs[i].nodeName != 'width') {
+          attrsString += attrs[i].nodeName + '="' + attrs[i].nodeValue + '" ';
+        }
+      }
+
+      // row attributes
+      var rowAttrsArr = [];
+      //compute height, special handling for ie needed :-(
+      var heightArr = [];
+      this.originalTable.el.find('tr').slice(0, this.options.maxMovingRows).each(function(i, v) {
+        // row attributes
+        var attrs = this.attributes;
+        var attrsString = "";
+        for (var j = 0; j < attrs.length; j++) {
+          if (attrs[j].nodeValue && attrs[j].nodeName != 'id') {
+            attrsString += " " + attrs[j].nodeName + '="' + attrs[j].nodeValue + '"';
+          }
+        }
+        rowAttrsArr.push(attrsString);
+        heightArr.push($(this).height());
+      });
+
+      // compute width, no special handling for ie needed :-)
+      var widthArr = [];
+      // compute total width, needed for not wrapping around after the screen ends (floating)
+      var totalWidth = 0;
+      /* Find children thead and tbody.
+       * Only to process the immediate tr-children. Bugfix for inner tables
+       */
+      var thtb = _this.originalTable.el.children();
+      if (this.options.excludeFooter) {
+        thtb = thtb.not('tfoot');
+      }
+      thtb.find('> tr > th').each(function(i, v) {
+        var w = $(this).outerWidth();
+        widthArr.push(w);
+        totalWidth += w;
+      });
+      if(_this.options.exact) {
+          var difference = totalWidth - _this.originalTable.el.outerWidth();
+          widthArr[0] -= difference;
+      }
+      // one extra px on right and left side
+      totalWidth += 2
+
+      var sortableHtml = '<ul class="dragtable-sortable" style="position:absolute; width:' + totalWidth + 'px;">';
+      // assemble the needed html
+      thtb.find('> tr > th').each(function(i, v) {
+        var width_li = $(this).outerWidth();
+        sortableHtml += '<li style="width:' + width_li + 'px;">';
+        sortableHtml += '<table ' + attrsString + '>';
+        var row = thtb.find('> tr > th:nth-child(' + (i + 1) + ')');
+        if (_this.options.maxMovingRows > 1) {
+          row = row.add(thtb.find('> tr > td:nth-child(' + (i + 1) + ')').slice(0, _this.options.maxMovingRows - 1));
+        }
+        row.each(function(j) {
+          // TODO: May cause duplicate style-Attribute
+          var row_content = $(this).clone().wrap('<div></div>').parent().html();
+          if (row_content.toLowerCase().indexOf('<th') === 0) sortableHtml += "<thead>";
+          sortableHtml += '<tr ' + rowAttrsArr[j] + '" style="height:' + heightArr[j] + 'px;">';
+          sortableHtml += row_content;
+          if (row_content.toLowerCase().indexOf('<th') === 0) sortableHtml += "</thead>";
+          sortableHtml += '</tr>';
+        });
+        sortableHtml += '</table>';
+        sortableHtml += '</li>';
+      });
+      sortableHtml += '</ul>';
+      this.sortableTable.el = this.originalTable.el.before(sortableHtml).prev();
+      // set width if necessary
+      this.sortableTable.el.find('> li > table').each(function(i, v) {
+        $(this).css('width', widthArr[i] + 'px');
+      });
+
+      // assign this.sortableTable.selectedHandle
+      this.sortableTable.selectedHandle = this.sortableTable.el.find('th .dragtable-handle-selected');
+
+      var items = !this.options.dragaccept ? 'li' : 'li:has(' + this.options.dragaccept + ')';
+      this.sortableTable.el.sortable({
+        items: items,
+        stop: this._rearrangeTable(),
+        // pass thru options for sortable widget
+        revert: this.options.revert,
+        tolerance: this.options.tolerance,
+        containment: this.options.containment,
+        cursor: this.options.cursor,
+        cursorAt: this.options.cursorAt,
+        distance: this.options.distance,
+        axis: this.options.axis
+      });
+
+      // assign start index
+      this.originalTable.startIndex = $(e.target).closest('th').prevAll().size() + 1;
+
+      this.options.beforeMoving(this.originalTable, this.sortableTable);
+      // Start moving by delegating the original event to the new sortable table
+      this.sortableTable.movingRow = this.sortableTable.el.find('> li:nth-child(' + this.originalTable.startIndex + ')');
+
+      // prevent the user from drag selecting "highlighting" surrounding page elements
+      disableTextSelection();
+      // clone the initial event and trigger the sort with it
+      this.sortableTable.movingRow.trigger($.extend($.Event(e.type), {
+        which: 1,
+        clientX: e.clientX,
+        clientY: e.clientY,
+        pageX: e.pageX,
+        pageY: e.pageY,
+        screenX: e.screenX,
+        screenY: e.screenY
+      }));
+
+      // Some inner divs to deliver the posibillity to style the placeholder more sophisticated
+      var placeholder = this.sortableTable.el.find('.ui-sortable-placeholder');
+      if(!placeholder.height()  <= 0) {
+        placeholder.css('height', this.sortableTable.el.find('.ui-sortable-helper').height());
+      }
+
+      placeholder.html('<div class="outer" style="height:100%;"><div class="inner" style="height:100%;"></div></div>');
+    },
+    bindTo: {},
+    _create: function() {
+      this.originalTable = {
+        el: this.element,
+        selectedHandle: $(),
+        sortOrder: {},
+        startIndex: 0,
+        endIndex: 0
+      };
+      // bind draggable to 'th' by default
+      this.bindTo = this.originalTable.el.find('th');
+      // bind draggable to handle if exists
+      if (this.bindTo.find(this.options.dragHandle).size() > 0) {
+        this.bindTo = this.bindTo.find(this.options.dragHandle);
+      }
+      // filter only the cols that are accepted
+      if (this.options.dragaccept) {
+        this.bindTo = this.bindTo.filter(this.options.dragaccept);
+      }
+      // restore state if necessary
+      if (this.options.restoreState !== null) {
+        $.isFunction(this.options.restoreState) ? this.options.restoreState(this.originalTable) : this._restoreState(this.options.restoreState);
+      }
+      var _this = this;
+      this.bindTo.mousedown(function(evt) {
+        // listen only to left mouse click
+        if(evt.which!==1) return;
+        if (_this.options.beforeStart(_this.originalTable) === false) {
+          return;
+        }
+        clearTimeout(this.downTimer);
+        this.downTimer = setTimeout(function() {
+          _this.originalTable.selectedHandle = $(this);
+          _this.originalTable.selectedHandle.addClass('dragtable-handle-selected');
+          _this._generateSortable(evt);
+        }, _this.options.clickDelay);
+      }).mouseup(function(evt) {
+        clearTimeout(this.downTimer);
+      });
+    },
+    redraw: function(){
+      this.destroy();
+      this._create();
+      this.bindTo.unbind('mousedown');
+      $.Widget.prototype.destroy.apply(this, arguments); // default destroy
+      // now do other stuff particular to this widget
+    }
+  });
+
+  /** closure-scoped "private" functions **/
+
+  var body_onselectstart_save = $(document.body).attr('onselectstart'),
+    body_unselectable_save = $(document.body).attr('unselectable');
+
+  // css properties to disable user-select on the body tag by appending a <style> tag to the <head>
+  // remove any current document selections
+
+  function disableTextSelection() {
+    // jQuery doesn't support the element.text attribute in MSIE 8
+    // http://stackoverflow.com/questions/2692770/style-style-textcss-appendtohead-does-not-work-in-ie
+    var $style = $('<style id="__dragtable_disable_text_selection__" type="text/css">body { -ms-user-select:none;-moz-user-select:-moz-none;-khtml-user-select:none;-webkit-user-select:none;user-select:none; }</style>');
+    $(document.head).append($style);
+    $(document.body).attr('onselectstart', 'return false;').attr('unselectable', 'on');
+    if (window.getSelection) {
+      window.getSelection().removeAllRanges();
+    } else {
+      document.selection.empty(); // MSIE http://msdn.microsoft.com/en-us/library/ms535869%28v=VS.85%29.aspx
+    }
+  }
+
+  // remove the <style> tag, and restore the original <body> onselectstart attribute
+
+  function restoreTextSelection() {
+    $('#__dragtable_disable_text_selection__').remove();
+    if (body_onselectstart_save) {
+      $(document.body).attr('onselectstart', body_onselectstart_save);
+    } else {
+      $(document.body).removeAttr('onselectstart');
+    }
+    if (body_unselectable_save) {
+      $(document.body).attr('unselectable', body_unselectable_save);
+    } else {
+      $(document.body).removeAttr('unselectable');
+    }
+  }
+
+  function swapNodes(a, b) {
+    var aparent = a.parentNode;
+    var asibling = a.nextSibling === b ? a : a.nextSibling;
+    b.parentNode.insertBefore(a, b);
+    aparent.insertBefore(b, asibling);
+  }
+})(jQuery);

+ 513 - 0
js/tabularfieldsselector.js

@@ -0,0 +1,513 @@
+// jQuery UI style "widget" for managing the "xlsx-exporter"
+$(function()
+{
+	// the widget definition, where "itop" is the namespace,
+	// "tabularfieldsselector" the widget name
+	$.widget( "itop.tabularfieldsselector",
+	{
+		// default options
+		options:
+		{
+			fields: [],
+			value_holder: '#tabular_fields',
+			sample_data: [],
+			total_count: 0,
+			preview_limit: 3,
+			labels: {
+				preview_header: "Drag and drop the columns to change their order. Preview of %1$s lines. Total number of lines to export: %2$s",
+				empty_preview: "Select the columns to be exported from the list above",
+				columns_order: "Columns order",
+				columns_selection: 'Available columns from %1$s',
+				check_all: 'Check all',
+				uncheck_all: 'Uncheck all',
+				no_field_selected: 'Select at least one column to be exported'
+			}
+		},
+	
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			this._flatten_fields(this.options.fields);
+			this.sId = this.element.attr('id');
+			this.element
+			.addClass('itop-tabularfieldsselector');
+			this.element.parent().bind('form-part-activate', function() { me._update_from_holder(); me._update_preview(); });
+			this.element.parent().bind('validate', function() { me.validate(); });
+			
+			this.aSelected = [];
+			var sContent = '';
+			
+			for(var i in this.options.fields)
+			{
+				sContent += '<fieldset><legend>'+this._format(this.options.labels.columns_selection, i)+'</legend>';
+				sContent += '<div style="text-align:right"><button class="check_all" type="button">'+this.options.labels.check_all+'</button>&nbsp;<button class="uncheck_all" type="button">'+this.options.labels.uncheck_all+'</button></div>';
+				for(var j in this.options.fields[i])
+				{
+					sContent += this._get_field_checkbox(this.options.fields[i][j].code, this.options.fields[i][j].label, (this.options.fields[i][j].subattr.length > 0), false, null);					
+				}
+				sContent += '</fieldset>';
+				this.element.append(sContent);
+			}
+			sContent = '<fieldset><legend>'+this.options.labels.columns_order+'</legend>';
+			
+			sContent += '<div class="preview_header">'+this._format(this.options.labels.preview_header, Math.min(this.options.preview_limit, this.options.total_count), this.options.total_count)+'</div>';
+			sContent += '<div class="table_preview"></div>';
+			sContent += '</fieldset>';
+			this.element.append(sContent);
+		
+			this._update_from_holder();
+			
+			$('body').on('click change', '.tfs_checkbox', function() {
+				var sInstanceId = $(this).attr('data-instance-id');
+				if (sInstanceId != me.sId) return;
+				me._on_click($(this));
+			});
+			
+			var maxWidth = 0;
+			$('#'+this.sId+' .tfs_checkbox, #'+this.sId+' .tfs_checkbox_multi').each(function() {
+				maxWidth = Math.max(maxWidth, $(this).parent().width());
+			});
+			$('#'+this.sId+' .tfs_checkbox, #'+this.sId+' .tfs_checkbox_multi').each(function() {
+				$(this).parent().parent().width(maxWidth).css({display: 'inline-block'});
+			});
+
+			$('#'+this.sId+' .tfs_checkbox_multi').click(function() {
+				me._on_multi_click($(this).val(), this.checked);
+			});
+			$('#'+this.sId+' .check_all').click(function() {
+				me._on_check_all($(this).closest('fieldset'), true);
+			});
+			$('#'+this.sId+' .uncheck_all').click(function() {
+				me._on_check_all($(this).closest('fieldset'), false);
+			});
+			
+
+			this._update_preview();
+			this._make_tooltips();
+		},
+		_on_click: function(jItemClicked)
+		{
+
+			var bChecked = jItemClicked.prop('checked');
+			var sValue = jItemClicked.val();
+			this._mark_as_selected(sValue, bChecked);
+			this._update_holder();
+			this._update_preview();
+			var sDataParent = jItemClicked.attr('data-parent');
+			if (sDataParent != '')
+			{
+				this._update_tristate(sDataParent+'_multi');
+			}
+		},
+		_on_multi_click: function(sMultiFieldCode, bChecked)
+		{
+			var oField = this._get_main_field_by_code(sMultiFieldCode);
+			if (oField != null)
+			{
+				var sPrefix = '#tfs_'+this.sId+'_';
+				for(var k in oField.subattr)
+				{
+					this._mark_as_selected(oField.subattr[k].code, bChecked);
+					// In case the tooltip is visible, also update the checkboxes
+					sElementId = (sPrefix+oField.subattr[k].code).replace('.', '_');
+					$(sElementId).prop('checked', bChecked);
+				}
+				this._update_holder();
+				this._update_preview();
+			}
+		},
+		_on_check_all: function(jSelector, bChecked)
+		{
+			var me = this;
+			jSelector.find('.tfs_checkbox').each(function() {
+				$(this).prop('checked', bChecked);
+				me._mark_as_selected($(this).val(), bChecked);
+			});
+			jSelector.find('.tfs_checkbox_multi').each(function() {
+				var oField = me._get_main_field_by_code($(this).val());
+				if (oField != null)
+				{
+					$(this).prop('checked', bChecked);
+					$(this).prop('indeterminate', false);
+					var sPrefix = '#tfs_'+this.sId+'_';
+					for(var k in oField.subattr)
+					{
+						me._mark_as_selected(oField.subattr[k].code, bChecked);
+						// In case the tooltip is visible, also update the checkboxes
+						sElementId = (sPrefix+oField.subattr[k].code).replace('.', '_');
+						$(sElementId).prop('checked', bChecked);
+					}
+				}				
+			});
+			this._update_holder();
+			this._update_preview();
+		},
+		_update_tristate: function(sParentId)
+		{
+			// Check if the parent is checked, unchecked or indeterminate
+			var sParentId = sParentId.replace('.', '_');
+			var sAttCode = $('#'+sParentId).val();
+			var oField = this._get_main_field_by_code(sAttCode);
+			if (oField != null)
+			{
+				var iNbChecked = 0;
+				var aDebug = [];
+				for(var j in oField.subattr)
+				{
+					if ($.inArray(oField.subattr[j].code, this.aSelected) != -1)
+					{
+						aDebug.push(oField.subattr[j].code);
+						iNbChecked++;
+					}
+				}
+				if (iNbChecked == oField.subattr.length)
+				{
+					$('#'+sParentId).prop('checked', true);
+					$('#'+sParentId).prop('indeterminate', false);
+				}
+				else if (iNbChecked == 0)
+				{
+					$('#'+sParentId).prop('checked', false);
+					$('#'+sParentId).prop('indeterminate', false);
+				}
+				else
+				{
+					$('#'+sParentId).prop('checked', false);
+					$('#'+sParentId).prop('indeterminate', true);
+				}			
+			}
+		},
+		_mark_as_selected: function(sValue, bSelected)
+		{
+			if(bSelected)
+			{
+				if ($.inArray(sValue, this.aSelected) == -1)
+				{
+					this.aSelected.push(sValue);					
+				}
+			}
+			else
+			{
+				aSelected = [];
+				for(var k in this.aSelected)
+				{
+					if (this.aSelected[k] != sValue)
+					{
+						aSelected.push(this.aSelected[k]);
+					}
+				}
+				this.aSelected = aSelected;
+			}			
+		},
+		_update_holder: function()
+		{
+			$(this.options.value_holder).val(this.aSelected.join(','));
+		},
+		_update_from_holder: function()
+		{
+			var sFields = $(this.options.value_holder).val();
+			var bAdvanced = parseInt($(this.options.advanced_holder).val(), 10);
+
+			if (sFields != '')
+			{
+				this.aSelected = sFields.split(',');
+				var safeSelected = [];
+				var me = this;
+				var bModified = false;
+				for(var k in this.aSelected)
+				{
+					var oField = this._get_field_by_code(this.aSelected[k])
+					if (oField == null)
+					{
+						// Invalid field code supplied, don't copy it
+						bModified = true;
+					}
+					else
+					{
+						safeSelected.push(this.aSelected[k]);
+					}
+				}
+				if (bModified)
+				{
+					this.aSelected = safeSelected;
+					this._update_holder();
+				}
+				$('#'+this.sId+' .tfs_checkbox').each(function() {
+					if ($.inArray($(this).val(), me.aSelected) != -1)
+					{
+						$(this).prop('checked', true);
+					}
+					else
+					{
+						$(this).prop('checked', false);
+					}
+				});
+			}
+			var me = this;
+			$('#'+this.sId+' .tfs_checkbox_multi').each(function() {
+				me._update_tristate($(this).attr('id'));
+			});
+
+		},
+		_update_preview: function()
+		{
+			var sHtml = '';
+			if(this.aSelected.length > 0)
+			{
+				sHtml += '<table><thead><tr>';
+				for(var k in this.aSelected)
+				{
+					var sField = this.aSelected[k];
+					if ($.inArray(sField, this.aSelected) != -1)
+					{
+						var sRemoveBtn = '&nbsp;<span style="display:inline-block;float:right;cursor:pointer;" class="export-field-close" data-attcode="'+sField+'">×</span>';
+						sHtml += '<th data-attcode="'+sField+'"><span class="drag-handle">'+this.aFieldsByCode[sField].unique_label+'</span>'+sRemoveBtn+'</th>';												
+					}
+				}
+				sHtml += '</tr></thead><tbody>';
+				
+				for(var i=0; i<Math.min(this.options.preview_limit, this.options.total_count); i++)
+				{
+					sHtml += '<tr>';
+					for(var k in this.aSelected)
+					{
+						var sField = this.aSelected[k];
+						sHtml += '<td>'+this.options.sample_data[i][sField]+'</td>';
+					}				
+					sHtml += '</tr>';
+				}
+				
+				sHtml += '</tbody></table>';
+				
+				$('#'+this.sId+' .preview_header').show();
+				$('#'+this.sId+' .table_preview').html(sHtml);
+				var me = this;
+				$('#'+this.sId+' .table_preview table').dragtable({persistState: function(table) { me._on_drag_columns(table); }, dragHandle: '.drag-handle'});
+				$('#'+this.sId+' .table_preview table .export-field-close').click( function(event) { me._on_remove_column($(this).attr('data-attcode')); event.preventDefault(); return false; } );
+			}
+			else
+			{
+				$('#'+this.sId+' .preview_header').hide();
+				$('#'+this.sId+' .table_preview').html('<div class="export_empty_preview">'+this.options.labels.empty_preview+'</div>');
+			}
+		},
+		_get_field_by_code: function(sFieldCode)
+		{
+			for(var k in this.aFieldsByCode)
+			{
+				if (k == sFieldCode)
+				{
+					return this.aFieldsByCode[k];
+				}
+			}
+			return null;
+		},
+		_get_main_field_by_code: function(sFieldCode)
+		{
+			for(var i in this.options.fields)
+			{
+				for(var j in this.options.fields[i])
+				{
+					if (this.options.fields[i][j].code == sFieldCode)
+					{
+						return this.options.fields[i][j];
+					}
+				}
+			}
+			return null;
+		},
+		_on_drag_columns: function(table)
+		{
+			var me = this;
+			me.aSelected = [];
+			table.el.find('th').each(function(i) { 
+				me.aSelected.push($(this).attr('data-attcode'));
+			});
+			this._update_holder();
+		},
+		_on_remove_column: function(sField)
+		{
+			var sElementId = this.sId+'_'+sField;
+			sElementId = '#tfs_'+sElementId.replace('.', '_');
+			$(sElementId).prop('checked', false);
+			
+			this._mark_as_selected(sField, false);
+			this._update_holder();
+			this._update_preview();
+			var me = this;
+			$('#'+this.sId+' .tfs_checkbox_multi').each(function() {
+				me._update_tristate($(this).attr('id'));
+			});
+		},
+		_format: function()
+		{
+			var s = arguments[0];
+			for (var i = 0; i < arguments.length - 1; i++) {       
+				var reg = new RegExp("%" + (i+1) + "\\$s", "gm");             
+				s = s.replace(reg, arguments[i+1]);
+			}
+			return s;
+		},
+		validate: function()
+		{
+			if (this.aSelected.length == 0)
+			{
+				var aMessages = $('#export-form').data('validation_messages');
+				aMessages.push(this.options.labels.no_field_selected);
+				$('#export-form').data('validation_messages', aMessages);
+			}
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		destroy: function()
+		{
+			this.element
+			.removeClass('itop-tabularfieldsselector');
+			
+			this.element.parent().unbind('activate');
+			this.element.parent().unbind('validate');
+		},
+		// _setOptions is called with a hash of all options that are changing
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			if (key == 'fields')
+			{
+				this._flatten_fields(value);
+			}
+			this._superApply(arguments);
+		},
+		_flatten_fields: function(aFields)
+		{
+			// Update the "flattened" via of the fields
+			this.aFieldsByCode = [];
+			for(var k in aFields)
+			{
+				for(var i in aFields[k])
+				{
+					this.aFieldsByCode[aFields[k][i].code] = aFields[k][i];
+					for(var j in aFields[k][i].subattr)
+					{
+						this.aFieldsByCode[aFields[k][i].subattr[j].code] = aFields[k][i].subattr[j];
+					}
+				}
+			}
+		},
+		_make_tooltips: function()
+		{
+			var me  = this;
+			$('#'+this.sId+' .tfs_advanced').tooltip({
+				content: function() {
+					var sDataAttcode = $(this).attr('data-attcode');
+					var sTooltipContent = '';
+					sTooltipContent += me._get_tooltip_content(sDataAttcode);
+					return sTooltipContent;
+				},
+				items: '.tfs_advanced',
+				position: {
+					my: "center bottom-10",
+					at: "center  top",					
+					using: function( position, feedback ) { 
+						$(this).css( position );  
+						$( "<div>" )
+						.addClass( "arrow" )
+						.addClass( feedback.vertical )
+						.addClass( feedback.horizontal )
+						.appendTo( this );
+						}
+				}
+			})
+			.off( "mouseover mouseout" )
+			.on( "mouseover", function(event){
+				event.stopImmediatePropagation();
+				var jMe = $(this);
+				$(this).data('openTimeoutId', setTimeout(function() {
+					var sDataId = jMe.attr('data-attcode');
+					if ($('.tooltip-close-button[data-attcode="'+sDataId+'"]').length == 0)
+					{
+						jMe.tooltip('open');						
+					}
+				}, 500));					
+			})
+			.on( "mouseout", function(event){
+				event.stopImmediatePropagation();
+				clearTimeout($(this).data('openTimeoutId'));					
+			});
+			/*
+			.on( "click", function(){
+				var sDataId = $(this).attr('data-attcode');
+				if ($('.tooltip-close-button[data-attcode="'+sDataId+'"]').length == 0)
+				{
+					$(this).tooltip( 'open' );							 
+				}
+				else
+				{
+					$(this).tooltip( 'close' );							 						
+				}           
+				$( this ).unbind( "mouseleave" );
+				return false;	
+			 });
+			 */
+		
+			$('body').on('click', '.tooltip-close-button', function() {
+				var sDataId = $(this).attr('data-attcode');
+				$('#'+me.sId+' .tfs_advanced[data-attcode="'+sDataId+'"]').tooltip('close');
+			});
+		},
+		_get_tooltip_content: function(sDataAttCode)
+		{
+			var oField = this._get_main_field_by_code(sDataAttCode);
+			var sContent = '';
+			if (oField != null)
+			{
+				sContent += '<div display:block;">'+oField.label+'<div class="tooltip-close-button" data-attcode="'+sDataAttCode+'" style="display:inline-block; float:right; cursor:pointer; padding-left:0.25em; padding-bottom:0.25em;">×</div></div>';
+				for(var k in oField.subattr)
+				{
+					bChecked = ($.inArray(oField.subattr[k].code, this.aSelected) != -1);
+					sContent += this._get_field_checkbox(oField.subattr[k].code, oField.subattr[k].label, false, bChecked, sDataAttCode);
+				}
+			}
+			return sContent;
+		},
+		_get_field_checkbox: function(sCode, sLabel, bHasTooltip, bChecked, sParentId)
+		{
+			var sPrefix = 'tfs_'+this.sId+'_';
+			sParentId = (sPrefix+sParentId).replace('.', '_');
+			sElementId = (sPrefix+sCode).replace('.', '_');
+			var aClasses = [];
+			if (bHasTooltip)
+			{
+				aClasses.push('tfs_advanced');
+				sLabel += ' [+]';
+			}
+			var sChecked = '';
+			if (bChecked)
+			{
+				sChecked = ' checked ';
+			}
+			var sDataParent = '';
+			if (sParentId != null)
+			{
+				sDataParent = ' data-parent="'+sParentId+'" ';
+			}
+			if (bHasTooltip)
+			{
+				sContent = '<div style="display:block; clear:both;"><span style="white-space: nowrap;"><input data-instance-id="'+this.sId+'" class="tfs_checkbox_multi" type="checkbox" id="'+sElementId+'_multi" value="'+sCode+'"'+sChecked+sDataParent+'><label data-attcode="'+sCode+'" class="'+aClasses.join(' ')+'" title="'+sCode+'">&nbsp;'+sLabel+'</label></div>';
+			}
+			else
+			{
+				sContent = '<div style="display:block; clear:both;"><span style="white-space: nowrap;"><input data-instance-id="'+this.sId+'" class="tfs_checkbox" type="checkbox" id="'+sElementId+'" value="'+sCode+'"'+sChecked+sDataParent+'><label data-attcode="'+sCode+'" class="'+aClasses.join(' ')+'" title="'+sCode+'" for="'+sElementId+'">&nbsp;'+sLabel+'</label></div>';				
+			}
+			return sContent;
+		},
+		_close_all_tooltips: function()
+		{
+			this.element.find('.tfs_item').tooltip('close');
+		}
+	});	
+});

+ 194 - 10
js/utils.js

@@ -414,31 +414,215 @@ function ShortcutListDlg(sOQL, sDataTableId, sContext)
 
 function ExportListDlg(sOQL, sDataTableId, sFormat, sDlgTitle)
 {
-	var sDataTableName = 'datatable_'+sDataTableId;
-	var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns');
 	var aFields = [];
-	for(var j in oColumns)
+	if (sDataTableId != '')
 	{
-		for(var k in oColumns[j])
+		var sDataTableName = 'datatable_'+sDataTableId;
+		var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns');
+		for(var j in oColumns)
 		{
-			if (oColumns[j][k].checked)
+			for(var k in oColumns[j])
 			{
-				var sCode = oColumns[j][k].code;
-				if (sCode == '_key_')
+				if (oColumns[j][k].checked)
 				{
-					sCode = 'id';
+					var sCode = oColumns[j][k].code;
+					if (sCode == '_key_')
+					{
+						sCode = 'id';
+					}
+					aFields.push(j+'.'+sCode);
 				}
-				aFields.push(j+'.'+sCode);
 			}
 		}
 	}
-
+	
 	$.post(GetAbsoluteUrlAppRoot()+'webservices/export-v2.php', {interactive: 1, advanced: 1, mode: 'dialog', format: sFormat, expression: sOQL, suggested_fields: aFields.join(','), dialog_title: sDlgTitle}, function(data) {
 		$('body').append(data);
 	});
 	return false;
 }
 
+function ExportToggleFormat(sFormat)
+{
+	$('.form_part').hide();
+	for(k in window.aFormParts[sFormat])
+	{
+		$('#form_part_'+window.aFormParts[sFormat][k]).show().trigger('form-part-activate');
+	}	 
+}
+		
+function ExportStartExport()
+{
+	var oParams = {};
+	$('.form_part:visible :input').each(function() {
+		if (this.name != '')
+		{
+			if ((this.type == 'radio') || (this.type == 'checkbox'))
+			{
+				if (this.checked)
+				{
+					oParams[this.name] = $(this).val();
+				}
+			}
+			else
+			{
+				oParams[this.name] = $(this).val();
+			}
+		}
+	});
+	$('#export-form').hide();
+	$('#export-feedback').show();
+	oParams.operation = 'export_build';
+	oParams.format = $('#export-form :input[name=format]').val();
+	var sQueryMode = $(':input[name=query_mode]:checked').val();
+	if($(':input[name=query_mode]:checked').length > 0)
+	{
+		if (sQueryMode == 'oql')
+		{
+			oParams.expression = $('#export-form :input[name=expression]').val();
+		}
+		else
+		{	
+			oParams.query = $('#export-form :input[name=query]').val();
+		}
+	}
+	else
+	{
+		oParams.expression = $('#export-form :input[name=expression]').val();
+		oParams.query = $('#export-form :input[name=query]').val();		
+	}
+	$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
+		ExportRun(data);
+	}, 'json');
+}
+		
+function ExportRun(data)
+{
+	switch(data.code)
+	{
+		case 'run':
+		// Continue
+		$('.export-progress-bar').progressbar({value: data.percentage });
+		$('.export-message').html(data.message); 
+		oParams = {};
+		oParams.token = data.token;
+		var sDataState = $('#export-form').attr('data-state');
+		if (sDataState == 'cancelled')
+		{
+			oParams.operation = 'export_cancel';
+		}
+		else
+		{
+			oParams.operation = 'export_build';
+		}
+		
+		$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
+			ExportRun(data);
+		},
+		'json');
+		break;
+
+		case 'done':
+		$('#export-btn').hide();
+		sMessage = '<a href="'+GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=export_download&token='+data.token+'" target="_blank">'+data.message+'</a>'; 
+		$('.export-message').html(sMessage);
+		$('.export-progress-bar').hide();
+		$('#export-btn').hide();
+		$('#export-form').attr('data-state', 'done');
+		if(data.text_result != undefined)
+		{
+			if (data.mime_type == 'text/html')
+			{
+				$('#export_content').parent().html(data.text_result);
+				$('#export_text_result').show();
+				$('#export_text_result .listResults').tableHover();
+				$('#export_text_result .listResults').tablesorter( { widgets: ['myZebra']} );
+			}
+			else
+			{
+				if ($('#export_text_result').closest('ui-dialog').length == 0)
+				{
+					// not inside a dialog box, adjust the height... approximately
+					var jPane = $('#export_text_result').closest('.ui-layout-content');
+					var iTotalHeight = jPane.height();
+					jPane.children(':visible').each(function() {
+						if ($(this).attr('id') != '')
+						{
+							iTotalHeight -= $(this).height();
+						}
+					});
+					$('#export_content').height(iTotalHeight - 80);
+				}
+				$('#export_content').val(data.text_result);
+				$('#export_text_result').show();
+			}
+		}
+		$('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
+		break;
+		
+		case 'error':
+		$('#export-form').attr('data-state', 'error');
+		$('.export-progress-bar').progressbar({value: data.percentage });
+		$('.export-message').html(data.message); 
+		$('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
+		$('#export-btn').hide();
+		default:
+	}
+}
+		
+function ExportInitButton(sSelector)
+{
+	$(sSelector).on('click', function() {
+		var sDataState = $('#export-form').attr('data-state');
+		switch(sDataState)
+		{
+			case 'not-yet-started':
+			$('.form_part:visible').each(function() {
+		 		$('#export-form').data('validation_messages', []);
+				var ret = $(this).trigger('validate');
+			});
+			var aMessages = $('#export-form').data('validation_messages');
+		
+			if(aMessages.length > 0)
+			{
+				alert(aMessages.join(''));
+				return;
+			}
+			if ($(this).hasClass('ui-button'))
+			{
+				$(this).button('option', 'label', Dict.S('UI:Button:Cancel'));
+			}
+			else
+			{
+				$(this).html(Dict.S('UI:Button:Cancel'));
+			}
+			$('#export-form').attr('data-state', 'running');
+			ExportStartExport();
+			break;
+		
+			case 'running':
+			if ($(this).hasClass('ui-button'))
+			{
+				$(this).button('disable');
+			}
+			else
+			{
+				$(this).attr('disabled', 'disabled');
+			}
+			$('#export-form').attr('data-state', 'cancelled');
+			break;
+		
+			case 'done':
+			case 'error':
+			$('#interactive_export_dlg').dialog('close');			
+			break;
+		
+			default:
+			// Do nothing
+		}
+	});
+}
+
 function DisplayHistory(sSelector, sFilter, iCount, iStart)
 {
 	$(sSelector).block();

+ 0 - 185
js/xlsx-export.js

@@ -1,185 +0,0 @@
-// jQuery UI style "widget" for managing the "xlsx-exporter"
-$(function()
-{
-	// the widget definition, where "itop" is the namespace,
-	// "xlsxexporter" the widget name
-	$.widget( "itop.xlsxexporter",
-	{
-		// default options
-		options:
-		{
-			filter: '',
-			ajax_page_url: '',
-			labels: {dialog_title: 'Excel Export', export_button: 'Export', cancel_button: 'Cancel', download_button: 'Download', complete: 'Complete', cancelled: 'Cancelled' }
-		},
-	
-		// the constructor
-		_create: function()
-		{
-			this.element
-			.addClass('itop-xlsxexporter');
-			
-			this.sToken = null;
-			this.ajaxCall = null;
-			this.oProgressBar = $('.progress-bar', this.element);
-			this.oStatusMessage = $('.status-message', this.element);
-			$('.progress', this.element).hide();
-			$('.statistics', this.element).hide();
-			
-			var me = this;
-			
-			this.element.dialog({
-				title: this.options.labels.dialog_title,
-				modal: true,
-				width: 500,
-				height: 300,
-				buttons: [
-				    { text: this.options.labels.export_button, 'class': 'export-button', click: function() {
-				    	me._start();
-				    } },
-				    { text: this.options.labels.cancel_button, 'class': 'cancel-button', click: function() {
-				    	$(this).dialog( "close" );
-				    } },
-				],
-				close: function() { me._abort(); $(this).remove(); }
-			});
-		},
-		// events bound via _bind are removed automatically
-		// revert other modifications here
-		destroy: function()
-		{
-			this.element
-			.removeClass('itop-xlsxexporter');
-		},
-		// _setOptions is called with a hash of all options that are changing
-		_setOptions: function()
-		{
-			this._superApply(arguments);
-		},
-		// _setOption is called for each individual option that is changing
-		_setOption: function( key, value )
-		{
-			this._superApply(arguments);
-		},
-		_start: function()
-		{
-			var me = this;
-			$('.export-options', this.element).hide();
-			$('.progress', this.element).show();
-			var bAdvanced = $('#export-advanced-mode').prop('checked');
-			this.bAutoDownload = $('#export-auto-download').prop('checked');
-			$('.export-button', this.element.parent()).button('disable');
-			
-			this.oProgressBar.progressbar({
-				 value: 0,
-				 change: function() {
-					 var progressLabel = $('.progress-label', me.element);
-					 progressLabel.text( $(this).progressbar( "value" ) + "%" );
-				 },
-				 complete: function() {
-					 var progressLabel = $('.progress-label', me.element);
-					 progressLabel.text( me.options.labels['complete'] );
-				 }
-			});
-
-			//TODO disable the "export" button
-			this.ajaxCall = $.post(this.options.ajax_page_url, {filter: this.options.filter, operation: 'xlsx_start', advanced: bAdvanced}, function(data) {
-				this.ajaxCall = null;
-				if (data && data.status == 'ok')
-				{
-					me.sToken = data.token;
-					me._run();
-				}
-				else
-				{
-					if (data == null)
-					{
-						me.oStatusMessage.html('Unexpected error (operation=xlsx_start).');	
-						me.oProgressBar.progressbar({value: 100});
-					}
-					else
-					{
-						me.oStatusMessage.html(data.message);										
-					}
-				}
-			}, 'json');
-
-		},
-		_abort: function()
-		{
-			$('.cancel-button', this.element.parent()).button('disable');
-			this.oStatusMessage.html(this.options.labels['cancelled']);
-			this.oProgressBar.progressbar({value: 100});
-			if (this.sToken != null)
-			{
-				// Cancel the operation in progress... or cleanup a completed export
-				// TODO
-				if (this.ajaxCall)
-				{
-					this.ajaxCall.abort();
-					this.ajaxClass = null;
-				}
-				var me = this;
-				$.post(this.options.ajax_page_url, {token: this.sToken, operation: 'xlsx_abort'}, function(data) {
-					me.sToken = null;
-				});
-			}
-		},
-		_run: function()
-		{
-			var me = this;
-			this.ajaxCall = $.post(this.options.ajax_page_url, {token: this.sToken, operation: 'xlsx_run'}, function(data) {
-				this.ajaxCall = null;
-				if (data == null)
-				{
-					me.oStatusMessage.html('Unexpected error (operation=xlsx_run).');
-					me.oProgressBar.progressbar({value: 100});			
-				}
-				else if (data.status == 'error')
-				{
-					me.oStatusMessage.html(data.message);
-					me.oProgressBar.progressbar({value: 100});
-				}
-				else if (data.status == 'done')
-				{
-					me.oStatusMessage.html(data.message);
-					me.oProgressBar.progressbar({value: 100});
-					$('.stats-data', this.element).html(data.statistics);
-					me._on_completion();
-				}
-				else
-				{
-					// continue running the export in the background
-					me.oStatusMessage.html(data.message);
-					me.oProgressBar.progressbar({value: data.percentage});
-					me._run();
-				}
-			}, 'json');
-		},
-		_on_completion: function()
-		{
-			var me = this;
-			$('.progress', this.element).html('<form class="download-form" method="post" action="'+this.options.ajax_page_url+'"><input type="hidden" name="operation" value="xlsx_download"/><input type="hidden" name="token" value="'+this.sToken+'"/><button type="submit">'+this.options.labels['download_button']+'</button></form>');
-			$('.download-form button', this.element).button().click(function() { me.sToken = null; window.setTimeout(function() { me.element.dialog('close'); }, 100); return true;});
-			if (this.bAutoDownload)
-			{
-				me.sToken = null;
-				$('.download-form').submit();
-				this.element.dialog('close');
-			}
-			else
-			{
-				$('.statistics', this.element).show();
-				$('.statistics .stats-toggle', this.element).click(function() { $(this).toggleClass('closed'); });
-			}
-		}
-	});	
-});
-
-function XlsxExportDialog(sFilter)
-{
-	var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
-	$.post(sUrl, {operation: 'xlsx_export_dialog', filter: sFilter}, function(data) {
-		$('body').append(data);
-	});
-}

+ 4 - 0
pages/UI.php

@@ -505,6 +505,10 @@ try
 				if ($iErrors == 0)
 				{
 					$oP->set_title(Dict::S('UI:SearchResultsPageTitle'));
+					$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
+					$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
+					$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
+					$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');					
 					$oP->add("<div style=\"padding: 10px;\">\n");
 					$oP->add("<div class=\"header_message\" id=\"full_text_progress\" style=\"position: fixed; background-color: #cccccc; opacity: 0.7; padding: 1.5em;\">\n");
 					$oP->add('<img id="full_text_indicator" src="../images/indicator.gif">&nbsp;<span style="padding: 1.5em;">'.Dict::Format('UI:Search:Ongoing', htmlentities($sFullText, ENT_QUOTES, 'UTF-8')).'</span>');

+ 2194 - 2083
pages/ajax.render.php

@@ -1,2084 +1,2195 @@
-<?php
-// Copyright (C) 2010-2015 Combodo SARL
-//
-//   This file is part of iTop.
-//
-//   iTop is free software; you can redistribute it and/or modify	
-//   it under the terms of the GNU Affero General Public License as published by
-//   the Free Software Foundation, either version 3 of the License, or
-//   (at your option) any later version.
-//
-//   iTop is distributed in the hope that it will be useful,
-//   but WITHOUT ANY WARRANTY; without even the implied warranty of
-//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-//   GNU Affero General Public License for more details.
-//
-//   You should have received a copy of the GNU Affero General Public License
-//   along with iTop. If not, see <http://www.gnu.org/licenses/>
-
-
-/**
- * Handles various ajax requests
- *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
- * @license     http://opensource.org/licenses/AGPL-3.0
- */
-
-require_once('../approot.inc.php');
-require_once(APPROOT.'/application/application.inc.php');
-require_once(APPROOT.'/application/webpage.class.inc.php');
-require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
-require_once(APPROOT.'/application/pdfpage.class.inc.php');
-require_once(APPROOT.'/application/wizardhelper.class.inc.php');
-require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
-require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
-require_once(APPROOT.'/application/datatable.class.inc.php');
-require_once(APPROOT.'/application/excelexporter.class.inc.php');
-
-try
-{
-	require_once(APPROOT.'/application/startup.inc.php');
-	require_once(APPROOT.'/application/user.preferences.class.inc.php');
-	
-	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
-	LoginWebPage::DoLoginEx(null /* any portal */, false);
-	
-	$oPage = new ajax_page("");
-	$oPage->no_cache();
-
-	
-	$operation = utils::ReadParam('operation', '');
-	$sFilter = stripslashes(utils::ReadParam('filter', '', false, 'raw_data'));
-	$sEncoding = utils::ReadParam('encoding', 'serialize');
-	$sClass = utils::ReadParam('class', 'MissingAjaxParam', false, 'class');
-	$sStyle = utils::ReadParam('style', 'list');
-
-	switch($operation)
-	{
-		case 'datatable':
-		case 'pagination':
-		$oPage->SetContentType('text/html');
-		$extraParams = utils::ReadParam('extra_param', '', false, 'raw_data');
-		$aExtraParams = array();
-		if (is_array($extraParams))
-		{
-			$aExtraParams = $extraParams;
-		}
-		else
-		{
-			$sExtraParams = stripslashes($extraParams);
-			if (!empty($sExtraParams))
-			{
-				$val = json_decode(str_replace("'", '"', $sExtraParams), true /* associative array */);
-				if ($val !== null)
-				{
-					$aExtraParams = $val;
-				}
-			}
-		}
-		if ($sEncoding == 'oql')
-		{
-			$oFilter = CMDBSearchFilter::FromOQL($sFilter);
-		}
-		else
-		{
-			$oFilter = CMDBSearchFilter::unserialize($sFilter);
-		}
-		$iStart = utils::ReadParam('start',0);
-		$iEnd = utils::ReadParam('end',1);
-		$iSortCol = utils::ReadParam('sort_col','null');
-		$sSelectMode = utils::ReadParam('select_mode', '');
-		if (!empty($sSelectMode) && ($sSelectMode != 'none'))
-		{
-			// The first column is used for the selection (radio / checkbox) and is not sortable
-			$iSortCol--;
-		}
-		$bDisplayKey = utils::ReadParam('display_key', 'true') == 'true';
-		$aColumns = utils::ReadParam('columns', array(), false, 'raw_data');
-		$aClassAliases = utils::ReadParam('class_aliases', array());
-		$iListId = utils::ReadParam('list_id', 0);
-		//$aList = cmdbAbstractObject::FlattenZList(MetaModel::GetZListItems($sClassName, 'list'));
-
-		// Filter the list to removed linked set since we are not able to display them here
-		$aOrderBy = array();
-		$iSortIndex = 0;
-		
-		$aColumnsLoad = array();
-		foreach($aClassAliases as $sAlias => $sClassName)
-		{
-			$aColumnsLoad[$sAlias] = array();
-			foreach($aColumns[$sAlias] as $sAttCode => $aData)
-			{
-				if ($aData['checked'] == 'true')
-				{
-					$aColumns[$sAlias][$sAttCode]['checked'] = true;
-					if ($sAttCode == '_key_')
-					{
-						if ($iSortCol == $iSortIndex)
-						{
-							if (!MetaModel::HasChildrenClasses($oFilter->GetClass()))
-							{
-								$aNameSpec = MetaModel::GetNameSpec($oFilter->GetClass());
-								if ($aNameSpec[0] == '%1$s')
-								{
-									// The name is made of a single column, let's sort according to the sort algorithm for this column
-									$aOrderBy[$sAlias.'.'.$aNameSpec[1][0]] = (utils::ReadParam('sort_order', 'asc') == 'asc');
-								}
-								else
-								{
-									$aOrderBy[$sAlias.'.'.'friendlyname'] = (utils::ReadParam('sort_order', 'asc') == 'asc');
-								}
-							}
-							else
-							{
-								$aOrderBy[$sAlias.'.'.'friendlyname'] = (utils::ReadParam('sort_order', 'asc') == 'asc');
-							}
-						}
-					}
-					else
-					{
-						$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
-						if ($oAttDef instanceof AttributeLinkedSet)
-						{
-							// Removed from the display list
-							unset($aColumns[$sAlias][$sAttCode]);
-						}
-						else
-						{
-							$aColumnsLoad[$sAlias][] = $sAttCode;
-						}
-						if ($iSortCol == $iSortIndex)
-						{
-							if ($oAttDef->IsExternalKey())
-							{
-								$sSortCol = $sAttCode.'_friendlyname';
-							}
-							else
-							{
-								$sSortCol = $sAttCode;
-							}
-							$aOrderBy[$sAlias.'.'.$sSortCol] = (utils::ReadParam('sort_order', 'asc') == 'asc');
-						}
-					}
-					$iSortIndex++;
-				}
-				else
-				{
-					$aColumns[$sAlias][$sAttCode]['checked'] = false;
-				}
-			}
-
-		}
-		
-		// Load only the requested columns
-		$oSet = new DBObjectSet($oFilter, $aOrderBy, $aExtraParams, null, $iEnd-$iStart, $iStart);
-		$oSet->OptimizeColumnLoad($aColumnsLoad);
-
-		$oDataTable = new DataTable($iListId, $oSet, $oSet->GetSelectedClasses());
-		if ($operation == 'datatable')
-		{
-			// Redraw the whole table
-			$sHtml = $oDataTable->UpdatePager($oPage, $iEnd-$iStart, $iStart); // Set the default page size
-			$sHtml .= $oDataTable->GetHTMLTable($oPage, $aColumns, $sSelectMode, $iEnd-$iStart, $bDisplayKey, $aExtraParams);
-		}
-		else
-		{
-			// redraw just the needed rows
-			$sHtml = $oDataTable->GetAsHTMLTableRows($oPage, $iEnd-$iStart, $aColumns, $sSelectMode, $bDisplayKey, $aExtraParams);
-		}
-		$oPage->add($sHtml);
-		break;
-		
-		case 'datatable_save_settings':
-		$oPage->SetContentType('text/plain');
-		$iPageSize = utils::ReadParam('page_size', 10);
-		$sTableId = utils::ReadParam('table_id', null, false, 'raw_data');
-		$bSaveAsDefaults = (utils::ReadParam('defaults', 'true') == 'true');
-		$aClassAliases = utils::ReadParam('class_aliases', array(), false, 'raw_data');
-		$aColumns = utils::ReadParam('columns', array(), false, 'raw_data');
-		
-		foreach($aColumns as $sAlias => $aList)
-		{
-			foreach($aList as $sAttCode => $aData)
-			{
-				$aColumns[$sAlias][$sAttCode]['checked'] = ($aData['checked'] == 'true');
-				$aColumns[$sAlias][$sAttCode]['disabled'] = ($aData['disabled'] == 'true');
-				$aColumns[$sAlias][$sAttCode]['sort'] = ($aData['sort']);
-			}
-		}
-		
-		$oSettings = new DataTableSettings($aClassAliases, $sTableId);
-		$oSettings->iDefaultPageSize = $iPageSize;
-		$oSettings->aColumns = $aColumns;
-
-		if ($bSaveAsDefaults)
-		{
-			if ($sTableId != null)
-			{
-				$oCurrSettings = DataTableSettings::GetTableSettings($aClassAliases, $sTableId, true /* bOnlyTable */ );
-				if ($oCurrSettings)
-				{
-					$oCurrSettings->ResetToDefault(false); // Reset this table to the defaults
-				}
-			}
-			$bRet = $oSettings->SaveAsDefault();
-		}
-		else
-		{
-			$bRet = $oSettings->Save();
-		}
-		$oPage->add($bRet ? 'Ok' : 'KO');
-		break;
-		
-		case 'datatable_reset_settings':
-		$oPage->SetContentType('text/plain');
-		$sTableId = utils::ReadParam('table_id', null, false, 'raw_data');
-		$aClassAliases = utils::ReadParam('class_aliases', array(), false, 'raw_data');
-		$bResetAll = (utils::ReadParam('defaults', 'true') == 'true');
-		
-		$oSettings = new DataTableSettings($aClassAliases, $sTableId);
-		$bRet = $oSettings->ResetToDefault($bResetAll);
-		$oPage->add($bRet ? 'Ok' : 'KO');
-		break;
-		
-		// ui.linkswidget
-		case 'addObjects':
-		$oPage->SetContentType('text/html');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sSuffix = utils::ReadParam('sSuffix', '');
-		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		if (!empty($sJson))
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		else
-		{
-			// Search form: no current object
-			$oObj = null;
-		}
-		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
-		$oWidget->GetObjectPickerDialog($oPage, $oObj);	
-		break;
-		
-		// ui.linkswidget
-		case 'searchObjectsToAdd':
-		$oPage->SetContentType('text/html');
-		$sRemoteClass = utils::ReadParam('sRemoteClass', '', false, 'class');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sSuffix = utils::ReadParam('sSuffix', '');
-		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
-		$aAlreadyLinked = utils::ReadParam('aAlreadyLinked', array());
-		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
-		$oWidget->SearchObjectsToAdd($oPage, $sRemoteClass, $aAlreadyLinked);	
-		break;
-		
-		//ui.linksdirectwidget
-		case 'createObject':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
-		$sAttCode = utils::ReadParam('att_code', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$oPage->SetContentType('text/html');
-		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oWidget->GetObjectCreationDlg($oPage, $sRealClass);
-		break;
-		
-		// ui.linksdirectwidget
-		case 'getLinksetRow':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
-		$sAttCode = utils::ReadParam('att_code', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$iTempId = utils::ReadParam('tempId', '');
-		$aValues = utils::ReadParam('values', array(), false, 'raw_data');
-		$oPage->SetContentType('text/html');
-		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oPage->add($oWidget->GetRow($oPage, $sRealClass, $aValues, -$iTempId));
-		break;
-		
-		// ui.linksdirectwidget
-		case 'selectObjectsToAdd':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$oObj = null;
-		if ($sJson != '')
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
-		$sAttCode = utils::ReadParam('att_code', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
-		$oPage->SetContentType('text/html');
-		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oWidget->GetObjectsSelectionDlg($oPage, $oObj);
-		break;
-			
-		// ui.linksdirectwidget
-		case 'searchObjectsToAdd2':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
-		$sAttCode = utils::ReadParam('att_code', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$aAlreadyLinked =  utils::ReadParam('aAlreadyLinked', array());
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$oObj = null;
-		if ($sJson != '')
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oWidget->SearchObjectsToAdd($oPage, $sRealClass, $aAlreadyLinked, $oObj);
-		break;
-		
-		// ui.linksdirectwidget
-		case 'doAddObjects2':
-		$oPage->SetContentType('text/html');
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
-		$sAttCode = utils::ReadParam('att_code', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
-		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
-		if ($sFilter != '')
-		{
-			$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
-		}
-		else
-		{
-			$oLinksetDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			$valuesDef = $oLinksetDef->GetValuesDef();				
-			if ($valuesDef === null)
-			{
-				$oFullSetFilter = new DBObjectSearch($oLinksetDef->GetLinkedClass());
-			}
-			else
-			{
-				if (!$valuesDef instanceof ValueSetObjects)
-				{
-					throw new Exception('Error: only ValueSetObjects are supported for "allowed_values" in AttributeLinkedSet ('.$this->sClass.'/'.$this->sAttCode.').');
-				}
-				$oFullSetFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression());
-			}		
-		}
-		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oWidget->DoAddObjects($oPage, $oFullSetFilter);	
-		break;
-		
-		////////////////////////////////////////////////////////////
-		
-		// ui.extkeywidget
-		case 'searchObjectsToSelect':
-		$oPage->SetContentType('text/html');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sRemoteClass = utils::ReadParam('sRemoteClass', '', false, 'class');
-		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
-		if (!empty($sJson))
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		else
-		{
-			// Search form: no current object
-			$oObj = null;
-		}
-		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, $bSearchMode);
-		$oWidget->SearchObjectsToSelect($oPage, $sFilter, $sRemoteClass, $oObj);	
-		break;
-	
-		// ui.extkeywidget: autocomplete
-		case 'ac_extkey':
-		$oPage->SetContentType('text/plain');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$sContains = utils::ReadParam('q', '', false, 'raw_data');
-		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
-		if ($sContains !='')
-		{
-			if (!empty($sJson))
-			{
-				$oWizardHelper = WizardHelper::FromJSON($sJson);
-				$oObj = $oWizardHelper->GetTargetObject();
-			}
-			else
-			{
-				// Search form: no current object
-				$oObj = null;
-			}
-			$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, '', $bSearchMode);
-			$oWidget->AutoComplete($oPage, $sFilter, $oObj, $sContains);
-		}
-		break;
-	
-		// ui.extkeywidget
-		case 'objectSearchForm':
-		$oPage->SetContentType('text/html');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sTitle = utils::ReadParam('sTitle', '', false, 'raw_data');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
-		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, $bSearchMode);
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		if (!empty($sJson))
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		else
-		{
-			// Search form: no current object
-			$oObj = null;
-		}
-		$oWidget->GetSearchDialog($oPage, $sTitle, $oObj);
-		break;
-
-		// ui.extkeywidget
-		case 'objectCreationForm':
-		$oPage->SetContentType('text/html');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		if (!empty($sJson))
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		else
-		{
-			// Search form: no current object
-			$oObj = null;
-		}
-		$oWidget->GetObjectCreationForm($oPage, $oObj);
-		break;
-		
-		// ui.extkeywidget
-		case 'doCreateObject':
-		$oPage->SetContentType('application/json');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sFormPrefix = utils::ReadParam('sFormPrefix', '');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
-		$aResult = $oWidget->DoCreateObject($oPage);
-		echo json_encode($aResult);
-		break;
-		
-		// ui.extkeywidget
-		case 'getObjectName':
-		$oPage->SetContentType('application/json');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$iObjectId = utils::ReadParam('iObjectId', '');
-		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
-		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, '', $bSearchMode);
-		$sName = $oWidget->GetObjectName($iObjectId);
-		echo json_encode(array('name' => $sName));	
-		break;
-		
-		// ui.extkeywidget
-		case 'displayHierarchy':
-		$oPage->SetContentType('text/html');
-		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
-		$sInputId = utils::ReadParam('sInputId', '');
-		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$currValue = utils::ReadParam('value', '');
-		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
-		if (!empty($sJson))
-		{
-			$oWizardHelper = WizardHelper::FromJSON($sJson);
-			$oObj = $oWizardHelper->GetTargetObject();
-		}
-		else
-		{
-			// Search form: no current object
-			$oObj = null;
-		}
-		$oWidget = new UIExtKeyWidget($sTargetClass, $sInputId, '', $bSearchMode);
-		$oWidget->DisplayHierarchy($oPage, $sFilter, $currValue, $oObj);
-		break;
-		
-		////////////////////////////////////////////////////
-		
-		// ui.linkswidget
-		case 'doAddObjects':
-		$oPage->SetContentType('text/html');
-		$sAttCode = utils::ReadParam('sAttCode', '');
-		$iInputId = utils::ReadParam('iInputId', '');
-		$sSuffix = utils::ReadParam('sSuffix', '');
-		$sRemoteClass = utils::ReadParam('sRemoteClass', $sClass, false, 'class');
-		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
-		$sJson = utils::ReadParam('json', '', false, 'raw_data');
-		$oWizardHelper = WizardHelper::FromJSON($sJson);
-		$oObj = $oWizardHelper->GetTargetObject();
-		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
-		if ($sFilter != '')
-		{
-			$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
-		}
-		else
-		{
-			$oFullSetFilter = new DBObjectSearch($sRemoteClass);		
-		}
-		$oWidget->DoAddObjects($oPage, $oFullSetFilter, $oObj);	
-		break;
-			
-		////////////////////////////////////////////////////////////
-		
-		case 'wizard_helper_preview':
-		$oPage->SetContentType('text/html');
-		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
-		$oWizardHelper = WizardHelper::FromJSON($sJson);
-		$oObj = $oWizardHelper->GetTargetObject();
-		$oObj->DisplayBareProperties($oPage); 
-		break;
-		
-		case 'wizard_helper':
-		$oPage->SetContentType('text/html');
-		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
-		$oWizardHelper = WizardHelper::FromJSON($sJson);
-		$oObj = $oWizardHelper->GetTargetObject(); 
-		$sClass = $oWizardHelper->GetTargetClass();
-		foreach($oWizardHelper->GetFieldsForDefaultValue() as $sAttCode)
-		{
-			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			$defaultValue = $oAttDef->GetDefaultValue();
-			$oWizardHelper->SetDefaultValue($sAttCode, $defaultValue);
-			$oObj->Set($sAttCode, $defaultValue);
-		}
-		$sFormPrefix = $oWizardHelper->GetFormPrefix();
-		foreach($oWizardHelper->GetFieldsForAllowedValues() as $sAttCode)
-		{
-			$sId = $oWizardHelper->GetIdForField($sAttCode);
-			if ($sId != '')
-			{
-				if ($oObj->IsNew())
-				{
-					$iFlags = $oObj->GetInitialStateAttributeFlags($sAttCode);
-				}
-				else
-				{
-					$iFlags = $oObj->GetAttributeFlags($sAttCode);
-				}
-				if ($iFlags & OPT_ATT_READONLY)
-				{
-					$sHTMLValue = "<span id=\"field_{$sId}\">".$oObj->GetAsHTML($sAttCode);
-					$sHTMLValue .= '<input type="hidden" id="'.$sId.'" name="attr_'.$sFormPrefix.$sAttCode.'" value="'.htmlentities($oObj->Get($sAttCode), ENT_QUOTES, 'UTF-8').'"/></span>';
-					$oWizardHelper->SetAllowedValuesHtml($sAttCode, $sHTMLValue);
-				}
-				else
-				{
-					// It may happen that the field we'd like to update does not
-					// exist in the form. For example, if the field should be hidden/read-only
-					// in the current state of the object
-					$value = $oObj->Get($sAttCode);
-					$displayValue = $oObj->GetEditValue($sAttCode);
-					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-					if (!$oAttDef->IsWritable())
-					{
-						// Even non-writable fields (like AttributeExternal) can be refreshed 
-						$sHTMLValue = $oObj->GetAsHTML($sAttCode);
-					}
-					else
-					{
-						$iFlags = MetaModel::GetAttributeFlags($sClass, $oObj->GetState(), $sAttCode);
-						$sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value, $displayValue, $sId, '', $iFlags, array('this' => $oObj, 'formPrefix' => $sFormPrefix));
-						// Make sure that we immediately validate the field when we reload it
-						$oPage->add_ready_script("$('#$sId').trigger('validate');");
-					}
-					$oWizardHelper->SetAllowedValuesHtml($sAttCode, $sHTMLValue);
-				}
-			}
-		}
-		$oPage->add_script("oWizardHelper{$sFormPrefix}.m_oData=".$oWizardHelper->ToJSON().";\noWizardHelper{$sFormPrefix}.UpdateFields();\n");
-		break;
-		
-		case 'obj_creation_form':
-		$oPage->SetContentType('text/html');
-		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
-		$oWizardHelper = WizardHelper::FromJSON($sJson);
-		$oObj = $oWizardHelper->GetTargetObject(); 
-		$sClass = $oWizardHelper->GetTargetClass();
-		$sTargetState = utils::ReadParam('target_state', '');
-		$iTransactionId = utils::ReadParam('transaction_id', '');
-		$oObj->Set(MetaModel::GetStateAttributeCode($sClass), $sTargetState);
-		cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, array(), array('action' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php', 'transaction_id' => $iTransactionId)); 
-		break;
-		
-		// DisplayBlock
-		case 'ajax':
-		$oPage->SetContentType('text/html');
-		if ($sFilter != "")
-		{
-			$sExtraParams = stripslashes(utils::ReadParam('extra_params', '', false, 'raw_data'));
-			$aExtraParams = array();
-			if (!empty($sExtraParams))
-			{
-				$aExtraParams = json_decode(str_replace("'", '"', $sExtraParams), true /* associative array */);
-			}
-			// Restore the app context from the ExtraParams
-			$oAppContext = new ApplicationContext(false); // false => don't read the context yet !
-			$aContext = array();
-			foreach($oAppContext->GetNames() as $sName)
-			{
-				$sParamName = 'c['.$sName.']';
-				if (isset($aExtraParams[$sParamName]))
-				{
-					$aContext[$sName] = $aExtraParams[$sParamName];
-				}
-			}
-			$_REQUEST['c'] = $aContext;
-			if ($sEncoding == 'oql')
-			{
-				$oFilter = CMDBSearchFilter::FromOQL($sFilter);
-			}
-			else
-			{
-				$oFilter = CMDBSearchFilter::unserialize($sFilter);
-			}
-			$oDisplayBlock = new DisplayBlock($oFilter, $sStyle, false);
-			$aExtraParams['display_limit'] = true;
-			$aExtraParams['truncated'] = true;
-			$oDisplayBlock->RenderContent($oPage, $aExtraParams);
-		}
-		else
-		{
-			$oPage->p("Invalid query (empty filter).");
-		}
-		break;
-		
-		case 'displayCSVHistory':
-		$oPage->SetContentType('text/html');
-		$bShowAll = (utils::ReadParam('showall', 'false') == 'true');
-		BulkChange::DisplayImportHistory($oPage, true, $bShowAll);
-		break;
-		
-		case 'details':
-		$oPage->SetContentType('text/html');
-		$key = utils::ReadParam('id', 0);
-		$oFilter = new DBObjectSearch($sClass);
-		$oFilter->AddCondition('id', $key, '=');
-		$oDisplayBlock = new DisplayBlock($oFilter, 'details', false);
-		$oDisplayBlock->RenderContent($oPage);
-		break;
-		
-		case 'pie_chart':
-		$oPage->SetContentType('application/json');
-		$sGroupBy = utils::ReadParam('group_by', '');
-		if ($sFilter != '')
-		{
-			if ($sEncoding == 'oql')
-			{
-				$oFilter = CMDBSearchFilter::FromOQL($sFilter);
-			}
-			else
-			{
-				$oFilter = CMDBSearchFilter::unserialize($sFilter);
-			}
-			$oDisplayBlock = new DisplayBlock($oFilter, 'pie_chart_ajax', false);
-			$oDisplayBlock->RenderContent($oPage, array('group_by' => $sGroupBy));
-		}
-		else
-		{
-		
-			$oPage->add("<chart>\n<chart_type>3d pie</chart_type><!-- empty filter '$sFilter' --></chart>\n.");
-		}
-		break;
-		
-		case 'open_flash_chart':
-		// Workaround for IE8 + IIS + HTTPS
-		// See TRAC #363, fix described here: http://forums.codecharge.com/posts.php?post_id=97771
-		$oPage->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT");
-		$oPage->add_header("Cache-Control: cache, must-revalidate");
-		$oPage->add_header("Pragma: public");
-
-		$oPage->SetContentType('application/json');
-		$aParams = utils::ReadParam('params', array(), false, 'raw_data');
-		if ($sFilter != '')
-		{
-			$oFilter = CMDBSearchFilter::unserialize($sFilter);
-			$oDisplayBlock = new DisplayBlock($oFilter, 'open_flash_chart_ajax', false);
-			$oDisplayBlock->RenderContent($oPage, $aParams);
-		}
-		else
-		{
-		
-			$oPage->add("<chart>\n<chart_type>3d pie</chart_type><!-- empty filter '$sFilter' --></chart>\n.");
-		}
-		break;
-	
-		case 'modal_details':
-		$oPage->SetContentType('text/html');
-		$key = utils::ReadParam('id', 0);
-		$oFilter = new DBObjectSearch($sClass);
-		$oFilter->AddCondition('id', $key, '=');
-		$oPage->Add("<p style=\"width:100%; margin-top:-5px;padding:3px; background-color:#33f; color:#fff;\">Object Details</p>\n");
-		$oDisplayBlock = new DisplayBlock($oFilter, 'details', false);
-		$oDisplayBlock->RenderContent($oPage);
-		$oPage->Add("<input type=\"button\" class=\"jqmClose\" value=\" Close \" />\n");
-		break;
-
-		case 'link':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('sclass', 'logInfra', false, 'class');
-		$sAttCode = utils::ReadParam('attCode', 'name');
-		//$sOrg = utils::ReadParam('org_id', '');
-		$sName = utils::ReadParam('q', '');
-		$iMaxCount = utils::ReadParam('max', 30);
-		$iCount = 0;
-		$oFilter = new DBObjectSearch($sClass);
-		$oFilter->AddCondition($sAttCode, $sName, 'Begins with');
-		//$oFilter->AddCondition('org_id', $sOrg, '=');
-		$oSet = new CMDBObjectSet($oFilter, array($sAttCode => true));
-		while( ($iCount < $iMaxCount) && ($oObj = $oSet->fetch()) )
-		{
-			$oPage->add($oObj->GetAsHTML($sAttCode)."|".$oObj->GetKey()."\n");
-			$iCount++;
-		}
-		break;
-	
-		case 'combo_options':
-		$oPage->SetContentType('text/html');
-		$oFilter = CMDBSearchFilter::FromOQL($sFilter);
-		$oSet = new CMDBObjectSet($oFilter);
-		while( $oObj = $oSet->fetch())
-		{
-			$oPage->add('<option title="Here is more information..." value="'.$oObj->GetKey().'">'.$oObj->GetName().'</option>');
-		}
-		break;
-		
-		case 'display_document':
-		$id = utils::ReadParam('id', '');
-		$sField = utils::ReadParam('field', '');
-		if (!empty($sClass) && !empty($id) && !empty($sField))
-		{
-			DownloadDocument($oPage, $sClass, $id, $sField, 'inline');
-		}
-		break;
-		
-		case 'download_document':
-		$id = utils::ReadParam('id', '');
-		$sField = utils::ReadParam('field', '');
-		$iCacheSec = (int) utils::ReadParam('cache', 0);
-		if (!empty($sClass) && !empty($id) && !empty($sField))
-		{
-			DownloadDocument($oPage, $sClass, $id, $sField, 'attachment');
-			if ($iCacheSec > 0)
-			{
-				$oPage->add_header("Expires: "); // Reset the value set in ajax_page
-				$oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec");
-			}
-		}
-		break;
-		
-		case 'search_form':
-		$oPage->SetContentType('text/html');
-		$sClass = utils::ReadParam('className', '', false, 'class');
-		$sRootClass = utils::ReadParam('baseClass', '', false, 'class');
-		$currentId = utils::ReadParam('currentId', '');
-		$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
-		$sAction = utils::ReadParam('action', '');
-		$oFilter = new DBObjectSearch($sClass);
-		$oSet = new CMDBObjectSet($oFilter); 
-		$sHtml = cmdbAbstractObject::GetSearchForm($oPage, $oSet, array('currentId' => $currentId, 'baseClass' => $sRootClass, 'action' => $sAction, 'table_id' => $sTableId));
-		$oPage->add($sHtml);
-		break;
-		
-		case 'set_pref':
-		$sCode = utils::ReadPostedParam('code', '');
-		$sValue = utils::ReadPostedParam('value', '', 'raw_data');
-		appUserPreferences::SetPref($sCode, $sValue);
-		break;
-	
-		case 'erase_all_pref':
-		// Can be useful in case a user got some corrupted prefs...
-		appUserPreferences::ClearPreferences();
-		break;
-
-		case 'on_form_cancel':
-		// Called when a creation/modification form is cancelled by the end-user
-		// Let's take this opportunity to inform the plug-ins so that they can perform some cleanup
-		$iTransactionId = utils::ReadParam('transaction_id', 0);
-		$sTempId = session_id().'_'.$iTransactionId;
-		foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
-		{
-			$oExtensionInstance->OnFormCancel($sTempId);
-		}
-		break;
-
-		case 'reload_dashboard':
-		$oPage->SetContentType('text/html');
-		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
-		$aExtraParams = utils::ReadParam('extra_params', '', false, 'raw_data');
-		ApplicationMenu::LoadAdditionalMenus();
-		$idx = ApplicationMenu::GetMenuIndexById($sDashboardId);
-		$oMenu = ApplicationMenu::GetMenuNode($idx);
-		$oDashboard = $oMenu->GetDashboard();
-		$oDashboard->Render($oPage, false, $aExtraParams);
-		$oPage->add_ready_script("$('.dashboard_contents table.listResults').tableHover(); $('.dashboard_contents table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
-		break;
-		
-		case 'dashboard_editor':
-		$sId = utils::ReadParam('id', '', false, 'raw_data');
-		ApplicationMenu::LoadAdditionalMenus();
-		$idx = ApplicationMenu::GetMenuIndexById($sId);
-		$oMenu = ApplicationMenu::GetMenuNode($idx);
-		$oMenu->RenderEditor($oPage);
-		break;
-		
-		case 'new_dashlet':
-		require_once(APPROOT.'application/forms.class.inc.php');
-		require_once(APPROOT.'application/dashlet.class.inc.php');
-		$sDashletClass = utils::ReadParam('dashlet_class', '');
-		$sDashletId =  utils::ReadParam('dashlet_id', '', false, 'raw_data');
-		if (is_subclass_of($sDashletClass, 'Dashlet'))
-		{
-			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
-			$offset = $oPage->start_capture();
-			$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
-			$sHtml = addslashes($oPage->end_capture($offset));
-			$sHtml = str_replace("\n", '', $sHtml);
-			$sHtml = str_replace("\r", '', $sHtml);
-			$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
-																			// but is executed BEFORE all 'ready_scripts'
-			$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
-			$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
-			$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
-			$sHtml = str_replace("\n", '', $sHtml);
-			$sHtml = str_replace("\r", '', $sHtml);
-			$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'
-		}
-		break;
-			
-		case 'update_dashlet_property':
-		require_once(APPROOT.'application/forms.class.inc.php');
-		require_once(APPROOT.'application/dashlet.class.inc.php');
-		$aParams = utils::ReadParam('params', '', false, 'raw_data');
-		$sDashletClass = $aParams['attr_dashlet_class'];
-		$sDashletId = $aParams['attr_dashlet_id'];
-		$aUpdatedProperties = $aParams['updated']; // Code of the changed properties as an array: 'attr_xxx', 'attr_xxy', etc...
-		$aPreviousValues = $aParams['previous_values']; // hash array: 'attr_xxx' => 'old_value'
-		if (is_subclass_of($sDashletClass, 'Dashlet'))
-		{
-			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
-			$oForm = $oDashlet->GetForm();
-			$aValues = $oForm->ReadParams(); // hash array: 'xxx' => 'new_value'
-			
-			$aCurrentValues = $aValues;
-			$aUpdatedDecoded = array();
-			foreach($aUpdatedProperties as $sProp)
-			{
-				$sDecodedProp = str_replace('attr_', '', $sProp); // Remove the attr_ prefix
-				$aCurrentValues[$sDecodedProp] = $aPreviousValues[$sProp]; // Set the previous value
-				$aUpdatedDecoded[] = $sDecodedProp;
-			}
-			
-			$oDashlet->FromParams($aCurrentValues);
-			$sPrevClass = get_class($oDashlet);
-			$oDashlet = $oDashlet->Update($aValues, $aUpdatedDecoded);
-			$sNewClass = get_class($oDashlet);
-			if ($sNewClass != $sPrevClass)
-			{
-				$oPage->add_ready_script("$('#dashlet_$sDashletId').dashlet('option', {dashlet_class: '$sNewClass'});");
-			}
-			if ($oDashlet->IsRedrawNeeded())
-			{
-				$offset = $oPage->start_capture();
-				$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
-				$sHtml = addslashes($oPage->end_capture($offset));
-				$sHtml = str_replace("\n", '', $sHtml);
-				$sHtml = str_replace("\r", '', $sHtml);
-				
-				$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
-																				// but is executed BEFORE all 'ready_scripts'
-			}
-			if ($oDashlet->IsFormRedrawNeeded())
-			{
-				$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
-				$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
-				$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
-				$sHtml = str_replace("\n", '', $sHtml);
-				$sHtml = str_replace("\r", '', $sHtml);
-				$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'
-																						   // but is executed BEFORE all 'ready_scripts'
-			}
-		}
-		break;
-		
-		case 'save_dashboard':
-		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
-		$aParams = array();
-		$aParams['layout_class'] = utils::ReadParam('layout_class', '');
-		$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
-		$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
-		$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
-		$aParams['cells'] = utils::ReadParam('cells', array(), false, 'raw_data');
-		$oDashboard = new RuntimeDashboard($sDashboardId);
-		$oDashboard->FromParams($aParams);
-		$oDashboard->Save();
-		// trigger a reload of the current page since the dashboard just changed
-		$oPage->add_ready_script(
-<<<EOF
-	var sLocation = new String(window.location.href);
-	var sNewLocation = sLocation.replace('&edit=1', '');
-	sNewLocation = sLocation.replace(/#(.?)$/, ''); // Strips everything after the hash, since IF the URL does not change AND contains a hash, then Chrome does not reload the page
-	window.location.href = sNewLocation;
-EOF
-		);
-		$oPage->add_ready_script("sLocation = new String(window.location.href); window.location.href=sLocation.replace('&edit=1', '');"); // reloads the page, doing a GET even if we arrived via a POST
-		break;
-
-		case 'revert_dashboard':
-		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
-		$oDashboard = new RuntimeDashboard($sDashboardId);
-		$oDashboard->Revert();
-		
-		// trigger a reload of the current page since the dashboard just changed
-		$oPage->add_ready_script("window.location.href=window.location.href;"); // reloads the page, doing a GET even if we arrived via a POST
-		break;
-		
-		case 'render_dashboard':
-		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
-		$aParams = array();
-		$aParams['layout_class'] = utils::ReadParam('layout_class', '');
-		$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
-		$aParams['cells'] = utils::ReadParam('cells', array(), false, 'raw_data');
-		$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
-		$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
-		$oDashboard = new RuntimeDashboard($sDashboardId);
-		$oDashboard->FromParams($aParams);
-		$oDashboard->Render($oPage, true /* bEditMode */);
-		break;
-		
-		case 'dashlet_creation_dlg':
-		$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
-		RuntimeDashboard::GetDashletCreationDlgFromOQL($oPage, $sOQL);
-		break;
-
-		case 'add_dashlet':
-		$oForm = RuntimeDashboard::GetDashletCreationForm();
-		$aValues = $oForm->ReadParams();
-		
-		$sDashletClass = $aValues['dashlet_class'];
-		$sMenuId = $aValues['menu_id'];
-		
-		if (is_subclass_of($sDashletClass, 'Dashlet'))
-		{
-			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
-			$oDashlet->FromParams($aValues);
-
-			ApplicationMenu::LoadAdditionalMenus();
-			$index = ApplicationMenu::GetMenuIndexById($sMenuId);
-			$oMenu = ApplicationMenu::GetMenuNode($index);
-			$oMenu->AddDashlet($oDashlet);
-			// navigate to the dashboard page
-			if ($aValues['open_editor'])
-			{
-				$oPage->add_ready_script("window.location.href='".addslashes(utils::GetAbsoluteUrlAppRoot().'pages/UI.php?c[menu]='.urlencode($sMenuId))."&edit=1';"); // reloads the page, doing a GET even if we arrived via a POST
-			}
-		}
-		break;
-		
-		case 'shortcut_list_dlg':
-		$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
-		$sTableSettings = utils::ReadParam('table_settings', '', false, 'raw_data');
-		ShortcutOQL::GetCreationDlgFromOQL($oPage, $sOQL, $sTableSettings);
-		break;
-		
-		case 'shortcut_list_create':
-		$oForm = ShortcutOQL::GetCreationForm();
-		$aValues = $oForm->ReadParams();
-
-		$oAppContext = new ApplicationContext();
-		$aContext = $oAppContext->GetAsHash();
-		$sContext = serialize($aContext);
-		
-		$oShortcut = MetaModel::NewObject("ShortcutOQL");
-		$oShortcut->Set('user_id', UserRights::GetUserId());
-		$oShortcut->Set("context", $sContext);
-		$oShortcut->Set("name", $aValues['name']);
-		$oShortcut->Set("oql", $aValues['oql']);
-		$iAutoReload = (int)$aValues['auto_reload_sec'];
-		if (($aValues['auto_reload']) && ($iAutoReload > 0))
-		{
-			$oShortcut->Set("auto_reload_sec", max(MetaModel::GetConfig()->Get('min_reload_interval'), $iAutoReload));
-			$oShortcut->Set("auto_reload", 'custom');
-		}
-		$iId = $oShortcut->DBInsertNoReload();
-
-		$oShortcut->CloneTableSettings($aValues['table_settings']);
-
-		// Add the menu node in the right place
-		//
-		// Mmmm... already done because the newly created menu is read from the DB
-		//         as soon as we invoke DisplayMenu 
-
-		// Refresh the menu pane
-		$aExtraParams = array();
-		ApplicationMenu::DisplayMenu($oPage, $aExtraParams);
-		break;
-
-		case 'shortcut_rename_dlg':
-		$oSearch = new DBObjectSearch('Shortcut');
-		$aShortcuts = utils::ReadMultipleSelection($oSearch);
-		$iShortcut = $aShortcuts[0];
-		$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
-		$oShortcut->StartRenameDialog($oPage);
-		break;
-
-		case 'shortcut_rename_go':
-		$iShortcut = utils::ReadParam('id', 0);
-		$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
-
-		$sName = utils::ReadParam('attr_name', '', false, 'raw_data');
-		if (strlen($sName) > 0)
-		{
-			$oShortcut->Set('name', $sName);
-			$oShortcut->DBUpdate();
-			$oPage->add_ready_script('window.location.reload();');
-		}
-		
-		break;
-
-		case 'shortcut_delete_go':
-		$oSearch = new DBObjectSearch('Shortcut');
-		$oSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
-		$aShortcuts = utils::ReadMultipleSelection($oSearch);
-		foreach ($aShortcuts as $iShortcut)
-		{
-			$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
-			$oShortcut->DBDelete();
-			$oPage->add_ready_script('window.location.reload();');
-		}
-		break;
-
-		case 'export_dashboard':
-		$sMenuId = utils::ReadParam('id', '', false, 'raw_data');
-		ApplicationMenu::LoadAdditionalMenus();
-		$index = ApplicationMenu::GetMenuIndexById($sMenuId);
-		$oMenu = ApplicationMenu::GetMenuNode($index);
-		if ($oMenu instanceof DashboardMenuNode)
-		{
-			$oDashboard = $oMenu->GetDashboard();
-
-			$oPage->TrashUnexpectedOutput();
-			$oPage->SetContentType('text/xml');
-			$oPage->SetContentDisposition('attachment', $oMenu->GetLabel().'.xml');
-			$oPage->add($oDashboard->ToXml());
-		}
-		break;
-		
-		case 'import_dashboard':
-		$sMenuId = utils::ReadParam('id', '', false, 'raw_data');
-		ApplicationMenu::LoadAdditionalMenus();
-		$index = ApplicationMenu::GetMenuIndexById($sMenuId);
-		$oMenu = ApplicationMenu::GetMenuNode($index);
-		$aResult = array('error' => '');
-		try
-		{
-			if ($oMenu instanceof DashboardMenuNode)
-			{
-				$oDoc = utils::ReadPostedDocument('dashboard_upload_file');
-				$oDashboard = $oMenu->GetDashboard();
-				$oDashboard->FromXml($oDoc->GetData());
-				$oDashboard->Save();
-			}
-			else
-			{
-				$aResult['error'] = 'Dashboard id="'.$sMenuId.'" not found.';
-			}
-		}
-		catch(DOMException $e)
-		{
-			$aResult = array('error' => Dict::S('UI:Error:InvalidDashboardFile'));
-		}
-		catch(Exception $e)
-		{
-			$aResult = array('error' => $e->getMessage());
-		}
-		$oPage->add(json_encode($aResult));
-		break;
-		
-		case 'about_box':
-		$oPage->SetContentType('text/html');
-
-		$sDialogTitle = addslashes(Dict::S('UI:About:Title'));
-		$oPage->add_ready_script(
-<<<EOF
-$('#about_box').dialog({
-	width: 700,
-	modal: true,
-	title: '$sDialogTitle',
-	close: function() { $(this).remove(); }
-});
-$("#collapse_support_details").click(function() {
-	$("#support_details").slideToggle('normal');
-	$("#collapse_support_details").toggleClass('open');
-});
-$('#support_details').toggle();
-EOF
-		);
-		$sVersionString = Dict::Format('UI:iTopVersion:Long', ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE);
-		$sMySQLVersion = CMDBSource::GetDBVersion();
-		$sPHPVersion = phpversion();
-		$sOSVersion = PHP_OS;
-		$sWebServerVersion = $_SERVER["SERVER_SOFTWARE"];
-		$sModules = implode(', ', get_loaded_extensions());
-
-		// Get the datamodel directory
-		$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation WHERE name="datamodel"');
-		$oSet = new DBObjectSet($oFilter, array('installed' => false)); // Most recent first
-		$oLastInstall = $oSet->Fetch();
-		$sLastInstallDate = $oLastInstall->Get('installed');
-		$sDataModelVersion = $oLastInstall->Get('version');
-		$aDataModelInfo = json_decode($oLastInstall->Get('comment'), true);
-		$sDataModelSourceDir = $aDataModelInfo['source_dir'];
-
-		require_once(APPROOT.'setup/runtimeenv.class.inc.php');
-		$sCurrEnv = utils::GetCurrentEnvironment();
-		$oRuntimeEnv = new RunTimeEnvironment($sCurrEnv);
-		$aSearchDirs = array(APPROOT.$sDataModelSourceDir);
-		if (file_exists(APPROOT.'extensions'))
-		{
-			$aSearchDirs[] = APPROOT.'extensions';
-		}
-		$sExtraDir = APPROOT.'data/'.$sCurrEnv.'-modules/';
-		if (file_exists($sExtraDir))
-		{
-			$aSearchDirs[] = $sExtraDir;
-		}
-		$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation(MetaModel::GetConfig(), $aSearchDirs);
-
-		require_once(APPROOT.'setup/setuputils.class.inc.php');
-		$aLicenses = SetupUtils::GetLicenses();
-
-		$aItopSettings = array('cron_max_execution_time', 'timezone');
-		$aPHPSettings = array('memory_limit', 'max_execution_time', 'upload_max_filesize', 'post_max_size');
-		$aMySQLSettings = array('max_allowed_packet', 'key_buffer_size', 'query_cache_size');
-		$aMySQLStatuses = array('Key_read_requests', 'Key_reads');
-
-		if (extension_loaded('suhosin'))
-		{
-			$aPHPSettings[] = 'suhosin.post.max_vars';
-			$aPHPSettings[] = 'suhosin.get.max_value_length';
-		}
-
-		$aMySQLVars = array();
-		foreach (CMDBSource::QueryToArray('SHOW VARIABLES') as $aRow)
-		{
-			$aMySQLVars[$aRow['Variable_name']] = $aRow['Value'];
-		}
-
-		$aMySQLStats = array();
-		foreach (CMDBSource::QueryToArray('SHOW GLOBAL STATUS') as $aRow)
-		{
-			$aMySQLStats[$aRow['Variable_name']] = $aRow['Value'];
-		}
-
-		// Display
-		//
-		$oPage->add("<div id=\"about_box\">");
-		$oPage->add('<div style="margin-left: 120px;">');
-		$oPage->add('<table>');
-		$oPage->add('<tr>');
-		$oPage->add('<td><a href="http://www.combodo.com" title="www.combodo.com" target="_blank" style="background: none;"><img src="../images/logo-combodo.png" style="float: right;"/></a></td>');
-		$oPage->add('<td style="padding-left: 20px;">');
-		$oPage->add($sVersionString.'<br/>');
-		$oPage->add(Dict::S('UI:About:DataModel').': '.$sDataModelVersion.'<br/>');
-		$oPage->add('MySQL: '.$sMySQLVersion.'<br/>');
-		$oPage->add('PHP: '.$sPHPVersion.'<br/>');
-		$oPage->add('</td>');
-		$oPage->add('</tr>');
-		$oPage->add('</table>');
-		$oPage->add("</div>");
-
-		$oPage->add("<div>");
-		$oPage->add('<fieldset>');
-		$oPage->add('<legend>'.Dict::S('UI:About:Licenses').'</legend>');
-		$oPage->add('<ul style="margin: 0; font-size: smaller;">');
-		foreach($aLicenses as $index => $oLicense)
-		{
-			$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<a id="toggle_'.$index.'" class="CollapsibleLabel" style="cursor:pointer;">Details</a>)');
-			$oPage->add('<div id="license_'.$index.'" class="license_text" style="display:none;overflow:auto;max-height:10em;font-size:small;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;">'.$oLicense->text.'</div>');
-			$oPage->add_ready_script('$("#toggle_'.$index.'").click( function() { $("#license_'.$index.'").slideToggle("normal"); } );');
-		}
-		$oPage->add('</ul>');
-		$oPage->add('</fieldset>');
-		$oPage->add("</div>");
-
-		$oPage->add('<fieldset>');
-		$oPage->add('<legend>'.Dict::S('UI:About:Modules').'</legend>');
-		//$oPage->add(print_r($aAvailableModules, true));
-		$oPage->add("<div style=\"height: 150px; overflow: auto; font-size: smaller;\">");
-		$oPage->add('<ul style="margin: 0;">');
-		foreach ($aAvailableModules as $sModuleId => $aModuleData)
-		{
-			if ($sModuleId == '_Root_') continue;
-			if (!$aModuleData['visible']) continue;
-			if ($aModuleData['version_db'] == '') continue;
-			$oPage->add('<li>'.$aModuleData['label'].' ('.$aModuleData['version_db'].')</li>');
-		}
-		$oPage->add('</ul>');
-		$oPage->add("</div>");
-		$oPage->add('</fieldset>');
-
-
-		// MUST NOT be localized, as the information given here will be sent to the support
-		$oPage->add("<a id=\"collapse_support_details\" class=\"CollapsibleLabel\" href=\"#\">".Dict::S('UI:About:Support')."</a></br>\n");
-		$oPage->add("<div id=\"support_details\">");
-		$oPage->add('<textarea readonly style="width: 660px; height: 150px; font-size: smaller;">');
-		$oPage->add("===== begin =====\n");
-		$oPage->add('iTopVersion: '.ITOP_VERSION."\n");
-		$oPage->add('iTopBuild: '.ITOP_REVISION."\n");
-		$oPage->add('iTopBuildDate: '.ITOP_BUILD_DATE."\n");
-		$oPage->add('DataModelVersion: '.$sDataModelVersion."\n");
-		$oPage->add('MySQLVersion: '.$sMySQLVersion."\n");
-		$oPage->add('PHPVersion: '. $sPHPVersion."\n");
-		$oPage->add('OSVersion: '.$sOSVersion."\n");
-		$oPage->add('WebServerVersion: '.$sWebServerVersion."\n");
-		$oPage->add('PHPModules: '.$sModules."\n");
-		foreach ($aItopSettings as $siTopVar)
-		{
-			$oPage->add('ItopSetting/'.$siTopVar.': '.MetaModel::GetConfig()->Get($siTopVar)."\n");
-		}
-		foreach ($aPHPSettings as $sPHPVar)
-		{
-			$oPage->add('PHPSetting/'.$sPHPVar.': '.ini_get($sPHPVar)."\n");
-		}
-		foreach ($aMySQLSettings as $sMySQLVar)
-		{
-			$oPage->add('MySQLSetting/'.$sMySQLVar.': '.$aMySQLVars[$sMySQLVar]."\n");
-		}
-		foreach ($aMySQLStatuses as $sMySQLStatus)
-		{
-			$oPage->add('MySQLStatus/'.$sMySQLStatus.': '.$aMySQLStats[$sMySQLStatus]."\n");
-		}
-
-		$oPage->add('InstallDate: '.$sLastInstallDate."\n");
-		$oPage->add('InstallPath: '.APPROOT."\n");
-		foreach ($aAvailableModules as $sModuleId => $aModuleData)
-		{
-			if ($sModuleId == '_Root_') continue;
-			if ($aModuleData['version_db'] == '') continue;
-			$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['version_db']."\n");
-		}
-
-		$oPage->add('===== end =====');
-		$oPage->add('</textarea>');
-		$oPage->add("</div>");
-
-		$oPage->add("</div>");
-		break;
-		
-		case 'history':
-		$oPage->SetContentType('text/html');
-		$id = (int)utils::ReadParam('id', 0);
-		$iStart = (int)utils::ReadParam('start', 0);
-		$iCount = (int)utils::ReadParam('count', MetaModel::GetConfig()->Get('max_history_length', '50'));
-		$oObj = MetaModel::GetObject($sClass, $id);
-		$oObj->DisplayBareHistory($oPage, false, $iCount, $iStart);
-		$oPage->add_ready_script("$('#history table.listResults').tableHover(); $('#history table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
-		break;
-
-		case 'history_from_filter':
-		$oPage->SetContentType('text/html');
-		$oHistoryFilter = CMDBSearchFilter::unserialize($sFilter);
-		$iStart = (int)utils::ReadParam('start', 0);
-		$iCount = (int)utils::ReadParam('count', MetaModel::GetConfig()->Get('max_history_length', '50'));
-		$oBlock = new HistoryBlock($oHistoryFilter, 'table', false);
-		$oBlock->SetLimit($iCount, $iStart);
-		$oBlock->Display($oPage, 'history');
-		$oPage->add_ready_script("$('#history table.listResults').tableHover(); $('#history table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
-		break;
-
-		case 'full_text_search':
-		$aFullTextNeedles = utils::ReadParam('needles', array(), false, 'raw_data');
-		$sFullText = trim(implode(' ', $aFullTextNeedles));
-		$sClassName = utils::ReadParam('class', '');
-		$iCount = utils::ReadParam('count', 0);
-		$iCurrentPos = utils::ReadParam('position', 0);
-		$iTune = utils::ReadParam('tune', 0);
-		if (empty($sFullText))
-		{
-			$oPage->p(Dict::S('UI:Search:NoSearch'));
-			break;
-		}
-
-		// Search in full text mode in all the classes
-		$aMatches = array();
-
-		// Build the ordered list of classes to search into
-		//
-		if (empty($sClassName))
-		{
-			$aSearchClasses = MetaModel::GetClasses('searchable');					
-		}
-		else
-		{
-			// Search is limited to a given class and its subclasses
-			$aSearchClasses = MetaModel::EnumChildClasses($sClassName, ENUM_CHILD_CLASSES_ALL);
-		}
-		// Skip abstract classes, since we search in all the child classes anyway
-		foreach($aSearchClasses as $idx => $sClass)
-		{
-			if (MetaModel::IsAbstract($sClass))
-			{
-				unset($aSearchClasses[$idx]);
-			}
-		}
-
-		$sMaxChunkDuration = MetaModel::GetConfig()->Get('full_text_chunk_duration');
-		$aAccelerators = MetaModel::GetConfig()->Get('full_text_accelerators');
-
-		foreach (array_reverse($aAccelerators) as $sClass => $aRestriction)
-		{
-			$bSkip = false;
-			$iPos = array_search($sClass, $aSearchClasses);
-			if ($iPos !== false)
-			{
-				unset($aSearchClasses[$iPos]);
-			}
-			else
-			{
-				$bSkip = true;
-			}
-			$bSkip |= array_key_exists('skip', $aRestriction) ? $aRestriction['skip'] : false ;
-			if (!in_array($sClass, $aSearchClasses))
-			if ($sClass == $sClassName)
-			{
-				// Class explicitely requested, do NOT skip it
-				// beware: there may not be a 'query' defined for a skipped class !
-				$bSkip = false;
-			}
-			if (!$bSkip)
-			{
-				// NOT skipped, add the class to the list of classes to search into
-				if (array_key_exists('query', $aRestriction))
-				{
-					array_unshift($aSearchClasses, $aRestriction['query']);
-				}
-				else
-				{
-					// No accelerator query
-					array_unshift($aSearchClasses, $sClassName);
-				}
-			}
-		}
-
-		$aSearchClasses = array_values($aSearchClasses); // renumbers the array starting from zero, removing the missing indexes
-		$fStarted = microtime(true);
-		$iFoundInThisRound = 0;
-		for($iPos = $iCurrentPos; $iPos < count($aSearchClasses) ; $iPos++)
-		{
-			if ($iFoundInThisRound && (microtime(true) - $fStarted >= $sMaxChunkDuration))
-			{
-				break;
-			}
-
-			$sClassSpec = $aSearchClasses[$iPos];
-			if (substr($sClassSpec, 0, 7) == 'SELECT ')
-			{
-				$oFilter = DBObjectSearch::FromOQL($sClassSpec);
-				$sClassName = $oFilter->GetClass();
-				$sNeedleFormat = isset($aAccelerators[$sClassName]['needle']) ? $aAccelerators[$sClassName]['needle'] : '%$needle$%';
-				$sNeedle = str_replace('$needle$', $sFullText, $sNeedleFormat);
-				$aParams = array('needle' => $sNeedle);
-			}
-			else
-			{
-				$sClassName = $sClassSpec;
-				$oFilter = new DBObjectSearch($sClassName);
-				$aParams = array();
-
-				foreach($aFullTextNeedles as $sSearchText)
-				{
-					$oFilter->AddCondition_FullText($sSearchText);
-				}
-			}
-			// Skip abstract classes
-			if (MetaModel::IsAbstract($sClassName)) continue;
-
-			if ($iTune > 0)
-			{
-				$fStartedClass = microtime(true);
-			}
-			$oSet = new DBObjectSet($oFilter, array(), $aParams);
-			if (array_key_exists($sClassName, $aAccelerators) && array_key_exists('attributes', $aAccelerators[$sClassName]))
-			{
-				$oSet->OptimizeColumnLoad(array($oFilter->GetClassAlias() => $aAccelerators[$sClassName]['attributes']));
-			}
-
-			$sFullTextJS = addslashes($sFullText);
-			$bEnableEnlarge =  array_key_exists($sClassName, $aAccelerators) && array_key_exists('query', $aAccelerators[$sClassName]);
-			if (array_key_exists($sClassName, $aAccelerators) && array_key_exists('enable_enlarge', $aAccelerators[$sClassName]))
-			{
-				$bEnableEnlarge &= $aAccelerators[$sClassName]['enable_enlarge'];
-			}
-			$sEnlargeTheSearch =
-<<<EOF
-			$('.search-class-$sClassName button').attr('disabled', 'disabled');
-
-			$('.search-class-$sClassName h2').append('&nbsp;<img id="indicator" src="../images/indicator.gif">');
-			var oParams = {operation: 'full_text_search_enlarge', class: '$sClassName', text: '$sFullTextJS'};
-			$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
-				$('.search-class-$sClassName').html(data);
-			});
-EOF
-			;
-
-			
-			$sEnlargeButton = '';
-			if ($bEnableEnlarge)
-			{
-				$sEnlargeButton = "&nbsp;<button onclick=\"".htmlentities($sEnlargeTheSearch, ENT_QUOTES, 'UTF-8')."\">".Dict::S('UI:Search:Enlarge')."</button>";
-			}
-			if ($oSet->Count() > 0)
-			{
-				$aLeafs = array();
-				while($oObj = $oSet->Fetch())
-				{
-					if (get_class($oObj) == $sClassName)
-					{
-						$aLeafs[] = $oObj->GetKey();
-						$iFoundInThisRound ++; 
-					}
-				}
-				$oLeafsFilter = new DBObjectSearch($sClassName);
-				if (count($aLeafs) > 0)
-				{
-					$iCount += count($aLeafs);
-					$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
-					$oPage->add("<div class=\"page_header\">\n");
-					if (array_key_exists($sClassName, $aAccelerators))
-					{
-						$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
-					}
-					else
-					{
-						$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName))."</h2>\n");
-					}
-					$oPage->add("</div>\n");
-					$oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
-					$oBlock = new DisplayBlock($oLeafsFilter, 'list', false);
-					$sBlockId = 'global_search_'.$sClassName;
-					$oPage->add('<div id="'.$sBlockId.'">');
-					$oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
-					$oPage->add("</div>\n");
-					$oPage->add("</div>\n");
-					$oPage->p('&nbsp;'); // Some space ?
-				}
-			}
-			else if (array_key_exists($sClassName, $aAccelerators))
-			{
-				$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
-				$oPage->add("<div class=\"page_header\">\n");
-				$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', 0, Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
-				$oPage->add("</div>\n");
-				$oPage->add("</div>\n");
-				$oPage->p('&nbsp;'); // Some space ?
-			}
-			if ($iTune > 0)
-			{
-				$fDurationClass = microtime(true) - $fStartedClass;
-				$oPage->add_script("oTimeStatistics.$sClassName = $fDurationClass;");
-			}
-		}
-		if ($iPos < count($aSearchClasses))
-		{
-			$sJSNeedle = json_encode($aFullTextNeedles);
-			$oPage->add_ready_script(
-<<<EOF
-				var oParams = {operation: 'full_text_search', position: $iPos, needles: $sJSNeedle, count: $iCount, tune: $iTune};
-				$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
-					$('#full_text_results').append(data);
-				});
-EOF
-			);
-		}
-		else
-		{
-			// We're done
-			$oPage->add_ready_script(
-<<<EOF
-$('#full_text_indicator').hide();
-$('#full_text_progress,#full_text_progress_placeholder').hide(500);
-EOF
-			);
-
-			if ($iTune > 0)
-			{
-				$oPage->add_ready_script(
-<<<EOF
-				var sRes = '<h4>Search statistics (tune = 1)</h4><table>';
-				sRes += '<thead><tr><th>Class</th><th>Time</th></tr></thead>';
-				sRes += '<tbody>';
-				var fTotal = 0;
-				for (var sClass in oTimeStatistics)
-				{
-					fTotal = fTotal + oTimeStatistics[sClass];
-					fRounded = Math.round(oTimeStatistics[sClass] * 1000) / 1000;
-					sRes += '<tr><td>' + sClass + '</td><td>' + fRounded + '</td></tr>';
-				}
-				
-				fRoundedTotal = Math.round(fTotal * 1000) / 1000;
-				sRes += '<tr><td><b>Total</b></td><td><b>' + fRoundedTotal + '</b></td></tr>';
-				sRes += '</tbody>';
-				sRes += '</table>';
-				$('#full_text_results').append(sRes);
-EOF
-				);
-			}
-
-			if ($iCount == 0)
-			{
-				$sFullTextSummary = addslashes(Dict::S('UI:Search:NoObjectFound'));
-				$oPage->add_ready_script("$('#full_text_results').append('<div id=\"no_object_found\">$sFullTextSummary</div>');");
-			}
-		}
-		break;
-
-		case 'full_text_search_enlarge':
-		$sFullText = trim(utils::ReadParam('text', '', false, 'raw_data'));
-		$sClass = trim(utils::ReadParam('class', ''));
-		$iTune = utils::ReadParam('tune', 0);
-
-		if (preg_match('/^"(.*)"$/', $sFullText, $aMatches))
-		{
-			// The text is surrounded by double-quotes, remove the quotes and treat it as one single expression
-			$aFullTextNeedles = array($aMatches[1]);
-		}
-		else
-		{
-			// Split the text on the blanks and treat this as a search for <word1> AND <word2> AND <word3>
-			$aFullTextNeedles = explode(' ', $sFullText);
-		}
-
-		$oFilter = new DBObjectSearch($sClass);
-		foreach($aFullTextNeedles as $sSearchText)
-		{
-			$oFilter->AddCondition_FullText($sSearchText);
-		}
-		$oSet = new DBObjectSet($oFilter);
-		$oPage->add("<div class=\"page_header\">\n");
-		$oPage->add("<h2>".MetaModel::GetClassIcon($sClass)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sClass))."</h2>\n");
-		$oPage->add("</div>\n");
-		if ($oSet->Count() > 0)
-		{
-			$aLeafs = array();
-			while($oObj = $oSet->Fetch())
-			{
-				if (get_class($oObj) == $sClass)
-				{
-					$aLeafs[] = $oObj->GetKey();
-				}
-			}
-			$oLeafsFilter = new DBObjectSearch($sClass);
-			if (count($aLeafs) > 0)
-			{
-				$oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
-				$oBlock = new DisplayBlock($oLeafsFilter, 'list', false);
-				$sBlockId = 'global_search_'.$sClass;
-				$oPage->add('<div id="'.$sBlockId.'">');
-				$oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
-				$oPage->add('</div>');
-				$oPage->P('&nbsp;'); // Some space ?
-				// Hide "no object found"
-				$oPage->add_ready_script('$("#no_object_found").hide();');
-			}
-		}
-		$oPage->add_ready_script(
-<<<EOF
-$('#full_text_indicator').hide();
-$('#full_text_progress,#full_text_progress_placeholder').hide(500);
-EOF
-		);
-		break;
-
-		case 'xlsx_export_dialog':
-		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
-		$oPage->SetContentType('text/html');
-		$oPage->add(
-<<<EOF
-<style>
- .ui-progressbar {
-	position: relative;
-}
-.progress-label {
-	position: absolute;
-	left: 50%;
-	top: 1px;
-	font-size: 11pt;
-}
-.download-form button {
-	display:block;
-	margin-left: auto;
-	margin-right: auto;
-	margin-top: 2em;
-}
-.ui-progressbar-value {
-	background: url(../setup/orange-progress.gif);
-}
-.progress-bar {
-	height: 20px;
-}
-.statistics > div {
-	padding-left: 16px;
-	cursor: pointer;
-	font-size: 10pt;
-	background: url(../images/minus.gif) 0 2px no-repeat;
-}				
-.statistics > div.closed {
-	padding-left: 16px;
-	background: url(../images/plus.gif) 0 2px no-repeat;
-}
-				
-.statistics .closed .stats-data {
-	display: none;
-}
-.stats-data td {
-	padding-right: 5px;
-}
-</style>				
-EOF
-		);
-		$oPage->add('<div id="XlsxExportDlg">');
-		$oPage->add('<div class="export-options">');
-		$oPage->add('<p><input type="checkbox" id="export-advanced-mode"/>&nbsp;<label for="export-advanced-mode">'.Dict::S('UI:CSVImport:AdvancedMode').'</label></p>');
-		$oPage->add('<p style="font-size:10pt;margin-left:2em;margin-top:-0.5em;padding-bottom:1em;">'.Dict::S('UI:CSVImport:AdvancedMode+').'</p>');
-		$oPage->add('<p><input type="checkbox" id="export-auto-download" checked="checked"/>&nbsp;<label for="export-auto-download">'.Dict::S('ExcelExport:AutoDownload').'</label></p>');
-		$oPage->add('</div>');
-		$oPage->add('<div class="progress"><p class="status-message">'.Dict::S('ExcelExport:PreparingExport').'</p><div class="progress-bar"><div class="progress-label"></div></div></div>');
-		$oPage->add('<div class="statistics"><div class="stats-toggle closed">'.Dict::S('ExcelExport:Statistics').'<div class="stats-data"></div></div></div>');
-		$oPage->add('</div>');
-		$aLabels = array(
-			'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
-			'cancel_button' => Dict::S('UI:Button:Cancel'),
-			'export_button' => Dict::S('ExcelExporter:ExportButton'),
-			'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
- 		);
-		$sJSLabels = json_encode($aLabels);
-		$sFilter = addslashes($sFilter);
-		$sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
-		$oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
-		break;
-		
-		case 'xlsx_start':
-		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
-		$bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
-		$oSearch = DBObjectSearch::unserialize($sFilter);
-		
-		$oExcelExporter = new ExcelExporter();
-		$oExcelExporter->SetObjectList($oSearch);
-		//$oExcelExporter->SetChunkSize(10); //Only for testing
-		$oExcelExporter->SetAdvancedMode($bAdvanced);
-		$sToken = $oExcelExporter->SaveState();
-		$oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
-		break;
-		
-		case 'xlsx_run':
-		$sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit');
-		ini_set('memory_limit', $sMemoryLimit);
-		ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes
-					
-		$sToken = utils::ReadParam('token', '', false, 'raw_data');
-		$oExcelExporter = new ExcelExporter($sToken);
-		$aStatus = $oExcelExporter->Run();
-		$aResults = array('status' => $aStatus['code'], 'percentage' =>  $aStatus['percentage'], 'message' =>  $aStatus['message']);
-		if ($aStatus['code'] == 'done')
-		{
-			$aResults['statistics'] = $oExcelExporter->GetStatistics('html');
-		}
-		$oPage->add(json_encode($aResults));
-		break;
-		
-		case 'xlsx_download':
-		$sToken = utils::ReadParam('token', '', false, 'raw_data');
-		$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
-		$oPage->SetContentDisposition('attachment', 'export.xlsx');
-		$sFileContent = ExcelExporter::GetExcelFileFromToken($sToken);
-		$oPage->add($sFileContent);
-		ExcelExporter::CleanupFromToken($sToken);
-		break;
-		
-		case 'xlsx_abort':
-		// Stop & cleanup an export...
-		$sToken = utils::ReadParam('token', '', false, 'raw_data');
-		ExcelExporter::CleanupFromToken($sToken);
-		break;
-
-		case 'relation_pdf':
-		case 'relation_attachment':
-		require_once(APPROOT.'core/simplegraph.class.inc.php');
-		require_once(APPROOT.'core/relationgraph.class.inc.php');
-		require_once(APPROOT.'core/displayablegraph.class.inc.php');
-		$sRelation = utils::ReadParam('relation', 'impacts');
-		$sDirection = utils::ReadParam('direction', 'down');
-		
-		$iGroupingThreshold = utils::ReadParam('g', 5, false, 'integer');
-		$sPageFormat = utils::ReadParam('p', 'A4');
-		$sPageOrientation = utils::ReadParam('o', 'L');
-		$sTitle = utils::ReadParam('title', '', false, 'raw_data');
-		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
-		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
-		$bIncludeList = (bool)utils::ReadParam('include_list', false);
-		$sComments = utils::ReadParam('comments', '', false, 'raw_data');
-		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
-		$sContextKey = utils::ReadParam('context_key', '', false, 'raw_data');
-		$aPositions = null;
-		if ($sPositions != null)
-		{
-			$aPositions = json_decode($sPositions, true);
-		}
-		
-		// Get the list of source objects
-		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
-		$aSourceObjects = array();
-		foreach($aSources as $sClass => $aIDs)
-		{
-			$oSearch = new DBObjectSearch($sClass);
-			$oSearch->AddCondition('id', $aIDs, 'IN');
-			$oSet = new DBObjectSet($oSearch);
-			while($oObj = $oSet->Fetch())
-			{
-				$aSourceObjects[] = $oObj;
-			}
-		}
-		
-		// Get the list of excluded objects
-		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
-		$aExcludedObjects = array();
-		foreach($aExcluded as $sClass => $aIDs)
-		{
-			$oSearch = new DBObjectSearch($sClass);
-			$oSearch->AddCondition('id', $aIDs, 'IN');
-			$oSet = new DBObjectSet($oSearch);
-			while($oObj = $oSet->Fetch())
-			{
-				$aExcludedObjects[] = $oObj;
-			}
-		}
-		
-		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
-		if ($sDirection == 'up')
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
-		}
-		else
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
-		}
-		
-		// Remove excluded classes from the graph
-		if (count($aExcludedClasses) > 0)
-		{
-			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
-			foreach($oIterator as $oNode)
-			{
-				$oObj = $oNode->GetProperty('object');
-				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
-				{
-					$oRelGraph->FilterNode($oNode);
-				}
-			}
-		}
-		
-		$oPage = new PDFPage($sTitle, $sPageFormat, $sPageOrientation);
-		
-		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
-		$oGraph->InitFromGraphviz();
-		if ($aPositions != null)
-		{
-			$oGraph->UpdatePositions($aPositions);
-		}
-
-		$aGroups = array();
-		$oIterator = new RelationTypeIterator($oGraph, 'Node');
-		foreach($oIterator as $oNode)
-		{
-			if ($oNode instanceof DisplayableGroupNode)
-			{
-				$aGroups[$oNode->GetProperty('group_index')] = $oNode->GetObjects();
-			}
-		}
-		// First page is the graph
-		$oGraph->RenderAsPDF($oPage, $sComments, $sContextKey);
-
-		if ($bIncludeList)
-		{
-			// Then the lists of objects (one table per finalclass)
-			$aResults = array();
-			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
-			foreach($oIterator as $oNode)
-			{
-				$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
-				if ($oObj)
-				{
-					$sObjClass  = get_class($oObj);
-					if (!array_key_exists($sObjClass, $aResults))
-					{
-						$aResults[$sObjClass] = array();
-					}
-					$aResults[$sObjClass][] = $oObj;
-				}
-			}
-			
-			$oPage->get_tcpdf()->AddPage();
-			$oPage->add('<div class="page_header"><h1>'.Dict::S('UI:RelationshipList').'</h1></div>');
-			$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
-			foreach($aResults as $sListClass => $aObjects)
-			{
-				set_time_limit($iLoopTimeLimit);
-				$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
-				$sHtml = "<div class=\"page_header\">\n";
-				$sHtml .= "<table class=\"section\"><tr><td>".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sListClass))."</td></tr></table>\n";
-				$sHtml .= "</div>\n";
-				$oPage->add($sHtml);
-				cmdbAbstractObject::DisplaySet($oPage, $oSet);
-				$oPage->p(''); // Some space
-			}
-			
-			// Then the content of the groups (one table per group)
-			if (count($aGroups) > 0)
-			{
-				$oPage->get_tcpdf()->AddPage();
-				$oPage->add('<div class="page_header"><h1>'.Dict::S('UI:RelationGroups').'</h1></div>');
-				foreach($aGroups as $idx => $aObjects)
-				{
-					set_time_limit($iLoopTimeLimit);
-					$sListClass = get_class(current($aObjects));
-					$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
-					$sHtml = "<div class=\"page_header\">\n";
-					$sHtml .= "<table class=\"section\"><tr><td>".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."</td></tr></table>\n";
-					$sHtml .= "</div>\n";
-					$oPage->add($sHtml);
-					cmdbAbstractObject::DisplaySet($oPage, $oSet);
-					$oPage->p(''); // Some space
-				}
-			}
-		}
-		if ($operation == 'relation_attachment')
-		{
-			$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
-			$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
-				
-			// Save the generated PDF as an attachment
-			$sPDF = $oPage->get_pdf();
-			$oPage = new ajax_page('');
-			$oAttachment = new Attachment();
-			$oAttachment->Set('item_class', $sObjClass);
-			$oAttachment->Set('item_id', $iObjKey);
-			$oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf');
-			$oAttachment->Set('contents', $oDoc);
-			$iAttachmentId = $oAttachment->DBInsert();
-			$aRet = array(
-				'status' => 'ok',
+<?php
+// Copyright (C) 2010-2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * Handles various ajax requests
+ *
+ * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+require_once('../approot.inc.php');
+require_once(APPROOT.'/application/application.inc.php');
+require_once(APPROOT.'/application/webpage.class.inc.php');
+require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
+require_once(APPROOT.'/application/pdfpage.class.inc.php');
+require_once(APPROOT.'/application/wizardhelper.class.inc.php');
+require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
+require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
+require_once(APPROOT.'/application/datatable.class.inc.php');
+require_once(APPROOT.'/application/excelexporter.class.inc.php');
+
+try
+{
+	require_once(APPROOT.'/application/startup.inc.php');
+	require_once(APPROOT.'/application/user.preferences.class.inc.php');
+	
+	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+	LoginWebPage::DoLoginEx(null /* any portal */, false);
+	
+	$oPage = new ajax_page("");
+	$oPage->no_cache();
+
+	
+	$operation = utils::ReadParam('operation', '');
+	$sFilter = stripslashes(utils::ReadParam('filter', '', false, 'raw_data'));
+	$sEncoding = utils::ReadParam('encoding', 'serialize');
+	$sClass = utils::ReadParam('class', 'MissingAjaxParam', false, 'class');
+	$sStyle = utils::ReadParam('style', 'list');
+
+	switch($operation)
+	{
+		case 'datatable':
+		case 'pagination':
+		$oPage->SetContentType('text/html');
+		$extraParams = utils::ReadParam('extra_param', '', false, 'raw_data');
+		$aExtraParams = array();
+		if (is_array($extraParams))
+		{
+			$aExtraParams = $extraParams;
+		}
+		else
+		{
+			$sExtraParams = stripslashes($extraParams);
+			if (!empty($sExtraParams))
+			{
+				$val = json_decode(str_replace("'", '"', $sExtraParams), true /* associative array */);
+				if ($val !== null)
+				{
+					$aExtraParams = $val;
+				}
+			}
+		}
+		if ($sEncoding == 'oql')
+		{
+			$oFilter = CMDBSearchFilter::FromOQL($sFilter);
+		}
+		else
+		{
+			$oFilter = CMDBSearchFilter::unserialize($sFilter);
+		}
+		$iStart = utils::ReadParam('start',0);
+		$iEnd = utils::ReadParam('end',1);
+		$iSortCol = utils::ReadParam('sort_col','null');
+		$sSelectMode = utils::ReadParam('select_mode', '');
+		if (!empty($sSelectMode) && ($sSelectMode != 'none'))
+		{
+			// The first column is used for the selection (radio / checkbox) and is not sortable
+			$iSortCol--;
+		}
+		$bDisplayKey = utils::ReadParam('display_key', 'true') == 'true';
+		$aColumns = utils::ReadParam('columns', array(), false, 'raw_data');
+		$aClassAliases = utils::ReadParam('class_aliases', array());
+		$iListId = utils::ReadParam('list_id', 0);
+		//$aList = cmdbAbstractObject::FlattenZList(MetaModel::GetZListItems($sClassName, 'list'));
+
+		// Filter the list to removed linked set since we are not able to display them here
+		$aOrderBy = array();
+		$iSortIndex = 0;
+		
+		$aColumnsLoad = array();
+		foreach($aClassAliases as $sAlias => $sClassName)
+		{
+			$aColumnsLoad[$sAlias] = array();
+			foreach($aColumns[$sAlias] as $sAttCode => $aData)
+			{
+				if ($aData['checked'] == 'true')
+				{
+					$aColumns[$sAlias][$sAttCode]['checked'] = true;
+					if ($sAttCode == '_key_')
+					{
+						if ($iSortCol == $iSortIndex)
+						{
+							if (!MetaModel::HasChildrenClasses($oFilter->GetClass()))
+							{
+								$aNameSpec = MetaModel::GetNameSpec($oFilter->GetClass());
+								if ($aNameSpec[0] == '%1$s')
+								{
+									// The name is made of a single column, let's sort according to the sort algorithm for this column
+									$aOrderBy[$sAlias.'.'.$aNameSpec[1][0]] = (utils::ReadParam('sort_order', 'asc') == 'asc');
+								}
+								else
+								{
+									$aOrderBy[$sAlias.'.'.'friendlyname'] = (utils::ReadParam('sort_order', 'asc') == 'asc');
+								}
+							}
+							else
+							{
+								$aOrderBy[$sAlias.'.'.'friendlyname'] = (utils::ReadParam('sort_order', 'asc') == 'asc');
+							}
+						}
+					}
+					else
+					{
+						$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
+						if ($oAttDef instanceof AttributeLinkedSet)
+						{
+							// Removed from the display list
+							unset($aColumns[$sAlias][$sAttCode]);
+						}
+						else
+						{
+							$aColumnsLoad[$sAlias][] = $sAttCode;
+						}
+						if ($iSortCol == $iSortIndex)
+						{
+							if ($oAttDef->IsExternalKey())
+							{
+								$sSortCol = $sAttCode.'_friendlyname';
+							}
+							else
+							{
+								$sSortCol = $sAttCode;
+							}
+							$aOrderBy[$sAlias.'.'.$sSortCol] = (utils::ReadParam('sort_order', 'asc') == 'asc');
+						}
+					}
+					$iSortIndex++;
+				}
+				else
+				{
+					$aColumns[$sAlias][$sAttCode]['checked'] = false;
+				}
+			}
+
+		}
+		
+		// Load only the requested columns
+		$oSet = new DBObjectSet($oFilter, $aOrderBy, $aExtraParams, null, $iEnd-$iStart, $iStart);
+		$oSet->OptimizeColumnLoad($aColumnsLoad);
+
+		$oDataTable = new DataTable($iListId, $oSet, $oSet->GetSelectedClasses());
+		if ($operation == 'datatable')
+		{
+			// Redraw the whole table
+			$sHtml = $oDataTable->UpdatePager($oPage, $iEnd-$iStart, $iStart); // Set the default page size
+			$sHtml .= $oDataTable->GetHTMLTable($oPage, $aColumns, $sSelectMode, $iEnd-$iStart, $bDisplayKey, $aExtraParams);
+		}
+		else
+		{
+			// redraw just the needed rows
+			$sHtml = $oDataTable->GetAsHTMLTableRows($oPage, $iEnd-$iStart, $aColumns, $sSelectMode, $bDisplayKey, $aExtraParams);
+		}
+		$oPage->add($sHtml);
+		break;
+		
+		case 'datatable_save_settings':
+		$oPage->SetContentType('text/plain');
+		$iPageSize = utils::ReadParam('page_size', 10);
+		$sTableId = utils::ReadParam('table_id', null, false, 'raw_data');
+		$bSaveAsDefaults = (utils::ReadParam('defaults', 'true') == 'true');
+		$aClassAliases = utils::ReadParam('class_aliases', array(), false, 'raw_data');
+		$aColumns = utils::ReadParam('columns', array(), false, 'raw_data');
+		
+		foreach($aColumns as $sAlias => $aList)
+		{
+			foreach($aList as $sAttCode => $aData)
+			{
+				$aColumns[$sAlias][$sAttCode]['checked'] = ($aData['checked'] == 'true');
+				$aColumns[$sAlias][$sAttCode]['disabled'] = ($aData['disabled'] == 'true');
+				$aColumns[$sAlias][$sAttCode]['sort'] = ($aData['sort']);
+			}
+		}
+		
+		$oSettings = new DataTableSettings($aClassAliases, $sTableId);
+		$oSettings->iDefaultPageSize = $iPageSize;
+		$oSettings->aColumns = $aColumns;
+
+		if ($bSaveAsDefaults)
+		{
+			if ($sTableId != null)
+			{
+				$oCurrSettings = DataTableSettings::GetTableSettings($aClassAliases, $sTableId, true /* bOnlyTable */ );
+				if ($oCurrSettings)
+				{
+					$oCurrSettings->ResetToDefault(false); // Reset this table to the defaults
+				}
+			}
+			$bRet = $oSettings->SaveAsDefault();
+		}
+		else
+		{
+			$bRet = $oSettings->Save();
+		}
+		$oPage->add($bRet ? 'Ok' : 'KO');
+		break;
+		
+		case 'datatable_reset_settings':
+		$oPage->SetContentType('text/plain');
+		$sTableId = utils::ReadParam('table_id', null, false, 'raw_data');
+		$aClassAliases = utils::ReadParam('class_aliases', array(), false, 'raw_data');
+		$bResetAll = (utils::ReadParam('defaults', 'true') == 'true');
+		
+		$oSettings = new DataTableSettings($aClassAliases, $sTableId);
+		$bRet = $oSettings->ResetToDefault($bResetAll);
+		$oPage->add($bRet ? 'Ok' : 'KO');
+		break;
+		
+		// ui.linkswidget
+		case 'addObjects':
+		$oPage->SetContentType('text/html');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sSuffix = utils::ReadParam('sSuffix', '');
+		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		if (!empty($sJson))
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		else
+		{
+			// Search form: no current object
+			$oObj = null;
+		}
+		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
+		$oWidget->GetObjectPickerDialog($oPage, $oObj);	
+		break;
+		
+		// ui.linkswidget
+		case 'searchObjectsToAdd':
+		$oPage->SetContentType('text/html');
+		$sRemoteClass = utils::ReadParam('sRemoteClass', '', false, 'class');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sSuffix = utils::ReadParam('sSuffix', '');
+		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
+		$aAlreadyLinked = utils::ReadParam('aAlreadyLinked', array());
+		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
+		$oWidget->SearchObjectsToAdd($oPage, $sRemoteClass, $aAlreadyLinked);	
+		break;
+		
+		//ui.linksdirectwidget
+		case 'createObject':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$oPage->SetContentType('text/html');
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->GetObjectCreationDlg($oPage, $sRealClass);
+		break;
+		
+		// ui.linksdirectwidget
+		case 'getLinksetRow':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iTempId = utils::ReadParam('tempId', '');
+		$aValues = utils::ReadParam('values', array(), false, 'raw_data');
+		$oPage->SetContentType('text/html');
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oPage->add($oWidget->GetRow($oPage, $sRealClass, $aValues, -$iTempId));
+		break;
+		
+		// ui.linksdirectwidget
+		case 'selectObjectsToAdd':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$oObj = null;
+		if ($sJson != '')
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
+		$oPage->SetContentType('text/html');
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->GetObjectsSelectionDlg($oPage, $oObj);
+		break;
+			
+		// ui.linksdirectwidget
+		case 'searchObjectsToAdd2':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$aAlreadyLinked =  utils::ReadParam('aAlreadyLinked', array());
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$oObj = null;
+		if ($sJson != '')
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->SearchObjectsToAdd($oPage, $sRealClass, $aAlreadyLinked, $oObj);
+		break;
+		
+		// ui.linksdirectwidget
+		case 'doAddObjects2':
+		$oPage->SetContentType('text/html');
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
+		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+		if ($sFilter != '')
+		{
+			$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
+		}
+		else
+		{
+			$oLinksetDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			$valuesDef = $oLinksetDef->GetValuesDef();				
+			if ($valuesDef === null)
+			{
+				$oFullSetFilter = new DBObjectSearch($oLinksetDef->GetLinkedClass());
+			}
+			else
+			{
+				if (!$valuesDef instanceof ValueSetObjects)
+				{
+					throw new Exception('Error: only ValueSetObjects are supported for "allowed_values" in AttributeLinkedSet ('.$this->sClass.'/'.$this->sAttCode.').');
+				}
+				$oFullSetFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression());
+			}		
+		}
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->DoAddObjects($oPage, $oFullSetFilter);	
+		break;
+		
+		////////////////////////////////////////////////////////////
+		
+		// ui.extkeywidget
+		case 'searchObjectsToSelect':
+		$oPage->SetContentType('text/html');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sRemoteClass = utils::ReadParam('sRemoteClass', '', false, 'class');
+		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
+		if (!empty($sJson))
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		else
+		{
+			// Search form: no current object
+			$oObj = null;
+		}
+		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, $bSearchMode);
+		$oWidget->SearchObjectsToSelect($oPage, $sFilter, $sRemoteClass, $oObj);	
+		break;
+	
+		// ui.extkeywidget: autocomplete
+		case 'ac_extkey':
+		$oPage->SetContentType('text/plain');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$sContains = utils::ReadParam('q', '', false, 'raw_data');
+		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
+		if ($sContains !='')
+		{
+			if (!empty($sJson))
+			{
+				$oWizardHelper = WizardHelper::FromJSON($sJson);
+				$oObj = $oWizardHelper->GetTargetObject();
+			}
+			else
+			{
+				// Search form: no current object
+				$oObj = null;
+			}
+			$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, '', $bSearchMode);
+			$oWidget->AutoComplete($oPage, $sFilter, $oObj, $sContains);
+		}
+		break;
+	
+		// ui.extkeywidget
+		case 'objectSearchForm':
+		$oPage->SetContentType('text/html');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sTitle = utils::ReadParam('sTitle', '', false, 'raw_data');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
+		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, $bSearchMode);
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		if (!empty($sJson))
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		else
+		{
+			// Search form: no current object
+			$oObj = null;
+		}
+		$oWidget->GetSearchDialog($oPage, $sTitle, $oObj);
+		break;
+
+		// ui.extkeywidget
+		case 'objectCreationForm':
+		$oPage->SetContentType('text/html');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		if (!empty($sJson))
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		else
+		{
+			// Search form: no current object
+			$oObj = null;
+		}
+		$oWidget->GetObjectCreationForm($oPage, $oObj);
+		break;
+		
+		// ui.extkeywidget
+		case 'doCreateObject':
+		$oPage->SetContentType('application/json');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sFormPrefix = utils::ReadParam('sFormPrefix', '');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
+		$aResult = $oWidget->DoCreateObject($oPage);
+		echo json_encode($aResult);
+		break;
+		
+		// ui.extkeywidget
+		case 'getObjectName':
+		$oPage->SetContentType('application/json');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iObjectId = utils::ReadParam('iObjectId', '');
+		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
+		$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, '', $bSearchMode);
+		$sName = $oWidget->GetObjectName($iObjectId);
+		echo json_encode(array('name' => $sName));	
+		break;
+		
+		// ui.extkeywidget
+		case 'displayHierarchy':
+		$oPage->SetContentType('text/html');
+		$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
+		$sInputId = utils::ReadParam('sInputId', '');
+		$sFilter = utils::ReadParam('sFilter', '', false, 'raw_data');
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$currValue = utils::ReadParam('value', '');
+		$bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true');
+		if (!empty($sJson))
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		else
+		{
+			// Search form: no current object
+			$oObj = null;
+		}
+		$oWidget = new UIExtKeyWidget($sTargetClass, $sInputId, '', $bSearchMode);
+		$oWidget->DisplayHierarchy($oPage, $sFilter, $currValue, $oObj);
+		break;
+		
+		////////////////////////////////////////////////////
+		
+		// ui.linkswidget
+		case 'doAddObjects':
+		$oPage->SetContentType('text/html');
+		$sAttCode = utils::ReadParam('sAttCode', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$sSuffix = utils::ReadParam('sSuffix', '');
+		$sRemoteClass = utils::ReadParam('sRemoteClass', $sClass, false, 'class');
+		$bDuplicates = (utils::ReadParam('bDuplicates', 'false') == 'false') ? false : true;
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$oWizardHelper = WizardHelper::FromJSON($sJson);
+		$oObj = $oWizardHelper->GetTargetObject();
+		$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
+		if ($sFilter != '')
+		{
+			$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
+		}
+		else
+		{
+			$oFullSetFilter = new DBObjectSearch($sRemoteClass);		
+		}
+		$oWidget->DoAddObjects($oPage, $oFullSetFilter, $oObj);	
+		break;
+			
+		////////////////////////////////////////////////////////////
+		
+		case 'wizard_helper_preview':
+		$oPage->SetContentType('text/html');
+		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
+		$oWizardHelper = WizardHelper::FromJSON($sJson);
+		$oObj = $oWizardHelper->GetTargetObject();
+		$oObj->DisplayBareProperties($oPage); 
+		break;
+		
+		case 'wizard_helper':
+		$oPage->SetContentType('text/html');
+		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
+		$oWizardHelper = WizardHelper::FromJSON($sJson);
+		$oObj = $oWizardHelper->GetTargetObject(); 
+		$sClass = $oWizardHelper->GetTargetClass();
+		foreach($oWizardHelper->GetFieldsForDefaultValue() as $sAttCode)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			$defaultValue = $oAttDef->GetDefaultValue();
+			$oWizardHelper->SetDefaultValue($sAttCode, $defaultValue);
+			$oObj->Set($sAttCode, $defaultValue);
+		}
+		$sFormPrefix = $oWizardHelper->GetFormPrefix();
+		foreach($oWizardHelper->GetFieldsForAllowedValues() as $sAttCode)
+		{
+			$sId = $oWizardHelper->GetIdForField($sAttCode);
+			if ($sId != '')
+			{
+				if ($oObj->IsNew())
+				{
+					$iFlags = $oObj->GetInitialStateAttributeFlags($sAttCode);
+				}
+				else
+				{
+					$iFlags = $oObj->GetAttributeFlags($sAttCode);
+				}
+				if ($iFlags & OPT_ATT_READONLY)
+				{
+					$sHTMLValue = "<span id=\"field_{$sId}\">".$oObj->GetAsHTML($sAttCode);
+					$sHTMLValue .= '<input type="hidden" id="'.$sId.'" name="attr_'.$sFormPrefix.$sAttCode.'" value="'.htmlentities($oObj->Get($sAttCode), ENT_QUOTES, 'UTF-8').'"/></span>';
+					$oWizardHelper->SetAllowedValuesHtml($sAttCode, $sHTMLValue);
+				}
+				else
+				{
+					// It may happen that the field we'd like to update does not
+					// exist in the form. For example, if the field should be hidden/read-only
+					// in the current state of the object
+					$value = $oObj->Get($sAttCode);
+					$displayValue = $oObj->GetEditValue($sAttCode);
+					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+					if (!$oAttDef->IsWritable())
+					{
+						// Even non-writable fields (like AttributeExternal) can be refreshed 
+						$sHTMLValue = $oObj->GetAsHTML($sAttCode);
+					}
+					else
+					{
+						$iFlags = MetaModel::GetAttributeFlags($sClass, $oObj->GetState(), $sAttCode);
+						$sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value, $displayValue, $sId, '', $iFlags, array('this' => $oObj, 'formPrefix' => $sFormPrefix));
+						// Make sure that we immediately validate the field when we reload it
+						$oPage->add_ready_script("$('#$sId').trigger('validate');");
+					}
+					$oWizardHelper->SetAllowedValuesHtml($sAttCode, $sHTMLValue);
+				}
+			}
+		}
+		$oPage->add_script("oWizardHelper{$sFormPrefix}.m_oData=".$oWizardHelper->ToJSON().";\noWizardHelper{$sFormPrefix}.UpdateFields();\n");
+		break;
+		
+		case 'obj_creation_form':
+		$oPage->SetContentType('text/html');
+		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
+		$oWizardHelper = WizardHelper::FromJSON($sJson);
+		$oObj = $oWizardHelper->GetTargetObject(); 
+		$sClass = $oWizardHelper->GetTargetClass();
+		$sTargetState = utils::ReadParam('target_state', '');
+		$iTransactionId = utils::ReadParam('transaction_id', '');
+		$oObj->Set(MetaModel::GetStateAttributeCode($sClass), $sTargetState);
+		cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, array(), array('action' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php', 'transaction_id' => $iTransactionId)); 
+		break;
+		
+		// DisplayBlock
+		case 'ajax':
+		$oPage->SetContentType('text/html');
+		if ($sFilter != "")
+		{
+			$sExtraParams = stripslashes(utils::ReadParam('extra_params', '', false, 'raw_data'));
+			$aExtraParams = array();
+			if (!empty($sExtraParams))
+			{
+				$aExtraParams = json_decode(str_replace("'", '"', $sExtraParams), true /* associative array */);
+			}
+			// Restore the app context from the ExtraParams
+			$oAppContext = new ApplicationContext(false); // false => don't read the context yet !
+			$aContext = array();
+			foreach($oAppContext->GetNames() as $sName)
+			{
+				$sParamName = 'c['.$sName.']';
+				if (isset($aExtraParams[$sParamName]))
+				{
+					$aContext[$sName] = $aExtraParams[$sParamName];
+				}
+			}
+			$_REQUEST['c'] = $aContext;
+			if ($sEncoding == 'oql')
+			{
+				$oFilter = CMDBSearchFilter::FromOQL($sFilter);
+			}
+			else
+			{
+				$oFilter = CMDBSearchFilter::unserialize($sFilter);
+			}
+			$oDisplayBlock = new DisplayBlock($oFilter, $sStyle, false);
+			$aExtraParams['display_limit'] = true;
+			$aExtraParams['truncated'] = true;
+			$oDisplayBlock->RenderContent($oPage, $aExtraParams);
+		}
+		else
+		{
+			$oPage->p("Invalid query (empty filter).");
+		}
+		break;
+		
+		case 'displayCSVHistory':
+		$oPage->SetContentType('text/html');
+		$bShowAll = (utils::ReadParam('showall', 'false') == 'true');
+		BulkChange::DisplayImportHistory($oPage, true, $bShowAll);
+		break;
+		
+		case 'details':
+		$oPage->SetContentType('text/html');
+		$key = utils::ReadParam('id', 0);
+		$oFilter = new DBObjectSearch($sClass);
+		$oFilter->AddCondition('id', $key, '=');
+		$oDisplayBlock = new DisplayBlock($oFilter, 'details', false);
+		$oDisplayBlock->RenderContent($oPage);
+		break;
+		
+		case 'pie_chart':
+		$oPage->SetContentType('application/json');
+		$sGroupBy = utils::ReadParam('group_by', '');
+		if ($sFilter != '')
+		{
+			if ($sEncoding == 'oql')
+			{
+				$oFilter = CMDBSearchFilter::FromOQL($sFilter);
+			}
+			else
+			{
+				$oFilter = CMDBSearchFilter::unserialize($sFilter);
+			}
+			$oDisplayBlock = new DisplayBlock($oFilter, 'pie_chart_ajax', false);
+			$oDisplayBlock->RenderContent($oPage, array('group_by' => $sGroupBy));
+		}
+		else
+		{
+		
+			$oPage->add("<chart>\n<chart_type>3d pie</chart_type><!-- empty filter '$sFilter' --></chart>\n.");
+		}
+		break;
+		
+		case 'open_flash_chart':
+		// Workaround for IE8 + IIS + HTTPS
+		// See TRAC #363, fix described here: http://forums.codecharge.com/posts.php?post_id=97771
+		$oPage->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT");
+		$oPage->add_header("Cache-Control: cache, must-revalidate");
+		$oPage->add_header("Pragma: public");
+
+		$oPage->SetContentType('application/json');
+		$aParams = utils::ReadParam('params', array(), false, 'raw_data');
+		if ($sFilter != '')
+		{
+			$oFilter = CMDBSearchFilter::unserialize($sFilter);
+			$oDisplayBlock = new DisplayBlock($oFilter, 'open_flash_chart_ajax', false);
+			$oDisplayBlock->RenderContent($oPage, $aParams);
+		}
+		else
+		{
+		
+			$oPage->add("<chart>\n<chart_type>3d pie</chart_type><!-- empty filter '$sFilter' --></chart>\n.");
+		}
+		break;
+	
+		case 'modal_details':
+		$oPage->SetContentType('text/html');
+		$key = utils::ReadParam('id', 0);
+		$oFilter = new DBObjectSearch($sClass);
+		$oFilter->AddCondition('id', $key, '=');
+		$oPage->Add("<p style=\"width:100%; margin-top:-5px;padding:3px; background-color:#33f; color:#fff;\">Object Details</p>\n");
+		$oDisplayBlock = new DisplayBlock($oFilter, 'details', false);
+		$oDisplayBlock->RenderContent($oPage);
+		$oPage->Add("<input type=\"button\" class=\"jqmClose\" value=\" Close \" />\n");
+		break;
+
+		case 'link':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('sclass', 'logInfra', false, 'class');
+		$sAttCode = utils::ReadParam('attCode', 'name');
+		//$sOrg = utils::ReadParam('org_id', '');
+		$sName = utils::ReadParam('q', '');
+		$iMaxCount = utils::ReadParam('max', 30);
+		$iCount = 0;
+		$oFilter = new DBObjectSearch($sClass);
+		$oFilter->AddCondition($sAttCode, $sName, 'Begins with');
+		//$oFilter->AddCondition('org_id', $sOrg, '=');
+		$oSet = new CMDBObjectSet($oFilter, array($sAttCode => true));
+		while( ($iCount < $iMaxCount) && ($oObj = $oSet->fetch()) )
+		{
+			$oPage->add($oObj->GetAsHTML($sAttCode)."|".$oObj->GetKey()."\n");
+			$iCount++;
+		}
+		break;
+	
+		case 'combo_options':
+		$oPage->SetContentType('text/html');
+		$oFilter = CMDBSearchFilter::FromOQL($sFilter);
+		$oSet = new CMDBObjectSet($oFilter);
+		while( $oObj = $oSet->fetch())
+		{
+			$oPage->add('<option title="Here is more information..." value="'.$oObj->GetKey().'">'.$oObj->GetName().'</option>');
+		}
+		break;
+		
+		case 'display_document':
+		$id = utils::ReadParam('id', '');
+		$sField = utils::ReadParam('field', '');
+		if (!empty($sClass) && !empty($id) && !empty($sField))
+		{
+			DownloadDocument($oPage, $sClass, $id, $sField, 'inline');
+		}
+		break;
+		
+		case 'download_document':
+		$id = utils::ReadParam('id', '');
+		$sField = utils::ReadParam('field', '');
+		$iCacheSec = (int) utils::ReadParam('cache', 0);
+		if (!empty($sClass) && !empty($id) && !empty($sField))
+		{
+			DownloadDocument($oPage, $sClass, $id, $sField, 'attachment');
+			if ($iCacheSec > 0)
+			{
+				$oPage->add_header("Expires: "); // Reset the value set in ajax_page
+				$oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec");
+			}
+		}
+		break;
+		
+		case 'search_form':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('className', '', false, 'class');
+		$sRootClass = utils::ReadParam('baseClass', '', false, 'class');
+		$currentId = utils::ReadParam('currentId', '');
+		$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
+		$sAction = utils::ReadParam('action', '');
+		$oFilter = new DBObjectSearch($sClass);
+		$oSet = new CMDBObjectSet($oFilter); 
+		$sHtml = cmdbAbstractObject::GetSearchForm($oPage, $oSet, array('currentId' => $currentId, 'baseClass' => $sRootClass, 'action' => $sAction, 'table_id' => $sTableId));
+		$oPage->add($sHtml);
+		break;
+		
+		case 'set_pref':
+		$sCode = utils::ReadPostedParam('code', '');
+		$sValue = utils::ReadPostedParam('value', '', 'raw_data');
+		appUserPreferences::SetPref($sCode, $sValue);
+		break;
+	
+		case 'erase_all_pref':
+		// Can be useful in case a user got some corrupted prefs...
+		appUserPreferences::ClearPreferences();
+		break;
+
+		case 'on_form_cancel':
+		// Called when a creation/modification form is cancelled by the end-user
+		// Let's take this opportunity to inform the plug-ins so that they can perform some cleanup
+		$iTransactionId = utils::ReadParam('transaction_id', 0);
+		$sTempId = session_id().'_'.$iTransactionId;
+		foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnFormCancel($sTempId);
+		}
+		break;
+
+		case 'reload_dashboard':
+		$oPage->SetContentType('text/html');
+		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
+		$aExtraParams = utils::ReadParam('extra_params', '', false, 'raw_data');
+		ApplicationMenu::LoadAdditionalMenus();
+		$idx = ApplicationMenu::GetMenuIndexById($sDashboardId);
+		$oMenu = ApplicationMenu::GetMenuNode($idx);
+		$oDashboard = $oMenu->GetDashboard();
+		$oDashboard->Render($oPage, false, $aExtraParams);
+		$oPage->add_ready_script("$('.dashboard_contents table.listResults').tableHover(); $('.dashboard_contents table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
+		break;
+		
+		case 'dashboard_editor':
+		$sId = utils::ReadParam('id', '', false, 'raw_data');
+		ApplicationMenu::LoadAdditionalMenus();
+		$idx = ApplicationMenu::GetMenuIndexById($sId);
+		$oMenu = ApplicationMenu::GetMenuNode($idx);
+		$oMenu->RenderEditor($oPage);
+		break;
+		
+		case 'new_dashlet':
+		require_once(APPROOT.'application/forms.class.inc.php');
+		require_once(APPROOT.'application/dashlet.class.inc.php');
+		$sDashletClass = utils::ReadParam('dashlet_class', '');
+		$sDashletId =  utils::ReadParam('dashlet_id', '', false, 'raw_data');
+		if (is_subclass_of($sDashletClass, 'Dashlet'))
+		{
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
+			$offset = $oPage->start_capture();
+			$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
+			$sHtml = addslashes($oPage->end_capture($offset));
+			$sHtml = str_replace("\n", '', $sHtml);
+			$sHtml = str_replace("\r", '', $sHtml);
+			$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
+																			// but is executed BEFORE all 'ready_scripts'
+			$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
+			$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
+			$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
+			$sHtml = str_replace("\n", '', $sHtml);
+			$sHtml = str_replace("\r", '', $sHtml);
+			$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'
+		}
+		break;
+			
+		case 'update_dashlet_property':
+		require_once(APPROOT.'application/forms.class.inc.php');
+		require_once(APPROOT.'application/dashlet.class.inc.php');
+		$aParams = utils::ReadParam('params', '', false, 'raw_data');
+		$sDashletClass = $aParams['attr_dashlet_class'];
+		$sDashletId = $aParams['attr_dashlet_id'];
+		$aUpdatedProperties = $aParams['updated']; // Code of the changed properties as an array: 'attr_xxx', 'attr_xxy', etc...
+		$aPreviousValues = $aParams['previous_values']; // hash array: 'attr_xxx' => 'old_value'
+		if (is_subclass_of($sDashletClass, 'Dashlet'))
+		{
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
+			$oForm = $oDashlet->GetForm();
+			$aValues = $oForm->ReadParams(); // hash array: 'xxx' => 'new_value'
+			
+			$aCurrentValues = $aValues;
+			$aUpdatedDecoded = array();
+			foreach($aUpdatedProperties as $sProp)
+			{
+				$sDecodedProp = str_replace('attr_', '', $sProp); // Remove the attr_ prefix
+				$aCurrentValues[$sDecodedProp] = $aPreviousValues[$sProp]; // Set the previous value
+				$aUpdatedDecoded[] = $sDecodedProp;
+			}
+			
+			$oDashlet->FromParams($aCurrentValues);
+			$sPrevClass = get_class($oDashlet);
+			$oDashlet = $oDashlet->Update($aValues, $aUpdatedDecoded);
+			$sNewClass = get_class($oDashlet);
+			if ($sNewClass != $sPrevClass)
+			{
+				$oPage->add_ready_script("$('#dashlet_$sDashletId').dashlet('option', {dashlet_class: '$sNewClass'});");
+			}
+			if ($oDashlet->IsRedrawNeeded())
+			{
+				$offset = $oPage->start_capture();
+				$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
+				$sHtml = addslashes($oPage->end_capture($offset));
+				$sHtml = str_replace("\n", '', $sHtml);
+				$sHtml = str_replace("\r", '', $sHtml);
+				
+				$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
+																				// but is executed BEFORE all 'ready_scripts'
+			}
+			if ($oDashlet->IsFormRedrawNeeded())
+			{
+				$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
+				$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
+				$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
+				$sHtml = str_replace("\n", '', $sHtml);
+				$sHtml = str_replace("\r", '', $sHtml);
+				$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'
+																						   // but is executed BEFORE all 'ready_scripts'
+			}
+		}
+		break;
+		
+		case 'save_dashboard':
+		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
+		$aParams = array();
+		$aParams['layout_class'] = utils::ReadParam('layout_class', '');
+		$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
+		$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
+		$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
+		$aParams['cells'] = utils::ReadParam('cells', array(), false, 'raw_data');
+		$oDashboard = new RuntimeDashboard($sDashboardId);
+		$oDashboard->FromParams($aParams);
+		$oDashboard->Save();
+		// trigger a reload of the current page since the dashboard just changed
+		$oPage->add_ready_script(
+<<<EOF
+	var sLocation = new String(window.location.href);
+	var sNewLocation = sLocation.replace('&edit=1', '');
+	sNewLocation = sLocation.replace(/#(.?)$/, ''); // Strips everything after the hash, since IF the URL does not change AND contains a hash, then Chrome does not reload the page
+	window.location.href = sNewLocation;
+EOF
+		);
+		$oPage->add_ready_script("sLocation = new String(window.location.href); window.location.href=sLocation.replace('&edit=1', '');"); // reloads the page, doing a GET even if we arrived via a POST
+		break;
+
+		case 'revert_dashboard':
+		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
+		$oDashboard = new RuntimeDashboard($sDashboardId);
+		$oDashboard->Revert();
+		
+		// trigger a reload of the current page since the dashboard just changed
+		$oPage->add_ready_script("window.location.href=window.location.href;"); // reloads the page, doing a GET even if we arrived via a POST
+		break;
+		
+		case 'render_dashboard':
+		$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
+		$aParams = array();
+		$aParams['layout_class'] = utils::ReadParam('layout_class', '');
+		$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
+		$aParams['cells'] = utils::ReadParam('cells', array(), false, 'raw_data');
+		$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
+		$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
+		$oDashboard = new RuntimeDashboard($sDashboardId);
+		$oDashboard->FromParams($aParams);
+		$oDashboard->Render($oPage, true /* bEditMode */);
+		break;
+		
+		case 'dashlet_creation_dlg':
+		$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
+		RuntimeDashboard::GetDashletCreationDlgFromOQL($oPage, $sOQL);
+		break;
+
+		case 'add_dashlet':
+		$oForm = RuntimeDashboard::GetDashletCreationForm();
+		$aValues = $oForm->ReadParams();
+		
+		$sDashletClass = $aValues['dashlet_class'];
+		$sMenuId = $aValues['menu_id'];
+		
+		if (is_subclass_of($sDashletClass, 'Dashlet'))
+		{
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
+			$oDashlet->FromParams($aValues);
+
+			ApplicationMenu::LoadAdditionalMenus();
+			$index = ApplicationMenu::GetMenuIndexById($sMenuId);
+			$oMenu = ApplicationMenu::GetMenuNode($index);
+			$oMenu->AddDashlet($oDashlet);
+			// navigate to the dashboard page
+			if ($aValues['open_editor'])
+			{
+				$oPage->add_ready_script("window.location.href='".addslashes(utils::GetAbsoluteUrlAppRoot().'pages/UI.php?c[menu]='.urlencode($sMenuId))."&edit=1';"); // reloads the page, doing a GET even if we arrived via a POST
+			}
+		}
+		break;
+		
+		case 'shortcut_list_dlg':
+		$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
+		$sTableSettings = utils::ReadParam('table_settings', '', false, 'raw_data');
+		ShortcutOQL::GetCreationDlgFromOQL($oPage, $sOQL, $sTableSettings);
+		break;
+		
+		case 'shortcut_list_create':
+		$oForm = ShortcutOQL::GetCreationForm();
+		$aValues = $oForm->ReadParams();
+
+		$oAppContext = new ApplicationContext();
+		$aContext = $oAppContext->GetAsHash();
+		$sContext = serialize($aContext);
+		
+		$oShortcut = MetaModel::NewObject("ShortcutOQL");
+		$oShortcut->Set('user_id', UserRights::GetUserId());
+		$oShortcut->Set("context", $sContext);
+		$oShortcut->Set("name", $aValues['name']);
+		$oShortcut->Set("oql", $aValues['oql']);
+		$iAutoReload = (int)$aValues['auto_reload_sec'];
+		if (($aValues['auto_reload']) && ($iAutoReload > 0))
+		{
+			$oShortcut->Set("auto_reload_sec", max(MetaModel::GetConfig()->Get('min_reload_interval'), $iAutoReload));
+			$oShortcut->Set("auto_reload", 'custom');
+		}
+		$iId = $oShortcut->DBInsertNoReload();
+
+		$oShortcut->CloneTableSettings($aValues['table_settings']);
+
+		// Add the menu node in the right place
+		//
+		// Mmmm... already done because the newly created menu is read from the DB
+		//         as soon as we invoke DisplayMenu 
+
+		// Refresh the menu pane
+		$aExtraParams = array();
+		ApplicationMenu::DisplayMenu($oPage, $aExtraParams);
+		break;
+
+		case 'shortcut_rename_dlg':
+		$oSearch = new DBObjectSearch('Shortcut');
+		$aShortcuts = utils::ReadMultipleSelection($oSearch);
+		$iShortcut = $aShortcuts[0];
+		$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
+		$oShortcut->StartRenameDialog($oPage);
+		break;
+
+		case 'shortcut_rename_go':
+		$iShortcut = utils::ReadParam('id', 0);
+		$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
+
+		$sName = utils::ReadParam('attr_name', '', false, 'raw_data');
+		if (strlen($sName) > 0)
+		{
+			$oShortcut->Set('name', $sName);
+			$oShortcut->DBUpdate();
+			$oPage->add_ready_script('window.location.reload();');
+		}
+		
+		break;
+
+		case 'shortcut_delete_go':
+		$oSearch = new DBObjectSearch('Shortcut');
+		$oSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
+		$aShortcuts = utils::ReadMultipleSelection($oSearch);
+		foreach ($aShortcuts as $iShortcut)
+		{
+			$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
+			$oShortcut->DBDelete();
+			$oPage->add_ready_script('window.location.reload();');
+		}
+		break;
+
+		case 'export_dashboard':
+		$sMenuId = utils::ReadParam('id', '', false, 'raw_data');
+		ApplicationMenu::LoadAdditionalMenus();
+		$index = ApplicationMenu::GetMenuIndexById($sMenuId);
+		$oMenu = ApplicationMenu::GetMenuNode($index);
+		if ($oMenu instanceof DashboardMenuNode)
+		{
+			$oDashboard = $oMenu->GetDashboard();
+
+			$oPage->TrashUnexpectedOutput();
+			$oPage->SetContentType('text/xml');
+			$oPage->SetContentDisposition('attachment', $oMenu->GetLabel().'.xml');
+			$oPage->add($oDashboard->ToXml());
+		}
+		break;
+		
+		case 'import_dashboard':
+		$sMenuId = utils::ReadParam('id', '', false, 'raw_data');
+		ApplicationMenu::LoadAdditionalMenus();
+		$index = ApplicationMenu::GetMenuIndexById($sMenuId);
+		$oMenu = ApplicationMenu::GetMenuNode($index);
+		$aResult = array('error' => '');
+		try
+		{
+			if ($oMenu instanceof DashboardMenuNode)
+			{
+				$oDoc = utils::ReadPostedDocument('dashboard_upload_file');
+				$oDashboard = $oMenu->GetDashboard();
+				$oDashboard->FromXml($oDoc->GetData());
+				$oDashboard->Save();
+			}
+			else
+			{
+				$aResult['error'] = 'Dashboard id="'.$sMenuId.'" not found.';
+			}
+		}
+		catch(DOMException $e)
+		{
+			$aResult = array('error' => Dict::S('UI:Error:InvalidDashboardFile'));
+		}
+		catch(Exception $e)
+		{
+			$aResult = array('error' => $e->getMessage());
+		}
+		$oPage->add(json_encode($aResult));
+		break;
+		
+		case 'about_box':
+		$oPage->SetContentType('text/html');
+
+		$sDialogTitle = addslashes(Dict::S('UI:About:Title'));
+		$oPage->add_ready_script(
+<<<EOF
+$('#about_box').dialog({
+	width: 700,
+	modal: true,
+	title: '$sDialogTitle',
+	close: function() { $(this).remove(); }
+});
+$("#collapse_support_details").click(function() {
+	$("#support_details").slideToggle('normal');
+	$("#collapse_support_details").toggleClass('open');
+});
+$('#support_details').toggle();
+EOF
+		);
+		$sVersionString = Dict::Format('UI:iTopVersion:Long', ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE);
+		$sMySQLVersion = CMDBSource::GetDBVersion();
+		$sPHPVersion = phpversion();
+		$sOSVersion = PHP_OS;
+		$sWebServerVersion = $_SERVER["SERVER_SOFTWARE"];
+		$sModules = implode(', ', get_loaded_extensions());
+
+		// Get the datamodel directory
+		$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation WHERE name="datamodel"');
+		$oSet = new DBObjectSet($oFilter, array('installed' => false)); // Most recent first
+		$oLastInstall = $oSet->Fetch();
+		$sLastInstallDate = $oLastInstall->Get('installed');
+		$sDataModelVersion = $oLastInstall->Get('version');
+		$aDataModelInfo = json_decode($oLastInstall->Get('comment'), true);
+		$sDataModelSourceDir = $aDataModelInfo['source_dir'];
+
+		require_once(APPROOT.'setup/runtimeenv.class.inc.php');
+		$sCurrEnv = utils::GetCurrentEnvironment();
+		$oRuntimeEnv = new RunTimeEnvironment($sCurrEnv);
+		$aSearchDirs = array(APPROOT.$sDataModelSourceDir);
+		if (file_exists(APPROOT.'extensions'))
+		{
+			$aSearchDirs[] = APPROOT.'extensions';
+		}
+		$sExtraDir = APPROOT.'data/'.$sCurrEnv.'-modules/';
+		if (file_exists($sExtraDir))
+		{
+			$aSearchDirs[] = $sExtraDir;
+		}
+		$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation(MetaModel::GetConfig(), $aSearchDirs);
+
+		require_once(APPROOT.'setup/setuputils.class.inc.php');
+		$aLicenses = SetupUtils::GetLicenses();
+
+		$aItopSettings = array('cron_max_execution_time', 'timezone');
+		$aPHPSettings = array('memory_limit', 'max_execution_time', 'upload_max_filesize', 'post_max_size');
+		$aMySQLSettings = array('max_allowed_packet', 'key_buffer_size', 'query_cache_size');
+		$aMySQLStatuses = array('Key_read_requests', 'Key_reads');
+
+		if (extension_loaded('suhosin'))
+		{
+			$aPHPSettings[] = 'suhosin.post.max_vars';
+			$aPHPSettings[] = 'suhosin.get.max_value_length';
+		}
+
+		$aMySQLVars = array();
+		foreach (CMDBSource::QueryToArray('SHOW VARIABLES') as $aRow)
+		{
+			$aMySQLVars[$aRow['Variable_name']] = $aRow['Value'];
+		}
+
+		$aMySQLStats = array();
+		foreach (CMDBSource::QueryToArray('SHOW GLOBAL STATUS') as $aRow)
+		{
+			$aMySQLStats[$aRow['Variable_name']] = $aRow['Value'];
+		}
+
+		// Display
+		//
+		$oPage->add("<div id=\"about_box\">");
+		$oPage->add('<div style="margin-left: 120px;">');
+		$oPage->add('<table>');
+		$oPage->add('<tr>');
+		$oPage->add('<td><a href="http://www.combodo.com" title="www.combodo.com" target="_blank" style="background: none;"><img src="../images/logo-combodo.png" style="float: right;"/></a></td>');
+		$oPage->add('<td style="padding-left: 20px;">');
+		$oPage->add($sVersionString.'<br/>');
+		$oPage->add(Dict::S('UI:About:DataModel').': '.$sDataModelVersion.'<br/>');
+		$oPage->add('MySQL: '.$sMySQLVersion.'<br/>');
+		$oPage->add('PHP: '.$sPHPVersion.'<br/>');
+		$oPage->add('</td>');
+		$oPage->add('</tr>');
+		$oPage->add('</table>');
+		$oPage->add("</div>");
+
+		$oPage->add("<div>");
+		$oPage->add('<fieldset>');
+		$oPage->add('<legend>'.Dict::S('UI:About:Licenses').'</legend>');
+		$oPage->add('<ul style="margin: 0; font-size: smaller;">');
+		foreach($aLicenses as $index => $oLicense)
+		{
+			$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<a id="toggle_'.$index.'" class="CollapsibleLabel" style="cursor:pointer;">Details</a>)');
+			$oPage->add('<div id="license_'.$index.'" class="license_text" style="display:none;overflow:auto;max-height:10em;font-size:small;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;">'.$oLicense->text.'</div>');
+			$oPage->add_ready_script('$("#toggle_'.$index.'").click( function() { $("#license_'.$index.'").slideToggle("normal"); } );');
+		}
+		$oPage->add('</ul>');
+		$oPage->add('</fieldset>');
+		$oPage->add("</div>");
+
+		$oPage->add('<fieldset>');
+		$oPage->add('<legend>'.Dict::S('UI:About:Modules').'</legend>');
+		//$oPage->add(print_r($aAvailableModules, true));
+		$oPage->add("<div style=\"height: 150px; overflow: auto; font-size: smaller;\">");
+		$oPage->add('<ul style="margin: 0;">');
+		foreach ($aAvailableModules as $sModuleId => $aModuleData)
+		{
+			if ($sModuleId == '_Root_') continue;
+			if (!$aModuleData['visible']) continue;
+			if ($aModuleData['version_db'] == '') continue;
+			$oPage->add('<li>'.$aModuleData['label'].' ('.$aModuleData['version_db'].')</li>');
+		}
+		$oPage->add('</ul>');
+		$oPage->add("</div>");
+		$oPage->add('</fieldset>');
+
+
+		// MUST NOT be localized, as the information given here will be sent to the support
+		$oPage->add("<a id=\"collapse_support_details\" class=\"CollapsibleLabel\" href=\"#\">".Dict::S('UI:About:Support')."</a></br>\n");
+		$oPage->add("<div id=\"support_details\">");
+		$oPage->add('<textarea readonly style="width: 660px; height: 150px; font-size: smaller;">');
+		$oPage->add("===== begin =====\n");
+		$oPage->add('iTopVersion: '.ITOP_VERSION."\n");
+		$oPage->add('iTopBuild: '.ITOP_REVISION."\n");
+		$oPage->add('iTopBuildDate: '.ITOP_BUILD_DATE."\n");
+		$oPage->add('DataModelVersion: '.$sDataModelVersion."\n");
+		$oPage->add('MySQLVersion: '.$sMySQLVersion."\n");
+		$oPage->add('PHPVersion: '. $sPHPVersion."\n");
+		$oPage->add('OSVersion: '.$sOSVersion."\n");
+		$oPage->add('WebServerVersion: '.$sWebServerVersion."\n");
+		$oPage->add('PHPModules: '.$sModules."\n");
+		foreach ($aItopSettings as $siTopVar)
+		{
+			$oPage->add('ItopSetting/'.$siTopVar.': '.MetaModel::GetConfig()->Get($siTopVar)."\n");
+		}
+		foreach ($aPHPSettings as $sPHPVar)
+		{
+			$oPage->add('PHPSetting/'.$sPHPVar.': '.ini_get($sPHPVar)."\n");
+		}
+		foreach ($aMySQLSettings as $sMySQLVar)
+		{
+			$oPage->add('MySQLSetting/'.$sMySQLVar.': '.$aMySQLVars[$sMySQLVar]."\n");
+		}
+		foreach ($aMySQLStatuses as $sMySQLStatus)
+		{
+			$oPage->add('MySQLStatus/'.$sMySQLStatus.': '.$aMySQLStats[$sMySQLStatus]."\n");
+		}
+
+		$oPage->add('InstallDate: '.$sLastInstallDate."\n");
+		$oPage->add('InstallPath: '.APPROOT."\n");
+		foreach ($aAvailableModules as $sModuleId => $aModuleData)
+		{
+			if ($sModuleId == '_Root_') continue;
+			if ($aModuleData['version_db'] == '') continue;
+			$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['version_db']."\n");
+		}
+
+		$oPage->add('===== end =====');
+		$oPage->add('</textarea>');
+		$oPage->add("</div>");
+
+		$oPage->add("</div>");
+		break;
+		
+		case 'history':
+		$oPage->SetContentType('text/html');
+		$id = (int)utils::ReadParam('id', 0);
+		$iStart = (int)utils::ReadParam('start', 0);
+		$iCount = (int)utils::ReadParam('count', MetaModel::GetConfig()->Get('max_history_length', '50'));
+		$oObj = MetaModel::GetObject($sClass, $id);
+		$oObj->DisplayBareHistory($oPage, false, $iCount, $iStart);
+		$oPage->add_ready_script("$('#history table.listResults').tableHover(); $('#history table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
+		break;
+
+		case 'history_from_filter':
+		$oPage->SetContentType('text/html');
+		$oHistoryFilter = CMDBSearchFilter::unserialize($sFilter);
+		$iStart = (int)utils::ReadParam('start', 0);
+		$iCount = (int)utils::ReadParam('count', MetaModel::GetConfig()->Get('max_history_length', '50'));
+		$oBlock = new HistoryBlock($oHistoryFilter, 'table', false);
+		$oBlock->SetLimit($iCount, $iStart);
+		$oBlock->Display($oPage, 'history');
+		$oPage->add_ready_script("$('#history table.listResults').tableHover(); $('#history table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );");
+		break;
+
+		case 'full_text_search':
+		$aFullTextNeedles = utils::ReadParam('needles', array(), false, 'raw_data');
+		$sFullText = trim(implode(' ', $aFullTextNeedles));
+		$sClassName = utils::ReadParam('class', '');
+		$iCount = utils::ReadParam('count', 0);
+		$iCurrentPos = utils::ReadParam('position', 0);
+		$iTune = utils::ReadParam('tune', 0);
+		if (empty($sFullText))
+		{
+			$oPage->p(Dict::S('UI:Search:NoSearch'));
+			break;
+		}
+
+		// Search in full text mode in all the classes
+		$aMatches = array();
+
+		// Build the ordered list of classes to search into
+		//
+		if (empty($sClassName))
+		{
+			$aSearchClasses = MetaModel::GetClasses('searchable');					
+		}
+		else
+		{
+			// Search is limited to a given class and its subclasses
+			$aSearchClasses = MetaModel::EnumChildClasses($sClassName, ENUM_CHILD_CLASSES_ALL);
+		}
+		// Skip abstract classes, since we search in all the child classes anyway
+		foreach($aSearchClasses as $idx => $sClass)
+		{
+			if (MetaModel::IsAbstract($sClass))
+			{
+				unset($aSearchClasses[$idx]);
+			}
+		}
+
+		$sMaxChunkDuration = MetaModel::GetConfig()->Get('full_text_chunk_duration');
+		$aAccelerators = MetaModel::GetConfig()->Get('full_text_accelerators');
+
+		foreach (array_reverse($aAccelerators) as $sClass => $aRestriction)
+		{
+			$bSkip = false;
+			$iPos = array_search($sClass, $aSearchClasses);
+			if ($iPos !== false)
+			{
+				unset($aSearchClasses[$iPos]);
+			}
+			else
+			{
+				$bSkip = true;
+			}
+			$bSkip |= array_key_exists('skip', $aRestriction) ? $aRestriction['skip'] : false ;
+			if (!in_array($sClass, $aSearchClasses))
+			if ($sClass == $sClassName)
+			{
+				// Class explicitely requested, do NOT skip it
+				// beware: there may not be a 'query' defined for a skipped class !
+				$bSkip = false;
+			}
+			if (!$bSkip)
+			{
+				// NOT skipped, add the class to the list of classes to search into
+				if (array_key_exists('query', $aRestriction))
+				{
+					array_unshift($aSearchClasses, $aRestriction['query']);
+				}
+				else
+				{
+					// No accelerator query
+					array_unshift($aSearchClasses, $sClassName);
+				}
+			}
+		}
+
+		$aSearchClasses = array_values($aSearchClasses); // renumbers the array starting from zero, removing the missing indexes
+		$fStarted = microtime(true);
+		$iFoundInThisRound = 0;
+		for($iPos = $iCurrentPos; $iPos < count($aSearchClasses) ; $iPos++)
+		{
+			if ($iFoundInThisRound && (microtime(true) - $fStarted >= $sMaxChunkDuration))
+			{
+				break;
+			}
+
+			$sClassSpec = $aSearchClasses[$iPos];
+			if (substr($sClassSpec, 0, 7) == 'SELECT ')
+			{
+				$oFilter = DBObjectSearch::FromOQL($sClassSpec);
+				$sClassName = $oFilter->GetClass();
+				$sNeedleFormat = isset($aAccelerators[$sClassName]['needle']) ? $aAccelerators[$sClassName]['needle'] : '%$needle$%';
+				$sNeedle = str_replace('$needle$', $sFullText, $sNeedleFormat);
+				$aParams = array('needle' => $sNeedle);
+			}
+			else
+			{
+				$sClassName = $sClassSpec;
+				$oFilter = new DBObjectSearch($sClassName);
+				$aParams = array();
+
+				foreach($aFullTextNeedles as $sSearchText)
+				{
+					$oFilter->AddCondition_FullText($sSearchText);
+				}
+			}
+			// Skip abstract classes
+			if (MetaModel::IsAbstract($sClassName)) continue;
+
+			if ($iTune > 0)
+			{
+				$fStartedClass = microtime(true);
+			}
+			$oSet = new DBObjectSet($oFilter, array(), $aParams);
+			if (array_key_exists($sClassName, $aAccelerators) && array_key_exists('attributes', $aAccelerators[$sClassName]))
+			{
+				$oSet->OptimizeColumnLoad(array($oFilter->GetClassAlias() => $aAccelerators[$sClassName]['attributes']));
+			}
+
+			$sFullTextJS = addslashes($sFullText);
+			$bEnableEnlarge =  array_key_exists($sClassName, $aAccelerators) && array_key_exists('query', $aAccelerators[$sClassName]);
+			if (array_key_exists($sClassName, $aAccelerators) && array_key_exists('enable_enlarge', $aAccelerators[$sClassName]))
+			{
+				$bEnableEnlarge &= $aAccelerators[$sClassName]['enable_enlarge'];
+			}
+			$sEnlargeTheSearch =
+<<<EOF
+			$('.search-class-$sClassName button').attr('disabled', 'disabled');
+
+			$('.search-class-$sClassName h2').append('&nbsp;<img id="indicator" src="../images/indicator.gif">');
+			var oParams = {operation: 'full_text_search_enlarge', class: '$sClassName', text: '$sFullTextJS'};
+			$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
+				$('.search-class-$sClassName').html(data);
+			});
+EOF
+			;
+
+			
+			$sEnlargeButton = '';
+			if ($bEnableEnlarge)
+			{
+				$sEnlargeButton = "&nbsp;<button onclick=\"".htmlentities($sEnlargeTheSearch, ENT_QUOTES, 'UTF-8')."\">".Dict::S('UI:Search:Enlarge')."</button>";
+			}
+			if ($oSet->Count() > 0)
+			{
+				$aLeafs = array();
+				while($oObj = $oSet->Fetch())
+				{
+					if (get_class($oObj) == $sClassName)
+					{
+						$aLeafs[] = $oObj->GetKey();
+						$iFoundInThisRound ++; 
+					}
+				}
+				$oLeafsFilter = new DBObjectSearch($sClassName);
+				if (count($aLeafs) > 0)
+				{
+					$iCount += count($aLeafs);
+					$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
+					$oPage->add("<div class=\"page_header\">\n");
+					if (array_key_exists($sClassName, $aAccelerators))
+					{
+						$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
+					}
+					else
+					{
+						$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName))."</h2>\n");
+					}
+					$oPage->add("</div>\n");
+					$oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
+					$oBlock = new DisplayBlock($oLeafsFilter, 'list', false);
+					$sBlockId = 'global_search_'.$sClassName;
+					$oPage->add('<div id="'.$sBlockId.'">');
+					$oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
+					$oPage->add("</div>\n");
+					$oPage->add("</div>\n");
+					$oPage->p('&nbsp;'); // Some space ?
+				}
+			}
+			else if (array_key_exists($sClassName, $aAccelerators))
+			{
+				$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
+				$oPage->add("<div class=\"page_header\">\n");
+				$oPage->add("<h2>".MetaModel::GetClassIcon($sClassName)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', 0, Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
+				$oPage->add("</div>\n");
+				$oPage->add("</div>\n");
+				$oPage->p('&nbsp;'); // Some space ?
+			}
+			if ($iTune > 0)
+			{
+				$fDurationClass = microtime(true) - $fStartedClass;
+				$oPage->add_script("oTimeStatistics.$sClassName = $fDurationClass;");
+			}
+		}
+		if ($iPos < count($aSearchClasses))
+		{
+			$sJSNeedle = json_encode($aFullTextNeedles);
+			$oPage->add_ready_script(
+<<<EOF
+				var oParams = {operation: 'full_text_search', position: $iPos, needles: $sJSNeedle, count: $iCount, tune: $iTune};
+				$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
+					$('#full_text_results').append(data);
+				});
+EOF
+			);
+		}
+		else
+		{
+			// We're done
+			$oPage->add_ready_script(
+<<<EOF
+$('#full_text_indicator').hide();
+$('#full_text_progress,#full_text_progress_placeholder').hide(500);
+EOF
+			);
+
+			if ($iTune > 0)
+			{
+				$oPage->add_ready_script(
+<<<EOF
+				var sRes = '<h4>Search statistics (tune = 1)</h4><table>';
+				sRes += '<thead><tr><th>Class</th><th>Time</th></tr></thead>';
+				sRes += '<tbody>';
+				var fTotal = 0;
+				for (var sClass in oTimeStatistics)
+				{
+					fTotal = fTotal + oTimeStatistics[sClass];
+					fRounded = Math.round(oTimeStatistics[sClass] * 1000) / 1000;
+					sRes += '<tr><td>' + sClass + '</td><td>' + fRounded + '</td></tr>';
+				}
+				
+				fRoundedTotal = Math.round(fTotal * 1000) / 1000;
+				sRes += '<tr><td><b>Total</b></td><td><b>' + fRoundedTotal + '</b></td></tr>';
+				sRes += '</tbody>';
+				sRes += '</table>';
+				$('#full_text_results').append(sRes);
+EOF
+				);
+			}
+
+			if ($iCount == 0)
+			{
+				$sFullTextSummary = addslashes(Dict::S('UI:Search:NoObjectFound'));
+				$oPage->add_ready_script("$('#full_text_results').append('<div id=\"no_object_found\">$sFullTextSummary</div>');");
+			}
+		}
+		break;
+
+		case 'full_text_search_enlarge':
+		$sFullText = trim(utils::ReadParam('text', '', false, 'raw_data'));
+		$sClass = trim(utils::ReadParam('class', ''));
+		$iTune = utils::ReadParam('tune', 0);
+
+		if (preg_match('/^"(.*)"$/', $sFullText, $aMatches))
+		{
+			// The text is surrounded by double-quotes, remove the quotes and treat it as one single expression
+			$aFullTextNeedles = array($aMatches[1]);
+		}
+		else
+		{
+			// Split the text on the blanks and treat this as a search for <word1> AND <word2> AND <word3>
+			$aFullTextNeedles = explode(' ', $sFullText);
+		}
+
+		$oFilter = new DBObjectSearch($sClass);
+		foreach($aFullTextNeedles as $sSearchText)
+		{
+			$oFilter->AddCondition_FullText($sSearchText);
+		}
+		$oSet = new DBObjectSet($oFilter);
+		$oPage->add("<div class=\"page_header\">\n");
+		$oPage->add("<h2>".MetaModel::GetClassIcon($sClass)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sClass))."</h2>\n");
+		$oPage->add("</div>\n");
+		if ($oSet->Count() > 0)
+		{
+			$aLeafs = array();
+			while($oObj = $oSet->Fetch())
+			{
+				if (get_class($oObj) == $sClass)
+				{
+					$aLeafs[] = $oObj->GetKey();
+				}
+			}
+			$oLeafsFilter = new DBObjectSearch($sClass);
+			if (count($aLeafs) > 0)
+			{
+				$oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
+				$oBlock = new DisplayBlock($oLeafsFilter, 'list', false);
+				$sBlockId = 'global_search_'.$sClass;
+				$oPage->add('<div id="'.$sBlockId.'">');
+				$oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
+				$oPage->add('</div>');
+				$oPage->P('&nbsp;'); // Some space ?
+				// Hide "no object found"
+				$oPage->add_ready_script('$("#no_object_found").hide();');
+			}
+		}
+		$oPage->add_ready_script(
+<<<EOF
+$('#full_text_indicator').hide();
+$('#full_text_progress,#full_text_progress_placeholder').hide(500);
+EOF
+		);
+		break;
+
+		case 'xlsx_export_dialog':
+		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+		$oPage->SetContentType('text/html');
+		$oPage->add(
+<<<EOF
+<style>
+ .ui-progressbar {
+	position: relative;
+}
+.progress-label {
+	position: absolute;
+	left: 50%;
+	top: 1px;
+	font-size: 11pt;
+}
+.download-form button {
+	display:block;
+	margin-left: auto;
+	margin-right: auto;
+	margin-top: 2em;
+}
+.ui-progressbar-value {
+	background: url(../setup/orange-progress.gif);
+}
+.progress-bar {
+	height: 20px;
+}
+.statistics > div {
+	padding-left: 16px;
+	cursor: pointer;
+	font-size: 10pt;
+	background: url(../images/minus.gif) 0 2px no-repeat;
+}				
+.statistics > div.closed {
+	padding-left: 16px;
+	background: url(../images/plus.gif) 0 2px no-repeat;
+}
+				
+.statistics .closed .stats-data {
+	display: none;
+}
+.stats-data td {
+	padding-right: 5px;
+}
+</style>				
+EOF
+		);
+		$oPage->add('<div id="XlsxExportDlg">');
+		$oPage->add('<div class="export-options">');
+		$oPage->add('<p><input type="checkbox" id="export-advanced-mode"/>&nbsp;<label for="export-advanced-mode">'.Dict::S('UI:CSVImport:AdvancedMode').'</label></p>');
+		$oPage->add('<p style="font-size:10pt;margin-left:2em;margin-top:-0.5em;padding-bottom:1em;">'.Dict::S('UI:CSVImport:AdvancedMode+').'</p>');
+		$oPage->add('<p><input type="checkbox" id="export-auto-download" checked="checked"/>&nbsp;<label for="export-auto-download">'.Dict::S('ExcelExport:AutoDownload').'</label></p>');
+		$oPage->add('</div>');
+		$oPage->add('<div class="progress"><p class="status-message">'.Dict::S('ExcelExport:PreparingExport').'</p><div class="progress-bar"><div class="progress-label"></div></div></div>');
+		$oPage->add('<div class="statistics"><div class="stats-toggle closed">'.Dict::S('ExcelExport:Statistics').'<div class="stats-data"></div></div></div>');
+		$oPage->add('</div>');
+		$aLabels = array(
+			'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
+			'cancel_button' => Dict::S('UI:Button:Cancel'),
+			'export_button' => Dict::S('ExcelExporter:ExportButton'),
+			'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
+ 		);
+		$sJSLabels = json_encode($aLabels);
+		$sFilter = addslashes($sFilter);
+		$sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
+		$oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
+		break;
+		
+		case 'xlsx_start':
+		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+		$bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
+		$oSearch = DBObjectSearch::unserialize($sFilter);
+		
+		$oExcelExporter = new ExcelExporter();
+		$oExcelExporter->SetObjectList($oSearch);
+		//$oExcelExporter->SetChunkSize(10); //Only for testing
+		$oExcelExporter->SetAdvancedMode($bAdvanced);
+		$sToken = $oExcelExporter->SaveState();
+		$oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
+		break;
+		
+		case 'xlsx_run':
+		$sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit');
+		ini_set('memory_limit', $sMemoryLimit);
+		ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes
+					
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		$oExcelExporter = new ExcelExporter($sToken);
+		$aStatus = $oExcelExporter->Run();
+		$aResults = array('status' => $aStatus['code'], 'percentage' =>  $aStatus['percentage'], 'message' =>  $aStatus['message']);
+		if ($aStatus['code'] == 'done')
+		{
+			$aResults['statistics'] = $oExcelExporter->GetStatistics('html');
+		}
+		$oPage->add(json_encode($aResults));
+		break;
+		
+		case 'xlsx_download':
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+		$oPage->SetContentDisposition('attachment', 'export.xlsx');
+		$sFileContent = ExcelExporter::GetExcelFileFromToken($sToken);
+		$oPage->add($sFileContent);
+		ExcelExporter::CleanupFromToken($sToken);
+		break;
+		
+		case 'xlsx_abort':
+		// Stop & cleanup an export...
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		ExcelExporter::CleanupFromToken($sToken);
+		break;
+
+		case 'relation_pdf':
+		case 'relation_attachment':
+		require_once(APPROOT.'core/simplegraph.class.inc.php');
+		require_once(APPROOT.'core/relationgraph.class.inc.php');
+		require_once(APPROOT.'core/displayablegraph.class.inc.php');
+		$sRelation = utils::ReadParam('relation', 'impacts');
+		$sDirection = utils::ReadParam('direction', 'down');
+		
+		$iGroupingThreshold = utils::ReadParam('g', 5, false, 'integer');
+		$sPageFormat = utils::ReadParam('p', 'A4');
+		$sPageOrientation = utils::ReadParam('o', 'L');
+		$sTitle = utils::ReadParam('title', '', false, 'raw_data');
+		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
+		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
+		$bIncludeList = (bool)utils::ReadParam('include_list', false);
+		$sComments = utils::ReadParam('comments', '', false, 'raw_data');
+		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
+		$sContextKey = utils::ReadParam('context_key', '', false, 'raw_data');
+		$aPositions = null;
+		if ($sPositions != null)
+		{
+			$aPositions = json_decode($sPositions, true);
+		}
+		
+		// Get the list of source objects
+		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
+		$aSourceObjects = array();
+		foreach($aSources as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aSourceObjects[] = $oObj;
+			}
+		}
+		
+		// Get the list of excluded objects
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedObjects = array();
+		foreach($aExcluded as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aExcludedObjects[] = $oObj;
+			}
+		}
+		
+		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+		if ($sDirection == 'up')
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
+		}
+		else
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
+		}
+		
+		// Remove excluded classes from the graph
+		if (count($aExcludedClasses) > 0)
+		{
+			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+			foreach($oIterator as $oNode)
+			{
+				$oObj = $oNode->GetProperty('object');
+				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
+				{
+					$oRelGraph->FilterNode($oNode);
+				}
+			}
+		}
+		
+		$oPage = new PDFPage($sTitle, $sPageFormat, $sPageOrientation);
+		
+		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		$oGraph->InitFromGraphviz();
+		if ($aPositions != null)
+		{
+			$oGraph->UpdatePositions($aPositions);
+		}
+
+		$aGroups = array();
+		$oIterator = new RelationTypeIterator($oGraph, 'Node');
+		foreach($oIterator as $oNode)
+		{
+			if ($oNode instanceof DisplayableGroupNode)
+			{
+				$aGroups[$oNode->GetProperty('group_index')] = $oNode->GetObjects();
+			}
+		}
+		// First page is the graph
+		$oGraph->RenderAsPDF($oPage, $sComments, $sContextKey);
+
+		if ($bIncludeList)
+		{
+			// Then the lists of objects (one table per finalclass)
+			$aResults = array();
+			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+			foreach($oIterator as $oNode)
+			{
+				$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
+				if ($oObj)
+				{
+					$sObjClass  = get_class($oObj);
+					if (!array_key_exists($sObjClass, $aResults))
+					{
+						$aResults[$sObjClass] = array();
+					}
+					$aResults[$sObjClass][] = $oObj;
+				}
+			}
+			
+			$oPage->get_tcpdf()->AddPage();
+			$oPage->add('<div class="page_header"><h1>'.Dict::S('UI:RelationshipList').'</h1></div>');
+			$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+			foreach($aResults as $sListClass => $aObjects)
+			{
+				set_time_limit($iLoopTimeLimit);
+				$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
+				$sHtml = "<div class=\"page_header\">\n";
+				$sHtml .= "<table class=\"section\"><tr><td>".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sListClass))."</td></tr></table>\n";
+				$sHtml .= "</div>\n";
+				$oPage->add($sHtml);
+				cmdbAbstractObject::DisplaySet($oPage, $oSet);
+				$oPage->p(''); // Some space
+			}
+			
+			// Then the content of the groups (one table per group)
+			if (count($aGroups) > 0)
+			{
+				$oPage->get_tcpdf()->AddPage();
+				$oPage->add('<div class="page_header"><h1>'.Dict::S('UI:RelationGroups').'</h1></div>');
+				foreach($aGroups as $idx => $aObjects)
+				{
+					set_time_limit($iLoopTimeLimit);
+					$sListClass = get_class(current($aObjects));
+					$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
+					$sHtml = "<div class=\"page_header\">\n";
+					$sHtml .= "<table class=\"section\"><tr><td>".MetaModel::GetClassIcon($sListClass, true, 'width: 24px; height: 24px;')." ".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."</td></tr></table>\n";
+					$sHtml .= "</div>\n";
+					$oPage->add($sHtml);
+					cmdbAbstractObject::DisplaySet($oPage, $oSet);
+					$oPage->p(''); // Some space
+				}
+			}
+		}
+		if ($operation == 'relation_attachment')
+		{
+			$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
+			$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
+				
+			// Save the generated PDF as an attachment
+			$sPDF = $oPage->get_pdf();
+			$oPage = new ajax_page('');
+			$oAttachment = new Attachment();
+			$oAttachment->Set('item_class', $sObjClass);
+			$oAttachment->Set('item_id', $iObjKey);
+			$oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf');
+			$oAttachment->Set('contents', $oDoc);
+			$iAttachmentId = $oAttachment->DBInsert();
+			$aRet = array(
+				'status' => 'ok',
 				'att_id' => $iAttachmentId,
-			);
-			$oPage->add(json_encode($aRet));
-		}
-		break;
-		
-		case 'relation_json':
-		require_once(APPROOT.'core/simplegraph.class.inc.php');
-		require_once(APPROOT.'core/relationgraph.class.inc.php');
-		require_once(APPROOT.'core/displayablegraph.class.inc.php');
-		$sRelation = utils::ReadParam('relation', 'impacts');
-		$sDirection = utils::ReadParam('direction', 'down');
-		$iGroupingThreshold = utils::ReadParam('g', 5);
-		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
-		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
-		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
-		$sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data');
-		$aPositions = null;
-		if ($sPositions != null)
-		{
-			$aPositions = json_decode($sPositions, true);
-		}
-		
-			// Get the list of source objects
-		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
-		$aSourceObjects = array();
-		foreach($aSources as $sClass => $aIDs)
-		{
-			$oSearch = new DBObjectSearch($sClass);
-			$oSearch->AddCondition('id', $aIDs, 'IN');
-			$oSet = new DBObjectSet($oSearch);
-			while($oObj = $oSet->Fetch())
-			{
-				$aSourceObjects[] = $oObj;
-			}
-		}
-		
-		// Get the list of excluded objects
-		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
-		$aExcludedObjects = array();
-		foreach($aExcluded as $sClass => $aIDs)
-		{
-			$oSearch = new DBObjectSearch($sClass);
-			$oSearch->AddCondition('id', $aIDs, 'IN');
-			$oSet = new DBObjectSet($oSearch);
-			while($oObj = $oSet->Fetch())
-			{
-				$aExcludedObjects[] = $oObj;
-			}
-		}
-		
-		// Compute the graph
-		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
-		if ($sDirection == 'up')
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
-		}
-		else
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
-		}
-		
-		// Remove excluded classes from the graph
-		if (count($aExcludedClasses) > 0)
-		{
-			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
-			foreach($oIterator as $oNode)
-			{
-				$oObj = $oNode->GetProperty('object');
-				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
-				{
-					$oRelGraph->FilterNode($oNode);
-				}
-			}
-		}
-		
-		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
-		$oGraph->InitFromGraphviz();
-		if ($aPositions != null)
-		{
-			$oGraph->UpdatePositions($aPositions);
-		}
-		$oPage->add($oGraph->GetAsJSON($sContextKey));
-		$oPage->SetContentType('application/json');
-		break;
-		
-		case 'ticket_impact':
-		require_once(APPROOT.'core/simplegraph.class.inc.php');
-		require_once(APPROOT.'core/relationgraph.class.inc.php');
-		require_once(APPROOT.'core/displayablegraph.class.inc.php');
-		$sRelation = utils::ReadParam('relation', 'impacts');
-		$sDirection = utils::ReadParam('direction', 'down');
-		$iGroupingThreshold = utils::ReadParam('g', 5);
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sAttCode = utils::ReadParam('attcode', 'functionalcis_list');
-		$sImpactAttCode = utils::ReadParam('impact_attcode', 'impact_code');
-		$sImpactAttCodeValue = utils::ReadParam('impact_attcode_value', 'manual');
-		$iId = (int)utils::ReadParam('id', 0, false, 'integer');
-		
-		// Get the list of source objects
-		$oTicket = MetaModel::GetObject($sClass, $iId);
-		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-		$sExtKeyToRemote = $oAttDef->GetExtKeyToRemote();
-		$oExtKeyToRemote = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sExtKeyToRemote);
-		$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
-		$oSet = $oTicket->Get($sAttCode);
-		$aSourceObjects = array();
-		$aExcludedObjects = array();
-		while($oLnk = $oSet->Fetch())
-		{
-			if ($oLnk->Get($sImpactAttCode) == 'manual')
-			{
-				$aSourceObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
-			}
-			if ($oLnk->Get($sImpactAttCode) == 'not_impacted')
-			{
-				$aExcludedObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
-			}
-		}
-		
-		// Compute the graph
-		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
-		if ($sDirection == 'up')
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
-		}
-		else
-		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, $aExcludedObjects);
-		}
-		
-		$aResults = $oRelGraph->GetObjectsByClass();
-		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
-		
-		$sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection;		
-		$oAppContext = new ApplicationContext();
-		$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket));		
-		break;
-		
-		default:
-		$oPage->p("Invalid query.");
-	}
-
-	$oPage->output();
-}
-catch (Exception $e)
-{
-	// note: transform to cope with XSS attacks
-	echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
-	echo "<p>Debug trace: <pre>".$e->getTraceAsString()."</pre></p>\n";
-	IssueLog::Error($e->getMessage());
-}
-
-
-
-/**
- * Downloads a document to the browser, either as 'inline' or 'attachment'
- *  
- * @param WebPage $oPage The web page for the output
- * @param string $sClass Class name of the object
- * @param mixed $id Identifier of the object
- * @param string $sAttCode Name of the attribute containing the document to download
- * @param string $sContentDisposition Either 'inline' or 'attachment'
- * @return none
- */   
-function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment')
-{
-	try
-	{
-		$oObj = MetaModel::GetObject($sClass, $id, false, false);
-		if (!is_object($oObj))
-		{
-			throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it");
-		}
-		$oDocument = $oObj->Get($sAttCode);
-		if (is_object($oDocument))
-		{
-			$oPage->TrashUnexpectedOutput();
-			$oPage->SetContentType($oDocument->GetMimeType());
-			$oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName());
-			$oPage->add($oDocument->GetData());
-		}
-	}
-	catch(Exception $e)
-	{
-		$oPage->p($e->getMessage());
-	}
-}
-?>
+			);
+			$oPage->add(json_encode($aRet));
+		}
+		break;
+		
+		case 'relation_json':
+		require_once(APPROOT.'core/simplegraph.class.inc.php');
+		require_once(APPROOT.'core/relationgraph.class.inc.php');
+		require_once(APPROOT.'core/displayablegraph.class.inc.php');
+		$sRelation = utils::ReadParam('relation', 'impacts');
+		$sDirection = utils::ReadParam('direction', 'down');
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
+		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
+		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
+		$sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data');
+		$aPositions = null;
+		if ($sPositions != null)
+		{
+			$aPositions = json_decode($sPositions, true);
+		}
+		
+			// Get the list of source objects
+		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
+		$aSourceObjects = array();
+		foreach($aSources as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aSourceObjects[] = $oObj;
+			}
+		}
+		
+		// Get the list of excluded objects
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedObjects = array();
+		foreach($aExcluded as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aExcludedObjects[] = $oObj;
+			}
+		}
+		
+		// Compute the graph
+		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+		if ($sDirection == 'up')
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
+		}
+		else
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
+		}
+		
+		// Remove excluded classes from the graph
+		if (count($aExcludedClasses) > 0)
+		{
+			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+			foreach($oIterator as $oNode)
+			{
+				$oObj = $oNode->GetProperty('object');
+				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
+				{
+					$oRelGraph->FilterNode($oNode);
+				}
+			}
+		}
+		
+		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		$oGraph->InitFromGraphviz();
+		if ($aPositions != null)
+		{
+			$oGraph->UpdatePositions($aPositions);
+		}
+		$oPage->add($oGraph->GetAsJSON($sContextKey));
+		$oPage->SetContentType('application/json');
+		break;
+		
+		case 'ticket_impact':
+		require_once(APPROOT.'core/simplegraph.class.inc.php');
+		require_once(APPROOT.'core/relationgraph.class.inc.php');
+		require_once(APPROOT.'core/displayablegraph.class.inc.php');
+		$sRelation = utils::ReadParam('relation', 'impacts');
+		$sDirection = utils::ReadParam('direction', 'down');
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sAttCode = utils::ReadParam('attcode', 'functionalcis_list');
+		$sImpactAttCode = utils::ReadParam('impact_attcode', 'impact_code');
+		$sImpactAttCodeValue = utils::ReadParam('impact_attcode_value', 'manual');
+		$iId = (int)utils::ReadParam('id', 0, false, 'integer');
+		
+		// Get the list of source objects
+		$oTicket = MetaModel::GetObject($sClass, $iId);
+		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		$sExtKeyToRemote = $oAttDef->GetExtKeyToRemote();
+		$oExtKeyToRemote = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sExtKeyToRemote);
+		$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+		$oSet = $oTicket->Get($sAttCode);
+		$aSourceObjects = array();
+		$aExcludedObjects = array();
+		while($oLnk = $oSet->Fetch())
+		{
+			if ($oLnk->Get($sImpactAttCode) == 'manual')
+			{
+				$aSourceObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
+			}
+			if ($oLnk->Get($sImpactAttCode) == 'not_impacted')
+			{
+				$aExcludedObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
+			}
+		}
+		
+		// Compute the graph
+		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+		if ($sDirection == 'up')
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+		}
+		else
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, $aExcludedObjects);
+		}
+		
+		$aResults = $oRelGraph->GetObjectsByClass();
+		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		
+		$sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection;		
+		$oAppContext = new ApplicationContext();
+		$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket));		
+		break;
+		
+		case 'export_build':
+		try
+		{
+			$token = utils::ReadParam('token', null);
+			$aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Export not found for token: '$token'"); // Fallback error, just in case
+			$data = '';
+			if ($token === null)
+			{
+				$sFormat = utils::ReadParam('format', '');
+				$sExpression = utils::ReadParam('expression', null, false, 'raw_data');
+				$iQueryId = utils::ReadParam('query', null);
+				if ($sExpression === null)
+				{
+					$oQuerySearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $iQueryId));
+					$oQueries = new DBObjectSet($oQuerySearch);
+					if ($oQueries->Count() > 0)
+					{
+						$oQuery = $oQueries->Fetch();
+						$sExpression = $oQuery->Get('oql');
+					}
+					else
+					{
+						$aResult = array('code' => 'error', 'percentage' => 100, 'message' => "Invalid query phrasebook identifier: '$iQueryId'");
+					}
+				}
+				if($sExpression !== null)
+				{ 
+					$oSearch = DBObjectSearch::FromOQL($sExpression);
+					$oExporter = BulkExport::FindExporter($sFormat, $oSearch);
+					$oExporter->SetObjectList($oSearch);
+					$oExporter->SetFormat($sFormat);
+					$oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE);
+					$oExporter->ReadParameters();
+				}
+				
+				// First pass, generate the headers
+				$data .= $oExporter->GetHeader();
+			}
+			else
+			{
+				$oExporter = BulkExport::FindExporterFromToken($token);
+			}
+			
+			if ($oExporter)
+			{
+				$data .= $oExporter->GetNextChunk($aResult);
+				if ($aResult['code'] != 'done')
+				{
+					$oExporter->AppendToTmpFile($data);
+					$aResult['token'] = $oExporter->SaveState();
+				}
+				else
+				{
+					// Last pass
+					$data .= $oExporter->GetFooter();
+					$oExporter->AppendToTmpFile($data);
+					$aResult['token'] = $oExporter->SaveState();
+					if (substr($oExporter->GetMimeType(), 0, 5) == 'text/')
+					{
+						$aResult['text_result'] = file_get_contents($oExporter->GetTmpFilePath());
+						$aResult['mime_type'] = $oExporter->GetMimeType();
+					}
+					$aResult['message'] = Dict::Format('Core:BulkExport:ClickHereToDownload_FileName', $oExporter->GetDownloadFileName());
+				}
+			}
+			$oPage->add(json_encode($aResult));
+		}
+		catch(BulkExportException $e)
+		{
+			$aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->GetLocalizedMessage());
+			$oPage->add(json_encode($aResult));
+		}
+		catch(Exception $e)
+		{
+			$aResult = array('code' => 'error', 'percentage' => 100, 'message' => $e->getMessage());
+			$oPage->add(json_encode($aResult));
+		}
+		break;
+		
+		case 'export_download':
+		$token = utils::ReadParam('token', null);
+		if ($token !== null)
+		{
+			$oExporter = BulkExport::FindExporterFromToken($token);
+			if ($oExporter)
+			{
+				$sMimeType = $oExporter->GetMimeType();
+				if (substr($sMimeType, 0, 5) == 'text/')
+				{
+					$sMimeType .= ';charset=utf-8';
+				}
+				$oPage->SetContentType($sMimeType);
+				$oPage->SetContentDisposition('attachment', $oExporter->GetDownloadFileName());
+				$oPage->add(file_get_contents($oExporter->GetTmpFilePath()));
+			}
+		}
+		break;
+		
+		case 'export_cancel':
+		$token = utils::ReadParam('token', null);
+		if ($token !== null)
+		{
+			$oExporter = BulkExport::FindExporterFromToken($token);
+			if ($oExporter)
+			{
+				$oExporter->Cleanup();
+			}
+		}
+		$aResult = array('code' => 'error', 'percentage' => 100, 'message' => Dict::S('Core:BulkExport:ExportCancelledByUser'));
+		$oPage->add(json_encode($aResult));
+		break;
+				
+		default:
+		$oPage->p("Invalid query.");
+	}
+
+	$oPage->output();
+}
+catch (Exception $e)
+{
+	// note: transform to cope with XSS attacks
+	echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
+	IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
+}
+
+
+
+/**
+ * Downloads a document to the browser, either as 'inline' or 'attachment'
+ *  
+ * @param WebPage $oPage The web page for the output
+ * @param string $sClass Class name of the object
+ * @param mixed $id Identifier of the object
+ * @param string $sAttCode Name of the attribute containing the document to download
+ * @param string $sContentDisposition Either 'inline' or 'attachment'
+ * @return none
+ */   
+function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment')
+{
+	try
+	{
+		$oObj = MetaModel::GetObject($sClass, $id, false, false);
+		if (!is_object($oObj))
+		{
+			throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it");
+		}
+		$oDocument = $oObj->Get($sAttCode);
+		if (is_object($oDocument))
+		{
+			$oPage->TrashUnexpectedOutput();
+			$oPage->SetContentType($oDocument->GetMimeType());
+			$oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName());
+			$oPage->add($oDocument->GetData());
+		}
+	}
+	catch(Exception $e)
+	{
+		$oPage->p($e->getMessage());
+	}
+}
+?>

+ 678 - 0
webservices/export-v2.php

@@ -0,0 +1,678 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Export data specified by an OQL or a query phrasebook entry
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
+require_once(__DIR__.'/../approot.inc.php');
+require_once(APPROOT.'/application/application.inc.php');
+require_once(APPROOT.'/application/nicewebpage.class.inc.php');
+require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
+require_once(APPROOT.'/application/csvpage.class.inc.php');
+require_once(APPROOT.'/application/itopwebpage.class.inc.php');
+require_once(APPROOT.'/application/xmlpage.class.inc.php');
+require_once(APPROOT.'/application/clipage.class.inc.php');
+require_once(APPROOT.'/application/excelexporter.class.inc.php');
+require_once(APPROOT.'/core/bulkexport.class.inc.php');
+
+require_once(APPROOT.'/application/startup.inc.php');
+
+function ReportErrorAndExit($sErrorMessage)
+{
+	if (utils::IsModeCLI())
+	{
+		$oP = new CLIPage("iTop - Export");
+		$oP->p('ERROR: '.$sErrorMessage);
+		$oP->output();
+		exit -1;
+	}
+	else
+	{
+		$oP = new WebPage("iTop - Export");
+		$oP->p('ERROR: '.$sErrorMessage);
+		$oP->output();
+		exit -1;		
+	}
+}
+
+function ReportErrorAndUsage($sErrorMessage)
+{
+	if (utils::IsModeCLI())
+	{
+		$oP = new CLIPage("iTop - Export");
+		$oP->p('ERROR: '.$sErrorMessage);
+		Usage($oP);
+		$oP->output();
+		exit -1;
+	}
+	else
+	{
+		$oP = new WebPage("iTop - Export");
+		$oP->p('ERROR: '.$sErrorMessage);
+		Usage($oP);
+		$oP->output();
+		exit -1;
+	}
+}
+
+function Usage(Page $oP)
+{
+	if (Utils::IsModeCLI())
+	{
+		$oP->p('Usage: php '.basename(__FILE__).' --auth_user=<user> --auth_pwd=<password> --expression=<OQL Query> --query=<phrasebook_id> [--arg_xxx=<query_arguments>] [--no_localize=0|1] [--format=<format>] [--format-options...]');
+		$oP->p("Parameters:");
+		$oP->p(" * auth_user: the iTop user account for authentication");
+		$oP->p(" * auth_pwd: the password of the iTop user account");
+	}
+	else
+	{
+		$oP->p("Parameters:");
+	}
+	$oP->p(" * expression: an OQL expression (e.g. SELECT Contact WHERE name LIKE 'm%')");
+	$oP->p(" * query: (alternative to 'expression') the id of an entry from the query phrasebook");
+	$oP->p(" * arg_xxx: (needed if the query has parameters) the value of the parameter 'xxx'");
+	$aSupportedFormats = BulkExport::FindSupportedFormats();
+	$oP->p(" * format: (optional, default is html) the desired output format. Can be one of '".implode("', '", array_keys($aSupportedFormats))."'");
+	foreach($aSupportedFormats as $sFormatCode => $sLabel)
+	{
+		$oExporter = BulkExport::FindExporter($sFormatCode);
+		if ($oExporter !== null)
+		{
+			if (!Utils::IsModeCLI())
+			{
+				$oP->add('<hr/>');
+			}
+			$oExporter->DisplayUsage($oP);
+			if (!Utils::IsModeCLI())
+			{
+				$oP->add('</div>');
+			}
+		}
+	}
+	if (!Utils::IsModeCLI())
+	{
+		//$oP->add('</pre>');
+	}
+}
+
+function DisplayExpressionForm(WebPage $oP, $sAction, $sExpression = '', $sExceptionMessage = '')
+{
+	$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:ScopeDefinition').'</legend>');
+	$oP->add('<form id="export-form" action="'.$sAction.'" method="post">');
+	$oP->add('<input type="hidden" name="interactive" value="1">');
+	$oP->add('<table style="width:100%" class="export_parameters">');
+	$sExpressionHint = empty($sExceptionMessage) ? '' : '<tr><td colspan="2">'.htmlentities($sExceptionMessage, ENT_QUOTES, 'UTF-8').'</td></tr>';
+	$oP->add('<tr><td class="column-label"><span style="white-space: nowrap;"><input type="radio" name="query_mode" value="oql" id="radio_oql" checked><label for="radio_oql">'.Dict::S('Core:BulkExportLabelOQLExpression').'</label></span></td>');
+	$oP->add('<td><textarea style="width:100%" cols="70" rows="8" name="expression" id="textarea_oql" placeholder="SELECT Server">'.htmlentities($sExpression, ENT_QUOTES, 'UTF-8').'</textarea></td></tr>');
+	$oP->add($sExpressionHint);
+	$oP->add('<tr><td class="column-label"><span style="white-space: nowrap;"><input type="radio" name="query_mode" value="phrasebook" id="radio_phrasebook"><label for="radio_phrasebook">'.Dict::S('Core:BulkExportLabelPhrasebookEntry').'</label></span></td>');
+	$oP->add('<td><select name="query" id="select_phrasebook">');
+	$oP->add('<option value="">'.Dict::S('UI:SelectOne').'</option>');
+	$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL');
+	$oQueries = new DBObjectSet($oSearch);
+	while ($oQuery = $oQueries->Fetch())
+	{
+		$oP->add('<option value="'.$oQuery->GetKey().'">'.htmlentities($oQuery->Get('name'), ENT_QUOTES, 'UTF-8').'</option>');
+	}
+	$oP->add('</select></td></tr>');	
+	$oP->add('<tr><td colspan="2" style="text-align:right"><button type="submit" id="next-btn">'.Dict::S('UI:Button:Next').'</button></td></tr>');
+	$oP->add('</table>');
+	$oP->add('</form>');
+	$oP->add('</fieldset>');
+	$sJSEmptyOQL = json_encode(Dict::S('Core:BulkExportMessageEmptyOQL'));
+	$sJSEmptyQueryId = json_encode(Dict::S('Core:BulkExportMessageEmptyPhrasebookEntry'));
+	
+	$oP->add_ready_script(
+<<<EOF
+var colWidth = 0;
+$('td.column-label').each(function() {
+	var jLabel = $(this).find('span');
+	colWidth = Math.max(colWidth, jLabel.width());
+});
+$('td.column-label').each(function() {
+	var jLabel = $(this).width(colWidth);
+});
+		
+$('#textarea_oql').on('change keyup', function() {
+	$('#radio_oql').prop('checked', true);
+});
+$('#select_phrasebook').on('change', function() {
+	$('#radio_phrasebook').prop('checked', true);
+});
+$('#export-form').on('submit', function() {
+	if ($('#radio_oql').prop('checked'))
+	{
+		var sOQL = $('#textarea_oql').val();
+		if (sOQL == '')
+		{
+			alert($sJSEmptyOQL);
+			return false;
+		}
+	}
+	else
+	{
+		var sQueryId = $('#select_phrasebook').val();
+		if (sQueryId == '')
+		{
+			alert($sJSEmptyQueryId);
+			return false;
+		}
+	}
+	return true;
+});
+EOF
+	);
+}
+
+function DisplayForm(WebPage $oP, $sAction = '', $sExpression = '', $sQueryId = '', $sFormat = null)
+{
+	$oExportSearch = null;
+	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
+	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
+	$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
+	$oP->add('<form id="export-form" action="'.$sAction.'" method="post" data-state="not-yet-started">');
+	$bExpressionIsValid = true;
+	$sExpressionError = '';
+	if (($sExpression === null) && ($sQueryId === null))
+	{
+		$bExpressionIsValid = false;	
+	}
+	else if ($sExpression !== '')
+	{
+		try
+		{
+			$oExportSearch = DBObjectSearch::FromOQL($sExpression);
+		}
+		catch(OQLException $e)
+		{
+			$bExpressionIsValid = false;
+			$sExpressionError = $e->getMessage();
+		}
+	}
+	
+	if (!$bExpressionIsValid)
+	{
+		DisplayExpressionForm($oP, $sAction, $sExpression, $sExpressionError);
+		return;
+	}
+	
+	if ($sExpression !== '')
+	{
+		$oP->add('<input type="hidden" name="expression" value="'.htmlentities($sExpression, ENT_QUOTES, 'UTF-8').'">');
+		$oExportSearch = DBObjectSearch::FromOQL($sExpression);
+	}
+	else
+	{
+		$oQuery = MetaModel::GetObject('QueryOQL', $sQueryId);
+		$oExportSearch = DBObjectSearch::FromOQL($oQuery->Get('oql'));
+		$oP->add('<input type="hidden" name="query" value="'.htmlentities($sQueryId, ENT_QUOTES, 'UTF-8').'">');
+	}
+	$aFormPartsByFormat = array();
+	$aAllFormParts = array();
+	if ($sFormat == null)
+	{
+		// No specific format chosen
+		$oP->add('<p>'.Dict::S('Core:BulkExport:ExportFormatPrompt').' <select name="format" id="format_selector">');
+		$aSupportedFormats = BulkExport::FindSupportedFormats();
+		asort($aSupportedFormats);
+		foreach($aSupportedFormats as $sFormatCode => $sLabel)
+		{
+			$oP->add('<option value="'.$sFormatCode.'">'.htmlentities($sLabel, ENT_QUOTES, 'UTF-8').'</option>');
+			$oExporter = BulkExport::FindExporter($sFormatCode);
+			$oExporter->SetObjectList($oExportSearch);
+			$aParts = $oExporter->EnumFormParts();
+			foreach($aParts as $sPartId => $void)
+			{
+				$aAllFormParts[$sPartId] = $oExporter;
+			}
+			$aFormPartsByFormat[$sFormatCode] = array_keys($aParts);
+		}
+		$oP->add('</select></p>');
+	}
+	else 
+	{
+		// One specific format was chosen
+		$oP->add('<input type="hidden" name="format" value="'.htmlentities($sFormat, ENT_QUOTES, 'UTF-8').'">');
+		
+		$oExporter = BulkExport::FindExporter($sFormat, $oExportSearch);
+		$aParts = $oExporter->EnumFormParts();
+		foreach($aParts as $sPartId => $void)
+		{
+			$aAllFormParts[$sPartId] = $oExporter;
+		}
+		$aFormPartsByFormat[$sFormat] = array_keys($aAllFormParts);
+	}
+	foreach($aAllFormParts as $sPartId => $oExport)
+	{
+		$oP->add('<div class="form_part" id="form_part_'.$sPartId.'">');
+		$oExport->DisplayFormPart($oP, $sPartId);
+		$oP->add('</div>');
+	}
+	$oP->add('</form>');
+	$oP->add('<div id="export-feedback" style="display:none;"><p class="export-message" style="text-align:center;">'.Dict::S('ExcelExport:PreparingExport').'</p><div class="export-progress-bar" style="max-width:30em; margin-left:auto;margin-right:auto;"><div class="export-progress-message" style="text-align:center;"></div></div></div>');
+	$oP->add('<button type="button" id="export-btn">'.Dict::S('UI:Button:Export').'</button>');
+	$oP->add('<div id="export_text_result" style="display:none;">');
+	$oP->add('<div>'.Dict::S('Core:BulkExport:ExportResult').'</div>');
+	$oP->add('<textarea id="export_content" style="width:100%;min-height:15em;"></textarea>');
+	$oP->add('</div>');
+	
+	$sJSParts = json_encode($aFormPartsByFormat);
+	$sJSCancel = json_encode(Dict::S('UI:Button:Cancel'));
+	$sJSClose = json_encode(Dict::S('UI:Button:Done'));
+	
+	$oP->add_ready_script(
+<<<EOF
+window.aFormParts = $sJSParts;
+$('#format_selector').on('change init', function() {
+	ExportToggleFormat($(this).val());
+}).trigger('init');
+		
+$('.export-progress-bar').progressbar({
+	 value: 0,
+	 change: function() {
+		$('.export-progress-message').text( $(this).progressbar( "value" ) + "%" );
+	 },
+	 complete: function() {
+		 $('.export-progress-message').text( '100 %' );
+	 }
+});
+
+ExportInitButton('#export-btn');
+
+EOF
+	);
+	
+}
+
+function InteractiveShell($sExpression, $sQueryId, $sFormat, $sFileName, $sMode)
+{
+	if ($sMode == 'dialog')
+	{
+		$oP = new ajax_page('');
+		$oP->add('<div id="interactive_export_dlg">');
+		$sExportBtnLabel = json_encode(Dict::S('UI:Button:Export'));
+		$sJSTitle = json_encode(htmlentities(utils::ReadParam('dialog_title', '', false, 'raw_data'), ENT_QUOTES, 'UTF-8'));
+		$oP->add_ready_script(
+<<<EOF
+		$('#interactive_export_dlg').dialog({
+			autoOpen: true,
+			modal: true,
+			width: '80%',
+			title: $sJSTitle,
+			close: function() { $(this).remove(); },
+			buttons: [
+				{text: $sExportBtnLabel, id: 'export-dlg-submit', click: function() {} }
+			]
+		});
+			
+		setTimeout(function() { $('#interactive_export_dlg').dialog('option', { position: { my: "center", at: "center", of: window }}); $('#export-btn').hide(); ExportInitButton('#export-dlg-submit'); }, 100);
+EOF
+		);
+	}
+	else
+	{
+		$oP = new iTopWebPage('iTop Export');
+	}
+	
+	if ($sExpression === null)
+	{
+		// No expression supplied, let's check if phrasebook entry is given
+		if ($sQueryId !== null)
+		{
+			$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId));
+			$oQueries = new DBObjectSet($oSearch);
+			if ($oQueries->Count() > 0)
+			{
+				$oQuery = $oQueries->Fetch();
+				$sExpression = $oQuery->Get('oql');
+				if (strlen($sFields) == 0)
+				{
+					$sFields = trim($oQuery->Get('fields'));
+				}
+			}
+			else
+			{
+				ReportErrorAndExit("Invalid query phrasebook identifier: '$sQueryId'");
+			}
+		}
+		else
+		{
+			if (utils::IsModeCLI())
+			{
+				Usage();
+				ReportErrorAndExit("No expression or query phrasebook identifier supplied.");
+			}
+			else
+			{
+				// form to enter an OQL query or pick a query phrasebook identifier
+				DisplayForm($oP, utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php', $sExpression, $sQueryId, $sFormat);
+				$oP->output();
+				exit;
+			}
+		}
+	}
+	
+	if ($sFormat !== null)
+	{
+		$oExporter = BulkExport::FindExporter($sFormat);
+		if ($oExporter === null)
+		{
+			$aSupportedFormats = BulkExport::FindSupportedFormats();
+			ReportErrorAndExit("Invalid output format: '$sFormat'. The supported formats are: ".implode(', ', array_keys($aSupportedFormats)));
+		}
+		else
+		{
+			DisplayForm($oP, utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php', $sExpression, $sQueryId, $sFormat);
+		}
+	}
+	else
+	{
+		DisplayForm($oP, utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php', $sExpression, $sQueryId, $sFormat);
+	}
+	if ($sMode == 'dialog')
+	{
+		$oP->add('</div>');
+	}
+	$oP->output();	
+}
+
+function CheckParameters($sExpression, $sQueryId, $sFormat)
+{
+	$oExporter  = null;	
+	
+	if (($sExpression === null) && ($sQueryId === null))
+	{
+		ReportErrorAndUsage("Missing parameter. The parameter 'expression' or 'query' must be specified.");
+	}
+	
+	// Either $sExpression or $sQueryId must be specified
+	if ($sExpression === null)
+	{
+		$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId));
+		$oQueries = new DBObjectSet($oSearch);
+		if ($oQueries->Count() > 0)
+		{
+			$oQuery = $oQueries->Fetch();
+			$sExpression = $oQuery->Get('oql');
+			$sFields = $oQuery->Get('fields');
+			if (strlen($sFields) == 0)
+			{
+				$sFields = trim($oQuery->Get('fields'));
+			}
+		}
+		else
+		{
+			ReportErrorAndExit("Invalid query phrasebook identifier: '$sQueryId'");
+		}
+	}
+	if ($sFormat === null)
+	{
+		ReportErrorAndUsage("Missing parameter 'format'.");
+	}
+	
+	// Check if the supplied query is valid (and all the parameters are supplied
+	try
+	{
+		$oSearch = DBObjectSearch::FromOQL($sExpression);
+		$aArgs = array();
+		foreach($oSearch->GetQueryParams() as $sParam => $foo)
+		{
+			$value = utils::ReadParam('arg_'.$sParam, null, true, 'raw_data');
+			if (!is_null($value))
+			{
+				$aArgs[$sParam] = $value;
+			}
+			else
+			{
+				throw new MissingQueryArgument("Missing parameter '--arg_$sParam'");
+			}
+		}
+		$oSearch->SetInternalParams($aArgs);
+	
+		$sFormat = utils::ReadParam('format', 'html', true /* Allow CLI */, 'raw_data');
+		$oExporter = BulkExport::FindExporter($sFormat, $oSearch);
+		if ($oExporter == null)
+		{
+			$aSupportedFormats = BulkExport::FindSupportedFormats();
+			ReportErrorAndExit("Invalid output format: '$sFormat'. The supported formats are: ".implode(', ', array_keys($aSupportedFormats)));
+		}
+	}
+	catch(MissingQueryArgument $e)
+	{
+		ReportErrorAndUsage("Invalid OQL query: '$sExpression'.\n".$e->getMessage());
+	}
+	catch(OQLException $e)
+	{
+		ReportErrorAndExit("Invalid OQL query: '$sExpression'.\n".$e->getMessage());
+	}
+	catch(Exception $e)
+	{
+		ReportErrorAndExit($e->getMessage());
+	}
+	
+	$oExporter->SetFormat($sFormat);
+	$oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE);
+	$oExporter->SetObjectList($oSearch);
+	$oExporter->ReadParameters();
+	
+	return $oExporter;
+}
+
+function DoExport(Page $oP, BulkExport $oExporter, $bInteractive = false)
+{
+	$exportResult = $oExporter->GetHeader();
+	$aStatus = array();
+	do
+	{
+		$exportResult .= $oExporter->GetNextChunk($aStatus);
+	}
+	while (($aStatus['code'] != 'done') && ($aStatus['code'] != 'error'));
+	
+	if ($aStatus['code'] == 'error')
+	{
+		ReportErrorAndExit("Export failed: '{$aStatus['message']}'");
+	}
+	else
+	{
+		$exportResult .= $oExporter->GetFooter();
+		$oP->SetContentType($oExporter->GetMimeType());
+		$oP->add($exportResult);
+		$oExporter->Cleanup();
+	}
+}
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Command Line mode
+//
+/////////////////////////////////////////////////////////////////////////////
+if (utils::IsModeCLI())
+{
+	try
+	{
+		// Do this before loging, in order to allow setting user credentials from within the file
+		utils::UseParamFile();
+	}
+	catch(Exception $e)
+	{
+		echo "Error: ".$e->GetMessage()."<br/>\n";
+		exit -2;
+	}
+	
+	$sAuthUser = utils::ReadParam('auth_user', null, true /* Allow CLI */, 'raw_data');
+	$sAuthPwd = utils::ReadParam('auth_pwd', null, true /* Allow CLI */, 'raw_data');
+	if ($sAuthUser == null)
+	{
+		ReportErrorAndUsage("Missing parameter '--auth_user'");
+	}
+	if ($sAuthPwd == null)
+	{
+		ReportErrorAndUsage("Missing parameter '--auth_pwd'");
+	}
+	
+	if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
+	{
+		UserRights::Login($sAuthUser); // Login & set the user's language
+	}
+	else
+	{
+		ReportErrorAndExit("Access restricted or wrong credentials for user '$sAuthUser'");
+	}
+	
+	$sExpression = utils::ReadParam('expression', null, true /* Allow CLI */, 'raw_data');
+	$sQueryId = utils::ReadParam('query', null, true /* Allow CLI */, 'raw_data');
+	$bLocalize = (utils::ReadParam('no_localize', 0) != 1);
+	
+	if (($sExpression == null) && ($sQueryId == null))
+	{
+		ReportErrorAndUsage("Missing parameter. At least one of '--expression' or '--query' must be specified.");
+	}
+	
+	if ($sExpression === null)
+	{
+		$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId));
+		$oQueries = new DBObjectSet($oSearch);
+		if ($oQueries->Count() > 0)
+		{
+			$oQuery = $oQueries->Fetch();
+			$sExpression = $oQuery->Get('oql');
+		}
+		else
+		{
+			ReportErrorAndExit("Invalid query phrasebook identifier: '$sQueryId'");
+		}		
+	}
+	try
+	{
+		$oSearch = DBObjectSearch::FromOQL($sExpression);
+		$aArgs = array();
+		foreach($oSearch->GetQueryParams() as $sParam => $foo)
+		{
+			$value = utils::ReadParam('arg_'.$sParam, null, true, 'raw_data');
+			if (!is_null($value))
+			{
+				$aArgs[$sParam] = $value;
+			}
+			else
+			{
+				throw new MissingQueryArgument("Missing parameter '--arg_$sParam'");
+			}
+		}
+		$oSearch->SetInternalParams($aArgs);
+		
+		$sFormat = utils::ReadParam('format', 'html', true /* Allow CLI */, 'raw_data');
+		$oExporter = BulkExport::FindExporter($sFormat);
+		if ($oExporter == null)
+		{
+			$aSupportedFormats = BulkExport::FindSupportedFormats();
+			ReportErrorAndExit("Invalid output format: '$sFormat'. The supported formats are: ".implode(', ', array_keys($aSupportedFormats)));
+		}
+		
+		$oExporter->SetFormat($sFormat);
+		$oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE);
+		$oExporter->SetObjectList($oSearch);
+		$oExporter->ReadParameters();
+		
+		$exportResult = $oExporter->GetHeader();
+		$aStatus = array();
+		
+		do
+		{
+			$exportResult .= $oExporter->GetNextChunk($aStatus);
+		}
+		while (($aStatus['code'] != 'done') && ($aStatus['code'] != 'error'));
+		
+		if ($aStatus['code'] == 'error')
+		{
+			ReportErrorAndExit("Export failed: '{$aStatus['message']}'");
+		}
+		else
+		{
+			$exportResult .= $oExporter->GetFooter();
+			echo $exportResult;
+		}
+		$oExporter->Cleanup();
+		
+	}
+	catch(MissingQueryArgument $e)
+	{
+		ReportErrorAndUsage("Invalid OQL query: '$sExpression'.\n".$e->getMessage());
+	}
+	catch(OQLException $e)
+	{
+		ReportErrorAndExit("Invalid OQL query: '$sExpression'.\n".$e->getMessage());
+	}
+	catch(Exception $e)
+	{
+		ReportErrorAndExit($e->getMessage());
+	}
+	
+	exit;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Web Server mode
+//
+/////////////////////////////////////////////////////////////////////////////
+
+try
+{
+	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+	LoginWebPage::DoLogin(); // Check user rights and prompt if needed
+	
+	ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker');
+	
+	// Main parameters
+	$sExpression = utils::ReadParam('expression', null, true /* Allow CLI */, 'raw_data');
+	$sQueryId = utils::ReadParam('query', null, true /* Allow CLI */, 'raw_data');
+	$sFormat = utils::ReadParam('format', null, true /* Allow CLI */);
+	$sFileName = utils::ReadParam('filename', '', true, 'string');
+	$bInteractive = utils::ReadParam('interactive', false);
+	$sMode = utils::ReadParam('mode', '');
+	
+	if ($bInteractive)
+	{
+		InteractiveShell($sExpression, $sQueryId, $sFormat, $sFileName, $sMode);
+	}
+	else 
+	{
+		$oExporter = CheckParameters($sExpression, $sQueryId, $sFormat);
+		$oP = new WebPage('iTop export');
+		DoExport($oP, $oExporter, false);
+		$oP->output();
+	}
+}
+catch (BulkExportMissingParameterException $e)
+{
+	$oP = new ajax_page('iTop Export');
+	$oP->add($e->getMessage());
+	Usage($oP);
+	$oP->output();
+}
+catch (Exception $e)
+{
+	$oP = new WebPage('iTop Export');
+	$oP->add($e->getMessage()."<br/>".$e->getTraceAsString());
+	$oP->output();
+}