Bladeren bron

Integration of the Excel (XLSX) export feature. (Limitation: export.php takes into account neither the "fields" parameter nor the list of fields defined in the QueryPhrasebook when exporting in XLSX format)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3398 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 10 jaren geleden
bovenliggende
commit
b744b7b9ba

+ 536 - 0
application/excelexporter.class.inc.php

@@ -0,0 +1,536 @@
+<?php
+require_once('xlsxwriter.class.php');
+
+class ExcelExporter
+{
+	protected $sToken;
+	protected $aStatistics;
+	protected $sState;
+	protected $fStartTime;
+	protected $oSearch;
+	protected $aObjectsIDs;
+	protected $aTableHeaders;
+	protected $aAuthorizedClasses;
+	protected $iChunkSize = 1000;
+	protected $iPosition;
+	protected $sOutputFilePath;
+	protected $bAdvancedMode;
+	
+	public function __construct($sToken = null)
+	{
+		$this->aStatistics = array(
+			'objects_count' => 0,
+			'total_duration' => 0,
+			'data_retrieval_duration' => 0,
+			'excel_build_duration' => 0,
+			'excel_write_duration' => 0,
+			'peak_memory_usage' => 0,		
+		);
+		$this->fStartTime = microtime(true);
+		$this->oSearch = null;
+		
+		$this->sState = 'new';
+		$this->aObjectsIDs = array();
+		$this->iPosition = 0;
+		$this->aAuthorizedClasses = null;
+		$this->aTableHeaders = null;
+		$this->sOutputFilePath = null;
+		$this->bAdvancedMode = false;
+		$this->CheckDataDir();
+		if ($sToken == null)
+		{
+			$this->sToken = $this->GetNewToken();
+		}
+		else
+		{
+			$this->sToken = $sToken;
+			$this->ReloadState();
+		}
+	}
+	
+	public function __destruct()
+	{
+		if (($this->sState != 'done') && ($this->sState != 'error') && ($this->sToken != null))
+		{
+			// Operation in progress, save the state
+			$this->SaveState();
+		}
+		else
+		{
+			// Operation completed, cleanup the temp files
+			@unlink($this->GetStateFile());
+			@unlink($this->GetDataFile());
+		}
+		self::CleanupOldFiles();	
+	}
+	
+	public function SetChunkSize($iChunkSize)
+	{
+		$this->iChunkSize = $iChunkSize;	
+	}
+	
+	public function SetOutputFilePath($sDestFilePath)
+	{
+		$this->sOutputFilePath = $sDestFilePath;
+	}
+	
+	public function SetAdvancedMode($bAdvanced)
+	{
+		$this->bAdvancedMode = $bAdvanced;
+	}
+	
+	public function SaveState()
+	{
+		$aState = array(
+			'state' => $this->sState,
+			'statistics' => $this->aStatistics,
+			'filter' => $this->oSearch->serialize(),
+			'position' => $this->iPosition,
+			'chunk_size' => $this->iChunkSize,
+			'object_ids' => $this->aObjectsIDs,
+			'output_file_path' => $this->sOutputFilePath,
+			'advanced_mode' => $this->bAdvancedMode,
+		);
+		
+		file_put_contents($this->GetStateFile(), json_encode($aState));
+		
+		return $this->sToken;
+	}
+	
+	public function ReloadState()
+	{
+		if ($this->sToken == null)
+		{
+			throw new Exception('ExcelExporter not initialized with a token, cannot reload state');
+		}
+		
+		if (!file_exists($this->GetStateFile()))
+		{
+			throw new Exception("ExcelExporter: missing status file '".$this->GetStateFile()."', cannot reload state.");
+		}
+		$sJson = file_get_contents($this->GetStateFile());
+		$aState = json_decode($sJson, true);
+		if ($aState === null)
+		{
+			throw new Exception("ExcelExporter:corrupted status file '".$this->GetStateFile()."', not a JSON, cannot reload state.");
+		}
+		
+		$this->sState = $aState['state'];
+		$this->aStatistics = $aState['statistics'];
+		$this->oSearch = DBObjectSearch::unserialize($aState['filter']);
+		$this->iPosition = $aState['position'];
+		$this->iChunkSize = $aState['chunk_size'];
+		$this->aObjectsIDs = $aState['object_ids'];
+		$this->sOutputFilePath  = $aState['output_file_path'];
+		$this->bAdvancedMode = $aState['advanced_mode'];
+	}
+	
+	public function SetObjectList($oSearch)
+	{
+		$this->oSearch = $oSearch;
+	}
+	
+	public function Run()
+	{
+		$sCode = 'error';
+		$iPercentage = 100;
+		$sMessage = Dict::Format('ExcelExporter:ErrorUnexpected_State', $this->sState);
+		$fTime = microtime(true);
+		
+		try
+		{
+			switch($this->sState)
+			{
+				case 'new':
+				$oIDSet = new DBObjectSet($this->oSearch);
+				$oIDSet->OptimizeColumnLoad(array('id'));
+				$this->aObjectsIDs = array();
+				while($oObj = $oIDSet->Fetch())
+				{
+					$this->aObjectsIDs[] = $oObj->GetKey();
+				}		
+				$sCode = 'retrieving-data';
+				$iPercentage = 5;
+				$sMessage = Dict::S('ExcelExporter:RetrievingData');
+				$this->iPosition = 0;
+				$this->aStatistics['objects_count'] = count($this->aObjectsIDs);
+				$this->aStatistics['data_retrieval_duration'] += microtime(true) - $fTime;
+				
+				// The first line of the file is the "headers" specifying the label and the type of each column
+				$this->GetFieldsList($oIDSet, $this->bAdvancedMode);
+				$sRow = json_encode($this->aTableHeaders);
+				$hFile = @fopen($this->GetDataFile(), 'ab');
+				if ($hFile === false)
+				{
+					throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
+				}
+				fwrite($hFile, $sRow."\n");
+				fclose($hFile);	
+				
+				// Next state
+				$this->sState = 'retrieving-data';
+				break;
+				
+				case 'retrieving-data':
+				$oCurrentSearch = clone $this->oSearch;
+				$aIDs = array_slice($this->aObjectsIDs, $this->iPosition, $this->iChunkSize);
+				
+				$oCurrentSearch->AddCondition('id', $aIDs, 'IN');
+				$hFile = @fopen($this->GetDataFile(), 'ab');
+				if ($hFile === false)
+				{
+					throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
+				}
+				$oSet = new DBObjectSet($oCurrentSearch);
+				$this->GetFieldsList($oSet, $this->bAdvancedMode);
+				while($aObjects = $oSet->FetchAssoc())
+				{
+					$aRow = array();
+					foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
+					{
+						$oObj = $aObjects[$sAlias];
+						if ($this->bAdvancedMode)
+						{
+							$aRow[] = $oObj->GetKey();
+						}
+						foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
+						{
+							$value = $oObj->Get($sAttCodeEx);
+							if ($value instanceOf ormCaseLog)
+							{
+								// Extract the case log as text and remove the "===" which make Excel think that the cell contains a formula the next time you edit it!
+								$sExcelVal = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $value->GetText()));
+							}
+							else
+							{
+								$sExcelVal =  $oAttDef->GetEditValue($value, $oObj);					
+							}
+							$aRow[] = $sExcelVal;				
+						}
+					}
+					$sRow = json_encode($aRow);
+					fwrite($hFile, $sRow."\n");
+				}
+				fclose($hFile);
+				
+				if (($this->iPosition + $this->iChunkSize) > count($this->aObjectsIDs))
+				{
+					// Next state
+					$this->sState = 'building-excel';
+					$sCode = 'building-excel';
+					$iPercentage = 80;
+					$sMessage = Dict::S('ExcelExporter:BuildingExcelFile');
+				}
+				else
+				{
+					$sCode = 'retrieving-data';
+					$this->iPosition += $this->iChunkSize;
+					$iPercentage = 5 + round(75 * ($this->iPosition / count($this->aObjectsIDs)));
+					$sMessage = Dict::S('ExcelExporter:RetrievingData');			
+				}
+				break;
+				
+				case 'building-excel':
+				$hFile = @fopen($this->GetDataFile(), 'rb');
+				if ($hFile === false)
+				{
+					throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for reading.');
+				}
+				$sHeaders = fgets($hFile);
+				$aHeaders = json_decode($sHeaders, true);
+				
+				$aData = array();
+				while($sLine = fgets($hFile))
+				{
+					$aRow = json_decode($sLine);
+					$aData[] = $aRow;
+				}
+				fclose($hFile);
+				@unlink($this->GetDataFile());
+					
+				$fStartExcel = microtime(true);
+				$writer = new XLSXWriter();
+				$writer->setAuthor(UserRights::GetUserFriendlyName());
+				$writer->writeSheet($aData,'Sheet1', $aHeaders);
+				$fExcelTime = microtime(true) - $fStartExcel;
+				$this->aStatistics['excel_build_duration'] = $fExcelTime;
+				
+				$fTime = microtime(true);
+				$writer->writeToFile($this->GetExcelFilePath());
+				$fExcelSaveTime = microtime(true) - $fTime;
+				$this->aStatistics['excel_write_duration'] = $fExcelSaveTime;
+				
+				// Next state
+				$this->sState = 'done';
+				$sCode = 'done';
+				$iPercentage = 100;
+				$sMessage = Dict::S('ExcelExporter:Done');
+				break;
+				
+				case 'done':
+				$this->sState = 'done';
+				$sCode = 'done';
+				$iPercentage = 100;
+				$sMessage = Dict::S('ExcelExporter:Done');
+				break;
+			}
+		}
+		catch(Exception $e)
+		{
+			$sCode = 'error';
+			$sMessage = $e->getMessage();
+		}
+		
+		$this->aStatistics['total_duration'] += microtime(true) - $fTime;
+		$peak_memory = memory_get_peak_usage(true);
+		if ($peak_memory > $this->aStatistics['peak_memory_usage'])
+		{
+			$this->aStatistics['peak_memory_usage'] = $peak_memory;
+		}
+		
+		return array(
+			'code' => $sCode,
+			'message' => $sMessage,
+			'percentage' => $iPercentage,
+		);
+	}
+	
+	public function GetExcelFilePath()
+	{
+		if ($this->sOutputFilePath == null)
+		{
+			return APPROOT.'data/bulk_export/'.$this->sToken.'.xlsx';
+		}
+		else
+		{
+			return $this->sOutputFilePath;
+		}
+	}
+	
+	public static function GetExcelFileFromToken($sToken)
+	{
+		return @file_get_contents(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
+	}
+	
+	public static function CleanupFromToken($sToken)
+	{
+		@unlink(APPROOT.'data/bulk_export/'.$sToken.'.status');
+		@unlink(APPROOT.'data/bulk_export/'.$sToken.'.data');
+		@unlink(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
+	}
+	
+	public function Cleanup()
+	{
+		self::CleanupFromToken($this->sToken);
+	}
+	
+	/**
+	 * Delete all files in the data/bulk_export directory which are older than 1 day
+	 * unless a different delay is configured.
+	 */
+	public static function CleanupOldFiles()
+	{
+		$aFiles = glob(APPROOT.'data/bulk_export/*.*');
+		$iDelay = MetaModel::GetConfig()->Get('xlsx_exporter_cleanup_old_files_delay');
+		
+		if($iDelay > 0)
+		{
+			foreach($aFiles as $sFile)
+			{
+				$iModificationTime = filemtime($sFile);
+				
+				if($iModificationTime < (time() - $iDelay))
+				{
+					// Temporary files older than one day are deleted
+					//echo "Supposed to delete: '".$sFile." (Unix Modification Time: $iModificationTime)'\n";
+					@unlink($sFile);
+				}
+			}
+		}
+	}
+	
+	public function DisplayStatistics(Page $oPage)
+	{
+		$aStats = array(
+				'Number of objects exported' => $this->aStatistics['objects_count'],
+				'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
+				'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
+				'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
+				'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
+				'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
+		);
+		
+		if ($oPage instanceof CLIPage)
+		{
+			$oPage->add($this->GetStatistics('text'));
+		}
+		else
+		{
+			$oPage->add($this->GetStatistics('html'));
+		}
+	}
+	
+	public function GetStatistics($sFormat = 'html')
+	{
+		$sStats = '';
+		$aStats = array(
+				'Number of objects exported' => $this->aStatistics['objects_count'],
+				'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
+				'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
+				'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
+				'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
+				'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
+		);
+		
+		if ($sFormat == 'text')
+		{
+			foreach($aStats as $sLabel => $sValue)
+			{
+				$sStats .= "+------------------------------+----------+\n";
+				$sStats .= sprintf("|%-30s|%10s|\n", $sLabel, $sValue);
+			}
+			$sStats .= "+------------------------------+----------+";
+		}
+		else
+		{
+			$sStats .= '<table><tbody>';
+			foreach($aStats as $sLabel => $sValue)
+			{
+				$sStats .= "<tr><td>$sLabel</td><td>$sValue</td></tr>";
+			}
+			$sStats .= '</tbody></table>';
+			
+		}
+		return $sStats;
+	}
+	
+	public static function HumanDisplay($iSize)
+	{
+		$aUnits = array('B','KB','MB','GB','TB','PB');
+		return @round($iSize/pow(1024,($i=floor(log($iSize,1024)))),2).' '.$aUnits[$i];
+	}
+	
+	protected function CheckDataDir()
+	{
+		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.');
+		}
+	}
+	
+	protected function GetStateFile($sToken = null)
+	{
+		if ($sToken == null)
+		{
+			$sToken = $this->sToken;
+		}
+		return APPROOT."data/bulk_export/$sToken.status";
+	}
+	
+	protected function GetDataFile()
+	{
+		return APPROOT.'data/bulk_export/'.$this->sToken.'.data';
+	}
+	
+	protected function GetNewToken()
+	{
+		$iNum = rand();
+		do
+		{
+			$iNum++;
+			$sToken = sprintf("%08x", $iNum);
+			$sFileName = $this->GetStateFile($sToken);
+			$hFile = @fopen($sFileName, 'x');
+		}
+		while($hFile === false);
+		
+		fclose($hFile);
+		return $sToken;
+	}
+	
+	protected function GetFieldsList($oSet, $bFieldsAdvanced = false, $bLocalize = true, $aFields = null)
+	{
+		$this->aFieldsList = array();
+	
+		$oAppContext = new ApplicationContext();
+		$aClasses = $oSet->GetFilter()->GetSelectedClasses();
+		$this->aAuthorizedClasses = array();
+		foreach($aClasses as $sAlias => $sClassName)
+		{
+			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
+			{
+				$this->aAuthorizedClasses[$sAlias] = $sClassName;
+			}
+		}
+		$aAttribs = array();
+		$this->aTableHeaders = array();
+		foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
+		{
+			$aList[$sAlias] = array();
+	
+			foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
+			{
+				if (is_null($aFields) || (count($aFields) == 0))
+				{
+					// Standard list of attributes (no link sets)
+					if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
+					{
+						$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
+						
+						if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
+						{
+							if ($bFieldsAdvanced)
+							{
+								$aList[$sAlias][$sAttCodeEx] = $oAttDef;
+	
+								if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE))
+								{
+							  		$sRemoteClass = $oAttDef->GetTargetClass();
+									foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode)
+								  	{
+										$this->aFieldsList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode);
+								  	}
+								}
+							}
+						}
+						else
+						{
+							// Any other attribute
+							$this->aFieldsList[$sAlias][$sAttCodeEx] = $oAttDef;
+						}
+					}
+				}
+				else
+				{
+					// User defined list of attributes
+					if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields))
+					{
+						$this->aFieldsList[$sAlias][$sAttCode] = $oAttDef;
+					}
+				}
+			}
+			if ($bFieldsAdvanced)
+			{
+				$this->aTableHeaders['id'] = '0';
+			}
+			foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
+			{
+				$sLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx, isset($aParams['showMandatoryFields'])) : $sAttCodeEx;
+				if($oAttDef instanceof AttributeDateTime)
+				{
+					$this->aTableHeaders[$sLabel] = 'datetime';
+				}
+				else
+				{
+					$this->aTableHeaders[$sLabel] = 'string';
+				}
+			}
+		}
+	}
+}
+

