1<?php 2// Copyright (C) 2010-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 19 20/** 21 * Class WebPage 22 * 23 * @copyright Copyright (C) 2010-2015 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27 28/** 29 * Generic interface common to CLI and Web pages 30 */ 31Interface Page 32{ 33 public function output(); 34 35 public function add($sText); 36 37 public function p($sText); 38 39 public function pre($sText); 40 41 public function add_comment($sText); 42 43 public function table($aConfig, $aData, $aParams = array()); 44} 45 46 47/** 48 * <p>Simple helper class to ease the production of HTML pages 49 * 50 * <p>This class provide methods to add content, scripts, includes... to a web page 51 * and renders the full web page by putting the elements in the proper place & order 52 * when the output() method is called. 53 * 54 * <p>Usage: 55 * ```php 56 * $oPage = new WebPage("Title of my page"); 57 * $oPage->p("Hello World !"); 58 * $oPage->output(); 59 * ``` 60 */ 61class WebPage implements Page 62{ 63 protected $s_title; 64 protected $s_content; 65 protected $s_deferred_content; 66 protected $a_scripts; 67 protected $a_dict_entries; 68 protected $a_dict_entries_prefixes; 69 protected $a_styles; 70 protected $a_linked_scripts; 71 protected $a_linked_stylesheets; 72 protected $a_headers; 73 protected $a_base; 74 protected $iNextId; 75 protected $iTransactionId; 76 protected $sContentType; 77 protected $sContentDisposition; 78 protected $sContentFileName; 79 protected $bTrashUnexpectedOutput; 80 protected $s_sOutputFormat; 81 protected $a_OutputOptions; 82 protected $bPrintable; 83 84 public function __construct($s_title, $bPrintable = false) 85 { 86 $this->s_title = $s_title; 87 $this->s_content = ""; 88 $this->s_deferred_content = ''; 89 $this->a_scripts = array(); 90 $this->a_dict_entries = array(); 91 $this->a_dict_entries_prefixes = array(); 92 $this->a_styles = array(); 93 $this->a_linked_scripts = array(); 94 $this->a_linked_stylesheets = array(); 95 $this->a_headers = array(); 96 $this->a_base = array('href' => '', 'target' => ''); 97 $this->iNextId = 0; 98 $this->iTransactionId = 0; 99 $this->sContentType = ''; 100 $this->sContentDisposition = ''; 101 $this->sContentFileName = ''; 102 $this->bTrashUnexpectedOutput = false; 103 $this->s_OutputFormat = utils::ReadParam('output_format', 'html'); 104 $this->a_OutputOptions = array(); 105 $this->bPrintable = $bPrintable; 106 ob_start(); // Start capturing the output 107 } 108 109 /** 110 * Change the title of the page after its creation 111 */ 112 public function set_title($s_title) 113 { 114 $this->s_title = $s_title; 115 } 116 117 /** 118 * Specify a default URL and a default target for all links on a page 119 */ 120 public function set_base($s_href = '', $s_target = '') 121 { 122 $this->a_base['href'] = $s_href; 123 $this->a_base['target'] = $s_target; 124 } 125 126 /** 127 * Add any text or HTML fragment to the body of the page 128 */ 129 public function add($s_html) 130 { 131 $this->s_content .= $s_html; 132 } 133 134 /** 135 * Add any text or HTML fragment (identified by an ID) at the end of the body of the page 136 * This is useful to add hidden content, DIVs or FORMs that should not 137 * be embedded into each other. 138 */ 139 public function add_at_the_end($s_html, $sId = '') 140 { 141 $this->s_deferred_content .= $s_html; 142 } 143 144 /** 145 * Add a paragraph to the body of the page 146 */ 147 public function p($s_html) 148 { 149 $this->add($this->GetP($s_html)); 150 } 151 152 /** 153 * Add a pre-formatted text to the body of the page 154 */ 155 public function pre($s_html) 156 { 157 $this->add('<pre>'.$s_html.'</pre>'); 158 } 159 160 /** 161 * Add a comment 162 */ 163 public function add_comment($sText) 164 { 165 $this->add('<!--'.$sText.'-->'); 166 } 167 168 /** 169 * Add a paragraph to the body of the page 170 */ 171 public function GetP($s_html) 172 { 173 return "<p>$s_html</p>\n"; 174 } 175 176 /** 177 * Adds a tabular content to the web page 178 * 179 * @param string[] $aConfig Configuration of the table: hash array of 'column_id' => 'Column Label' 180 * @param string[] $aData Hash array. Data to display in the table: each row is made of 'column_id' => Data. A 181 * column 'pkey' is expected for each row 182 * @param array $aParams Hash array. Extra parameters for the table. 183 * 184 * @return void 185 */ 186 public function table($aConfig, $aData, $aParams = array()) 187 { 188 $this->add($this->GetTable($aConfig, $aData, $aParams)); 189 } 190 191 public function GetTable($aConfig, $aData, $aParams = array()) 192 { 193 $oAppContext = new ApplicationContext(); 194 195 static $iNbTables = 0; 196 $iNbTables++; 197 $sHtml = ""; 198 $sHtml .= "<table class=\"listResults\">\n"; 199 $sHtml .= "<thead>\n"; 200 $sHtml .= "<tr>\n"; 201 foreach ($aConfig as $sName => $aDef) 202 { 203 $sHtml .= "<th title=\"".$aDef['description']."\">".$aDef['label']."</th>\n"; 204 } 205 $sHtml .= "</tr>\n"; 206 $sHtml .= "</thead>\n"; 207 $sHtml .= "<tbody>\n"; 208 foreach ($aData as $aRow) 209 { 210 $sHtml .= $this->GetTableRow($aRow, $aConfig); 211 } 212 $sHtml .= "</tbody>\n"; 213 $sHtml .= "</table>\n"; 214 215 return $sHtml; 216 } 217 218 public function GetTableRow($aRow, $aConfig) 219 { 220 $sHtml = ''; 221 if (isset($aRow['@class'])) // Row specific class, for hilighting certain rows 222 { 223 $sHtml .= "<tr class=\"{$aRow['@class']}\">"; 224 } 225 else 226 { 227 $sHtml .= "<tr>"; 228 } 229 foreach ($aConfig as $sName => $aAttribs) 230 { 231 $sClass = isset($aAttribs['class']) ? 'class="'.$aAttribs['class'].'"' : ''; 232 $sValue = ($aRow[$sName] === '') ? ' ' : $aRow[$sName]; 233 $sHtml .= "<td $sClass>$sValue</td>"; 234 } 235 $sHtml .= "</tr>"; 236 237 return $sHtml; 238 } 239 240 /** 241 * Add some Javascript to the header of the page 242 */ 243 public function add_script($s_script) 244 { 245 $this->a_scripts[] = $s_script; 246 } 247 248 /** 249 * Add some Javascript to the header of the page 250 */ 251 public function add_ready_script($s_script) 252 { 253 // Do nothing silently... this is not supported by this type of page... 254 } 255 256 /** 257 * Allow a dictionnary entry to be used client side with Dict.S() 258 * 259 * @param string $s_entryId a translation label key 260 * 261 * @see \WebPage::add_dict_entries() 262 * @see utils.js 263 */ 264 public function add_dict_entry($s_entryId) 265 { 266 $this->a_dict_entries[] = $s_entryId; 267 } 268 269 /** 270 * Add a set of dictionary entries (based on the given prefix) for the Javascript side 271 * 272 * @param string $s_entriesPrefix translation label prefix (eg 'UI:Button:' to add all keys beginning with this) 273 * 274 * @see \WebPage::add_dict_entry() 275 * @see utils.js 276 */ 277 public function add_dict_entries($s_entriesPrefix) 278 { 279 $this->a_dict_entries_prefixes[] = $s_entriesPrefix; 280 } 281 282 protected function get_dict_signature() 283 { 284 return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',', 285 $this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes)); 286 } 287 288 protected function get_dict_file_content() 289 { 290 $aEntries = array(); 291 foreach ($this->a_dict_entries as $sCode) 292 { 293 $aEntries[$sCode] = Dict::S($sCode); 294 } 295 foreach ($this->a_dict_entries_prefixes as $sPrefix) 296 { 297 $aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix)); 298 } 299 $sJSFile = 'var aDictEntries = '.json_encode($aEntries); 300 301 return $sJSFile; 302 } 303 304 305 /** 306 * Add some CSS definitions to the header of the page 307 */ 308 public function add_style($s_style) 309 { 310 $this->a_styles[] = $s_style; 311 } 312 313 /** 314 * Add a script (as an include, i.e. link) to the header of the page.<br> 315 * Handles duplicates : calling twice with the same script will add the script only once 316 * 317 * @param string $s_linked_script 318 */ 319 public function add_linked_script($s_linked_script) 320 { 321 $this->a_linked_scripts[$s_linked_script] = $s_linked_script; 322 } 323 324 /** 325 * Add a CSS stylesheet (as an include, i.e. link) to the header of the page 326 */ 327 public function add_linked_stylesheet($s_linked_stylesheet, $s_condition = "") 328 { 329 $this->a_linked_stylesheets[] = array('link' => $s_linked_stylesheet, 'condition' => $s_condition); 330 } 331 332 public function add_saas($sSaasRelPath) 333 { 334 $sCssRelPath = utils::GetCSSFromSASS($sSaasRelPath); 335 $sRootUrl = utils::GetAbsoluteUrlAppRoot(); 336 if ($sRootUrl === '') 337 { 338 // We're running the setup of the first install... 339 $sRootUrl = '../'; 340 } 341 $sCSSUrl = $sRootUrl.$sCssRelPath; 342 $this->add_linked_stylesheet($sCSSUrl); 343 } 344 345 /** 346 * Add some custom header to the page 347 */ 348 public function add_header($s_header) 349 { 350 $this->a_headers[] = $s_header; 351 } 352 353 /** 354 * Add needed headers to the page so that it will no be cached 355 */ 356 public function no_cache() 357 { 358 $this->add_header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 359 $this->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past 360 } 361 362 /** 363 * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data 364 */ 365 public function details($aFields) 366 { 367 368 $this->add($this->GetDetails($aFields)); 369 } 370 371 /** 372 * Whether or not the page is a PDF page 373 * 374 * @return boolean 375 */ 376 public function is_pdf() 377 { 378 return false; 379 } 380 381 /** 382 * Records the current state of the 'html' part of the page output 383 * 384 * @return mixed The current state of the 'html' output 385 */ 386 public function start_capture() 387 { 388 return strlen($this->s_content); 389 } 390 391 /** 392 * Returns the part of the html output that occurred since the call to start_capture 393 * and removes this part from the current html output 394 * 395 * @param $offset mixed The value returned by start_capture 396 * 397 * @return string The part of the html output that was added since the call to start_capture 398 */ 399 public function end_capture($offset) 400 { 401 $sCaptured = substr($this->s_content, $offset); 402 $this->s_content = substr($this->s_content, 0, $offset); 403 404 return $sCaptured; 405 } 406 407 /** 408 * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data 409 */ 410 public function GetDetails($aFields) 411 { 412 $sHtml = "<div class=\"details\" id='search-widget-results-outer'>\n"; 413 foreach ($aFields as $aAttrib) 414 { 415 $sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : ''; 416 $sLayout = isset($aAttrib['layout']) ? $aAttrib['layout'] : 'small'; 417 $sHtml .= "<div class=\"field_container field_{$sLayout}\" $sDataAttCode>\n"; 418 $sHtml .= "<div class=\"field_label label\">{$aAttrib['label']}</div>\n"; 419 420 $sHtml .= "<div class=\"field_data\">\n"; 421 // By Rom, for csv import, proposed to show several values for column selection 422 if (is_array($aAttrib['value'])) 423 { 424 $sHtml .= "<div class=\"field_value\">".implode("</div><div>", $aAttrib['value'])."</div>\n"; 425 } 426 else 427 { 428 $sHtml .= "<div class=\"field_value\">".$aAttrib['value']."</div>\n"; 429 } 430 // Checking if we should add comments & infos 431 $sComment = (isset($aAttrib['comments'])) ? $aAttrib['comments'] : ''; 432 $sInfo = (isset($aAttrib['infos'])) ? $aAttrib['infos'] : ''; 433 if ($sComment !== '') 434 { 435 $sHtml .= "<div class=\"field_comments\">$sComment</div>\n"; 436 } 437 if ($sInfo !== '') 438 { 439 $sHtml .= "<div class=\"field_infos\">$sInfo</div>\n"; 440 } 441 $sHtml .= "</div>\n"; 442 443 $sHtml .= "</div>\n"; 444 } 445 $sHtml .= "</div>\n"; 446 447 return $sHtml; 448 } 449 450 /** 451 * Build a set of radio buttons suitable for editing a field/attribute of an object (including its validation) 452 * 453 * @param $aAllowedValues hash Array of value => display_value 454 * @param $value mixed Current value for the field/attribute 455 * @param $iId mixed Unique Id for the input control in the page 456 * @param $sFieldName string The name of the field, attr_<$sFieldName> will hold the value for the field 457 * @param $bMandatory bool Whether or not the field is mandatory 458 * @param $bVertical bool Disposition of the radio buttons vertical or horizontal 459 * @param $sValidationField string HTML fragment holding the validation field (exclamation icon...) 460 * 461 * @return string The HTML fragment corresponding to the radio buttons 462 */ 463 public function GetRadioButtons( 464 $aAllowedValues, $value, $iId, $sFieldName, $bMandatory, $bVertical, $sValidationField 465 ) { 466 $idx = 0; 467 $sHTMLValue = ''; 468 foreach ($aAllowedValues as $key => $display_value) 469 { 470 if ((count($aAllowedValues) == 1) && ($bMandatory == 'true')) 471 { 472 // When there is only once choice, select it by default 473 $sSelected = ' checked'; 474 } 475 else 476 { 477 $sSelected = ($value == $key) ? ' checked' : ''; 478 } 479 $sHTMLValue .= "<input type=\"radio\" id=\"{$iId}_{$key}\" name=\"radio_$sFieldName\" onChange=\"$('#{$iId}').val(this.value).trigger('change');\" value=\"$key\"$sSelected><label class=\"radio\" for=\"{$iId}_{$key}\"> $display_value</label> "; 480 if ($bVertical) 481 { 482 if ($idx == 0) 483 { 484 // Validation icon at the end of the first line 485 $sHTMLValue .= " {$sValidationField}\n"; 486 } 487 $sHTMLValue .= "<br>\n"; 488 } 489 $idx++; 490 } 491 $sHTMLValue .= "<input type=\"hidden\" id=\"$iId\" name=\"$sFieldName\" value=\"$value\"/>"; 492 if (!$bVertical) 493 { 494 // Validation icon at the end of the line 495 $sHTMLValue .= " {$sValidationField}\n"; 496 } 497 498 return $sHTMLValue; 499 } 500 501 /** 502 * Discard unexpected output data (such as PHP warnings) 503 * This is a MUST when the Page output is DATA (download of a document, download CSV export, download ...) 504 */ 505 public function TrashUnexpectedOutput() 506 { 507 $this->bTrashUnexpectedOutput = true; 508 } 509 510 /** 511 * Read the output buffer and deal with its contents: 512 * - trash unexpected output if the flag has been set 513 * - report unexpected behaviors such as the output buffering being stopped 514 * 515 * Possible improvement: I've noticed that several output buffers are stacked, 516 * if they are not empty, the output will be corrupted. The solution would 517 * consist in unstacking all of them (and concatenate the contents). 518 */ 519 protected function ob_get_clean_safe() 520 { 521 $sOutput = ob_get_contents(); 522 if ($sOutput === false) 523 { 524 $sMsg = "Design/integration issue: No output buffer. Some piece of code has called ob_get_clean() or ob_end_clean() without calling ob_start()"; 525 if ($this->bTrashUnexpectedOutput) 526 { 527 IssueLog::Error($sMsg); 528 $sOutput = ''; 529 } 530 else 531 { 532 $sOutput = $sMsg; 533 } 534 } 535 else 536 { 537 ob_end_clean(); // on some versions of PHP doing so when the output buffering is stopped can cause a notice 538 if ($this->bTrashUnexpectedOutput) 539 { 540 if (trim($sOutput) != '') 541 { 542 if (Utils::GetConfig() && Utils::GetConfig()->Get('debug_report_spurious_chars')) 543 { 544 IssueLog::Error("Trashing unexpected output:'$sOutput'\n"); 545 } 546 } 547 $sOutput = ''; 548 } 549 } 550 551 return $sOutput; 552 } 553 554 /** 555 * Outputs (via some echo) the complete HTML page by assembling all its elements 556 */ 557 public function output() 558 { 559 foreach ($this->a_headers as $s_header) 560 { 561 header($s_header); 562 } 563 564 $s_captured_output = $this->ob_get_clean_safe(); 565 echo "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"; 566 echo "<html>\n"; 567 echo "<head>\n"; 568 echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n"; 569 echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\" />"; 570 echo "<title>".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."</title>\n"; 571 echo $this->get_base_tag(); 572 573 $this->output_dict_entries(); 574 575 foreach ($this->a_linked_scripts as $s_script) 576 { 577 // Make sure that the URL to the script contains the application's version number 578 // so that the new script do NOT get reloaded from the cache when the application is upgraded 579 if (strpos($s_script, '?') === false) 580 { 581 $s_script .= "?t=".utils::GetCacheBusterTimestamp(); 582 } 583 else 584 { 585 $s_script .= "&t=".utils::GetCacheBusterTimestamp(); 586 } 587 echo "<script type=\"text/javascript\" src=\"$s_script\"></script>\n"; 588 } 589 if (count($this->a_scripts) > 0) 590 { 591 echo "<script type=\"text/javascript\">\n"; 592 foreach ($this->a_scripts as $s_script) 593 { 594 echo "$s_script\n"; 595 } 596 echo "</script>\n"; 597 } 598 foreach ($this->a_linked_stylesheets as $a_stylesheet) 599 { 600 if (strpos($a_stylesheet['link'], '?') === false) 601 { 602 $s_stylesheet = $a_stylesheet['link']."?t=".utils::GetCacheBusterTimestamp(); 603 } 604 else 605 { 606 $s_stylesheet = $a_stylesheet['link']."&t=".utils::GetCacheBusterTimestamp(); 607 } 608 if ($a_stylesheet['condition'] != "") 609 { 610 echo "<!--[if {$a_stylesheet['condition']}]>\n"; 611 } 612 echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$s_stylesheet}\" />\n"; 613 if ($a_stylesheet['condition'] != "") 614 { 615 echo "<![endif]-->\n"; 616 } 617 } 618 619 if (count($this->a_styles) > 0) 620 { 621 echo "<style>\n"; 622 foreach ($this->a_styles as $s_style) 623 { 624 echo "$s_style\n"; 625 } 626 echo "</style>\n"; 627 } 628 if (class_exists('MetaModel') && MetaModel::GetConfig()) 629 { 630 echo "<link rel=\"shortcut icon\" href=\"".utils::GetAbsoluteUrlAppRoot()."images/favicon.ico?t=".utils::GetCacheBusterTimestamp()."\" />\n"; 631 } 632 echo "</head>\n"; 633 echo "<body>\n"; 634 echo self::FilterXSS($this->s_content); 635 if (trim($s_captured_output) != "") 636 { 637 echo "<div class=\"raw_output\">".self::FilterXSS($s_captured_output)."</div>\n"; 638 } 639 echo '<div id="at_the_end">'.self::FilterXSS($this->s_deferred_content).'</div>'; 640 echo "</body>\n"; 641 echo "</html>\n"; 642 643 if (class_exists('DBSearch')) 644 { 645 DBSearch::RecordQueryTrace(); 646 } 647 if (class_exists('ExecutionKPI')) 648 { 649 ExecutionKPI::ReportStats(); 650 } 651 } 652 653 /** 654 * Build a series of hidden field[s] from an array 655 */ 656 public function add_input_hidden($sLabel, $aData) 657 { 658 foreach ($aData as $sKey => $sValue) 659 { 660 // Note: protection added to protect against the Notice 'array to string conversion' that appeared with PHP 5.4 661 // (this function seems unused though!) 662 if (is_scalar($sValue)) 663 { 664 $this->add("<input type=\"hidden\" name=\"".$sLabel."[$sKey]\" value=\"$sValue\">"); 665 } 666 } 667 } 668 669 protected function get_base_tag() 670 { 671 $sTag = ''; 672 if (($this->a_base['href'] != '') || ($this->a_base['target'] != '')) 673 { 674 $sTag = '<base '; 675 if (($this->a_base['href'] != '')) 676 { 677 $sTag .= "href =\"{$this->a_base['href']}\" "; 678 } 679 if (($this->a_base['target'] != '')) 680 { 681 $sTag .= "target =\"{$this->a_base['target']}\" "; 682 } 683 $sTag .= " />\n"; 684 } 685 686 return $sTag; 687 } 688 689 /** 690 * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page 691 * 692 * @return int The unique ID (in this page) 693 */ 694 public function GetUniqueId() 695 { 696 return $this->iNextId++; 697 } 698 699 /** 700 * Set the content-type (mime type) for the page's content 701 * 702 * @param $sContentType string 703 * 704 * @return void 705 */ 706 public function SetContentType($sContentType) 707 { 708 $this->sContentType = $sContentType; 709 } 710 711 /** 712 * Set the content-disposition (mime type) for the page's content 713 * 714 * @param $sDisposition string The disposition: 'inline' or 'attachment' 715 * @param $sFileName string The original name of the file 716 * 717 * @return void 718 */ 719 public function SetContentDisposition($sDisposition, $sFileName) 720 { 721 $this->sContentDisposition = $sDisposition; 722 $this->sContentFileName = $sFileName; 723 } 724 725 /** 726 * Set the transactionId of the current form 727 * 728 * @param $iTransactionId integer 729 * 730 * @return void 731 */ 732 public function SetTransactionId($iTransactionId) 733 { 734 $this->iTransactionId = $iTransactionId; 735 } 736 737 /** 738 * Returns the transactionId of the current form 739 * 740 * @return integer The current transactionID 741 */ 742 public function GetTransactionId() 743 { 744 return $this->iTransactionId; 745 } 746 747 public static function FilterXSS($sHTML) 748 { 749 return str_ireplace('<script', '<script', $sHTML); 750 } 751 752 /** 753 * What is the currently selected output format 754 * 755 * @return string The selected output format: html, pdf... 756 */ 757 public function GetOutputFormat() 758 { 759 return $this->s_OutputFormat; 760 } 761 762 /** 763 * Check whether the desired output format is possible or not 764 * 765 * @param string $sOutputFormat The desired output format: html, pdf... 766 * 767 * @return bool True if the format is Ok, false otherwise 768 */ 769 function IsOutputFormatAvailable($sOutputFormat) 770 { 771 $bResult = false; 772 switch ($sOutputFormat) 773 { 774 case 'html': 775 $bResult = true; // Always supported 776 break; 777 778 case 'pdf': 779 $bResult = @is_readable(APPROOT.'lib/MPDF/mpdf.php'); 780 break; 781 } 782 783 return $bResult; 784 } 785 786 /** 787 * Check whether the output must be printable (using print.css, for sure!) 788 * 789 * @return bool ... 790 */ 791 public function IsPrintableVersion() 792 { 793 return $this->bPrintable; 794 } 795 796 /** 797 * Retrieves the value of a named output option for the given format 798 * 799 * @param string $sFormat The format: html or pdf 800 * @param string $sOptionName The name of the option 801 * 802 * @return mixed false if the option was never set or the options's value 803 */ 804 public function GetOutputOption($sFormat, $sOptionName) 805 { 806 if (isset($this->a_OutputOptions[$sFormat][$sOptionName])) 807 { 808 return $this->a_OutputOptions[$sFormat][$sOptionName]; 809 } 810 811 return false; 812 } 813 814 /** 815 * Sets a named output option for the given format 816 * 817 * @param string $sFormat The format for which to set the option: html or pdf 818 * @param string $sOptionName the name of the option 819 * @param mixed $sValue The value of the option 820 */ 821 public function SetOutputOption($sFormat, $sOptionName, $sValue) 822 { 823 if (!isset($this->a_OutputOptions[$sFormat])) 824 { 825 $this->a_OutputOptions[$sFormat] = array($sOptionName => $sValue); 826 } 827 else 828 { 829 $this->a_OutputOptions[$sFormat][$sOptionName] = $sValue; 830 } 831 } 832 833 public function RenderPopupMenuItems($aActions, $aFavoriteActions = array()) 834 { 835 $sPrevUrl = ''; 836 $sHtml = ''; 837 if (!$this->IsPrintableVersion()) 838 { 839 foreach ($aActions as $aAction) 840 { 841 $sClass = isset($aAction['css_classes']) ? ' class="'.implode(' ', $aAction['css_classes']).'"' : ''; 842 $sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES, 843 "UTF-8").'"' : ''; 844 $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; 845 if (empty($aAction['url'])) 846 { 847 if ($sPrevUrl != '') // Don't output consecutively two separators... 848 { 849 $sHtml .= "<li>{$aAction['label']}</li>"; 850 } 851 $sPrevUrl = ''; 852 } 853 else 854 { 855 $sHtml .= "<li><a $sTarget href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>"; 856 $sPrevUrl = $aAction['url']; 857 } 858 } 859 $sHtml .= "</ul></li></ul></div>"; 860 foreach (array_reverse($aFavoriteActions) as $aAction) 861 { 862 $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; 863 $sHtml .= "<div class=\"actions_button\"><a $sTarget href='{$aAction['url']}'>{$aAction['label']}</a></div>"; 864 } 865 } 866 867 return $sHtml; 868 } 869 870 protected function output_dict_entries($bReturnOutput = false) 871 { 872 if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0)) 873 { 874 if (class_exists('Dict')) 875 { 876 // The dictionary may not be available for example during the setup... 877 // Create a specific dictionary file and load it as a JS script 878 $sSignature = $this->get_dict_signature(); 879 $sJSFileName = utils::GetCachePath().$sSignature.'.js'; 880 if (!file_exists($sJSFileName) && is_writable(utils::GetCachePath())) 881 { 882 file_put_contents($sJSFileName, $this->get_dict_file_content()); 883 } 884 // Load the dictionary as the first javascript file, so that other JS file benefit from the translations 885 array_unshift($this->a_linked_scripts, 886 utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=dict&s='.$sSignature); 887 } 888 } 889 } 890} 891 892 893interface iTabbedPage 894{ 895 public function AddTabContainer($sTabContainer, $sPrefix = ''); 896 897 public function AddToTab($sTabContainer, $sTabLabel, $sHtml); 898 899 public function SetCurrentTabContainer($sTabContainer = ''); 900 901 public function SetCurrentTab($sTabLabel = ''); 902 903 /** 904 * Add a tab which content will be loaded asynchronously via the supplied URL 905 * 906 * Limitations: 907 * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to 908 * pull content from another server. Static content cannot be added inside such tabs. 909 * 910 * @param string $sTabLabel The (localised) label of the tab 911 * @param string $sUrl The URL to load (on the same server) 912 * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause 913 * the tab to be reloaded upon each activation. 914 * 915 * @since 2.0.3 916 */ 917 public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true); 918 919 public function GetCurrentTab(); 920 921 public function RemoveTab($sTabLabel, $sTabContainer = null); 922 923 /** 924 * Finds the tab whose title matches a given pattern 925 * 926 * @return mixed The name of the tab as a string or false if not found 927 */ 928 public function FindTab($sPattern, $sTabContainer = null); 929} 930 931/** 932 * Helper class to implement JQueryUI tabs inside a page 933 */ 934class TabManager 935{ 936 protected $m_aTabs; 937 protected $m_sCurrentTabContainer; 938 protected $m_sCurrentTab; 939 940 public function __construct() 941 { 942 $this->m_aTabs = array(); 943 $this->m_sCurrentTabContainer = ''; 944 $this->m_sCurrentTab = ''; 945 } 946 947 public function AddTabContainer($sTabContainer, $sPrefix = '') 948 { 949 $this->m_aTabs[$sTabContainer] = array('prefix' => $sPrefix, 'tabs' => array()); 950 951 return "\$Tabs:$sTabContainer\$"; 952 } 953 954 public function AddToCurrentTab($sHtml) 955 { 956 $this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml); 957 } 958 959 public function GetCurrentTabLength($sHtml) 960 { 961 $iLength = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) : 0; 962 963 return $iLength; 964 } 965 966 /** 967 * Truncates the given tab to the specifed length and returns the truncated part 968 * 969 * @param string $sTabContainer The tab container in which to truncate the tab 970 * @param string $sTab The name/identifier of the tab to truncate 971 * @param integer $iLength The length/offset at which to truncate the tab 972 * 973 * @return string The truncated part 974 */ 975 public function TruncateTab($sTabContainer, $sTab, $iLength) 976 { 977 $sResult = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], 978 $iLength); 979 $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'] = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], 980 0, $iLength); 981 982 return $sResult; 983 } 984 985 public function TabExists($sTabContainer, $sTab) 986 { 987 return isset($this->m_aTabs[$sTabContainer]['tabs'][$sTab]); 988 } 989 990 public function TabsContainerCount() 991 { 992 return count($this->m_aTabs); 993 } 994 995 public function AddToTab($sTabContainer, $sTabLabel, $sHtml) 996 { 997 if (!isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) 998 { 999 // Set the content of the tab 1000 $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel] = array( 1001 'type' => 'html', 1002 'html' => $sHtml, 1003 ); 1004 } 1005 else 1006 { 1007 if ($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type'] != 'html') 1008 { 1009 throw new Exception("Cannot add HTML content to the tab '$sTabLabel' of type '{$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type']}'"); 1010 } 1011 // Append to the content of the tab 1012 $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['html'] .= $sHtml; 1013 } 1014 1015 return ''; // Nothing to add to the page for now 1016 } 1017 1018 public function SetCurrentTabContainer($sTabContainer = '') 1019 { 1020 $sPreviousTabContainer = $this->m_sCurrentTabContainer; 1021 $this->m_sCurrentTabContainer = $sTabContainer; 1022 1023 return $sPreviousTabContainer; 1024 } 1025 1026 public function SetCurrentTab($sTabLabel = '') 1027 { 1028 $sPreviousTab = $this->m_sCurrentTab; 1029 $this->m_sCurrentTab = $sTabLabel; 1030 1031 return $sPreviousTab; 1032 } 1033 1034 /** 1035 * Add a tab which content will be loaded asynchronously via the supplied URL 1036 * 1037 * Limitations: 1038 * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to 1039 * pull content from another server. Static content cannot be added inside such tabs. 1040 * 1041 * @param string $sTabLabel The (localised) label of the tab 1042 * @param string $sUrl The URL to load (on the same server) 1043 * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause 1044 * the tab to be reloaded upon each activation. 1045 * 1046 * @since 2.0.3 1047 */ 1048 public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) 1049 { 1050 // Set the content of the tab 1051 $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$sTabLabel] = array( 1052 'type' => 'ajax', 1053 'url' => $sUrl, 1054 'cache' => $bCache, 1055 ); 1056 1057 return ''; // Nothing to add to the page for now 1058 } 1059 1060 1061 public function GetCurrentTabContainer() 1062 { 1063 return $this->m_sCurrentTabContainer; 1064 } 1065 1066 public function GetCurrentTab() 1067 { 1068 return $this->m_sCurrentTab; 1069 } 1070 1071 public function RemoveTab($sTabLabel, $sTabContainer = null) 1072 { 1073 if ($sTabContainer == null) 1074 { 1075 $sTabContainer = $this->m_sCurrentTabContainer; 1076 } 1077 if (isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) 1078 { 1079 // Delete the content of the tab 1080 unset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]); 1081 1082 // If we just removed the active tab, let's reset the active tab 1083 if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel)) 1084 { 1085 $this->m_sCurrentTab = ''; 1086 } 1087 } 1088 } 1089 1090 /** 1091 * Finds the tab whose title matches a given pattern 1092 * 1093 * @return mixed The actual name of the tab (as a string) or false if not found 1094 */ 1095 public function FindTab($sPattern, $sTabContainer = null) 1096 { 1097 $result = false; 1098 if ($sTabContainer == null) 1099 { 1100 $sTabContainer = $this->m_sCurrentTabContainer; 1101 } 1102 foreach ($this->m_aTabs[$sTabContainer]['tabs'] as $sTabLabel => $void) 1103 { 1104 if (preg_match($sPattern, $sTabLabel)) 1105 { 1106 $result = $sTabLabel; 1107 break; 1108 } 1109 } 1110 1111 return $result; 1112 } 1113 1114 /** 1115 * Make the given tab the active one, as if it were clicked 1116 * DOES NOT WORK: apparently in the *old* version of jquery 1117 * that we are using this is not supported... TO DO upgrade 1118 * the whole jquery bundle... 1119 */ 1120 public function SelectTab($sTabContainer, $sTabLabel) 1121 { 1122 $container_index = 0; 1123 $tab_index = 0; 1124 foreach ($this->m_aTabs as $sCurrentTabContainerName => $aTabs) 1125 { 1126 if ($sTabContainer == $sCurrentTabContainerName) 1127 { 1128 foreach ($aTabs['tabs'] as $sCurrentTabLabel => $void) 1129 { 1130 if ($sCurrentTabLabel == $sTabLabel) 1131 { 1132 break; 1133 } 1134 $tab_index++; 1135 } 1136 break; 1137 } 1138 $container_index++; 1139 } 1140 $sSelector = '#tabbedContent_'.$container_index.' > ul'; 1141 1142 return "window.setTimeout(\"$('$sSelector').tabs('select', $tab_index);\", 100);"; // Let the time to the tabs widget to initialize 1143 } 1144 1145 public function RenderIntoContent($sContent, WebPage $oPage) 1146 { 1147 // Render the tabs in the page (if any) 1148 foreach ($this->m_aTabs as $sTabContainerName => $aTabs) 1149 { 1150 $sTabs = ''; 1151 $sPrefix = $aTabs['prefix']; 1152 $container_index = 0; 1153 if (count($aTabs['tabs']) > 0) 1154 { 1155 if ($oPage->IsPrintableVersion()) 1156 { 1157 $oPage->add_ready_script( 1158 <<< EOF 1159oHiddeableChapters = {}; 1160EOF 1161 ); 1162 $sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$container_index}\" class=\"light\">\n"; 1163 $i = 0; 1164 foreach ($aTabs['tabs'] as $sTabName => $aTabData) 1165 { 1166 $sTabNameEsc = addslashes($sTabName); 1167 $sTabId = "tab_{$sPrefix}{$container_index}$i"; 1168 switch ($aTabData['type']) 1169 { 1170 case 'ajax': 1171 $sTabHtml = ''; 1172 $sUrl = $aTabData['url']; 1173 $oPage->add_ready_script( 1174 <<< EOF 1175$.post('$sUrl', {printable: '1'}, function(data){ 1176 $('#$sTabId > .printable-tab-content').append(data); 1177}); 1178EOF 1179 ); 1180 break; 1181 1182 case 'html': 1183 default: 1184 $sTabHtml = $aTabData['html']; 1185 } 1186 $sTabs .= "<div class=\"printable-tab\" id=\"$sTabId\"><h2 class=\"printable-tab-title\">".htmlentities($sTabName, 1187 ENT_QUOTES, 1188 'UTF-8')."</h2><div class=\"printable-tab-content\">".$sTabHtml."</div></div>\n"; 1189 $oPage->add_ready_script( 1190 <<< EOF 1191oHiddeableChapters['$sTabId'] = '$sTabNameEsc'; 1192EOF 1193 ); 1194 $i++; 1195 } 1196 $sTabs .= "</div>\n<!-- end of tabs-->\n"; 1197 } 1198 else 1199 { 1200 $sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$container_index}\" class=\"light\">\n"; 1201 $sTabs .= "<ul>\n"; 1202 // Display the unordered list that will be rendered as the tabs 1203 $i = 0; 1204 foreach ($aTabs['tabs'] as $sTabName => $aTabData) 1205 { 1206 switch ($aTabData['type']) 1207 { 1208 case 'ajax': 1209 $sTabs .= "<li data-cache=\"".($aTabData['cache'] ? 'true' : 'false')."\"><a href=\"{$aTabData['url']}\" class=\"tab\"><span>".htmlentities($sTabName, 1210 ENT_QUOTES, 'UTF-8')."</span></a></li>\n"; 1211 break; 1212 1213 case 'html': 1214 default: 1215 $sTabs .= "<li><a href=\"#tab_{$sPrefix}{$container_index}$i\" class=\"tab\"><span>".htmlentities($sTabName, 1216 ENT_QUOTES, 'UTF-8')."</span></a></li>\n"; 1217 } 1218 $i++; 1219 } 1220 $sTabs .= "</ul>\n"; 1221 // Now add the content of the tabs themselves 1222 $i = 0; 1223 foreach ($aTabs['tabs'] as $sTabName => $aTabData) 1224 { 1225 switch ($aTabData['type']) 1226 { 1227 case 'ajax': 1228 // Nothing to add 1229 break; 1230 1231 case 'html': 1232 default: 1233 $sTabs .= "<div id=\"tab_{$sPrefix}{$container_index}$i\">".$aTabData['html']."</div>\n"; 1234 } 1235 $i++; 1236 } 1237 $sTabs .= "</div>\n<!-- end of tabs-->\n"; 1238 } 1239 } 1240 $sContent = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $sContent); 1241 $container_index++; 1242 } 1243 1244 return $sContent; 1245 } 1246}