1<?php 2 3/** 4 * Observium 5 * 6 * This file is part of Observium. 7 * 8 * These functions perform operations with templates. 9 * 10 * @package observium 11 * @subpackage templates 12 * @author Adam Armstrong <adama@observium.org> 13 * @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited 14 * 15 */ 16 17/* WARNING. This file should be load after config.php! */ 18 19/** 20 * The function returns content of specific template 21 * 22 * @param string $type Type of template (currently only 'alert', 'group', 'notification') 23 * @param string $subtype Subtype of template type, examples: 'email' for notification, 'device' for group or alert 24 * @param string $name Name for template, also can used as name for group/alert/etc (lowercase!) 25 * 26 * @return string $template Content of specific template 27 */ 28function get_template($type, $subtype, $name = '') 29{ 30 $template = ''; // If template not found, return empty string 31 $template_dir = $GLOBALS['config']['template_dir']; 32 $default_dir = $GLOBALS['config']['install_dir'] . '/includes/templates'; 33 34 if (empty($name)) 35 { 36 // If name empty, than seems as we use filename instead (ie: email_html.tpl, type_somename.xml) 37 $basename = basename($subtype); 38 list($subtype, $name) = explode('_', $basename, 2); 39 } 40 41 switch ($type) 42 { 43 case 'alert': 44 case 'group': 45 case 'notification': 46 $name = preg_replace('/\.(tpl|xml)$/', '', strtolower($name)); 47 // Notifications used raw text templates (with mustache format), 48 // all other used XML templates 49 // Examples: 50 // /opt/observium/templates/alert/device_myname.xml 51 // /opt/observium/templates/notification/email_html.tpl 52 if ($type == 'notification') 53 { 54 $ext = '.tpl'; 55 } else { 56 $ext = '.xml'; 57 } 58 $template_file = $type . '/' . $subtype . '_' . $name . $ext; 59 if (is_file($template_dir . '/' . $template_file)) 60 { 61 // User templates 62 $template = file_get_contents($template_dir . '/' . $template_file); 63 } 64 else if (is_file($default_dir . '/' . $template_file)) 65 { 66 // Default templates 67 $template = file_get_contents($default_dir . '/' . $template_file); 68 } 69 break; 70 default: 71 print_debug("Template type '$type' with subtype '$subtype' and name '$name' not found!"); 72 } 73 74 return $template; 75} 76 77/** 78 * The function returns list of all template files for specific template type(s) 79 * 80 * @param mixed $types Type name of list of types as array 81 * @return array $template_list List of template files with type as array keys 82 */ 83function get_templates_list($types) 84{ 85 $template_list = array(); // If templates not found, return empty list 86 $template_dir = $GLOBALS['config']['template_dir']; 87 $default_dir = $GLOBALS['config']['install_dir'] . '/includes/templates'; 88 89 if (!is_array($types)) 90 { 91 $types = array($types); 92 } 93 foreach ($types as $type) 94 { 95 switch ($type) 96 { 97 case 'alert': 98 case 'group': 99 case 'notification': 100 if ($type == 'notification') 101 { 102 $ext = '.tpl'; 103 } else { 104 $ext = '.xml'; 105 } 106 foreach (glob($default_dir . '/' . $type . '/?*_?*' . $ext) as $filename) 107 { 108 // Default templates, before user templates for override 109 $template_list[$type][] = $filename; 110 } 111 // Examples: 112 // /opt/observium/templates/alert/device_myname.xml 113 // /opt/observium/templates/notification/email_html.tpl 114 foreach (glob($template_dir . '/' . $type . '/?*_?*' . $ext) as $filename) 115 { 116 // User templates 117 $template_list[$type][] = $filename; 118 } 119 break; 120 default: 121 print_debug("Template type '$type' unknown!"); 122 } 123 } 124 125 return $template_list; 126} 127 128/** 129 * The function returns array with all avialable templates 130 * 131 * @param mixed $types Type name of list of types as array 132 * @return array $template_array List of template with type and subtype as keys and name as values 133 */ 134function get_templates_array($types) 135{ 136 $template_array = array(); // If templates not found, return empty array 137 138 $template_list = get_templates_list($types); // Get templates file list 139 140 foreach ($template_list as $type => $list) 141 { 142 foreach ($list as $filename) 143 { 144 $basename = basename($filename); 145 $basename = preg_replace('/\.(tpl|xml)$/', '', $basename); 146 list($subtype, $name) = explode('_', $basename, 2); 147 $template_array[$type][$subtype] = strtolower($name); 148 } 149 } 150 151 return $template_array; 152} 153 154/** 155 * This is very-very-very simple template engine (or not simple?), 156 * only some basic conversions and uses Mustache/CTemplate syntax. 157 * 158 * no cache/logging and others, for now support only this tags: 159 * standart php comments 160 * {{! %^ }} - intext comments 161 * {{var}} - escaped var 162 * {{{var}}} - unescaped var 163 * {{var.subvar}} - dot notation vars 164 * {{.}} - implicit iterator 165 * {{#var}} some text {{/var}} - if/list condition 166 * {{^var}} some text {{/var}} - inverted (negative) if condition 167 * options: 168 * 'is_file', if set to TRUE, than get template from file $config['install_dir']/includes/templates/$template.tpl 169 * if set to FALSE (default), than use template from variable. 170 */ 171// NOTE, do NOT use this function for generate pages, as adama said! 172function simple_template($template, $tags, $options = array('is_file' => FALSE, 'use_cache' => FALSE)) 173{ 174 if (!is_string($template) || !is_array($tags)) 175 { 176 // Return false if template not string (or filename) and tags not array 177 return FALSE; 178 } 179 180 if (isset($options['is_file']) && $options['is_file']) 181 { 182 // Get template from file 183 $template = get_template('notification', $template); 184 185 // Return false if no file content or false file read 186 if (!$template) { return FALSE; } 187 } 188 189 // Cache disabled for now, i think this can generate huge array 190 /** 191 $use_cache = isset($options['use_cache']) && $options['use_cache'] && $tags; 192 if ($use_cache) 193 { 194 global $cache; 195 196 $timestamp = time(); 197 $template_csum = md5($template); 198 $tags_csum = md5(json_encode($tags)); 199 200 if (isset($cache['templates'][$template_csum][$tags_csum])) 201 { 202 if (($timestamp - $cache['templates'][$template_csum][$tags_csum]['timestamp']) < 600) 203 { 204 return $cache['templates'][$template_csum][$tags_csum]['string']; 205 } 206 } 207 } 208 */ 209 210 $string = $template; 211 212 // Removes multi-line comments and does not create 213 // a blank line, also treats white spaces/tabs 214 $string = preg_replace('![ \t]*/\*.*?\*/[ \t]*[\r\n]?!s', '', $string); 215 216 // Removes single line '//' comments, treats blank characters 217 $string = preg_replace('![ \t]*//.*[ \t]*[\r\n]?!', '', $string); 218 219 // Removes in-text comments {{! any text }} 220 $string = preg_replace('/{{!.*?}}/', '', $string); 221 222 // Strip blank lines 223 //$string = preg_replace('/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/', PHP_EOL, $string); 224 225 // Replace keys, loops and other template sintax 226 $string = simple_template_replace($string, $tags); 227 228 /** 229 if ($use_cache) 230 { 231 $cache['templates'][$template_csum][$tags_csum] = array('timestamp' => $timestamp, 232 'string' => $string); 233 } 234 */ 235 236 return $string; 237} 238 239function simple_template_replace($string, $tags) 240{ 241 // Note for future: to match Unix LF (\n), MacOS<9 CR (\r), Windows CR+LF (\r\n) and rare LF+CR (\n\r) 242 // EOL patern should be: /((\r?\n)|(\n?\r))/ 243 $patterns = array( 244 // {{#var}} some text {{/var}} 245 'list_condition' => '![ \t]*{{#[ \t]*([ \w[:punct:]]+?)[ \t]*}}[ \t]*[\r\n]?(.*?){{/[ \t]*\1[ \t]*}}[ \t]*([\r\n]?)!s', 246 // {{^var}} some text {{/var}} 247 'negative_condition' => '![ \t]*{{\^[ \t]*([ \w[:punct:]]+?)[ \t]*}}[ \t]*[\r\n]?(.*?){{/[ \t]*\1[ \t]*}}[ \t]*([\r\n]?)!s', 248 // {{{var}}} 249 'var_noescape' => '!{{{[ \t]*([^}{#\^\?/]+?)[ \t]*}}}!', 250 // {{var}} 251 'var_escape' => '!{{[ \t]*([^}{#\^\?/]+?)[ \t]*}}!', 252 ); 253 // Main loop 254 foreach ($patterns as $condition => $pattern) 255 { 256 switch ($condition) 257 { 258 // LIST condition first! 259 case 'list_condition': 260 // NEGATIVE condition second! 261 case 'negative_condition': 262 if (preg_match_all($pattern, $string, $matches)) 263 { 264 foreach ($matches[1] as $key => $var) 265 { 266 $test_tags = isset($tags[$var]) && $tags[$var]; 267 if (($condition == 'list_condition' && $test_tags) || 268 ($condition == 'negative_condition' && !$test_tags)) 269 { 270 $replace = preg_replace('/[\t\ ]+$/', '', $matches[2][$key]); 271 //if (!$matches[3][$key]) 272 //{ 273 // // Remove last newline if condition at EOF 274 // $replace = preg_replace('/[\r\n]$/', '', $replace); 275 //} 276 if ($condition == 'list_condition' && is_array($tags[$var])) 277 { 278 // Additional remove first newline if pressent 279 $replace = preg_replace('/^[\r\n]/', '', $matches[2][$key]); 280 // If tag is array, use recurcive repeater 281 $repeate = array(); 282 foreach ($tags[$var] as $item => $entry) 283 { 284 $repeate[] = simple_template_replace($replace, $entry); 285 } 286 $replace = implode('', $repeate); 287 } 288 } else { 289 $replace = ''; 290 } 291 $string = str_replace($matches[0][$key], $replace, $string); 292 } 293 } 294 break; 295 // Next var not escaped 296 case 'var_noescape': 297 // Next var escaped 298 case 'var_escape': 299 if (preg_match_all($pattern, $string, $matches)) 300 { 301 foreach ($matches[1] as $key => $var) 302 { 303 if ($var === '.' && is_string($tags)) 304 { 305 // This conversion for implicit iterator {{.}} 306 $tags = array('.' => $tags); 307 $subvars = array(); 308 } else { 309 $subvars = explode('.', $var); 310 } 311 312 if (isset($tags[$var])) 313 { 314 // {{ var }}, {{{ var_noescape }}} 315 $replace = ($condition === 'var_noescape' ? $tags[$var] : htmlspecialchars($tags[$var], ENT_QUOTES, 'UTF-8')); 316 } 317 else if (count($subvars) > 1 && is_array($tags[$subvars[0]])) 318 { 319 // {{ var.with.iterator }}, {{{ var.with.iterator.noescape }}} 320 $replace = $tags[$subvars[0]]; 321 array_shift($subvars); 322 foreach ($subvars as $subvar) 323 { 324 if (isset($replace[$subvar])) 325 { 326 $replace = $replace[$subvar]; 327 } else { 328 unset($replace); 329 break; 330 } 331 } 332 $replace = ($condition === 'var_noescape' ? $replace : htmlspecialchars($replace, ENT_QUOTES, 'UTF-8')); 333 } else { 334 // By default if tag not exist, remove var from template 335 $replace = ''; 336 } 337 $string = str_replace($matches[0][$key], $replace, $string); 338 } 339 } 340 break; 341 } 342 } 343 //var_dump($string); 344 return $string; 345} 346 347/** 348 * This function convert array based group/alerts to observium xml based template 349 * 350 * Template attributes: 351 * type - Type (ie: alert, group, notification) 352 * description - Description 353 * version - Template format version 354 * created - Created date 355 * observium - Used observium version 356 * id - Unique template id, based on conditions/associations/text 357 * 358 * Template params: 359 * entity - Type of entity 360 * name - Unique name for current set of params 361 * description - Description for current set of params 362 * message - Text message 363 * conditions - Set of conditions 364 * conditions_and - 1 - require all conditions, 0 - require any condition 365 * conditions_complex - oneline conditions set (not used for now) 366 * associations - Set of associations 367 * device - Set of device associations 368 * entity - Set of entity associations 369 * 370 * @param string $type Current template type for generate (alert or group) 371 * @param array $params 372 * @param boolean $as_xml_object If set to TRUE, return template as SimpleXMLElement object 373 * 374 * @return mixed XML based template (as string or SimpleXMLElement object if $as_xml_object set to true) 375 */ 376function generate_template($type, $params, $as_xml_object = FALSE) 377{ 378 if (!check_extension_exists('SimpleXML', 'SimpleXML php extension not found, it\'s required for generate templates.')) 379 { 380 return ''; 381 } 382 // r($params); var_export($params); 383 384 $type = strtolower(trim($type, " '\"\t\n\r\0\x0B")); // Clean template type 385 386 $template_xml = new SimpleXMLElement('<template/>'); 387 // Template type 388 $template_xml->addAttribute('type', $type); 389 // Template description 390 $template_xml->addAttribute('description', 'Autogenerated observium template'); 391 // Format version. If something changed in templates format, increase version! 392 $template_xml->addAttribute('version', '0.91'); 393 // Template created date and time 394 $template_xml->addAttribute('created', date('r')); 395 // Used observium version 396 $template_xml->addAttribute('observium', OBSERVIUM_VERSION); 397 398 $template_array = array(); 399 switch ($type) 400 { 401 case 'group': 402 $template_array['entity_type'] = strtolower(trim($params['entity_type'], " '\"\t\n\r\0\x0B")); 403 $template_array['name'] = strtolower(trim($params['group_name'], " '\"\t\n\r\0\x0B")); 404 $template_array['description'] = trim($params['group_descr'], " '\"\t\n\r\0\x0B"); 405 406 break; 407 case 'alert': 408 $template_array['entity_type'] = strtolower(trim($params['entity_type'], " '\"\t\n\r\0\x0B")); 409 $template_array['name'] = strtolower(trim($params['alert_name'], " '\"\t\n\r\0\x0B")); 410 //$template_array['description'] = trim($params['alert_descr'], " '\"\t\n\r\0\x0B"); 411 $template_array['message'] = $params['alert_message']; 412 413 $template_array['severity'] = strtolower(trim($params['severity'], " '\"\t\n\r\0\x0B")); 414 if (in_array($params['suppress_recovery'], array('1', 'on', 'yes', TRUE))) 415 { 416 $template_array['suppress_recovery'] = 1; 417 } else { 418 $template_array['suppress_recovery'] = 0; 419 } 420 $template_array['delay'] = trim($params['delay'], " '\"\t\n\r\0\x0B"); 421 $template_array['delay'] = (int)$template_array['delay']; 422 423 $template_array['conditions_and'] = (int)$params['and']; 424 $and_or = ($params['and'] ? " AND " : " OR "); 425 $conds = array(); 426 if (!is_array($params['conditions'])) 427 { 428 $params['conditions'] = json_decode($params['conditions'], TRUE); 429 } 430 foreach ($params['conditions'] as $cond) 431 { 432 if (!is_array($cond)) 433 { 434 $cond = json_decode($cond, TRUE); 435 } 436 $count = count($cond); 437 if (isset($cond['metric']) && $count >= 3) 438 { 439 $line = $cond['metric'] . ' ' . $cond['condition'] . ' ' . $cond['value']; 440 } 441 else if ($count === 3) 442 { 443 $line = implode(' ', $cond); 444 } else { 445 continue; 446 } 447 $conds[] = $line; 448 } 449 if ($conds) 450 { 451 $template_array['conditions'] = $conds; 452 $template_array['conditions_complex'] = implode($and_or, $conds); 453 } 454 455 break; 456 case 'notification': 457 $template_array['name'] = strtolower(trim($params['name'], " '\"\t\n\r\0\x0B")); 458 $template_array['description'] = trim($params['description'], " '\"\t\n\r\0\x0B"); 459 460 $template_array['message'] = $params['message']; 461 break; 462 default: 463 print_error("Unknown template type '$type' passed to " . __FUNCTION__ . "()."); 464 return ''; 465 } 466 467 // Associations 468 $associations = array(); 469 foreach ($params['associations'] as $assoc) 470 { 471 // Each associations set 472 if (!is_array($assoc)) 473 { 474 $assoc = json_decode($assoc, TRUE); 475 } 476 //r($assoc); 477 foreach (array('device', 'entity') as $param) 478 { 479 if (isset($assoc[$param . '_attribs'])) 480 { 481 $association[$param] = array(); 482 if (!is_array($assoc[$param . '_attribs'])) 483 { 484 $assoc[$param . '_attribs'] = json_decode($assoc[$param . '_attribs'], TRUE); 485 } 486 foreach ($assoc[$param . '_attribs'] as $attrib) 487 { 488 if (!is_array($attrib)) 489 { 490 $attrib = json_decode($attrib, TRUE); 491 } 492 //r($attrib); 493 494 $count = count($attrib); 495 if (empty($attrib) || $attrib['attrib'] == '*') 496 { 497 $association[$param] = array('*'); 498 break; 499 } 500 else if (isset($attrib['attrib']) && $count >= 3) 501 { 502 $line = $attrib['attrib'] . ' ' . $attrib['condition'] . ' ' . $attrib['value']; 503 } 504 else if ($count === 3) 505 { 506 $line = implode(' ', $attrib); 507 } else { 508 continue; 509 } 510 $association[$param][] = $line; 511 512 } 513 } 514 } 515 $associations[] = $association; 516 } 517 //r($associations); 518 if ($associations) 519 { 520 $template_array['associations'] = $associations; 521 } 522 523 //foreach (array('device', 'entity') as $param) 524 //{ 525 // $conds = array(); 526 // if (isset($params['assoc_' . $param . '_conditions'])) 527 // { 528 // foreach (explode("\n", $params['assoc_' . $param . '_conditions']) as $cond) 529 // { 530 // $line = trim($cond); 531 // if ($line == "*") 532 // { 533 // $conds = array($line); 534 // break; 535 // } 536 // $count = count(explode(" ", $line, 3)); 537 // if ($count === 3) 538 // { 539 // $line = implode(' ', $cond); 540 // $conds[] = $line; 541 // } 542 // } 543 // } 544 // else if (isset($params[$param . '_attribs'])) 545 // { 546 // if (!is_array($params[$param . '_attribs'])) 547 // { 548 // $params[$param . '_attribs'] = json_decode($params[$param . '_attribs'], TRUE); 549 // } 550 // foreach ($params[$param . '_attribs'] as $attribs) 551 // { 552 // if (!is_array($attribs)) 553 // { 554 // $attribs = json_decode($attribs, TRUE); 555 // } 556 // foreach ($attribs as $cond) 557 // { 558 // $count = count($cond); 559 // if (empty($cond) || $cond['attrib'] == '*') 560 // { 561 // $conds = array('*'); 562 // break; 563 // } 564 // else if ($count === 3) 565 // { 566 // if (isset($cond['attrib'])) 567 // { 568 // $line = $cond['attrib'] . ' ' . $cond['condition'] . ' ' . $cond['value']; 569 // } else { 570 // $line = implode(' ', $cond); 571 // } 572 // $attrib[] = $line; 573 // } 574 // } 575 // $conds[] = $attrib; 576 // } 577 // } 578 // r($conds); 579 // if ($conds) 580 // { 581 // $and_or = " AND "; 582 // $template_array['associations'][$param] = $conds; 583 // } 584 //} 585 586 // Convert template array to xml 587 array_to_xml($template_array, $template_xml); 588 589 // Add unique id, based on conditions/associations (can used for quick compare templates) 590 if ($type != 'notification') 591 { 592 $template_id = md5(serialize(array($template_array['conditions'], $template_array['associations']))); 593 } else { 594 $template_id = md5($template_array['message']); 595 } 596 $template_xml->addAttribute('id', $template_id); 597 598 // Name must be safe and not empty! 599 if (!empty($template_array['name'])) 600 { 601 $template_array['name'] = safename($template_array['name']); 602 } else { 603 $template_array['name'] = 'autogenerated_' .$template_id; 604 } 605 606 if ($as_xml_object) 607 { 608 return $template_xml; 609 } else { 610 // Convert objected template to XML string 611 return $template_xml->asXML(); 612 } 613} 614 615/** 616 * Very simple combinate multiple templates from generate_template() into one XML templates 617 * 618 */ 619function generate_templates($type, $params) 620{ 621 $templates_xml = '<?xml version="1.0"?>' . PHP_EOL . '<templates>' . PHP_EOL; 622 if (!is_array_assoc($params)) 623 { 624 foreach ($params as $entry) 625 { 626 $template = generate_template($type, $entry); 627 $templates_xml .= PHP_EOL . preg_replace('/^\s*<\?xml.+?\?>\s*(<template)/s', '\1', $template); 628 } 629 } else { 630 $template = generate_template($type, $params); 631 $templates_xml .= PHP_EOL . preg_replace('/^\s*<\?xml.+?\?>\s*(<template)/s', '\1', $template); 632 } 633 634 $templates_xml .= PHP_EOL . '</templates>' . PHP_EOL; 635 636 return($templates_xml); 637} 638 639/** 640 * Convert an multi-dimensional array to xml. 641 * http://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml 642 * 643 * @param object $object Link to SimpleXMLElement object 644 * @param array $data Array which need to convert into xml 645 */ 646function array_to_xml(array $data, SimpleXMLElement $object) 647{ 648 foreach ($data as $key => $value) 649 { 650 if (is_array($value)) 651 { 652 if (is_array_assoc($value)) 653 { 654 // For associative arrays use keys as child object 655 $new_object = $object->addChild($key); 656 array_to_xml($value, $new_object); 657 } else { 658 // For sequential arrays use parent key as child 659 foreach ($value as $new_value) 660 { 661 if (is_array($new_value)) 662 { 663 array_to_xml(array($key => $new_value), $object); 664 } else { 665 $object->addChild($key, $new_value); 666 } 667 } 668 } 669 } else { 670 //$object->$key = $value; // See note about & here - http://php.net/manual/en/simplexmlelement.addchild.php#112204 671 $object->addChild($key, $value); 672 } 673 } 674} 675 676function xml_to_array($xml_string) 677{ 678 $xml = simplexml_load_string($xml_string); 679 // r($xml); 680 $json = json_encode($xml); 681 $array = json_decode($json, TRUE); 682 683 return $array; 684} 685 686/** 687 * Pretty print for xml string 688 * 689 * @param string $xml An xml string 690 * @param boolean $formatted Convert or not output to human formatted xml 691 */ 692function print_xml($xml, $formatted = TRUE) 693{ 694 if ($formatted) 695 { 696 $xml = format_xml($xml); 697 } 698 if (is_cli()) 699 { 700 echo $xml; 701 } else { 702 703 echo generate_box_open(array('title' => 'Output', 'padding' => TRUE)); 704 echo ' 705 <pre class="prettyprint lang-xml small">' . escape_html($xml) . '</pre> 706 <span><em>NOTE: XML values are always escaped, that\'s why you can see this <mark>' . escape_html(escape_html('< > & " \'')) . 707 '</mark> instead of this <mark>' . escape_html('< > & " \'') . '</mark>. <u>Leave them as is</u>.</em></span> 708 <script type="text/javascript">window.prettyPrint && prettyPrint();</script>' . PHP_EOL; 709 echo generate_box_close(); 710 } 711} 712 713/** 714 * Convert unformatted XML string to human readable string 715 * 716 * @param string $xml Unformatted XML string 717 * @return string Human formatted XML string 718 */ 719function format_xml($xml) 720{ 721 if (!class_exists('DOMDocument')) 722 { 723 // If not exist class, just return original string 724 return $xml; 725 } 726 727 $dom = new DOMDocument("1.0"); 728 $dom->preserveWhiteSpace = FALSE; 729 $dom->formatOutput = TRUE; 730 $dom->loadXML($xml); 731 732 return $dom->saveXML(); 733} 734 735/** 736 * Send any string to browser as file 737 * 738 * @param string $string String content for save as file 739 * @param string $filename Filename 740 * @param array $vars Vars with some options 741 */ 742function download_as_file($string, $filename = "observium_export.xml", $vars = array()) 743{ 744 //$echo = ob_get_contents(); 745 ob_end_clean(); // Clean and disable buffer 746 747 $ext = pathinfo($filename, PATHINFO_EXTENSION); 748 if ($ext == 'xml') 749 { 750 header('Content-type: text/xml'); 751 if ($vars['formatted'] == 'yes') 752 { 753 $string = format_xml($string); 754 } 755 } else { 756 header('Content-type: text/plain'); 757 } 758 header('Content-Disposition: attachment; filename="'.$filename.'";'); 759 header("Content-Length: " . strlen($string)); 760 header("Pragma: no-cache"); 761 header("Expires: 0"); 762 763 echo($string); // Send string content to browser output 764 765 exit(0); // Stop any other output 766} 767 768// EOF 769