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