1<?php
2// Copyright (C) 2015 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19define('EXPORTER_DEFAULT_CHUNK_SIZE', 1000);
20
21class BulkExportException extends Exception
22{
23	protected $sLocalizedMessage;
24	public function __construct($message, $sLocalizedMessage, $code = null, $previous = null)
25	{
26		parent::__construct($message, $code, $previous);
27		$this->sLocalizedMessage = $sLocalizedMessage;
28	}
29
30	public function GetLocalizedMessage()
31	{
32		return $this->sLocalizedMessage;
33	}
34}
35class BulkExportMissingParameterException extends BulkExportException
36{
37	public function __construct($sFieldCode)
38	{
39		parent::__construct('Missing parameter: '.$sFieldCode, Dict::Format('Core:BulkExport:MissingParameter_Param', $sFieldCode));
40	}
41
42}
43
44/**
45 * Class BulkExport
46 *
47 * @copyright   Copyright (C) 2015 Combodo SARL
48 * @license     http://opensource.org/licenses/AGPL-3.0
49 */
50
51class BulkExportResult extends DBObject
52{
53	public static function Init()
54	{
55		$aParams = array
56		(
57			"category" => 'core/cmdb',
58			"key_type" => 'autoincrement',
59			"name_attcode" => array('created'),
60			"state_attcode" => '',
61			"reconc_keys" => array(),
62			"db_table" => 'priv_bulk_export_result',
63			"db_key_field" => 'id',
64			"db_finalclass_field" => '',
65			"display_template" => '',
66		);
67		MetaModel::Init_Params($aParams);
68
69		MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
70		MetaModel::Init_AddAttribute(new AttributeInteger("user_id", array("allowed_values"=>null, "sql"=>"user_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
71		MetaModel::Init_AddAttribute(new AttributeInteger("chunk_size", array("allowed_values"=>null, "sql"=>"chunk_size", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
72		MetaModel::Init_AddAttribute(new AttributeString("format", array("allowed_values"=>null, "sql"=>"format", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
73		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())));
74		MetaModel::Init_AddAttribute(new AttributeLongText("search", array("allowed_values"=>null, "sql"=>"search", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
75		MetaModel::Init_AddAttribute(new AttributeLongText("status_info", array("allowed_values"=>null, "sql"=>"status_info", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
76        MetaModel::Init_AddAttribute(new AttributeBoolean("localize_output", array("allowed_values"=>null, "sql"=>"localize_output", "default_value"=>true, "is_null_allowed"=>true, "depends_on"=>array())));
77
78	}
79
80	/**
81	 * @throws CoreUnexpectedValue
82	 * @throws Exception
83	 */
84	public function ComputeValues()
85	{
86		$this->Set('user_id', UserRights::GetUserId());
87	}
88}
89
90/**
91 * Garbage collector for cleaning "old" export results from the database and the disk.
92 * This background process runs once per day and deletes the results of all exports which
93 * are older than one day.
94 */
95class BulkExportResultGC implements iBackgroundProcess
96{
97	public function GetPeriodicity()
98	{
99		return 24*3600; // seconds
100	}
101
102	public function Process($iTimeLimit)
103	{
104		$sDateLimit = date(AttributeDateTime::GetSQLFormat(), time() - 24*3600); // Every BulkExportResult older than one day will be deleted
105
106		$sOQL = "SELECT BulkExportResult WHERE created < '$sDateLimit'";
107		$iProcessed = 0;
108		while (time() < $iTimeLimit)
109		{
110			// Next one ?
111			$oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */);
112			$oSet->OptimizeColumnLoad(array('temp_file_path'));
113			$oResult = $oSet->Fetch();
114			if (is_null($oResult))
115			{
116				// Nothing to be done
117				break;
118			}
119			$iProcessed++;
120			@unlink($oResult->Get('temp_file_path'));
121			utils::PushArchiveMode(false);
122			$oResult->DBDelete();
123			utils::PopArchiveMode();
124		}
125		return "Cleaned $iProcessed old export results(s).";
126	}
127}
128
129/**
130 * Class BulkExport
131 *
132 * @copyright   Copyright (C) 2015 Combodo SARL
133 * @license     http://opensource.org/licenses/AGPL-3.0
134 */
135
136abstract class BulkExport
137{
138	protected $oSearch;
139	protected $iChunkSize;
140	protected $sFormatCode;
141	protected $aStatusInfo;
142	protected $oBulkExportResult;
143	protected $sTmpFile;
144	protected $bLocalizeOutput;
145
146	public function __construct()
147	{
148		$this->oSearch = null;
149		$this->iChunkSize = 0;
150		$this->sFormatCode = null;
151		$this->aStatusInfo = array();
152		$this->oBulkExportResult = null;
153		$this->sTmpFile = '';
154		$this->bLocalizeOutput = false;
155	}
156
157    /**
158     * Find the first class capable of exporting the data in the given format
159     *
160     * @param string   $sFormatCode The lowercase format (e.g. html, csv, spreadsheet, xlsx, xml, json, pdf...)
161     * @param DBSearch $oSearch     The search/filter defining the set of objects to export or null when listing the supported formats
162     *
163     * @return BulkExport|null
164     * @throws ReflectionException
165     */
166	static public function FindExporter($sFormatCode, $oSearch = null)
167	{
168		foreach(get_declared_classes() as $sPHPClass)
169		{
170			$oRefClass = new ReflectionClass($sPHPClass);
171			if ($oRefClass->isSubclassOf('BulkExport') && !$oRefClass->isAbstract())
172			{
173				$oBulkExporter = new $sPHPClass();
174				if ($oBulkExporter->IsFormatSupported($sFormatCode, $oSearch))
175				{
176					if ($oSearch)
177					{
178						$oBulkExporter->SetObjectList($oSearch);
179					}
180					return $oBulkExporter;
181				}
182			}
183		}
184		return null;
185	}
186
187    /**
188     * Find the exporter corresponding to the given persistent token
189     *
190     * @param int $iPersistentToken The identifier of the BulkExportResult object storing the information
191     *
192     * @return iBulkExport|null
193     * @throws ArchivedObjectException
194     * @throws CoreException
195     * @throws ReflectionException
196     */
197	static public function FindExporterFromToken($iPersistentToken = null)
198	{
199		$oBulkExporter = null;
200		$oInfo = MetaModel::GetObject('BulkExportResult', $iPersistentToken, false);
201		if ($oInfo && ($oInfo->Get('user_id') == UserRights::GetUserId()))
202		{
203			$sFormatCode = $oInfo->Get('format');
204			$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
205
206			$oBulkExporter = self::FindExporter($sFormatCode, $oSearch);
207			if ($oBulkExporter)
208			{
209				$oBulkExporter->SetFormat($sFormatCode);
210				$oBulkExporter->SetObjectList($oSearch);
211				$oBulkExporter->SetChunkSize($oInfo->Get('chunk_size'));
212				$oBulkExporter->SetStatusInfo(json_decode($oInfo->Get('status_info'), true));
213
214                $oBulkExporter->SetLocalizeOutput($oInfo->Get('localize_output'));
215
216
217				$oBulkExporter->sTmpFile = $oInfo->Get('temp_file_path');
218				$oBulkExporter->oBulkExportResult = $oInfo;
219			}
220		}
221		return $oBulkExporter;
222	}
223
224	/**
225	 * @param $data
226	 * @throws Exception
227	 */
228	public function AppendToTmpFile($data)
229	{
230		if ($this->sTmpFile == '')
231		{
232			$this->sTmpFile = $this->MakeTmpFile($this->GetFileExtension());
233		}
234		$hFile = fopen($this->sTmpFile, 'ab');
235		if ($hFile !== false)
236		{
237			fwrite($hFile, $data);
238			fclose($hFile);
239		}
240	}
241
242	public function GetTmpFilePath()
243	{
244		return $this->sTmpFile;
245	}
246
247	/**
248	 * Lists all possible export formats. The output is a hash array in the form: 'format_code' => 'localized format label'
249	 * @return array :string
250	 */
251	static public function FindSupportedFormats()
252	{
253		$aSupportedFormats = array();
254		foreach(get_declared_classes() as $sPHPClass)
255		{
256			$oRefClass = new ReflectionClass($sPHPClass);
257			if ($oRefClass->isSubClassOf('BulkExport') && !$oRefClass->isAbstract())
258			{
259				$oBulkExporter = new $sPHPClass;
260				$aFormats = $oBulkExporter->GetSupportedFormats();
261				$aSupportedFormats = array_merge($aSupportedFormats, $aFormats);
262			}
263		}
264		return $aSupportedFormats;
265	}
266
267	/**
268	 * (non-PHPdoc)
269	 * @see iBulkExport::SetChunkSize()
270	 */
271	public function SetChunkSize($iChunkSize)
272	{
273		$this->iChunkSize = $iChunkSize;
274	}
275
276    /**
277     * @param $bLocalizeOutput
278     */
279    public function SetLocalizeOutput($bLocalizeOutput)
280    {
281        $this->bLocalizeOutput = $bLocalizeOutput;
282    }
283
284	/**
285	 * (non-PHPdoc)
286	 * @see iBulkExport::SetObjectList()
287	 */
288	public function SetObjectList(DBSearch $oSearch)
289	{
290		$this->oSearch = $oSearch;
291	}
292
293	public function SetFormat($sFormatCode)
294	{
295		$this->sFormatCode = $sFormatCode;
296	}
297
298	/**
299	 * (non-PHPdoc)
300	 * @see iBulkExport::IsFormatSupported()
301	 */
302	public function IsFormatSupported($sFormatCode, $oSearch = null)
303	{
304		return array_key_exists($sFormatCode, $this->GetSupportedFormats());
305	}
306
307	/**
308	 * (non-PHPdoc)
309	 * @see iBulkExport::GetSupportedFormats()
310	 */
311	public function GetSupportedFormats()
312	{
313		return array(); // return array('csv' => Dict::S('UI:ExportFormatCSV'));
314	}
315
316
317	public function SetHttpHeaders(WebPage $oPage)
318	{
319	}
320
321	/**
322	 * @return string
323	 */
324	public function GetHeader()
325	{
326		return '';
327	}
328	abstract public function GetNextChunk(&$aStatus);
329
330	/**
331	 * @return string
332	 */
333	public function GetFooter()
334	{
335		return '';
336	}
337
338	public function SaveState()
339	{
340		if ($this->oBulkExportResult === null)
341		{
342			$this->oBulkExportResult = new BulkExportResult();
343			$this->oBulkExportResult->Set('format', $this->sFormatCode);
344			$this->oBulkExportResult->Set('search', $this->oSearch->serialize());
345			$this->oBulkExportResult->Set('chunk_size', $this->iChunkSize);
346            $this->oBulkExportResult->Set('temp_file_path', $this->sTmpFile);
347            $this->oBulkExportResult->Set('localize_output', $this->bLocalizeOutput);
348        }
349		$this->oBulkExportResult->Set('status_info', json_encode($this->GetStatusInfo()));
350		utils::PushArchiveMode(false);
351		$ret = $this->oBulkExportResult->DBWrite();
352		utils::PopArchiveMode();
353		return $ret;
354	}
355
356	public function Cleanup()
357	{
358		if (($this->oBulkExportResult &&  (!$this->oBulkExportResult->IsNew())))
359		{
360			$sFilename = $this->oBulkExportResult->Get('temp_file_path');
361			if ($sFilename != '')
362			{
363				@unlink($sFilename);
364			}
365			utils::PushArchiveMode(false);
366			$this->oBulkExportResult->DBDelete();
367			utils::PopArchiveMode();
368		}
369	}
370
371	public function EnumFormParts()
372	{
373		return array();
374	}
375
376	public function DisplayFormPart(WebPage $oP, $sPartId)
377	{
378	}
379
380	public function DisplayUsage(Page $oP)
381	{
382
383	}
384	public function ReadParameters()
385	{
386		$this->bLocalizeOutput = !((bool)utils::ReadParam('no_localize', 0, true, 'integer'));
387	}
388
389	public function GetResultAsHtml()
390	{
391
392	}
393	public function GetRawResult()
394	{
395
396	}
397
398	/**
399	 * @return string
400	 */
401	public function GetMimeType()
402	{
403		return '';
404	}
405
406	/**
407	 * @return string
408	 */
409	public function GetFileExtension()
410	{
411		return '';
412	}
413	public function GetCharacterSet()
414	{
415		return 'UTF-8';
416	}
417
418	public function GetStatistics()
419	{
420
421	}
422
423	public function GetDownloadFileName()
424	{
425		return Dict::Format('Core:BulkExportOf_Class', MetaModel::GetName($this->oSearch->GetClass())).'.'.$this->GetFileExtension();
426	}
427
428	public function SetStatusInfo($aStatusInfo)
429	{
430		$this->aStatusInfo = $aStatusInfo;
431	}
432
433	public function GetStatusInfo()
434	{
435		return $this->aStatusInfo;
436	}
437
438	/**
439	 * @param $sExtension
440	 * @return string
441	 * @throws Exception
442	 */
443	protected function MakeTmpFile($sExtension)
444	{
445		if(!is_dir(APPROOT."data/bulk_export"))
446		{
447			@mkdir(APPROOT."data/bulk_export", 0777, true /* recursive */);
448			clearstatcache();
449		}
450		if (!is_writable(APPROOT."data/bulk_export"))
451		{
452			throw new Exception('Data directory "'.APPROOT.'data/bulk_export" could not be written.');
453		}
454
455		$iNum = rand();
456		do
457		{
458			$iNum++;
459			$sToken = sprintf("%08x", $iNum);
460			$sFileName = APPROOT."data/bulk_export/$sToken.".$sExtension;
461			$hFile = @fopen($sFileName, 'x');
462		}
463		while($hFile === false);
464
465		fclose($hFile);
466		return $sFileName;
467	}
468}
469
470// The built-in exports
471require_once(APPROOT.'core/tabularbulkexport.class.inc.php');
472require_once(APPROOT.'core/htmlbulkexport.class.inc.php');
473if (extension_loaded('gd'))
474{
475	// PDF export - via TCPDF - requires GD
476	require_once(APPROOT.'core/pdfbulkexport.class.inc.php');
477}
478require_once(APPROOT.'core/csvbulkexport.class.inc.php');
479require_once(APPROOT.'core/excelbulkexport.class.inc.php');
480require_once(APPROOT.'core/spreadsheetbulkexport.class.inc.php');
481require_once(APPROOT.'core/xmlbulkexport.class.inc.php');
482
483