+ 9 - 0
application/utils.inc.php

@@ -783,11 +783,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);
+			
 			$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:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL')"),
 				new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"),
 			);
@@ -802,11 +807,15 @@ class utils
 			$sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj));
 			$oAppContext = new ApplicationContext();
 			$sContext = $oAppContext->GetForLink();
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
+			$sXlsxFilter = $param->GetFilter()->serialize();
+			$sXlsxJSFilter = addslashes($sXlsxFilter);
 			$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()),
 			);
 			break;
 

+ 456 - 0
application/xlsxwriter.class.php

@@ -0,0 +1,456 @@
+<?php
+/* @author Mark Jones
+ * @license MIT License
+ * */
+
+if (!class_exists('ZipArchive')) { throw new Exception('ZipArchive not found'); }
+
+Class XLSXWriter
+{
+	//------------------------------------------------------------------
+	protected $author ='Doc Author';
+	protected $sheets_meta = array();
+	protected $shared_strings = array();//unique set
+	protected $shared_string_count = 0;//count of non-unique references to the unique set
+	protected $temp_files = array();
+
+	public function __construct(){}
+	public function setAuthor($author='') { $this->author=$author; }
+
+	public function __destruct()
+	{
+		if (!empty($this->temp_files)) {
+			foreach($this->temp_files as $temp_file) {
+				@unlink($temp_file);
+			}
+		}
+	}
+	
+	protected function tempFilename()
+	{
+		$filename = tempnam("/tmp", "xlsx_writer_");
+		$this->temp_files[] = $filename;
+		return $filename;
+	}
+
+	public function writeToStdOut()
+	{
+		$temp_file = $this->tempFilename();
+		self::writeToFile($temp_file);
+		readfile($temp_file);
+	}
+
+	public function writeToString()
+	{
+		$temp_file = $this->tempFilename();
+		self::writeToFile($temp_file);
+		$string = file_get_contents($temp_file);
+		return $string;
+	}
+
+	public function writeToFile($filename)
+	{
+		@unlink($filename);//if the zip already exists, overwrite it
+		$zip = new ZipArchive();
+		if (empty($this->sheets_meta))                  { self::log("Error in ".__CLASS__."::".__FUNCTION__.", no worksheets defined."); return; }
+		if (!$zip->open($filename, ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
+		
+		$zip->addEmptyDir("docProps/");
+		$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
+		$zip->addFromString("docProps/core.xml", self::buildCoreXML());
+
+		$zip->addEmptyDir("_rels/");
+		$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
+
+		$zip->addEmptyDir("xl/worksheets/");
+		foreach($this->sheets_meta as $sheet_meta) {
+			$zip->addFile($sheet_meta['filename'], "xl/worksheets/".$sheet_meta['xmlname'] );
+		}
+		if (!empty($this->shared_strings)) {
+			$zip->addFile($this->writeSharedStringsXML(), "xl/sharedStrings.xml" );  //$zip->addFromString("xl/sharedStrings.xml",     self::buildSharedStringsXML() );
+		}
+		$zip->addFromString("xl/workbook.xml"         , self::buildWorkbookXML() );
+		$zip->addFile($this->writeStylesXML(), "xl/styles.xml" );  //$zip->addFromString("xl/styles.xml"           , self::buildStylesXML() );
+		$zip->addFromString("[Content_Types].xml"     , self::buildContentTypesXML() );
+
+		$zip->addEmptyDir("xl/_rels/");
+		$zip->addFromString("xl/_rels/workbook.xml.rels", self::buildWorkbookRelsXML() );
+		$zip->close();
+	}
+
+	
+	public function writeSheet(array $data, $sheet_name='', array $header_types=array() )
+	{
+		$data = empty($data) ? array( array('') ) : $data;
+		
+		$sheet_filename = $this->tempFilename();
+		$sheet_default = 'Sheet'.(count($this->sheets_meta)+1);
+		$sheet_name = !empty($sheet_name) ? $sheet_name : $sheet_default;
+		$this->sheets_meta[] = array('filename'=>$sheet_filename, 'sheetname'=>$sheet_name ,'xmlname'=>strtolower($sheet_default).".xml" );
+
+		$header_offset = empty($header_types) ? 0 : 1;
+		$row_count = count($data) + $header_offset;
+		$column_count = count($data[self::array_first_key($data)]);
+		$max_cell = self::xlsCell( $row_count-1, $column_count-1 );
+
+		$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);
+
+		$fd = fopen($sheet_filename, "w+");
+		if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
+		
+		fwrite($fd,'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
+		fwrite($fd,'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">');
+		fwrite($fd,    '<sheetPr filterMode="false">');
+		fwrite($fd,        '<pageSetUpPr fitToPage="false"/>');
+		fwrite($fd,    '</sheetPr>');
+		fwrite($fd,    '<dimension ref="A1:'.$max_cell.'"/>');
+		fwrite($fd,    '<sheetViews>');
+		fwrite($fd,        '<sheetView colorId="64" defaultGridColor="true" rightToLeft="false" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="'.$tabselected.'" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
+		fwrite($fd,            '<selection activeCell="A1" activeCellId="0" pane="topLeft" sqref="A1"/>');
+		fwrite($fd,        '</sheetView>');
+		fwrite($fd,    '</sheetViews>');
+		fwrite($fd,    '<cols>');
+		fwrite($fd,        '<col collapsed="false" hidden="false" max="1025" min="1" style="0" width="19"/>');
+		fwrite($fd,    '</cols>');
+		fwrite($fd,    '<sheetData>');
+		if (!empty($header_row))
+		{
+			fwrite($fd, '<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="'.(1).'">');
+			foreach($header_row as $k=>$v)
+			{
+				$this->writeCell($fd, 0, $k, $v, $cell_format='string');
+			}
+			fwrite($fd, '</row>');
+		}
+		foreach($data as $i=>$row)
+		{
+			fwrite($fd, '<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="'.($i+$header_offset+1).'">');
+			foreach($row as $k=>$v)
+			{
+				$this->writeCell($fd, $i+$header_offset, $k, $v, $cell_formats_arr[$k]);
+			}
+			fwrite($fd, '</row>');
+		}
+		fwrite($fd,    '</sheetData>');
+		fwrite($fd,    '<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
+		fwrite($fd,    '<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
+		fwrite($fd,    '<pageSetup blackAndWhite="false" cellComments="none" copies="1" draft="false" firstPageNumber="1" fitToHeight="1" fitToWidth="1" horizontalDpi="300" orientation="portrait" pageOrder="downThenOver" paperSize="1" scale="100" useFirstPageNumber="true" usePrinterDefaults="false" verticalDpi="300"/>');
+		fwrite($fd,    '<headerFooter differentFirst="false" differentOddEven="false">');
+		fwrite($fd,        '<oddHeader>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12&amp;A</oddHeader>');
+		fwrite($fd,        '<oddFooter>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12Page &amp;P</oddFooter>');
+		fwrite($fd,    '</headerFooter>');
+		fwrite($fd,'</worksheet>');
+		fclose($fd);
+	}
+
+	protected function writeCell($fd, $row_number, $column_number, $value, $cell_format)
+	{
+		static $styles = array('money'=>1,'dollar'=>1,'datetime'=>2,'date'=>3,'string'=>0);
+		$cell = self::xlsCell($row_number, $column_number);
+		$s = isset($styles[$cell_format]) ? $styles[$cell_format] : '0';
+		
+		if (is_numeric($value)) {
+			fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.($value*1).'</v></c>');//int,float, etc
+		} else if ($cell_format=='date') {
+			fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');
+		} else if ($cell_format=='datetime') {
+			if ($value === '') {
+				fwrite($fd,'<c r="'.$cell.'" s="0"/>');
+			} else {
+				fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.self::convert_date_time($value).'</v></c>');
+			}			
+		} else if ($value==''){
+			fwrite($fd,'<c r="'.$cell.'" s="'.$s.'"/>');
+		} else if ($value{0}=='='){
+			fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="s"><f>'.self::xmlspecialchars($value).'</f></c>');
+		} else if ($value!==''){
+			fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="s"><v>'.self::xmlspecialchars($this->setSharedString($value)).'</v></c>');
+		}
+	}
+
+	protected function writeStylesXML()
+	{
+		$tempfile = $this->tempFilename();
+		$fd = fopen($tempfile, "w+");
+		if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
+		fwrite($fd, '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
+		fwrite($fd, '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
+		fwrite($fd, '<numFmts count="4">');
+		fwrite($fd, 		'<numFmt formatCode="GENERAL" numFmtId="164"/>');
+		fwrite($fd, 		'<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
+		fwrite($fd, 		'<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
+		fwrite($fd, 		'<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
+		fwrite($fd, '</numFmts>');
+		fwrite($fd, '<fonts count="4">');
+		fwrite($fd, 		'<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
+		fwrite($fd, 		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		fwrite($fd, 		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		fwrite($fd, 		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		fwrite($fd, '</fonts>');
+		fwrite($fd, '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>');
+		fwrite($fd, '<borders count="1"><border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border></borders>');
+		fwrite($fd, 	'<cellStyleXfs count="15">');
+		fwrite($fd, 		'<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
+		fwrite($fd, 		'<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
+		fwrite($fd, 		'<protection hidden="false" locked="true"/>');
+		fwrite($fd, 		'</xf>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		//fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
+		//fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
+		//fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
+		//fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
+		//fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
+		fwrite($fd, 	'</cellStyleXfs>');
+		fwrite($fd, 	'<cellXfs count="4">');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
+		fwrite($fd, 		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
+		fwrite($fd, 	'</cellXfs>');
+		fwrite($fd, 	'<cellStyles count="1">');
+		fwrite($fd, 		'<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
+		//fwrite($fd, 		'<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
+		//fwrite($fd, 		'<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
+		//fwrite($fd, 		'<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
+		//fwrite($fd, 		'<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
+		//fwrite($fd, 		'<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
+		fwrite($fd, 	'</cellStyles>');
+		fwrite($fd, '</styleSheet>');
+		fclose($fd);
+		return $tempfile;
+	}
+
+	protected function setSharedString($v)
+	{
+		// Strip control characters which Excel does not seem to like...
+		$v = preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F]/u', '', $v);
+		if (isset($this->shared_strings[$v]))
+		{
+			$string_value = $this->shared_strings[$v];
+		}
+		else
+		{
+			$string_value = count($this->shared_strings);
+			$this->shared_strings[$v] = $string_value;
+		}
+		$this->shared_string_count++;//non-unique count
+		return $string_value;
+	}
+
+	protected function writeSharedStringsXML()
+	{
+		$tempfile = $this->tempFilename();
+		$fd = fopen($tempfile, "w+");
+		if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
+		
+		fwrite($fd,'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
+		fwrite($fd,'<sst count="'.($this->shared_string_count).'" uniqueCount="'.count($this->shared_strings).'" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
+		foreach($this->shared_strings as $s=>$c)
+		{
+			fwrite($fd,'<si><t>'.self::xmlspecialchars($s).'</t></si>');
+		}
+		fwrite($fd, '</sst>');
+		fclose($fd);
+		return $tempfile;
+	}
+
+	protected function buildAppXML()
+	{
+		$app_xml="";
+		$app_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
+		$app_xml.='<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime></Properties>';
+		return $app_xml;
+	}
+
+	protected function buildCoreXML()
+	{
+		$core_xml="";
+		$core_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
+		$core_xml.='<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
+		$core_xml.='<dcterms:created xsi:type="dcterms:W3CDTF">'.date("Y-m-d\TH:i:s.00\Z").'</dcterms:created>';//$date_time = '2013-07-25T15:54:37.00Z';
+		$core_xml.='<dc:creator>'.self::xmlspecialchars($this->author).'</dc:creator>';
+		$core_xml.='<cp:revision>0</cp:revision>';
+		$core_xml.='</cp:coreProperties>';
+		return $core_xml;
+	}
+
+	protected function buildRelationshipsXML()
+	{
+		$rels_xml="";
+		$rels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
+		$rels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
+		$rels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
+		$rels_xml.='<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>';
+		$rels_xml.='<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>';
+		$rels_xml.="\n";
+		$rels_xml.='</Relationships>';
+		return $rels_xml;
+	}
+
+	protected function buildWorkbookXML()
+	{
+		$workbook_xml="";
+		$workbook_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
+		$workbook_xml.='<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
+		$workbook_xml.='<fileVersion appName="Calc"/><workbookPr backupFile="false" showObjects="all" date1904="false"/><workbookProtection/>';
+		$workbook_xml.='<bookViews><workbookView activeTab="0" firstSheet="0" showHorizontalScroll="true" showSheetTabs="true" showVerticalScroll="true" tabRatio="212" windowHeight="8192" windowWidth="16384" xWindow="0" yWindow="0"/></bookViews>';
+		$workbook_xml.='<sheets>';
+		foreach($this->sheets_meta as $i=>$sheet_meta) {
+			$workbook_xml.='<sheet name="'.self::xmlspecialchars($sheet_meta['sheetname']).'" sheetId="'.($i+1).'" state="visible" r:id="rId'.($i+2).'"/>';
+		}
+		$workbook_xml.='</sheets>';
+		$workbook_xml.='<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/></workbook>';
+		return $workbook_xml;
+	}
+
+	protected function buildWorkbookRelsXML()
+	{
+		$wkbkrels_xml="";
+		$wkbkrels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
+		$wkbkrels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
+		$wkbkrels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
+		foreach($this->sheets_meta as $i=>$sheet_meta) {
+			$wkbkrels_xml.='<Relationship Id="rId'.($i+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/'.($sheet_meta['xmlname']).'"/>';
+		}
+		if (!empty($this->shared_strings)) {
+			$wkbkrels_xml.='<Relationship Id="rId'.(count($this->sheets_meta)+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>';
+		}
+		$wkbkrels_xml.="\n";
+		$wkbkrels_xml.='</Relationships>';
+		return $wkbkrels_xml;
+	}
+
+	protected function buildContentTypesXML()
+	{
+		$content_types_xml="";
+		$content_types_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
+		$content_types_xml.='<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
+		$content_types_xml.='<Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
+		$content_types_xml.='<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
+		foreach($this->sheets_meta as $i=>$sheet_meta) {
+			$content_types_xml.='<Override PartName="/xl/worksheets/'.($sheet_meta['xmlname']).'" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
+		}
+		if (!empty($this->shared_strings)) {
+			$content_types_xml.='<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>';
+		}
+		$content_types_xml.='<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
+		$content_types_xml.='<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
+		$content_types_xml.='<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>';
+		$content_types_xml.='<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>';
+		$content_types_xml.="\n";
+		$content_types_xml.='</Types>';
+		return $content_types_xml;
+	}
+
+	//------------------------------------------------------------------
+	/*
+	 * @param $row_number int, zero based
+	 * @param $column_number int, zero based
+	 * @return Cell label/coordinates, ex: A1, C3, AA42
+	 * */
+	public static function xlsCell($row_number, $column_number)
+	{
+		$n = $column_number;
+		for($r = ""; $n >= 0; $n = intval($n / 26) - 1) {
+			$r = chr($n%26 + 0x41) . $r;
+		}
+		return $r . ($row_number+1);
+	}
+	//------------------------------------------------------------------
+	public static function log($string)
+	{
+		file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
+	}
+	//------------------------------------------------------------------
+	public static function xmlspecialchars($val)
+	{
+		return str_replace("'", "&#39;", htmlspecialchars($val));
+	}
+	//------------------------------------------------------------------
+	public static function array_first_key(array $arr)
+	{
+		reset($arr);
+		$first_key = key($arr);
+		return $first_key;
+	}
+	//------------------------------------------------------------------
+	public static function convert_date_time($date_input) //thanks to Excel::Writer::XLSX::Worksheet.pm (perl)
+	{
+		$days    = 0;    # Number of days since epoch
+		$seconds = 0;    # Time expressed as fraction of 24h hours in seconds
+		$year=$month=$day=0;
+		$hour=$min  =$sec=0;
+
+		$date_time = $date_input;
+		if (preg_match("/(\d{4})\-(\d{2})\-(\d{2})/", $date_time, $matches))
+		{
+			list($junk,$year,$month,$day) = $matches;
+		}
+		if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", $date_time, $matches))
+		{
+			list($junk,$hour,$min,$sec) = $matches;
+			$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
+		}
+
+		//using 1900 as epoch, not 1904, ignoring 1904 special case
+		
+		# Special cases for Excel.
+		if ("$year-$month-$day"=='1899-12-31')  return $seconds      ;    # Excel 1900 epoch
+		if ("$year-$month-$day"=='1900-01-00')  return $seconds      ;    # Excel 1900 epoch
+		if ("$year-$month-$day"=='1900-02-29')  return 60 + $seconds ;    # Excel false leapday
+
+		# We calculate the date by calculating the number of days since the epoch
+		# and adjust for the number of leap days. We calculate the number of leap
+		# days by normalising the year in relation to the epoch. Thus the year 2000
+		# becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
+		$epoch  = 1900;
+		$offset = 0;
+		$norm   = 300;
+		$range  = $year - $epoch;
+
+		# Set month days and check for leap year.
+		$leap = (($year % 400 == 0) || (($year % 4 == 0) && ($year % 100)) ) ? 1 : 0;
+		$mdays = array( 31, ($leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
+
+		# Some boundary checks
+		if($year < $epoch || $year > 9999) return 0;
+		if($month < 1     || $month > 12)  return 0;
+		if($day < 1       || $day > $mdays[ $month - 1 ]) return 0;
+
+		# Accumulate the number of days since the epoch.
+		$days = $day;    # Add days for current month
+		$days += array_sum( array_slice($mdays, 0, $month-1 ) );    # Add days for past months
+		$days += $range * 365;                      # Add days for past years
+		$days += intval( ( $range ) / 4 );             # Add leapdays
+		$days -= intval( ( $range + $offset ) / 100 ); # Subtract 100 year leapdays
+		$days += intval( ( $range + $offset + $norm ) / 400 );  # Add 400 year leapdays
+		$days -= $leap;                                      # Already counted above
+
+		# Adjust for Excel erroneously treating 1900 as a leap year.
+		if ($days > 59) { $days++;}
+
+		return $days + $seconds;
+	}
+	//------------------------------------------------------------------
+}
+
+
+
+
+
+

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

@@ -777,6 +777,22 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => false,
 		),
+		'xlsx_exporter_cleanup_old_files_delay' => array(
+			'type' => 'int',
+			'description' => 'Delay (in seconds) for which to let the exported XLSX files on the server so that the user who initiated the export can download the result',
+			'default' => 86400,
+			'value' => '',
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
+		'xlsx_exporter_memory_limit' => array(
+			'type' => 'string',
+			'description' => 'Memory limit to use when (interactively) exporting data to Excel',
+			'default' => '2048M', // Huuuuuuge 2GB!
+			'value' => '',
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),		
 	);
 
 	public function IsProperty($sPropCode)

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

@@ -1217,5 +1217,16 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:About:Support' => 'Support information',
 	'UI:About:Licenses' => 'Licenses',
 	'UI:About:Modules' => 'Installed modules',
+	
+	'ExcelExporter:ExportMenu' => 'Excel Export...',
+	'ExcelExporter:ExportDialogTitle' => 'Excel Export',
+	'ExcelExporter:ExportButton' => 'Export',
+	'ExcelExporter:DownloadButton' => 'Download %1$s',
+	'ExcelExporter:RetrievingData' => 'Retrieving data...',
+	'ExcelExporter:BuildingExcelFile' => 'Building the Excel file...',
+	'ExcelExporter:Done' => 'Done.',
+	'ExcelExport:AutoDownload' => 'Start the download automatically when the export is ready',
+	'ExcelExport:PreparingExport' => 'Preparing the export...',
+	'ExcelExport:Statistics' => 'Statistics',
 ));
 ?>

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

@@ -1057,5 +1057,16 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:About:Support' => 'Informations pour le support',
 	'UI:About:Licenses' => 'Licences',
 	'UI:About:Modules' => 'Modules installés',
+	
+	'ExcelExporter:ExportMenu' => 'Exporter pour Excel...',
+	'ExcelExporter:ExportDialogTitle' => 'Export au format Excel',
+	'ExcelExporter:ExportButton' => 'Exporter',
+	'ExcelExporter:DownloadButton' => 'Télécharger %1$s',
+	'ExcelExporter:RetrievingData' => 'Récupération des données...',
+	'ExcelExporter:BuildingExcelFile' => 'Construction du fichier Excel...',
+	'ExcelExporter:Done' => 'Terminé.',
+	'ExcelExport:AutoDownload' => 'Téléchargement automatique dès que le fichier est prêt',
+	'ExcelExport:PreparingExport' => 'Préparation de l\'export...',
+	'ExcelExport:Statistics' => 'Statistiques',	
 ));
 ?>

BIN
images/xlsx.png


+ 185 - 0
js/xlsx-export.js

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

+ 27 - 6
pages/ajax.csvimport.php

@@ -418,6 +418,7 @@ EOF
 
 		case 'get_csv_template':
 		$sClassName = utils::ReadParam('class_name');
+		$sFormat = utils::ReadParam('format', 'csv');
 		if (MetaModel::IsValidClass($sClassName))
 		{
 			$oSearch = new DBObjectSearch($sClassName);
@@ -429,17 +430,37 @@ EOF
 			$sDisposition = utils::ReadParam('disposition', 'inline');
 			if ($sDisposition == 'attachment')
 			{
-				$oPage = new CSVPage("");
-				$oPage->add_header("Content-type: text/csv; charset=utf-8");
-				$oPage->add_header("Content-disposition: attachment; filename=\"{$sClassDisplayName}.csv\"");
-				$oPage->no_cache();		
-				$oPage->add($sResult);	
+				switch($sFormat)
+				{
+					case 'xlsx':
+					$oPage = new ajax_page("");
+					$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+					$oPage->SetContentDisposition('attachment', $sClassDisplayName.'.xlsx');
+					require_once(APPROOT.'/application/excelexporter.class.inc.php');
+					$writer = new XLSXWriter();
+					$writer->setAuthor(UserRights::GetUserFriendlyName());
+					$aHeaders = array( 0 => explode(',', $sResult)); // comma is the default separator
+					$writer->writeSheet($aHeaders, $sClassDisplayName, array());
+					$oPage->add($writer->writeToString());
+					break;
+				
+					case 'csv':
+					default:
+					$oPage = new CSVPage("");
+					$oPage->add_header("Content-type: text/csv; charset=utf-8");
+					$oPage->add_header("Content-disposition: attachment; filename=\"{$sClassDisplayName}.csv\"");
+					$oPage->no_cache();		
+					$oPage->add($sResult);
+				}
 			}
 			else
 			{
 				$oPage = new ajax_page("");
 				$oPage->no_cache();
-				$oPage->add('<p style="text-align:center"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&class_name='.$sClassName.'"><img border="0" src="../images/csv.png"><br/>'.$sClassDisplayName.'.csv</a></p>');		
+				$oPage->add('<p style="text-align:center">');
+				$oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&class_name='.$sClassName.'"><img border="0" src="../images/csv.png"><br/>'.$sClassDisplayName.'.csv</a></div>');		
+				$oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&format=xlsx&class_name='.$sClassName.'"><img border="0" src="../images/xlsx.png"><br/>'.$sClassDisplayName.'.xlsx</a></div>');		
+				$oPage->add('</p>');		
 				$oPage->add('<p><textarea rows="5" cols="100">'.$sResult.'</textarea></p>');
 			}		
 		}

+ 114 - 0
pages/ajax.render.php

@@ -32,6 +32,7 @@ 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
 {
@@ -1608,6 +1609,119 @@ EOF
 		);
 		break;
 
+		case 'xlsx_export_dialog':
+		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+		$oPage->SetContentType('text/html');
+		$oPage->add(
+<<<EOF
+<style>
+ .ui-progressbar {
+	position: relative;
+}
+.progress-label {
+	position: absolute;
+	left: 50%;
+	top: 1px;
+	font-size: 11pt;
+}
+.download-form button {
+	display:block;
+	margin-left: auto;
+	margin-right: auto;
+	margin-top: 2em;
+}
+.ui-progressbar-value {
+	background: url(../setup/orange-progress.gif);
+}
+.progress-bar {
+	height: 20px;
+}
+.statistics > div {
+	padding-left: 16px;
+	cursor: pointer;
+	font-size: 10pt;
+	background: url(../images/minus.gif) 0 2px no-repeat;
+}				
+.statistics > div.closed {
+	padding-left: 16px;
+	background: url(../images/plus.gif) 0 2px no-repeat;
+}
+				
+.statistics .closed .stats-data {
+	display: none;
+}
+.stats-data td {
+	padding-right: 5px;
+}
+</style>				
+EOF
+		);
+		$oPage->add('<div id="XlsxExportDlg">');
+		$oPage->add('<div class="export-options">');
+		$oPage->add('<p><input type="checkbox" id="export-advanced-mode"/>&nbsp;<label for="export-advanced-mode">'.Dict::S('UI:CSVImport:AdvancedMode').'</label></p>');
+		$oPage->add('<p style="font-size:10pt;margin-left:2em;margin-top:-0.5em;padding-bottom:1em;">'.Dict::S('UI:CSVImport:AdvancedMode+').'</p>');
+		$oPage->add('<p><input type="checkbox" id="export-auto-download" checked="checked"/>&nbsp;<label for="export-auto-download">'.Dict::S('ExcelExport:AutoDownload').'</label></p>');
+		$oPage->add('</div>');
+		$oPage->add('<div class="progress"><p class="status-message">'.Dict::S('ExcelExport:PreparingExport').'</p><div class="progress-bar"><div class="progress-label"></div></div></div>');
+		$oPage->add('<div class="statistics"><div class="stats-toggle closed">'.Dict::S('ExcelExport:Statistics').'<div class="stats-data"></div></div></div>');
+		$oPage->add('</div>');
+		$aLabels = array(
+			'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
+			'cancel_button' => Dict::S('UI:Button:Cancel'),
+			'export_button' => Dict::S('ExcelExporter:ExportButton'),
+			'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
+ 		);
+		$sJSLabels = json_encode($aLabels);
+		$sFilter = addslashes($sFilter);
+		$sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
+		$oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
+		break;
+		
+		case 'xlsx_start':
+		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
+		$bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
+		$oSearch = DBObjectSearch::unserialize($sFilter);
+		
+		$oExcelExporter = new ExcelExporter();
+		$oExcelExporter->SetObjectList($oSearch);
+		//$oExcelExporter->SetChunkSize(10); //Only for testing
+		$oExcelExporter->SetAdvancedMode($bAdvanced);
+		$sToken = $oExcelExporter->SaveState();
+		$oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
+		break;
+		
+		case 'xlsx_run':
+		$sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit');
+		ini_set('memory_limit', $sMemoryLimit);
+		ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes
+					
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		$oExcelExporter = new ExcelExporter($sToken);
+		$aStatus = $oExcelExporter->Run();
+		$aResults = array('status' => $aStatus['code'], 'percentage' =>  $aStatus['percentage'], 'message' =>  $aStatus['message']);
+		if ($aStatus['code'] == 'done')
+		{
+			$aResults['statistics'] = $oExcelExporter->GetStatistics('html');
+		}
+		$oPage->add(json_encode($aResults));
+		break;
+		
+		case 'xlsx_download':
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+		$oPage->SetContentDisposition('attachment', 'export.xlsx');
+		$sFileContent = ExcelExporter::GetExcelFileFromToken($sToken);
+		$oPage->add($sFileContent);
+		ExcelExporter::CleanupFromToken($sToken);
+		break;
+		
+		case 'xlsx_abort':
+		// Stop & cleanup an export...
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		ExcelExporter::CleanupFromToken($sToken);
+		break;		
+		
+
 		default:
 		$oPage->p("Invalid query.");
 	}

+ 251 - 163
portal/index.php

@@ -60,8 +60,7 @@ function ValidateObject($oObject)
 	if (IsPowerUser())
 	{
 		$sValidationDefine = 'PORTAL_'.strtoupper(get_class($oObject)).'_DISPLAY_POWERUSER_QUERY';
-	}
-	else
+	} else
 	{
 		$sValidationDefine = 'PORTAL_'.strtoupper(get_class($oObject)).'_DISPLAY_QUERY';
 	}
@@ -162,10 +161,17 @@ function DisplayMainMenu(WebPage $oP)
 	$oP->AddMenuButton('showongoing', 'Portal:ShowOngoing', '../portal/index.php?operation=show_ongoing');
 	$oP->AddMenuButton('newrequest', 'Portal:CreateNewRequest', '../portal/index.php?operation=create_request');
 	$oP->AddMenuButton('showclosed', 'Portal:ShowClosed', '../portal/index.php?operation=show_closed');
-	if (UserRights::CanChangePassword())
+	$oP->AddMenuButton('showtoapprove', 'Portal:ShowToApprove', '../portal/index.php?operation=show_toapprove');
+
+	if (isKeyUser()) {
+	   $oP->AddMenuButton('showtoresolve', 'Portal:ShowToResolve', '../portal/index.php?operation=show_toresolve');
+	}
+/* THEBEN
+ 	if (UserRights::CanChangePassword())
 	{
 		$oP->AddMenuButton('change_pwd', 'Portal:ChangeMyPassword', '../portal/index.php?loginop=change_pwd');
-	}
+	}  */
+	
 }
 
 /**
@@ -184,147 +190,61 @@ function ShowOngoingTickets(WebPage $oP)
 	$oP->add("<h1 id=\"#resolved_requests\">".Dict::S('Portal:ResolvedRequests')."</h1>\n");
 	ListResolvedRequests($oP);
 	$oP->add("</div>\n");
+	
+	$oP->add("<div id=\"#requests_to_approve\">\n");
+	$oP->add("<h1 id=\"#title_requests_to_approve\">".Dict::S('Portal:RequestsToApprove')."</h1>\n");
+	ListRequestsToApprove($oP);
+	$oP->add("</div>\n");
+	
+	if (isKeyUser()) {
+	$oP->add("<div id=\"open_requests\">\n");
+	$oP->add("<h1 id=\"title_requests_to_resolve\">".Dict::S('Portal:RequestsToResolve')."</h1>\n");
+	ListRequestsToResolve($oP);
+	$oP->add("</div>\n");
+	}
 }
 
 /**
- * Displays the closed tickets
+ * Displays the tickets which need approval by mysel
  * @param WebPage $oP The current web page
  * @return void
  */
-function ShowClosedTickets(WebPage $oP)
+// =========================== THEBEN ==================================
+function ShowTicketsToApprove(WebPage $oP)
 {
-	$oP->add("<div id=\"#closed_tickets\">\n");
-	//$oP->add("<h1 id=\"#closed_tickets\">".Dict::S('Portal:ListClosedTickets')."</h1>\n");
-	ListClosedTickets($oP);
+	$oP->add("<div id=\"open_requests\">\n");
+	$oP->add("<h1 id=\"title_open_requests\">".Dict::S('Portal:RequestsToApprove')."</h1>\n");
+	ListRequestsToApprove($oP);
 	$oP->add("</div>\n");
+
 }
 
 /**
- * Displays the form to select a Service Category Id (among the valid ones for the specified user Organization)
- * @param WebPage $oP Web page for the form output
- * @param Organization $oUserOrg The organization of the current user
+ * Displays the tickets which need approval by mysel
+ * @param WebPage $oP The current web page
  * @return void
  */
-function SelectServiceCategory($oP, $oUserOrg)
+// =========================== THEBEN ==================================
+function ShowTicketsToResolve(WebPage $oP)
 {
-	$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
-	
-	$oSearch = DBObjectSearch::FromOQL(PORTAL_SERVICECATEGORY_QUERY);
-	$oSearch->AllowAllData(); // In case the user has the rights on his org only
-	$oSet = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
-	if ($oSet->Count() == 1)
-	{
-		$oService = $oSet->Fetch();
-		$iSvcCategory = $oService->GetKey();
-		// Only one Category, skip this step in the wizard
-		SelectServiceSubCategory($oP, $oUserOrg, $iSvcCategory);
-	}
-	else
-	{
-		$oP->add("<div class=\"wizContainer\" id=\"form_select_service\">\n");
-		$oP->WizardFormStart('request_wizard', 1);
+	$oP->add("<div id=\"open_requests\">\n");
+	$oP->add("<h1 id=\"title_requests_to_resolve\">".Dict::S('Portal:RequestsToResolve')."</h1>\n");
+	ListRequestsToResolve($oP);
+	$oP->add("</div>\n");
 
-		$oP->add("<h1 id=\"select_category\">".Dict::S('Portal:SelectService')."</h1>\n");
-		$oP->add("<table>\n");
-		while($oService = $oSet->Fetch())
-		{
-			$id = $oService->GetKey();
-			$sChecked = "";
-			if (isset($aParameters['service_id']) && ($id == $aParameters['service_id']))
-			{
-				$sChecked = "checked";
-			}
-			$oP->p("<tr><td style=\"vertical-align:top\"><p><input name=\"attr_service_id\" $sChecked type=\"radio\" id=\"service_$id\" value=\"$id\"></p></td><td style=\"vertical-align:top\"><p><b><label for=\"service_$id\">".$oService->GetName()."</label></b></p>");
-			$oP->p("<p>".$oService->GetAsHTML('description')."</p></td></tr>");		
-		}
-		$oP->add("</table>\n");	
-	
-		$oP->DumpHiddenParams($aParameters, array('service_id'));
-		$oP->add("<input type=\"hidden\" name=\"operation\" value=\"create_request\">");
-		$oP->WizardFormButtons(BUTTON_NEXT | BUTTON_CANCEL); // NO back button since it's the first step of the Wizard
-		$oP->WizardFormEnd();
-		$oP->WizardCheckSelectionOnSubmit(Dict::S('Portal:PleaseSelectOneService'));
-		$oP->add("</div>\n");
-	}
 }
 
 /**
- * Displays the form to select a Service Subcategory Id (among the valid ones for the specified user Organization)
- * and based on the page's parameter 'service_id'
- * @param WebPage $oP Web page for the form output
- * @param Organization $oUserOrg The organization of the current user
- * @param $iSvcId Id of the selected service in case of pass-through (when there is only one service)
+ * Displays the closed tickets
+ * @param WebPage $oP The current web page
  * @return void
  */
-function SelectServiceSubCategory($oP, $oUserOrg, $iSvcId = null)
+function ShowClosedTickets(WebPage $oP)
 {
-	$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
-	if ($iSvcId == null)
-	{
-		$iSvcId = $aParameters['service_id'];
-	}
-	else
-	{
-		$aParameters['service_id'] = $iSvcId;
-	}
-	$iDefaultSubSvcId = isset($aParameters['servicesubcategory_id']) ? $aParameters['servicesubcategory_id'] : 0;
-
-	$iDefaultWizNext = 2;
-
-	$oSearch = DBObjectSearch::FromOQL(PORTAL_SERVICE_SUBCATEGORY_QUERY);
-	RestrictSubcategories($oSearch);
-	$oSearch->AllowAllData(); // In case the user has the rights on his org only
-	$oSet = new CMDBObjectSet($oSearch, array(), array('svc_id' => $iSvcId, 'org_id' => $oUserOrg->GetKey()));
-	if ($oSet->Count() == 1)
-	{
-		// Only one sub service, skip this step of the wizard
-		$oSubService = $oSet->Fetch();
-		$iSubSvdId = $oSubService->GetKey();
-		SelectRequestTemplate($oP, $oUserOrg, $iSvcId, $iSubSvdId);
-	}
-	else
-	{
-		$oServiceCategory = MetaModel::GetObject('Service', $iSvcId, false, true /* allow all data*/);
-		if (is_object($oServiceCategory))
-		{
-			$oP->add("<div class=\"wizContainer\" id=\"form_select_servicesubcategory\">\n");
-			$oP->add("<h1 id=\"select_subcategory\">".Dict::Format('Portal:SelectSubcategoryFrom_Service', $oServiceCategory->GetName())."</h1>\n");
-			$oP->WizardFormStart('request_wizard', $iDefaultWizNext);
-			$oP->add("<table>\n");
-			while($oSubService = $oSet->Fetch())
-			{
-				$id = $oSubService->GetKey();
-				$sChecked = "";
-				if ($id == $iDefaultSubSvcId)
-				{
-					$sChecked = "checked";
-				}
-	
-				$oP->add("<tr>");
-	
-				$oP->add("<td style=\"vertical-align:top\">");
-				$oP->add("<p><input name=\"attr_servicesubcategory_id\" $sChecked type=\"radio\" id=\"servicesubcategory_$id\" value=\"$id\"></p>");
-				$oP->add("</td>");
-	
-				$oP->add("<td style=\"vertical-align:top\">");
-				$oP->add("<p><b><label for=\"servicesubcategory_$id\">".$oSubService->GetName()."</label></b></p>");
-				$oP->add("<p>".$oSubService->GetAsHTML('description')."</p>");
-				$oP->add("</td>");
-				$oP->add("</tr>");
-			}
-			$oP->add("</table>\n");	
-			$oP->DumpHiddenParams($aParameters, array('servicesubcategory_id'));
-			$oP->add("<input type=\"hidden\" name=\"operation\" value=\"create_request\">");
-			$oP->WizardFormButtons(BUTTON_BACK | BUTTON_NEXT | BUTTON_CANCEL); //Back button automatically discarded if on the first page
-			$oP->WizardFormEnd();
-			$oP->WizardCheckSelectionOnSubmit(Dict::S('Portal:PleaseSelectAServiceSubCategory'));
-			$oP->add("</div>\n");
-		}
-		else
-		{
-			$oP->p("Error: Invalid Service: id = $iSvcId");
-		}
-	}
+	$oP->add("<div id=\"#closed_tickets\">\n");
+	//$oP->add("<h1 id=\"#closed_tickets\">".Dict::S('Portal:ListClosedTickets')."</h1>\n");
+	ListClosedTickets($oP);
+	$oP->add("</div>\n");
 }
 
 /**
@@ -437,21 +357,9 @@ function SelectRequestTemplate($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null
  * @param integer $iTemplateId The identifier of the template (fall through when there is only one template)
  * @return void
  */
-function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null, $iTemplateId = null)
+function RequestCreationForm($oP, $oUserOrg)
 {
 	$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
-	if (!is_null($iSvcId))
-	{
-		$aParameters['service_id'] = $iSvcId;
-	}
-	if (!is_null($iSubSvcId))
-	{
-		$aParameters['servicesubcategory_id'] = $iSubSvcId;
-	}
-	if (!is_null($iTemplateId))
-	{
-		$aParameters['template_id'] = $iTemplateId;
-	}
 	
 	$sDescription = '';
 	if (isset($aParameters['template_id']) && ($aParameters['template_id'] != 0))
@@ -475,25 +383,33 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
 		}
 	}
 
-	$oServiceCategory = MetaModel::GetObject('Service', $aParameters['service_id'], false, true /* allow all data*/);
-	$oServiceSubCategory = MetaModel::GetObject('ServiceSubcategory', $aParameters['servicesubcategory_id'], false, true /* allow all data*/);
-	if (is_object($oServiceCategory) && is_object($oServiceSubCategory))
+	//$oServiceCategory = MetaModel::GetObject('Service', $aParameters['service_id'], false, true /* allow all data*/);
+	//$oServiceSubCategory = MetaModel::GetObject('ServiceSubcategory', $aParameters['servicesubcategory_id'], false, true /* allow all data*/);
+	//if (is_object($oServiceCategory) && is_object($oServiceSubCategory))
 	{
-		$sClass = ComputeClass($oServiceSubCategory->GetKey());
+		$sClass = "UserRequest"; // ComputeClass($oServiceSubCategory->GetKey());
 		$oRequest = MetaModel::NewObject($sClass);
 		$oRequest->Set('org_id', $oUserOrg->GetKey());
-		$oRequest->Set('caller_id', UserRights::GetContactId());
-		$oRequest->Set('service_id', $aParameters['service_id']);
-		$oRequest->Set('servicesubcategory_id', $aParameters['servicesubcategory_id']);
+	 	$oRequest->Set('caller_id', UserRights::GetContactId());
+//	    $oRequest->Set('service_id', $aParameters['service_id']);
+//	    $oRequest->Set('servicesubcategory_id', $aParameters['servicesubcategory_id']);
 
-		$oAttDef = MetaModel::GetAttributeDef($sClass, 'service_id');
+/*		$oAttDef = MetaModel::GetAttributeDef($sClass, 'service_id');
 		$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceCategory->GetName());
 
 		$oAttDef = MetaModel::GetAttributeDef($sClass, 'servicesubcategory_id');
 		$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceSubCategory->GetName());
 
+*/
 		$aList = explode(',', GetConstant($sClass, 'FORM_ATTRIBUTES'));
-
+		
+		IssueLog::info("aList, FORM_ATTRIBUTES=".print_r($aList,true));
+		// TODO
+	/*	echo "<select name='userCharacters' id='userCharacter'>";
+		echo "<option value='1'>Kid Wonder</option>";
+		echo "<option value='3'>Oriel</option>";
+		echo "</select>";  */
+		
 		$iFlags = 0;
 		foreach($aList as $sAttCode)
 		{
@@ -507,8 +423,10 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
 		$aFieldsMap = array();
 		foreach($aList as $sAttCode)
 		{
+			IssueLog::Info("sAttCode=".$sAttCode);
 			$value = '';
 			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			IssueLog::Info("oAttDef=".print_r($oAttDef,true));
 			$iFlags = $oRequest->GetAttributeFlags($sAttCode);
 			if (isset($aParameters[$sAttCode]))
 			{
@@ -517,8 +435,10 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
 			$aArgs = array('this' => $oRequest);
 				
 			$sInputId = 'attr_'.$sAttCode;
+			IssueLog::Info("sInputId=".$sInputId);
 			$aFieldsMap[$sAttCode] = $sInputId;
 			$sValue = "<span id=\"field_{$sInputId}\">".$oRequest->GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $value, '', 'attr_'.$sAttCode, '', $iFlags, $aArgs).'</span>';
+			IssueLog::Info("sValue=".$sValue);
 			$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $sValue);
 		}
 		$aHidden = array();
@@ -527,6 +447,7 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
 			foreach ($aTemplateFields as $sAttCode =>  $oField)
 			{
 				$sValue = $oField->GetFormElement($oP, $sClass);
+				
 				if ($oField->Get('input_type') == 'hidden')
 				{
 					$aHidden[] = $sValue;
@@ -554,14 +475,14 @@ EOF
 		$oP->add_linked_script("../js/jquery.blockUI.js");
 		$oP->add("<div class=\"wizContainer\" id=\"form_request_description\">\n");
 		$oP->add("<h1 id=\"title_request_form\">".Dict::S('Portal:DescriptionOfTheRequest')."</h1>\n");
-		$oP->WizardFormStart('request_form', 4);
+		$oP->WizardFormStart('request_form', 1);
 
 		$oP->details($aDetails);
 
 		// Add hidden fields for known values, enabling dependant attributes to be computed correctly
 		//
 		foreach($oRequest->ListChanges() as $sAttCode => $value)
-		{
+		{   
 			if (!in_array($sAttCode, $aList))
 			{
 				$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
@@ -587,7 +508,7 @@ EOF
 		$oP->add("</div>\n");
 		$iFieldsCount = count($aFieldsMap);
 		$sJsonFieldsMap = json_encode($aFieldsMap);
-
+        // IssueLog::Info("sJsonFieldsMap=".$sJsonFieldsMap);
 		$oP->add_ready_script(
 <<<EOF
 		oWizardHelper.SetFieldsMap($sJsonFieldsMap);
@@ -601,11 +522,11 @@ EOF
 EOF
 );
 	}
-	else
-	{
+//	else
+//	{
 		// User not authorized to use this service ?
 		//ShowOngoingTickets($oP);
-	}
+//	}
 }
 
 /**
@@ -615,7 +536,7 @@ EOF
  * @return void
  */
 function DoCreateRequest($oP, $oUserOrg)
-{
+{   IssueLog::Info("DoCreateRequest");
 	$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
 	$sTransactionId = utils::ReadPostedParam('transaction_id', '');
 	if (!utils::IsTransactionValid($sTransactionId))
@@ -713,8 +634,8 @@ function CreateRequest(WebPage $oP, Organization $oUserOrg)
 {
 	switch($oP->GetWizardStep())
 	{
-		case 0:
-		default:
+//		case 0:
+/*		default:
 		SelectServiceCategory($oP, $oUserOrg);
 		break;
 		
@@ -724,13 +645,13 @@ function CreateRequest(WebPage $oP, Organization $oUserOrg)
 
 		case 2:
 		SelectRequestTemplate($oP, $oUserOrg);
-		break;
+		break;  */
 		
-		case 3:
+		case 0:
 		RequestCreationForm($oP, $oUserOrg);
 		break;
 
-		case 4:
+		case 1:
 		DoCreateRequest($oP, $oUserOrg);
 		break;
 	}
@@ -826,6 +747,54 @@ function ListResolvedRequests(WebPage $oP)
 }
 
 /**
+ * Lists all the currently resolved (not yet closed) User Requests for the current user
+ * @param WebPage $oP The current web page
+ * @return void
+ */
+// ========================= THEBEN ===================================
+function ListRequestsToApprove(WebPage $oP)
+{
+	$oUserOrg = GetUserOrg();
+
+	$aClassToSet = array();
+	foreach (GetTicketClasses() as $sClass)
+	{
+		$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status = 'waiting_for_approval'";
+		$oSearch = DBObjectSearch::FromOQL($sOQL);
+		$iUser = UserRights::GetContactId();
+		
+		$oSearch->AddCondition('approver_id', $iUser);
+
+		$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
+	}
+	DisplayRequestLists($oP, $aClassToSet);
+}
+
+/**
+ * Lists all the currently resolved (not yet closed) User Requests for the current user
+ * @param WebPage $oP The current web page
+ * @return void
+ */
+// ========================= THEBEN ===================================
+function ListRequestsToResolve(WebPage $oP)
+{
+	$oUserOrg = GetUserOrg();
+
+	$aClassToSet = array();
+	foreach (GetTicketClasses() as $sClass)
+	{
+		$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status = 'assigned'";
+		$oSearch = DBObjectSearch::FromOQL($sOQL);
+		$iUser = UserRights::GetContactId();
+
+		$oSearch->AddCondition('agent_id', $iUser);
+
+		$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
+	}
+	DisplayRequestLists($oP, $aClassToSet);
+}
+
+/**
  * Lists all the currently closed tickets
  * @param WebPage $oP The current web page
  * @return void
@@ -896,6 +865,8 @@ function DisplayObject($oP, $oObj, $oUserOrg)
  * @param Object $oObj The target object
  * @return void
  */
+//============= THEBEN ============= ApproveButton === ResolveButton ===============
+
 function ShowDetailsRequest(WebPage $oP, $oObj)
 {	
 	$sClass = get_class($oObj);
@@ -905,6 +876,10 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 	$bIsReopenButton = false;
 	$bIsCloseButton = false;
 	$bIsEscalateButton = false;
+	$bIsApproveButton = false;  
+	$bIsRejectButton = false; 
+	$bIsResolveButton = false; 
+	
 	$bEditAttachments = false;
 	$aEditAtt = array(); // List of attributes editable in the main form
 	if (!MetaModel::DBIsReadOnly())
@@ -923,16 +898,66 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 			}
 			// Add the "Close" button if this is valid action
 			if (array_key_exists('ev_close', $aTransitions) && UserRights::IsStimulusAllowed($sClass, 'ev_close', $oSet))
-			{
-				$bIsCloseButton = true;
-				MakeStimulusForm($oP, $oObj, 'ev_close', array('user_satisfaction', $sUserCommentAttCode));
+			{   if (($oObj->Get('caller_id') == UserRights::GetContactId()) 
+			         || IsPowerUSer()) { 
+				  // I can only close a ticket if I'm the caller or a power user
+					$bIsCloseButton = true;
+					MakeStimulusForm($oP, $oObj, 'ev_close', array('user_satisfaction', $sUserCommentAttCode));
+			    }
+			    IssueLog::Info('caller_id='.$oObj->Get('caller_id')." user-id=".UserRights::GetContactId());
 			}
 			break;
+			
+			case 'waiting_for_approval':
+				$aEditAtt = array();
+				$aTransitions = $oObj->EnumTransitions();
+				$oSet = DBObjectSet::FromObject($oObj);
+				// Add the "Approve" button if this is valid action
+				if (array_key_exists('ev_approve', $aTransitions)) //TODO check if current user is approver && UserRights::IsStimulusAllowed($sClass, 'ev_reopen', $oSet))
+				{
+					$bIsApproveButton = true;
+					MakeStimulusForm($oP, $oObj, 'ev_approve', array($sLogAttCode));
+				}
+				// Add the "Reject" button if this is valid action
+				if (array_key_exists('ev_reject', $aTransitions)) // TODO check if current user is approve && UserRights::IsStimulusAllowed($sClass, 'ev_close', $oSet))
+				{
+					$bIsRejectButton = true;
+					MakeStimulusForm($oP, $oObj, 'ev_reject', array($sLogAttCode) ); //array('user_satisfaction', $sUserCommentAttCode));
+				}
+				break;
 	
 			case 'closed':
 			// By convention 'closed' is the final state of a ticket and nothing can be done in such a state
 			break;
 
+			case 'assigned':
+				
+				$iFlags = $oObj->GetAttributeFlags($sLogAttCode);
+				$bReadOnly = (($iFlags & (OPT_ATT_READONLY | OPT_ATT_HIDDEN)) != 0);
+				if ($bReadOnly)
+				{
+					$aEditAtt = array();
+					$bEditAttachments = false;
+				}
+				else
+				{
+					$aEditAtt = array(
+								$sLogAttCode => '????'
+						);
+					$bEditAttachments = true;
+				}
+				if (isKeyUser()) {
+					$aTransitions = $oObj->EnumTransitions();
+					$oSet = DBObjectSet::FromObject($oObj);
+					// Add the "Resolver" button if this is valid action
+					if (array_key_exists('ev_resolve', $aTransitions)) //TODO check if current user is approver && UserRights::IsStimulusAllowed($sClass, 'ev_reopen', $oSet))
+					{
+						$bIsResolveButton = true;
+						MakeStimulusForm($oP, $oObj, 'ev_resolve', array($sLogAttCode));
+					}		
+				} 
+				break;
+				
 			default:
 			// In all other states, the only possible action is to update the ticket (both the case log and the attachments)
 			// This update is possible only if the case log field is not read-only or hidden in the current state
@@ -1073,6 +1098,32 @@ EOF
 		$sOk = addslashes(Dict::S('UI:Button:Ok'));
 		$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
 	}
+	
+	if($bIsApproveButton)
+	{
+		$sStimulusCode = 'ev_approve';
+		$sTitle = addslashes(Dict::S('Portal:Button:ApproveTicket'));
+		$sOk = addslashes(Dict::S('UI:Button:Ok'));
+		$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');		
+	}
+	
+	if($bIsRejectButton)
+	{
+		$sStimulusCode = 'ev_reject';
+		$sTitle = addslashes(Dict::S('Portal:Button:RejectTicket'));
+		$sOk = addslashes(Dict::S('UI:Button:Ok'));
+		$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
+	}
+	
+	if($bIsResolveButton)
+	{
+		$sStimulusCode = 'ev_resolve';
+		$sTitle = addslashes(Dict::S('Portal:Button:ResolveTicket'));
+		$sOk = addslashes(Dict::S('UI:Button:Ok'));
+		$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
+	}
+	
+	
 	if($bIsCloseButton)
 	{
 		$sStimulusCode = 'ev_close';
@@ -1257,6 +1308,26 @@ function IsPowerUSer()
 	return $bRes;
 }
 
+/**
+ * Determine if the current user can be considered as being a portal key user
+ * (can update tickets where he is agent and can resolve them)
+ */
+function IsKeyUSer()
+{
+	$iUserID = UserRights::GetUserId();
+	$sOQLprofile = "SELECT URP_Profiles AS p JOIN URP_UserProfile AS up ON up.profileid=p.id WHERE up.userid = :user AND p.name = :profile";
+	$oProfileSet = new DBObjectSet(
+			DBObjectSearch::FromOQL($sOQLprofile),
+			array(),
+			array(
+					'user' => $iUserID,
+					'profile' => 'PORTAL_KEY_USER_PROFILE',
+			)
+	);
+	$bRes = ($oProfileSet->count() > 0);
+	return $bRes;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 //
 // Main program
@@ -1273,7 +1344,7 @@ try
 	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
 	LoginWebPage::DoLogin(false /* bMustBeAdmin */, true /* IsAllowedToPortalUsers */); // Check user rights and prompt if needed
 
-   ApplicationContext::SetUrlMakerClass('MyPortalURLMaker');
+    ApplicationContext::SetUrlMakerClass('MyPortalURLMaker');
 
 	$aClasses = explode(',', MetaModel::GetConfig()->Get('portal_tickets'));
 	$sMainClass = trim(reset($aClasses));
@@ -1288,9 +1359,11 @@ try
 	
 		$sCode = $oUserOrg->Get('code');
 		$sAlternateStylesheet = '';
+        //IssueLog::Info("org code of user=".$sCode);
 		if (@file_exists("./$sCode/portal.css"))
 		{
 			$sAlternateStylesheet = "$sCode";
+                        IssueLog::Info("using Alt Stylesheet: ".$sAlternateStylesheet);
 		}
 	
 		$oP = new PortalWebPage(Dict::S('Portal:Title'), $sAlternateStylesheet);
@@ -1349,12 +1422,27 @@ try
 					DisplayObject($oP, $oObj, $oUserOrg);
 				}
 				break;
-	
+				
+				// =================== THEBEN ===========================
+				case 'show_toapprove':
+				$oP->set_title(Dict::S('Portal:ShowToApprove'));
+				DisplayMainMenu($oP);
+				ShowTicketsToApprove($oP);
+				break;
+				
+				// =================== THEBEN ===========================
+				case 'show_toresolve':
+					$oP->set_title(Dict::S('Portal:ShowToResolve'));
+					DisplayMainMenu($oP);
+					ShowTicketsToResolve($oP);
+					break;
+				
 				case 'show_ongoing':
 				default:
 				$oP->set_title(Dict::S('Portal:ShowOngoing'));
 				DisplayMainMenu($oP);
 				ShowOngoingTickets($oP);
+				break;
 			} 
 		}
 	}

+ 25 - 0
setup/licenses/community-licences.xml

@@ -803,5 +803,30 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
     ]]></text>
   </license>
+  <license>
+    <product>PHP XLSXWriter</product>
+    <author>Mark Jones</author>
+    <license_type>MIT</license_type>
+    <text><![CDATA[
+<pre>Copyright (c) 2013 Mark Jones
+    
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
+    ]]></text>
+  </license>
 </licenses>
 

+ 29 - 1
webservices/export.php

@@ -28,9 +28,11 @@ 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/xmlpage.class.inc.php');
 require_once(APPROOT.'/application/clipage.class.inc.php');
+require_once(APPROOT.'/application/excelexporter.class.inc.php');
 
 require_once(APPROOT.'/application/startup.inc.php');
 
@@ -264,6 +266,32 @@ if (!empty($sExpression))
 				cmdbAbstractObject::DisplaySetAsXML($oP, $oSet, array('localize_values' => $bLocalize));
 				break;
 				
+				case 'xlsx':
+				$oP = new ajax_page('');
+				$oExporter = new ExcelExporter();
+				$oExporter->SetObjectList($oFilter);
+				
+				// Run the export by chunk of 1000 objects to limit memory usage
+				$oExporter->SetChunkSize(1000);
+				do
+				{
+					$aStatus = $oExporter->Run(); // process one chunk
+				}
+				while( ($aStatus['code'] != 'done') && ($aStatus['code'] != 'error'));
+				
+				if ($aStatus['code'] == 'done')
+				{
+					$oP->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+					$oP->SetContentDisposition('attachment', $oFilter->GetClass().'.xlsx');
+					$oP->add(file_get_contents($oExporter->GetExcelFilePath()));
+					$oExporter->Cleanup();
+				}
+				else
+				{
+					$oP->add('Error, xlsx export failed: '.$aStatus['message']);
+				}
+				break;
+				
 				default:
 				$oP = new WebPage("iTop - Export");
 				$oP->add("Unsupported format '$sFormat'. Possible values are: html, csv, spreadsheet or xml.");
@@ -301,7 +329,7 @@ if (!$oP)
 	$oP->p(" * expression: an OQL expression (URL encoded if needed)");
 	$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'");
-	$oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv' or 'xml'");
+	$oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv', 'xlsx' or 'xml'");
 	$oP->p(" * fields: (optional, no effect on XML format) list of fields (attribute codes, or alias.attcode) separated by a coma");
 	$oP->p(" * fields_advanced: (optional, no effect on XML/HTML formats ; ignored is fields is specified) If set to 1, the default list of fields will include the external keys and their reconciliation keys");
 	$oP->p(" * filename: (optional, no effect in CLI mode) if set then the results will be downloaded as a file");