xht.lib.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php /* <!-- xht.lib.php -->
  2. <!-- XML HTML Templates, 2006/12/14 -->
  3. <!-- Copyright (C) 2006 rene.haentjens@UGent.be - see note at end of text -->
  4. <!-- Released under the GNU GPL V2, see http://www.gnu.org/licenses/gpl.html -->
  5. */
  6. /**
  7. * This is an XML HTML template library.
  8. * Include/require it in your code to use its functionality.
  9. *
  10. * This library defines function xht_htmlwchars & class xhtdoc with methods:
  11. * - xht_fill_template($template_name)
  12. * - xht_substitute($subtext)
  13. *
  14. * Check htt_error after defining a new xhtdoc(htt_file_contents).
  15. *
  16. * Assign xht_xmldoc (, xht_get_lang, xht_resource, xht_dbgn)
  17. * before calling the class methods.
  18. *
  19. * @package chamilo.library
  20. */
  21. function xht_htmlwchars($s) // use only where ISO-8859-1 is not required!
  22. {
  23. //return ereg_replace('\[((/?(b|big|i|small|sub|sup|u))|br/)\]', '<\\1>',
  24. // str_replace('@�@', '&',
  25. // htmlspecialchars(ereg_replace('&#([0-9]+);', '@�@#\\1;', $s))));
  26. global $charset;
  27. return api_ereg_replace('\[((/?(b|big|i|small|sub|sup|u))|br/)\]', '<\\1>',
  28. str_replace('@�@', '&',
  29. htmlspecialchars(api_ereg_replace('&#([0-9]+);', '@�@#\\1;', $s), ENT_QUOTES, $charset)));
  30. // replaces htmlspecialchars for double-escaped xml chars like '&amp;#nnn;'
  31. // and when html tags <...> are represented as [...]
  32. }
  33. function xht_is_assoclist($s) // ":key1:value1,, key2:value2,, ..."
  34. {
  35. return is_string($s) && strpos($s, ',,');
  36. }
  37. function xht_explode_assoclist($s)
  38. {
  39. $result = array(); if (!xht_is_assoclist($s)) return $result;
  40. foreach (explode(',,', api_substr($s, 1)) as $keyplusvalue)
  41. if ($cp = api_strpos($keyplusvalue, api_substr($s, 0, 1)))
  42. $result[trim(api_substr($keyplusvalue, 0, $cp))] =
  43. api_substr($keyplusvalue, $cp+1);
  44. return $result;
  45. }
  46. class xhtdoc
  47. {
  48. var $htt_array; // array with HTML templates
  49. var $htt_error; // error while parsing htt_file_contents to templates
  50. var $xht_xmldoc; // XML Mini-DOM document for which templates are processed
  51. var $xht_param; // parameter array for template processing
  52. var $xht_get_lang; // user-supplied function for {-L xx-}, e.g. Dokeos get_lang
  53. var $xht_resource; // user-supplied function for '=/' in path
  54. var $xht_dbgn; // set to a positive value for debugging output
  55. var $xht_dbgo; // debugging output accumulates here
  56. var $_prev_param; // old parameter values
  57. function xht_fill_template($template_name, $cur_elem = 0)
  58. {
  59. $template_name = trim($template_name);
  60. if (!$template_name || (api_strpos($template_name, ' ') !== FALSE)) return '';
  61. if (!is_string($httext = $this->htt_array[$template_name])) return '';
  62. if ($this->xht_dbgn) $this->xht_dbgo .= '<!-- ' . XHT_LP . $template_name .
  63. ' ' . XHT_RP . $this->_show_param() . " -->\n";
  64. $prev_lpp = 0; $prev_sub = ''; $scanpos = 0;
  65. while (($rpp = api_strpos($httext, XHT_RP, $scanpos)) !== FALSE) // first -}
  66. {
  67. if (($lpp = api_strpos($httext, XHT_LP)) === FALSE) break; // no {- for -}
  68. if ($lpp > $rpp) break; // no {- preceding -}
  69. while (($next_lpp = api_strpos($httext, XHT_LP, $lpp + XHT_PL)) !== FALSE)
  70. if ($next_lpp > $rpp) break;
  71. else $lpp = $next_lpp; // find {- closest to -}
  72. $subtext = api_substr($httext, $lpp + XHT_PL, $rpp - $lpp - XHT_PL);
  73. $httext = api_substr($httext, 0, $lpp) .
  74. $this->xht_substitute($subtext, $cur_elem, XHT_LP, XHT_RP) .
  75. api_substr($httext, $rpp + XHT_PL); // substitute or leave intact
  76. if ($lpp == $prev_lpp && $subtext == $prev_sub) // prevent looping
  77. {
  78. $scanpos = $rpp + 1; $prev_lpp = 0; $prev_sub = '';
  79. }
  80. else
  81. {
  82. $prev_lpp = $lpp; $prev_sub = $subtext;
  83. }
  84. }
  85. return $httext;
  86. }
  87. function xht_substitute($subtext, $cur_elem = 0, $pre = '', $post = '')
  88. {
  89. global $charset;
  90. $regs = array(); // for use with ereg()
  91. if (!api_ereg(XHT_SUBS2, $subtext, $regs) && !api_ereg(XHT_SUBS1, $subtext, $regs))
  92. return $pre . $subtext . $post;
  93. $type = $regs[1]; $text = $regs[2]; $result = ''; $subnum = FALSE;
  94. $subtext = isset($regs[3]) ? $regs[3] : '';
  95. if ($this->xht_dbgn) // generate debugging output, with new number $subnum
  96. {
  97. $subnum = ++ $this->xht_dbgn;
  98. $this->xht_dbgo .= '<!-- ' . XHT_LP . $type . $subnum . '+ ' .
  99. htmlspecialchars($text, ENT_QUOTES, $charset) . ' ' . XHT_RP .
  100. $this->_show_param() . " -->\n";
  101. }
  102. if ($type == 'D') // Define, e.g. {-D key value-}
  103. {
  104. // Assign the value to parameter [key]
  105. $this->xht_param[$text] = $subtext;
  106. // used to be: = $this->xht_substitute($subtext, $cur_elem);
  107. }
  108. elseif ($type == 'E') // Escape, e.g. {-E userFunction subtext-}
  109. {
  110. $result = call_user_func($text, FALSE); // get cached result, if any
  111. if ($result === FALSE) // no cached result available
  112. {
  113. $result = $this->xht_substitute($subtext, $cur_elem);
  114. $result = call_user_func($text, $result); // allow to cache
  115. }
  116. }
  117. elseif ($type == 'R') // Repeat, e.g. {-R general/title/string subtext-}
  118. {
  119. $rdepthn = 'rdepth' . (++ $this->xht_param['rdepth']);
  120. $n = 0; $this->xht_param['number'] = '0';
  121. if (is_array($a = $this->_lang($text, $cur_elem))) // repeat on array
  122. {
  123. foreach ($a as $key => $value)
  124. {
  125. $this->xht_param['number'] = (string) (++ $n);
  126. $this->xht_param[$rdepthn] = (string) ($n);
  127. $this->xht_param['key'] = $key;
  128. $this->xht_param['value'] = $value;
  129. $result .= $this->xht_substitute($subtext, $cur_elem);
  130. }
  131. }
  132. elseif (xht_is_assoclist($a)) // repeat on associative list
  133. {
  134. foreach (xht_explode_assoclist($a) as $key => $value)
  135. {
  136. $this->xht_param['number'] = (string) (++ $n);
  137. $this->xht_param[$rdepthn] = (string) ($n);
  138. $this->xht_param['key'] = $key;
  139. $this->xht_param['value'] = $value;
  140. $result .= $this->xht_substitute($subtext, $cur_elem);
  141. }
  142. }
  143. elseif (!is_object($this->xht_xmldoc))
  144. {
  145. $result = '? R Error: no XML doc has been assigned to xht_xmldoc';
  146. }
  147. else // repeat on XML elements
  148. {
  149. $sub_elems =
  150. $this->xht_xmldoc->xmd_select_elements($text, $cur_elem);
  151. foreach ($sub_elems as $subElem)
  152. {
  153. $this->xht_param['number'] = (string) (++ $n);
  154. $this->xht_param[$rdepthn] = (string) ($n);
  155. $result .= $this->xht_substitute($subtext, $subElem);
  156. }
  157. }
  158. // removed 2004/10/05: template_array (security)
  159. // added 2005/03/08: associative list (lang arrays deprecated)
  160. $this->xht_param['rdepth'] --;
  161. // As there is only one ['number'] or one set ['key'] + ['value'],
  162. // using them in nested repeats may not have the desired result.
  163. }
  164. elseif ($type == 'T') // Test, e.g. {-T key1 == key2 text-}
  165. {
  166. if (api_ereg('^(=|==|<=|<|!=|<>|>|>=) +([^ ]+) +(.*)$', $subtext, $regs))
  167. {
  168. // Comparand= parameter value if set, else languagevar value
  169. $cmp1 = isset($this->xht_param[$text]) ?
  170. $this->xht_param[$text] : $this->_lang($text, $cur_elem);
  171. $cmp3 = isset($this->xht_param[$cmp3 = $regs[2]]) ?
  172. $this->xht_param[$cmp3] : $this->_lang($cmp3, $cur_elem);
  173. $cmp = strcmp($cmp1, $cmp3); $op = ' ' . $regs[1] . ' ';
  174. if ($subnum) $this->xht_dbgo .= '<!-- ' . XHT_LP . $type . $subnum .
  175. ' ' . htmlspecialchars($cmp1.$op.$cmp3.' = '.$cmp, ENT_QUOTES, $charset) .
  176. ' ' . XHT_RP . " -->\n"; // debugging output
  177. if ( ($cmp < 0 && api_strpos(' <= < != <> ', $op)) ||
  178. ($cmp == 0 && api_strpos(' = == <= >= ', $op)) ||
  179. ($cmp > 0 && api_strpos(' != <> > >= ', $op)) )
  180. $result = $this->xht_substitute($regs[3], $cur_elem);
  181. // else $result is empty
  182. }
  183. else
  184. {
  185. $result = $pre . $subtext . $post;
  186. }
  187. }
  188. else
  189. {
  190. if (api_strpos('CLPVX', $type) !== FALSE) // used to be always
  191. $text = $this->xht_substitute($text, $cur_elem); // nested escape
  192. if ($type == 'C') // Call, e.g. {-C SUBTEMPLATE-}
  193. $result = $this->xht_fill_template($text, $cur_elem);
  194. elseif ($type == 'H') $result = htmlspecialchars($text, ENT_QUOTES, $charset);
  195. elseif ($type == 'L') $result = $this->_lang($text, $cur_elem);
  196. elseif ($type == 'P') $result = $this->xht_param[$text];
  197. elseif ($type == 'U') $result = urlencode($text);
  198. elseif ($type == 'W') $result = xht_htmlwchars($text);
  199. elseif (!is_object($this->xht_xmldoc))
  200. {
  201. $result = '? V/X Error: no XML doc has been assigned to xht_xmldoc';
  202. }
  203. else // $type == 'V' or 'X'
  204. {
  205. if (api_ereg('^(.*)=/(.+)$', $text, $regs)) // special resource-marker
  206. {
  207. $path = $regs[1]; $text = $regs[2];
  208. if (api_substr($path, -1) == '/') $path = api_substr($path, 0, -1);
  209. if ($path) $cur_elem = $this->xht_xmldoc->
  210. xmd_select_single_element($path, $cur_elem);
  211. $cur_elem = call_user_func($this->xht_resource, $cur_elem);
  212. }
  213. $result = ($type == 'V') ?
  214. $this->xht_xmldoc->xmd_value($text, $cur_elem) :
  215. $this->xht_xmldoc->
  216. xmd_html_value($text, $cur_elem, 'xht_htmlwchars');
  217. }
  218. }
  219. if ($subnum) $this->xht_dbgo .= '<!-- ' . XHT_LP . $type . $subnum . '- ' .
  220. htmlspecialchars((api_strlen($result) <= 60) ?
  221. $result . ' ': api_substr($result, 0, 57).'...', ENT_QUOTES, $charset) . XHT_RP . " -->\n";
  222. return $result;
  223. }
  224. function xht_add_template($template_name, $httext)
  225. {
  226. $this->htt_array[$template_name] = $httext;
  227. // removed 2004/10/05: (substr($httext, 0, 6) == 'array(') ?
  228. // removed 2004/10/05: @eval('return ' . $httext . ';') : $httext;
  229. if (!$this->htt_array[$template_name])
  230. {
  231. $this->htt_error = 'template ' . $template_name .
  232. ' is empty or invalid'; return;
  233. }
  234. }
  235. function xhtdoc($htt_file_contents)
  236. {
  237. $htt_file_contents = // normalize \r (Mac) and \r\n (Windows) to \n
  238. str_replace("\r", "\n", str_replace("\r\n", "\n", $htt_file_contents));
  239. while (api_substr($htt_file_contents, -1) == "\n")
  240. $htt_file_contents = api_substr($htt_file_contents, 0, -1);
  241. $last_line = api_strrchr($htt_file_contents, "\n"); $this->htt_error = '';
  242. if (api_strlen($last_line) < 12 || api_substr($last_line, 0, 6) != "\n<!-- "
  243. || api_strlen($last_line) % 2 != 0 || api_substr($last_line, -4) != " -->")
  244. {
  245. $this->htt_error = 'last line must be of the form <!-- {--} -->';
  246. return;
  247. }
  248. define('XHT_PL', (int) (api_strlen($last_line) - 10) / 2); // Parentheses Lth
  249. define('XHT_LP', api_substr($last_line, 6, XHT_PL)); // Left Par
  250. define('XHT_RP', api_substr($last_line, 6 + XHT_PL, XHT_PL)); // Right Par
  251. if (XHT_LP == XHT_RP)
  252. {
  253. $this->htt_error =
  254. 'parentheses in last line <!-- {--} --> must not be identical';
  255. return;
  256. }
  257. $this->htt_array = array(); // named elements are arrays and strings
  258. foreach (explode("\n<!-- " . XHT_LP, "\n" . $htt_file_contents)
  259. as $name_and_text)
  260. {
  261. if (($name_length = api_strpos($name_and_text, XHT_RP . " -->\n")))
  262. {
  263. $template_name = trim(api_substr($name_and_text, 0, $name_length));
  264. if (api_strpos($template_name, ' ') !== FALSE) give_up('Template ' .
  265. $template_name . ' has a space in its name');
  266. $httext = api_substr($name_and_text, $name_length + XHT_PL + 5);
  267. while (api_substr($httext, 0, 1) == "\n") $httext = api_substr($httext, 1);
  268. while (api_substr($httext, -1) == "\n") $httext = api_substr($httext,0,-1);
  269. $this->xht_add_template($template_name, $httext);
  270. }
  271. }
  272. define('XHT_SUBS1', '^(C|H|L|P|U|V|W|X) +(.*)$'); // substitution types 1:
  273. // Call, Htmlchars, Lang, Param, Urlencode, Value, Wchars, Xvalue
  274. define('XHT_SUBS2', '^(D|E|R|T) +([^ ]+) +(.*)$'); // substitution types 2:
  275. // Define, Escape, Repeat, Test
  276. $this->xht_dbgo = '';
  277. $this->xht_param = array(0 => '0', 1 => '1',
  278. '' => '', 'empty' => '', 'rdepth' => 0);
  279. $this->_prev_param = $this->xht_param;
  280. // empty: {-R * P empty-} puts the number of subelements in {-P number-}
  281. // rdepth: current number of nested R's
  282. // rdepth1, rdepth2, ...: key or number, see R
  283. }
  284. function _show_param() // for debugging info
  285. {
  286. global $charset;
  287. $result = '';
  288. foreach ($this->xht_param as $k => $v)
  289. if ($v !== $this->_prev_param[$k]) {
  290. $this->_prev_param[$k] = $v; $result .= ', ' . $k . ': ' .
  291. ((api_strlen($v) <= 20) ? $v : api_substr($v, 0, 17).'...');
  292. }
  293. return $result ? htmlspecialchars(api_substr($result, 1), ENT_QUOTES, $charset) : '';
  294. }
  295. function _lang($var, $cur_elem = 0)
  296. {
  297. $result = @call_user_func($this->xht_get_lang, $var, $cur_elem);
  298. // This is a hack for proper working of the language selectors
  299. // in the forms that are generated through templates.
  300. if ($var == 'Langs')
  301. {
  302. global $langLangs;
  303. if (isset($langLangs))
  304. {
  305. $result = $langLangs;
  306. }
  307. }
  308. return isset($result) ? $result : $var;
  309. }
  310. }
  311. /*
  312. A word of explanation...
  313. The last line of a template file (example below) defines the special markers
  314. that are used around template names such as INPUT and OPTION and around HTML
  315. escapes such as 'P value' and 'L Store', { and } in the example. You can also
  316. use markers of more than one character as long as both have the same length,
  317. e.g. <% and %>. The markers must not be equal. In templates with JavaScript
  318. or <style>, the use of { and } might be confusing; however, it does work.
  319. A template starts with a special comment line giving the name of the template
  320. and ends where the next template starts.
  321. Templates contain escapes of the form {one-letter the-rest}, e.g.
  322. {L IdentifierTip}, where the-rest can contain a nested-escape as in e.g.
  323. {R metadata/lom/general/keyword C KEYWORDTEMPLATE}.
  324. Multiple spaces count as one in many places, but not in all. Best is to use
  325. correct spacing. In particular, a T escape such as the example below requires
  326. a space at the end of the first line:
  327. {T x == y
  328. text
  329. }
  330. Names (of templates, parameters, langvars and user-functions)
  331. are case sensitive and cannot contain spaces.
  332. Templates are called with a certain currency in the XML doc, usually the root
  333. element; for an xml-path repeat, the found element is the current one.
  334. One single array with named elements and string values contains the
  335. template parameters; they are shared for all template processing.
  336. Initially, params contains
  337. 0 => '0', 1 => '1', '' => '', 'empty' => '', 'rdepth' => 0.
  338. rdepth keeps track of the current number of nested repeats. In a repeat,
  339. params number, key, value and rdepthN (N is 1, 2, ...) get assigned values.
  340. Types of escapes:
  341. C template-name/nested-escape Call a sub-template
  342. D parameter-name text Define a parameter value
  343. E user-function nested-escape Escape to user-function or do nested-escape
  344. H text HtmlSpecialChars (e.g. transform < to &lt;)
  345. L languagevar/nested-escape Language variable value (see also below)
  346. P parameter-name/nested-escape Parameter value
  347. R repeat-part nested-escape Repeat nested-escape for each ... (see below)
  348. T key1 operator key2 nested-esc Test, do nested-esc if test succeeds (id.)
  349. U text UrlEncode (e.g. transform < to %XX)
  350. V xml-path/nested-escape Value from XML document element or attribute
  351. W text xht_htmlwchars (see below)
  352. X extended-xml-path/nested-esc eXtended Value from XML (see below)
  353. nested-escape= without the special markers. Nesting with special markers is
  354. always possible and in that case the inner nesting is evaluated first.
  355. Note that as from the 2005/03/15 version, some implicit nestings are no
  356. longer possible, e.g. {H {P key}} and {D label {L Keyword}} now require
  357. the inner markers.
  358. Escape allows the caller to cache template output:
  359. user-function is called with parameter (FALSE);
  360. if it returns a string, that string is the result;
  361. if it returns FALSE, nested-escape is executed to produce a result
  362. and user-function is called again with the result, allowing it
  363. to cache the result somewhere; user-function then returns the real result.
  364. repeat-part can be:
  365. the name of a languagevar which contains an associative list such as:
  366. ":key1:value1,, key2:value2,, key3:value3" (note double ,,):
  367. nested-escape is repeated for each list element,
  368. params 'key' and 'value' refer to the current element;
  369. first character defines key-value-separator (here a colon);
  370. key cannot contain key-value-separators and is trimmed;
  371. value can contain anything except ,, and it is not trimmed;
  372. put ',,' at the end of a 0- or 1-element-list.
  373. the name of a languagevar which has an array value:
  374. nested-escape is repeated for each array element,
  375. params 'key' and 'value' refer to the current element.
  376. an xml-path to 0, 1 or more elements in the XML document:
  377. nested-escape is repeated for each element, param 'number' = 1, 2, ...
  378. test operators compare strings: = == <= < != <> > >=
  379. key1 and key2 can be parameter-name or languagevar-name.
  380. xml-path: see XML Mini-DOM; examples:
  381. organizations/@default, body/p[1] (1=first), keyword/*, node/*[-3]
  382. / * /... starts from XML document root, regardless of context
  383. other extensions: .. - + @* @. (parent, prev, next, number, name)
  384. .. stops at the root if too many
  385. -name, +name and @*name means only elements of that name
  386. xht_htmlwchars is like HtmlSpecialChars, but moreover translates:
  387. [b] to <b>, likewise for big, i, small, sub, sup, u, br, and closing tags;
  388. &amp;#nnn; to &#nnn; (double-escape for non-UTF8-channels)
  389. eXtended Value: see method xmd_html_value of the XML Mini-DOM;
  390. the values (but not the pre-, in- and suffixes) are always W-processed;
  391. extended-xml-path examples:
  392. 'keyword/string ,' generates e.g. 'kwd1,kwd2,kwd3'
  393. 'keyword/string , ' generates e.g. 'kwd1, kwd2, kwd3'
  394. '( -% keyword/string , %- )' generates e.g. '(kwd1,kwd2,kwd3)'
  395. but will generate nothing if no keywords are found in the XML doc;
  396. note that the special markers ' -% ' and ' %- ' must have the spaces.
  397. V and X escapes can have an xml-path containing '=/': this calls a user-supplied
  398. function for finding an associated element. It can e.g. be used when reading
  399. a SCORM manifest, for finding the <resource> of an <item>.
  400. L also calls a user-supplied function. Its main target is language-dependent
  401. texts, e.g. Dokeos 'get_lang'. But the current element is passed as second
  402. argument, allowing other functionality in the callback.
  403. Array templates functionality has been removed on 2004/10/05, because of a
  404. security issue when users can submit templates.
  405. --------------------------------------------------------------------------------
  406. Example template file:
  407. <!-- {HEAD} -->
  408. <style type="text/css">
  409. body {font-family: Arial}
  410. </style>
  411. <!-- {METADATA} -->
  412. <h3>{H {L Tool}: {P entry}}</h3>
  413. <table>
  414. <tr>
  415. <td>{D label {L Language}}{D tip {L LanguageTip}}{C LABEL}</td>
  416. <td><select>{D thislang {V general/language}}{R Langs C OPTION}</select></td>
  417. <td><input type="text" disabled value="urn:ugent-be:minerva."/></td>
  418. </tr>
  419. {R general/keyword C KEYWORD}
  420. </table>
  421. <!---------------------- E-n-d Of script Output ---------------------->
  422. <!-- {LABEL} -->
  423. <span title="{H {P tip}}">{H {P label}}&#xa0;:</span>
  424. <!-- {OPTION} -->
  425. <option value="{H {P key}}" {T key == thislang selected}>{H {P value}}</option>
  426. <!-- {INPUT} -->
  427. <input type="text" title="{P title}" class="wide" value="{H {P value}}"/>
  428. <!-- {KEYWORD} -->
  429. <tr>
  430. <td>{D label {L Keyword}}{D tip {L KeywordTip}}{C LABEL}</td>
  431. <td nowrap><select>{D thislang {V string/@language}}{R Langs C OPTION}</select></td>
  432. <td>{D value {X string}}{D title general/keyword[{P number}]/string}{C INPUT}</td>
  433. </tr>
  434. <!-- {} -->
  435. --------------------------------------------------------------------------------
  436. <!--
  437. This program is free software; you can redistribute it and/or
  438. modify it under the terms of the GNU General Public License
  439. as published by the Free Software Foundation; either version 2
  440. of the License, or (at your option) any later version.
  441. This program is distributed in the hope that it will be useful,
  442. but WITHOUT ANY WARRANTY; without even the implied warranty of
  443. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  444. GNU General Public License for more details.
  445. -->
  446. */
  447. ?>