diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php
index abb3a6646..79bc67dd9 100644
--- a/application/itopwebpage.class.inc.php
+++ b/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 = '';
diff --git a/application/query.class.inc.php b/application/query.class.inc.php
index 82dd3333d..9f8656f71 100644
--- a/application/query.class.inc.php
+++ b/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
diff --git a/application/utils.inc.php b/application/utils.inc.php
index e021eaffe..365c230a4 100644
--- a/application/utils.inc.php
+++ b/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'];
+ if (isset($_SERVER['SERVER_NAME']))
+ {
+ $sServerName = $_SERVER['SERVER_NAME'];
+ }
+ else
+ {
+ // CLI mode ?
+ $sServerName = php_uname('n');
+ }
+ $sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl);
}
- else
- {
- // CLI mode ?
- $sServerName = php_uname('n');
- }
- $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;
diff --git a/application/xlsxwriter.class.php b/application/xlsxwriter.class.php
index 8d42980d7..0a5ad2ad0 100644
--- a/application/xlsxwriter.class.php
+++ b/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; }
diff --git a/core/bulkexport.class.inc.php b/core/bulkexport.class.inc.php
new file mode 100644
index 000000000..5ef3a91f3
--- /dev/null
+++ b/core/bulkexport.class.inc.php
@@ -0,0 +1,410 @@
+
+
+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');
+
diff --git a/core/config.class.inc.php b/core/config.class.inc.php
index e9fdc3e30..d9113bd92 100644
--- a/core/config.class.inc.php
+++ b/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',
);
diff --git a/core/csvbulkexport.class.inc.php b/core/csvbulkexport.class.inc.php
new file mode 100644
index 000000000..b236c1c80
--- /dev/null
+++ b/core/csvbulkexport.class.inc.php
@@ -0,0 +1,352 @@
+
+
+/**
+ * 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('
'.Dict::S('Core:BulkExport:CSVOptions').' ');
+ $oP->add('');
+ $oP->add(''.Dict::S('UI:CSVImport:SeparatorCharacter').' ');
+ $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').' ';
+
+ foreach($aSep as $sVal => $sLabel)
+ {
+ $sChecked = ($sVal == $sRawSeparator) ? 'checked' : '';
+ $oP->add(' '.$sLabel.' ');
+ }
+
+ $oP->add(' ');
+
+ $oP->add(''.Dict::S('UI:CSVImport:TextQualifierCharacter').' ');
+
+ $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').' ';
+
+ foreach($aQualifiers as $sVal => $sLabel)
+ {
+ $sChecked = ($sVal == $sRawQualifier) ? 'checked' : '';
+ $oP->add(' '.$sLabel.' ');
+ }
+
+ $sChecked = (utils::ReadParam('no_localize', 0) == 1) ? ' checked ' : '';
+ $oP->add(' ');
+ $oP->add(''.Dict::S('Core:BulkExport:CSVLocalization').' ');
+ $oP->add(' '.Dict::S('Core:BulkExport:OptionNoLocalize').' ');
+ $oP->add('
');
+
+ $oP->add(' ');
+ 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';
+ }
+
+}
diff --git a/core/excelbulkexport.class.inc.php b/core/excelbulkexport.class.inc.php
new file mode 100644
index 000000000..69743db5d
--- /dev/null
+++ b/core/excelbulkexport.class.inc.php
@@ -0,0 +1,309 @@
+
+
+/**
+ * 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'));
+ }
+}
\ No newline at end of file
diff --git a/core/htmlbulkexport.class.inc.php b/core/htmlbulkexport.class.inc.php
new file mode 100644
index 000000000..be3ff36ad
--- /dev/null
+++ b/core/htmlbulkexport.class.inc.php
@@ -0,0 +1,243 @@
+
+
+/**
+ * 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 = "\n";
+ $sData .= "\n";
+ $sData .= "\n";
+ foreach($aData as $sLabel)
+ {
+ $sData .= "".$sLabel." \n";
+ }
+ $sData .= " \n";
+ $sData .= " \n";
+ $sData .= "\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 .= "";
+ }
+ else
+ {
+ $sData .= " ";
+ }
+ 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 === '') ? ' ' : $sField;
+ $sData .= "$sValue ";
+ }
+ $sData .= " ";
+ $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 = " \n";
+ $sData .= "
\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';
+ }
+}
diff --git a/core/pdfbulkexport.class.inc.php b/core/pdfbulkexport.class.inc.php
new file mode 100644
index 000000000..3b3561547
--- /dev/null
+++ b/core/pdfbulkexport.class.inc.php
@@ -0,0 +1,149 @@
+
+
+/**
+ * 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(''.Dict::S('Core:BulkExport:PDFOptions').' ');
+ $oP->add('');
+ $oP->add('');
+ $oP->add(''.Dict::S('Core:BulkExport:PDFPageSize').' ');
+ $oP->add(''.$this->GetSelectCtrl('page_size', array('A3', 'A4', 'Letter'), 'Core:BulkExport:PageSize-', 'A4').' ');
+ $oP->add(' ');
+ $oP->add(''.Dict::S('Core:BulkExport:PDFPageOrientation').' ');
+ $oP->add(''.$this->GetSelectCtrl('page_orientation', array('P', 'L'), 'Core:BulkExport:PageOrientation-', 'L').' ');
+ $oP->add('');
+ $oP->add('
');
+
+ $oP->add(' ');
+ 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 = ' ';
+ foreach($aLabels as $sVal => $sLabel)
+ {
+ $sSelected = ($sVal == $sCurrentValue) ? 'selected' : '';
+ $sHtml .= ''.htmlentities($sLabel, ENT_QUOTES, 'UTF-8').' ';
+ }
+ $sHtml .= '';
+ 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';
+ }
+}
diff --git a/core/spreadsheetbulkexport.class.inc.php b/core/spreadsheetbulkexport.class.inc.php
new file mode 100644
index 000000000..b088cce79
--- /dev/null
+++ b/core/spreadsheetbulkexport.class.inc.php
@@ -0,0 +1,316 @@
+
+
+/**
+ * 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(''.Dict::S('Core:BulkExport:SpreadsheetOptions').' ');
+ $oP->add('');
+ $oP->add(' ');
+ 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 = "\n";
+ $sData .= "\n";
+ foreach($aData as $sLabel)
+ {
+ $sData .= "".$sLabel." \n";
+ }
+ $sData .= " \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 .= "";
+ foreach($aAliasByField as $aAttCode)
+ {
+ $sField = '';
+ $oObj = $aRow[$aAttCode['alias']];
+ if ($oObj == null)
+ {
+ $sData .= "$sField ";
+ continue;
+ }
+
+ switch($aAttCode['attcode'])
+ {
+ case 'id':
+ $sField = $oObj->GetName();
+ $sData .= "$sField ";
+ 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 .= ''.date('Y-m-d', $iDate).' '; // Add the first column directly
+ $sField = date('H:i:s', $iDate); // Will add the second column below
+ $sData .= "$sField ";
+ }
+ else if($oAttDef instanceof AttributeCaseLog)
+ {
+ $rawValue = $oObj->Get($aAttCode['attcode']);
+ $sField = str_replace("\n", " ", htmlentities($rawValue->__toString(), ENT_QUOTES, 'UTF-8'));
+ // Trick for Excel: treat the content as text even if it begins with an equal sign
+ $sData .= "$sField ";
+ }
+ 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 .= "$sField ";
+ }
+ }
+
+ }
+ $sData .= " ";
+ $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 = "
\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';
+ }
+}
diff --git a/core/tabularbulkexport.class.inc.php b/core/tabularbulkexport.class.inc.php
new file mode 100644
index 000000000..f1640c558
--- /dev/null
+++ b/core/tabularbulkexport.class.inc.php
@@ -0,0 +1,323 @@
+
+
+/**
+ * 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(' ');
+ 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('
');
+ $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(
+<<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);
+ }
+}
diff --git a/core/xmlbulkexport.class.inc.php b/core/xmlbulkexport.class.inc.php
new file mode 100644
index 000000000..d030b6700
--- /dev/null
+++ b/core/xmlbulkexport.class.inc.php
@@ -0,0 +1,196 @@
+
+
+/**
+ * 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(''.Dict::S('Core:BulkExport:XMLOptions').' ');
+ $oP->add('');
+ $oP->add(' ');
+ 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\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 .= "\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 .= "
\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 = " \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';
+ }
+}
diff --git a/css/dragtable.css b/css/dragtable.css
new file mode 100644
index 000000000..eea04bbf3
--- /dev/null
+++ b/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;
+}
diff --git a/css/light-grey.css b/css/light-grey.css
index 39e1204bc..e3aa0e624 100644
--- a/css/light-grey.css
+++ b/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;
+}
+
+
diff --git a/css/light-grey.scss b/css/light-grey.scss
index 46ffc0481..4a53a929a 100644
--- a/css/light-grey.scss
+++ b/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;
}
\ No newline at end of file
diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php
index 4a7f25552..8207d4832 100644
--- a/dictionaries/da.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php
index 9f03599f2..bbe91fb79 100644
--- a/dictionaries/de.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/dictionary.itop.core.php b/dictionaries/dictionary.itop.core.php
index f38280cf4..e5a31b6e9 100644
--- a/dictionaries/dictionary.itop.core.php
+++ b/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.',
+
));
diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php
index 97e8ce3d0..de56913f9 100644
--- a/dictionaries/dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/es_cr.dictionary.itop.ui.php b/dictionaries/es_cr.dictionary.itop.ui.php
index b073c2dfd..df1f47463 100644
--- a/dictionaries/es_cr.dictionary.itop.ui.php
+++ b/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',
diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php
index 6224a8805..106430408 100644
--- a/dictionaries/fr.dictionary.itop.core.php
+++ b/dictionaries/fr.dictionary.itop.core.php
@@ -648,5 +648,48 @@ Opérateurs :
'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.',
));
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index c42808792..16a5a72a9 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php
index f521f7198..a1e0ef4bb 100755
--- a/dictionaries/hu.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php
index db6339d1e..5198b1443 100644
--- a/dictionaries/it.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php
index 688c03ec7..016eaba4e 100644
--- a/dictionaries/ja.dictionary.itop.ui.php
+++ b/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' => '削除...',
diff --git a/dictionaries/nl.dictionary.itop.ui.php b/dictionaries/nl.dictionary.itop.ui.php
index fff8668ee..022b12e10 100644
--- a/dictionaries/nl.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/pt_br.dictionary.itop.ui.php b/dictionaries/pt_br.dictionary.itop.ui.php
index 8ad0d7d0d..9c2c0e49a 100644
--- a/dictionaries/pt_br.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/ru.dictionary.itop.ui.php b/dictionaries/ru.dictionary.itop.ui.php
index d0285daee..972f2cf38 100644
--- a/dictionaries/ru.dictionary.itop.ui.php
+++ b/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' => 'Управление...',
diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php
index af73daf80..8fc027824 100644
--- a/dictionaries/tr.dictionary.itop.ui.php
+++ b/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...',
diff --git a/dictionaries/zh.dictionary.itop.ui.php b/dictionaries/zh.dictionary.itop.ui.php
index 74c3b1165..f711dbe9d 100644
--- a/dictionaries/zh.dictionary.itop.ui.php
+++ b/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' => '管理...',
diff --git a/js/jquery.dragtable.js b/js/jquery.dragtable.js
new file mode 100644
index 000000000..9595eb731
--- /dev/null
+++ b/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 = '';
+ // assemble the needed html
+ thtb.find('> tr > th').each(function(i, v) {
+ var width_li = $(this).outerWidth();
+ sortableHtml += '';
+ sortableHtml += '';
+ 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('
').parent().html();
+ if (row_content.toLowerCase().indexOf('";
+ sortableHtml += ' ';
+ sortableHtml += row_content;
+ if (row_content.toLowerCase().indexOf('";
+ sortableHtml += ' ';
+ });
+ sortableHtml += '
';
+ sortableHtml += ' ';
+ });
+ sortableHtml += ' ';
+ 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('');
+ },
+ 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 ');
+ $(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
-EOF
- );
- $oPage->add('');
- $oPage->add('
');
- $oPage->add('
'.Dict::S('UI:CSVImport:AdvancedMode').'
');
- $oPage->add('
'.Dict::S('UI:CSVImport:AdvancedMode+').'
');
- $oPage->add('
'.Dict::S('ExcelExport:AutoDownload').'
');
- $oPage->add('
');
- $oPage->add('
'.Dict::S('ExcelExport:PreparingExport').'
');
- $oPage->add('
'.Dict::S('ExcelExport:Statistics').'
');
- $oPage->add('
');
- $aLabels = array(
- 'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
- 'cancel_button' => Dict::S('UI:Button:Cancel'),
- 'export_button' => Dict::S('ExcelExporter:ExportButton'),
- 'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
- );
- $sJSLabels = json_encode($aLabels);
- $sFilter = addslashes($sFilter);
- $sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
- $oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
- break;
-
- case 'xlsx_start':
- $sFilter = utils::ReadParam('filter', '', false, 'raw_data');
- $bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
- $oSearch = DBObjectSearch::unserialize($sFilter);
-
- $oExcelExporter = new ExcelExporter();
- $oExcelExporter->SetObjectList($oSearch);
- //$oExcelExporter->SetChunkSize(10); //Only for testing
- $oExcelExporter->SetAdvancedMode($bAdvanced);
- $sToken = $oExcelExporter->SaveState();
- $oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
- break;
-
- 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('');
- $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
- foreach($aResults as $sListClass => $aObjects)
- {
- set_time_limit($iLoopTimeLimit);
- $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
- $sHtml = "\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('');
- foreach($aGroups as $idx => $aObjects)
- {
- set_time_limit($iLoopTimeLimit);
- $sListClass = get_class(current($aObjects));
- $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
- $sHtml = "\n";
- $oPage->add($sHtml);
- cmdbAbstractObject::DisplaySet($oPage, $oSet);
- $oPage->p(''); // Some space
- }
- }
- }
- if ($operation == 'relation_attachment')
- {
- $sObjClass = utils::ReadParam('obj_class', '', false, 'class');
- $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
-
- // Save the generated PDF as an attachment
- $sPDF = $oPage->get_pdf();
- $oPage = new ajax_page('');
- $oAttachment = new Attachment();
- $oAttachment->Set('item_class', $sObjClass);
- $oAttachment->Set('item_id', $iObjKey);
- $oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf');
- $oAttachment->Set('contents', $oDoc);
- $iAttachmentId = $oAttachment->DBInsert();
- $aRet = array(
- 'status' => 'ok',
+
+
+
+/**
+ * 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 = "".$oObj->GetAsHTML($sAttCode);
+ $sHTMLValue .= ' ';
+ $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("\n3d pie \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("\n3d pie \n.");
+ }
+ break;
+
+ case 'modal_details':
+ $oPage->SetContentType('text/html');
+ $key = utils::ReadParam('id', 0);
+ $oFilter = new DBObjectSearch($sClass);
+ $oFilter->AddCondition('id', $key, '=');
+ $oPage->Add("Object Details
\n");
+ $oDisplayBlock = new DisplayBlock($oFilter, 'details', false);
+ $oDisplayBlock->RenderContent($oPage);
+ $oPage->Add(" \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(''.$oObj->GetName().' ');
+ }
+ 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(
+<<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(
+<< 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("");
+ $oPage->add('
');
+ $oPage->add('
');
+ $oPage->add('');
+ $oPage->add(' ');
+ $oPage->add('');
+ $oPage->add($sVersionString.' ');
+ $oPage->add(Dict::S('UI:About:DataModel').': '.$sDataModelVersion.' ');
+ $oPage->add('MySQL: '.$sMySQLVersion.' ');
+ $oPage->add('PHP: '.$sPHPVersion.' ');
+ $oPage->add(' ');
+ $oPage->add(' ');
+ $oPage->add('
');
+ $oPage->add("
");
+
+ $oPage->add("
");
+ $oPage->add('
');
+ $oPage->add(''.Dict::S('UI:About:Licenses').' ');
+ $oPage->add('');
+ foreach($aLicenses as $index => $oLicense)
+ {
+ $oPage->add(''.$oLicense->product.' , © '.$oLicense->author.' is licensed under the '.$oLicense->license_type.' license . (Details )');
+ $oPage->add(''.$oLicense->text.'
');
+ $oPage->add_ready_script('$("#toggle_'.$index.'").click( function() { $("#license_'.$index.'").slideToggle("normal"); } );');
+ }
+ $oPage->add(' ');
+ $oPage->add(' ');
+ $oPage->add("
");
+
+ $oPage->add('
');
+ $oPage->add(''.Dict::S('UI:About:Modules').' ');
+ //$oPage->add(print_r($aAvailableModules, true));
+ $oPage->add("");
+ $oPage->add('
');
+ foreach ($aAvailableModules as $sModuleId => $aModuleData)
+ {
+ if ($sModuleId == '_Root_') continue;
+ if (!$aModuleData['visible']) continue;
+ if ($aModuleData['version_db'] == '') continue;
+ $oPage->add(''.$aModuleData['label'].' ('.$aModuleData['version_db'].') ');
+ }
+ $oPage->add(' ');
+ $oPage->add("
");
+ $oPage->add(' ');
+
+
+ // MUST NOT be localized, as the information given here will be sent to the support
+ $oPage->add("
".Dict::S('UI:About:Support')." \n");
+ $oPage->add("
");
+ $oPage->add('');
+ $oPage->add("
");
+
+ $oPage->add("
");
+ 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 =
+<<');
+ 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 = " ".Dict::S('UI:Search:Enlarge')." ";
+ }
+ 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("\n");
+ $oPage->add("\n");
+ $oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
+ $oBlock = new DisplayBlock($oLeafsFilter, 'list', false);
+ $sBlockId = 'global_search_'.$sClassName;
+ $oPage->add('
');
+ $oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
+ $oPage->add("
\n");
+ $oPage->add("
\n");
+ $oPage->p(' '); // Some space ?
+ }
+ }
+ else if (array_key_exists($sClassName, $aAccelerators))
+ {
+ $oPage->add("\n");
+ $oPage->add("\n");
+ $oPage->add("
\n");
+ $oPage->p(' '); // 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(
+<<add_ready_script(
+<< 0)
+ {
+ $oPage->add_ready_script(
+<<Class Time ';
+ sRes += '';
+ var fTotal = 0;
+ for (var sClass in oTimeStatistics)
+ {
+ fTotal = fTotal + oTimeStatistics[sClass];
+ fRounded = Math.round(oTimeStatistics[sClass] * 1000) / 1000;
+ sRes += '' + sClass + ' ' + fRounded + ' ';
+ }
+
+ fRoundedTotal = Math.round(fTotal * 1000) / 1000;
+ sRes += 'Total ' + fRoundedTotal + ' ';
+ sRes += ' ';
+ sRes += '';
+ $('#full_text_results').append(sRes);
+EOF
+ );
+ }
+
+ if ($iCount == 0)
+ {
+ $sFullTextSummary = addslashes(Dict::S('UI:Search:NoObjectFound'));
+ $oPage->add_ready_script("$('#full_text_results').append('$sFullTextSummary
');");
+ }
+ }
+ 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 AND AND
+ $aFullTextNeedles = explode(' ', $sFullText);
+ }
+
+ $oFilter = new DBObjectSearch($sClass);
+ foreach($aFullTextNeedles as $sSearchText)
+ {
+ $oFilter->AddCondition_FullText($sSearchText);
+ }
+ $oSet = new DBObjectSet($oFilter);
+ $oPage->add("\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('');
+ $oBlock->RenderContent($oPage, array('table_id' => $sBlockId, 'currentId' => $sBlockId));
+ $oPage->add('
');
+ $oPage->P(' '); // Some space ?
+ // Hide "no object found"
+ $oPage->add_ready_script('$("#no_object_found").hide();');
+ }
+ }
+ $oPage->add_ready_script(
+<<SetContentType('text/html');
+ $oPage->add(
+<<
+ .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;
+}
+
+EOF
+ );
+ $oPage->add('');
+ $oPage->add('
');
+ $oPage->add('
'.Dict::S('UI:CSVImport:AdvancedMode').'
');
+ $oPage->add('
'.Dict::S('UI:CSVImport:AdvancedMode+').'
');
+ $oPage->add('
'.Dict::S('ExcelExport:AutoDownload').'
');
+ $oPage->add('
');
+ $oPage->add('
'.Dict::S('ExcelExport:PreparingExport').'
');
+ $oPage->add('
'.Dict::S('ExcelExport:Statistics').'
');
+ $oPage->add('
');
+ $aLabels = array(
+ 'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
+ 'cancel_button' => Dict::S('UI:Button:Cancel'),
+ 'export_button' => Dict::S('ExcelExporter:ExportButton'),
+ 'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
+ );
+ $sJSLabels = json_encode($aLabels);
+ $sFilter = addslashes($sFilter);
+ $sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
+ $oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
+ break;
+
+ case 'xlsx_start':
+ $sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+ $bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
+ $oSearch = DBObjectSearch::unserialize($sFilter);
+
+ $oExcelExporter = new ExcelExporter();
+ $oExcelExporter->SetObjectList($oSearch);
+ //$oExcelExporter->SetChunkSize(10); //Only for testing
+ $oExcelExporter->SetAdvancedMode($bAdvanced);
+ $sToken = $oExcelExporter->SaveState();
+ $oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
+ break;
+
+ 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('');
+ $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+ foreach($aResults as $sListClass => $aObjects)
+ {
+ set_time_limit($iLoopTimeLimit);
+ $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
+ $sHtml = "\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('');
+ foreach($aGroups as $idx => $aObjects)
+ {
+ set_time_limit($iLoopTimeLimit);
+ $sListClass = get_class(current($aObjects));
+ $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
+ $sHtml = "\n";
+ $oPage->add($sHtml);
+ cmdbAbstractObject::DisplaySet($oPage, $oSet);
+ $oPage->p(''); // Some space
+ }
+ }
+ }
+ if ($operation == 'relation_attachment')
+ {
+ $sObjClass = utils::ReadParam('obj_class', '', false, 'class');
+ $iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
+
+ // Save the generated PDF as an attachment
+ $sPDF = $oPage->get_pdf();
+ $oPage = new ajax_page('');
+ $oAttachment = new Attachment();
+ $oAttachment->Set('item_class', $sObjClass);
+ $oAttachment->Set('item_id', $iObjKey);
+ $oDoc = new ormDocument($sPDF, 'application/pdf', $sTitle.'.pdf');
+ $oAttachment->Set('contents', $oDoc);
+ $iAttachmentId = $oAttachment->DBInsert();
+ $aRet = array(
+ 'status' => 'ok',
'att_id' => $iAttachmentId,
- );
- $oPage->add(json_encode($aRet));
- }
- break;
-
- case 'relation_json':
- require_once(APPROOT.'core/simplegraph.class.inc.php');
- require_once(APPROOT.'core/relationgraph.class.inc.php');
- require_once(APPROOT.'core/displayablegraph.class.inc.php');
- $sRelation = utils::ReadParam('relation', 'impacts');
- $sDirection = utils::ReadParam('direction', 'down');
- $iGroupingThreshold = utils::ReadParam('g', 5);
- $sPositions = utils::ReadParam('positions', null, false, 'raw_data');
- $aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
- $aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
- $sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data');
- $aPositions = null;
- if ($sPositions != null)
- {
- $aPositions = json_decode($sPositions, true);
- }
-
- // Get the list of source objects
- $aSources = utils::ReadParam('sources', array(), false, 'raw_data');
- $aSourceObjects = array();
- foreach($aSources as $sClass => $aIDs)
- {
- $oSearch = new DBObjectSearch($sClass);
- $oSearch->AddCondition('id', $aIDs, 'IN');
- $oSet = new DBObjectSet($oSearch);
- while($oObj = $oSet->Fetch())
- {
- $aSourceObjects[] = $oObj;
- }
- }
-
- // Get the list of excluded objects
- $aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
- $aExcludedObjects = array();
- foreach($aExcluded as $sClass => $aIDs)
- {
- $oSearch = new DBObjectSearch($sClass);
- $oSearch->AddCondition('id', $aIDs, 'IN');
- $oSet = new DBObjectSet($oSearch);
- while($oObj = $oSet->Fetch())
- {
- $aExcludedObjects[] = $oObj;
- }
- }
-
- // Compute the graph
- $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
- if ($sDirection == 'up')
- {
- $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
- }
- else
- {
- $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
- }
-
- // Remove excluded classes from the graph
- if (count($aExcludedClasses) > 0)
- {
- $oIterator = new RelationTypeIterator($oRelGraph, 'Node');
- foreach($oIterator as $oNode)
- {
- $oObj = $oNode->GetProperty('object');
- if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
- {
- $oRelGraph->FilterNode($oNode);
- }
- }
- }
-
- $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
- $oGraph->InitFromGraphviz();
- if ($aPositions != null)
- {
- $oGraph->UpdatePositions($aPositions);
- }
- $oPage->add($oGraph->GetAsJSON($sContextKey));
- $oPage->SetContentType('application/json');
- break;
-
- case '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 "Debug trace:
".$e->getTraceAsString()." \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());
+ }
+}
+?>
diff --git a/webservices/export-v2.php b/webservices/export-v2.php
new file mode 100644
index 000000000..4b02bf043
--- /dev/null
+++ b/webservices/export-v2.php
@@ -0,0 +1,678 @@
+
+
+/**
+ * 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= --auth_pwd= --expression= --query= [--arg_xxx=] [--no_localize=0|1] [--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(' ');
+ }
+ $oExporter->DisplayUsage($oP);
+ if (!Utils::IsModeCLI())
+ {
+ $oP->add('');
+ }
+ }
+ }
+ if (!Utils::IsModeCLI())
+ {
+ //$oP->add('');
+ }
+}
+
+function DisplayExpressionForm(WebPage $oP, $sAction, $sExpression = '', $sExceptionMessage = '')
+{
+ $oP->add(''.Dict::S('Core:BulkExport:ScopeDefinition').' ');
+ $oP->add('');
+ $oP->add(' ');
+ $sJSEmptyOQL = json_encode(Dict::S('Core:BulkExportMessageEmptyOQL'));
+ $sJSEmptyQueryId = json_encode(Dict::S('Core:BulkExportMessageEmptyPhrasebookEntry'));
+
+ $oP->add_ready_script(
+<<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('');
+ $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(' ');
+ $oExportSearch = DBObjectSearch::FromOQL($sExpression);
+ }
+ else
+ {
+ $oQuery = MetaModel::GetObject('QueryOQL', $sQueryId);
+ $oExportSearch = DBObjectSearch::FromOQL($oQuery->Get('oql'));
+ $oP->add(' ');
+ }
+ $aFormPartsByFormat = array();
+ $aAllFormParts = array();
+ if ($sFormat == null)
+ {
+ // No specific format chosen
+ $oP->add(''.Dict::S('Core:BulkExport:ExportFormatPrompt').' ');
+ $aSupportedFormats = BulkExport::FindSupportedFormats();
+ asort($aSupportedFormats);
+ foreach($aSupportedFormats as $sFormatCode => $sLabel)
+ {
+ $oP->add(''.htmlentities($sLabel, ENT_QUOTES, 'UTF-8').' ');
+ $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('
');
+ }
+ else
+ {
+ // One specific format was chosen
+ $oP->add(' ');
+
+ $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('');
+ $oExport->DisplayFormPart($oP, $sPartId);
+ $oP->add('
');
+ }
+ $oP->add(' ');
+ $oP->add(''.Dict::S('ExcelExport:PreparingExport').'
');
+ $oP->add(''.Dict::S('UI:Button:Export').' ');
+ $oP->add('');
+ $oP->add('
'.Dict::S('Core:BulkExport:ExportResult').'
');
+ $oP->add('
');
+ $oP->add('
');
+
+ $sJSParts = json_encode($aFormPartsByFormat);
+ $sJSCancel = json_encode(Dict::S('UI:Button:Cancel'));
+ $sJSClose = json_encode(Dict::S('UI:Button:Done'));
+
+ $oP->add_ready_script(
+<<add('');
+ $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(
+<< $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('
');
+ }
+ $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()." \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()." ".$e->getTraceAsString());
+ $oP->output();
+}
\ No newline at end of file