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