1<?php 2// Copyright (C) 2010-2018 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 * Simple web page with no includes, header or fancy formatting, useful to 21 * generate HTML fragments when called by an AJAX method 22 * 23 * @copyright Copyright (C) 2010-2017 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27require_once(APPROOT."/application/webpage.class.inc.php"); 28 29class ajax_page extends WebPage implements iTabbedPage 30{ 31 /** 32 * Jquery style ready script 33 * @var Hash 34 */ 35 protected $m_sReadyScript; 36 protected $m_oTabs; 37 private $m_sMenu; // If set, then the menu will be updated 38 39 /** 40 * constructor for the web page 41 * @param string $s_title Not used 42 */ 43 function __construct($s_title) 44 { 45 $sPrintable = utils::ReadParam('printable', '0'); 46 $bPrintable = ($sPrintable == '1'); 47 48 parent::__construct($s_title, $bPrintable); 49 $this->m_sReadyScript = ""; 50 //$this->add_header("Content-type: text/html; charset=utf-8"); 51 $this->add_header("Cache-control: no-cache"); 52 $this->m_oTabs = new TabManager(); 53 $this->sContentType = 'text/html'; 54 $this->sContentDisposition = 'inline'; 55 $this->m_sMenu = ""; 56 57 utils::InitArchiveMode(); 58 } 59 60 public function AddTabContainer($sTabContainer, $sPrefix = '') 61 { 62 $this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix)); 63 } 64 65 public function AddToTab($sTabContainer, $sTabLabel, $sHtml) 66 { 67 $this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml)); 68 } 69 70 public function SetCurrentTabContainer($sTabContainer = '') 71 { 72 return $this->m_oTabs->SetCurrentTabContainer($sTabContainer); 73 } 74 75 public function SetCurrentTab($sTabLabel = '') 76 { 77 return $this->m_oTabs->SetCurrentTab($sTabLabel); 78 } 79 80 /** 81 * Add a tab which content will be loaded asynchronously via the supplied URL 82 * 83 * Limitations: 84 * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server. 85 * Static content cannot be added inside such tabs. 86 * 87 * @param string $sTabLabel The (localised) label of the tab 88 * @param string $sUrl The URL to load (on the same server) 89 * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation. 90 * @since 2.0.3 91 */ 92 public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) 93 { 94 $this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache)); 95 } 96 97 public function GetCurrentTab() 98 { 99 return $this->m_oTabs->GetCurrentTab(); 100 } 101 102 public function RemoveTab($sTabLabel, $sTabContainer = null) 103 { 104 $this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer); 105 } 106 107 /** 108 * Finds the tab whose title matches a given pattern 109 * @return mixed The name of the tab as a string or false if not found 110 */ 111 public function FindTab($sPattern, $sTabContainer = null) 112 { 113 return $this->m_oTabs->FindTab($sPattern, $sTabContainer); 114 } 115 116 /** 117 * Make the given tab the active one, as if it were clicked 118 * DOES NOT WORK: apparently in the *old* version of jquery 119 * that we are using this is not supported... TO DO upgrade 120 * the whole jquery bundle... 121 */ 122 public function SelectTab($sTabContainer, $sTabLabel) 123 { 124 $this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel)); 125 } 126 127 public function AddToMenu($sHtml) 128 { 129 $this->m_sMenu .= $sHtml; 130 } 131 132 /** 133 * Echoes the content of the whole page 134 * @return void 135 */ 136 public function output() 137 { 138 if (!empty($this->sContentType)) 139 { 140 $this->add_header('Content-type: '.$this->sContentType); 141 } 142 if (!empty($this->sContentDisposition)) 143 { 144 $this->add_header('Content-Disposition: '.$this->sContentDisposition.'; filename="'.$this->sContentFileName.'"'); 145 } 146 foreach($this->a_headers as $s_header) 147 { 148 header($s_header); 149 } 150 if ($this->m_oTabs->TabsContainerCount() > 0) 151 { 152 $this->add_ready_script( 153<<<EOF 154 // The "tab widgets" to handle. 155 var tabs = $('div[id^=tabbedContent]'); 156 157 // Ugly patch for a change in the behavior of jQuery UI: 158 // Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax) 159 // when their href was beginning by #. Starting with 1.9, a <base> tag in the page 160 // is taken into account and causes "local" tabs to be considered as Ajax 161 // unless their URL is equal to the URL of the page... 162 if ($('base').length > 0) 163 { 164 $('div[id^=tabbedContent] > ul > li > a').each(function() { 165 var sHash = location.hash; 166 var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, ''); 167 $(this).attr("href", sCleanLocation+$(this).attr("href")); 168 }); 169 } 170 if ($.bbq) 171 { 172 // This selector will be reused when selecting actual tab widget A elements. 173 var tab_a_selector = 'ul.ui-tabs-nav a'; 174 175 // Enable tabs on all tab widgets. The `event` property must be overridden so 176 // that the tabs aren't changed on click, and any custom event name can be 177 // specified. Note that if you define a callback for the 'select' event, it 178 // will be executed for the selected tab whenever the hash changes. 179 tabs.tabs({ event: 'change' }); 180 181 // Define our own click handler for the tabs, overriding the default. 182 tabs.find( tab_a_selector ).click(function() 183 { 184 var state = {}; 185 186 // Get the id of this tab widget. 187 var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); 188 189 // Get the index of this tab. 190 var idx = $(this).parent().prevAll().length; 191 192 // Set the state! 193 state[ id ] = idx; 194 $.bbq.pushState( state ); 195 }); 196 } 197 else 198 { 199 tabs.tabs(); 200 } 201EOF 202); 203 } 204 // Render the tabs in the page (if any) 205 $this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content, $this); 206 207 // Additional UI widgets to be activated inside the ajax fragment 208 // Important: Testing the content type is not enough because some ajax handlers have not correctly positionned the flag (e.g json response corrupted by the script) 209 if (($this->sContentType == 'text/html') && (preg_match('/class="date-pick"/', $this->s_content) || preg_match('/class="datetime-pick"/', $this->s_content)) ) 210 { 211 $this->add_ready_script( 212<<<EOF 213PrepareWidgets(); 214EOF 215 ); 216 } 217 $s_captured_output = $this->ob_get_clean_safe(); 218 if (($this->sContentType == 'text/html') && ($this->sContentDisposition == 'inline')) 219 { 220 // inline content != attachment && html => filter all scripts for malicious XSS scripts 221 echo self::FilterXSS($this->s_content); 222 } 223 else 224 { 225 echo $this->s_content; 226 } 227 if (!empty($this->m_sMenu)) 228 { 229 $uid = time(); 230 echo "<div id=\"accordion_temp_$uid\">\n"; 231 echo "<div id=\"accordion\">\n"; 232 echo "<!-- Beginning of the accordion menu -->\n"; 233 echo self::FilterXSS($this->m_sMenu); 234 echo "<!-- End of the accordion menu-->\n"; 235 echo "</div>\n"; 236 echo "</div>\n"; 237 238 echo "<script type=\"text/javascript\">\n"; 239 echo "$('#inner_menu').html($('#accordion_temp_$uid').html());\n"; 240 echo "$('#accordion_temp_$uid').remove();\n"; 241 echo "\n</script>\n"; 242 } 243 244 //echo $this->s_deferred_content; 245 if (count($this->a_scripts) > 0) 246 { 247 echo "<script type=\"text/javascript\">\n"; 248 echo implode("\n", $this->a_scripts); 249 echo "\n</script>\n"; 250 } 251 if (count($this->a_linked_scripts) > 0) 252 { 253 echo "<script type=\"text/javascript\">\n"; 254 foreach($this->a_linked_scripts as $sScriptUrl) 255 { 256 echo '$.getScript('.json_encode($sScriptUrl).");\n"; 257 } 258 echo "\n</script>\n"; 259 } 260 if (!empty($this->s_deferred_content)) 261 { 262 echo "<script type=\"text/javascript\">\n"; 263 echo "\$('body').append('".addslashes(str_replace("\n", '', $this->s_deferred_content))."');\n"; 264 echo "\n</script>\n"; 265 } 266 if (!empty($this->m_sReadyScript)) 267 { 268 echo "<script type=\"text/javascript\">\n"; 269 echo $this->m_sReadyScript; // Ready Scripts are output as simple scripts 270 echo "\n</script>\n"; 271 } 272 if(count($this->a_linked_stylesheets) > 0) 273 { 274 echo "<script type=\"text/javascript\">"; 275 foreach($this->a_linked_stylesheets as $aStylesheet) 276 { 277 $sStylesheetUrl = $aStylesheet['link']; 278 echo "if (!$('link[href=\"{$sStylesheetUrl}\"]').length) $('<link href=\"{$sStylesheetUrl}\" rel=\"stylesheet\">').appendTo('head');\n"; 279 } 280 echo "\n</script>\n"; 281 } 282 283 if (trim($s_captured_output) != "") 284 { 285 echo self::FilterXSS($s_captured_output); 286 } 287 288 if (class_exists('DBSearch')) 289 { 290 DBSearch::RecordQueryTrace(); 291 } 292 } 293 294 /** 295 * Adds a paragraph with a smaller font into the page 296 * NOT implemented (i.e does nothing) 297 * @param string $sText Content of the (small) paragraph 298 * @return void 299 */ 300 public function small_p($sText) 301 { 302 } 303 304 public function add($sHtml) 305 { 306 if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != '')) 307 { 308 $this->m_oTabs->AddToTab($this->m_oTabs->GetCurrentTabContainer(), $this->m_oTabs->GetCurrentTab(), $sHtml); 309 } 310 else 311 { 312 parent::add($sHtml); 313 } 314 } 315 316 /** 317 * Records the current state of the 'html' part of the page output 318 * @return mixed The current state of the 'html' output 319 */ 320 public function start_capture() 321 { 322 $sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer(); 323 $sCurrentTab = $this->m_oTabs->GetCurrentTab(); 324 325 if (!empty($sCurrentTabContainer) && !empty($sCurrentTab)) 326 { 327 $iOffset = $this->m_oTabs->GetCurrentTabLength(); 328 return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset); 329 } 330 else 331 { 332 return parent::start_capture(); 333 } 334 } 335 336 /** 337 * Returns the part of the html output that occurred since the call to start_capture 338 * and removes this part from the current html output 339 * @param $offset mixed The value returned by start_capture 340 * @return string The part of the html output that was added since the call to start_capture 341 */ 342 public function end_capture($offset) 343 { 344 if (is_array($offset)) 345 { 346 if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab'])) 347 { 348 $sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']); 349 } 350 else 351 { 352 $sCaptured = ''; 353 } 354 } 355 else 356 { 357 $sCaptured = parent::end_capture($offset); 358 } 359 return $sCaptured; 360 } 361 362 /** 363 * Add any text or HTML fragment (identified by an ID) at the end of the body of the page 364 * This is useful to add hidden content, DIVs or FORMs that should not 365 * be embedded into each other. 366 */ 367 public function add_at_the_end($s_html, $sId = '') 368 { 369 if ($sId != '') 370 { 371 $this->add_script("$('#{$sId}').remove();"); // Remove any previous instance of the same Id 372 } 373 $this->s_deferred_content .= $s_html; 374 } 375 376 /** 377 * Adds a script to be executed when the DOM is ready (typical JQuery use) 378 * NOT implemented in this version of the class. 379 * @return void 380 */ 381 public function add_ready_script($sScript) 382 { 383 $this->m_sReadyScript .= $sScript."\n"; 384 } 385 386 /** 387 * Cannot be called in this context, since Ajax pages do not share 388 * any context with the calling page !! 389 */ 390 public function GetUniqueId() 391 { 392 assert(false); 393 return 0; 394 } 395 396 public static function FilterXSS($sHTML) 397 { 398 return str_ireplace(array('<script', '</script>'), array('<!-- <removed-script', '</removed-script> -->'), $sHTML); 399 } 400} 401 402