[ Index ] |
PHP Cross Reference of Eventum |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */ 3 // +----------------------------------------------------------------------+ 4 // | Eventum - Issue Tracking System | 5 // +----------------------------------------------------------------------+ 6 // | Copyright (c) 2003, 2004, 2005, 2006, 2007 MySQL AB | 7 // | | 8 // | This program is free software; you can redistribute it and/or modify | 9 // | it under the terms of the GNU General Public License as published by | 10 // | the Free Software Foundation; either version 2 of the License, or | 11 // | (at your option) any later version. | 12 // | | 13 // | This program is distributed in the hope that it will be useful, | 14 // | but WITHOUT ANY WARRANTY; without even the implied warranty of | 15 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 16 // | GNU General Public License for more details. | 17 // | | 18 // | You should have received a copy of the GNU General Public License | 19 // | along with this program; if not, write to: | 20 // | | 21 // | Free Software Foundation, Inc. | 22 // | 59 Temple Place - Suite 330 | 23 // | Boston, MA 02111-1307, USA. | 24 // +----------------------------------------------------------------------+ 25 // | Authors: João Prado Maia <jpm@mysql.com> | 26 // +----------------------------------------------------------------------+ 27 // 28 29 /** 30 * The MIME:: class provides methods for dealing with MIME standards. 31 * 32 * $Horde: horde/lib/MIME.php,v 1.121 2003/11/06 15:26:17 chuck Exp $ 33 * 34 * Copyright 1999-2003 Chuck Hagenbuch <chuck@horde.org> 35 * 36 * See the enclosed file COPYING for license information (LGPL). If you 37 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. 38 * 39 */ 40 41 require_once(APP_PEAR_PATH . "Mail/mimeDecode.php"); 42 require_once (APP_INC_PATH . "class.error_handler.php"); 43 44 /** 45 * Class to handle the business logic related to the MIME email 46 * processing. The is8bit(), endode() and _encode() functions come from 47 * the excellent Horde package at http://www.horde.org. These functions are 48 * licensed under the LGPL, and Horde's copyright notice is available 49 * above. 50 * 51 * @version 1.0 52 * @author João Prado Maia <jpm@mysql.com> 53 */ 54 class Mime_Helper 55 { 56 /** 57 * Method used to get charset from raw email. 58 * 59 * @access public 60 * @param mixed $input The full body of the message or decoded email. 61 * @return string charset extracted from Content-Type header of email. 62 */ 63 function getCharacterSet($input) 64 { 65 if (!is_object($input)) { 66 $structure = Mime_Helper::decode($input, false, false); 67 } else { 68 $structure = $input; 69 } 70 if (empty($structure)) { 71 return false; 72 } 73 74 if ($structure->ctype_primary == 'multipart' and $structure->ctype_secondary == 'mixed' 75 and count($structure->parts) >= 1 and $structure->parts[0]->ctype_primary == 'text') { 76 $content_type = $structure->parts[0]->headers['content-type']; 77 } else { 78 $content_type = @$structure->headers['content-type']; 79 } 80 81 if (preg_match('/charset\s*=\s*(["\'])?([-\w\d]+)(\1)?;?/i', $content_type, $matches)) { 82 return $matches[2]; 83 } 84 85 return false; 86 } 87 88 89 /** 90 * Returns the appropriate message body for a given MIME-based decoded 91 * structure. 92 * 93 * @access public 94 * @param object $output The parsed message structure 95 * @return string The message body 96 * @see Mime_Helper::decode() 97 */ 98 function getMessageBody(&$output) 99 { 100 $parts = array(); 101 Mime_Helper::parse_output($output, $parts); 102 if (empty($parts)) { 103 Error_Handler::logError(array("Mime_Helper::parse_output failed. Corrupted MIME in email?", $output), __FILE__, __LINE__); 104 // we continue as if nothing happened until it's clear it's right check to do. 105 } 106 $str = ''; 107 $is_html = false; 108 if (isset($parts["text"])) { 109 $str = join("\n\n", $parts["text"]); 110 } elseif (isset($parts["html"])) { 111 $is_html = true; 112 $str = join("\n\n", $parts["html"]); 113 114 // hack for inotes to prevent content from being displayed all on one line. 115 $str = str_replace("</DIV><DIV>", "\n", $str); 116 $str = str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $str); 117 } 118 // XXX: do we also need to do something here about base64 encoding? 119 if ($is_html) { 120 $str = strip_tags($str); 121 } 122 return $str; 123 } 124 125 126 /** 127 * Method used to fix the encoding of MIME based strings. 128 * 129 * @access public 130 * @param string $input The string to be fixed 131 * @return string The fixed string 132 */ 133 function fixEncoding($input) 134 { 135 // Remove white space between encoded-words 136 $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); 137 // For each encoded-word... 138 while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { 139 $encoded = $matches[1]; 140 $charset = $matches[2]; 141 $encoding = $matches[3]; 142 $text = $matches[4]; 143 switch (strtolower($encoding)) { 144 case 'b': 145 $text = base64_decode($text); 146 break; 147 case 'q': 148 $text = str_replace('_', ' ', $text); 149 preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); 150 foreach($matches[1] as $value) 151 $text = str_replace('='.$value, chr(hexdec($value)), $text); 152 break; 153 } 154 $input = str_replace($encoded, $text, $input); 155 } 156 return $input; 157 } 158 159 160 /** 161 * Method used to properly quote the sender of a given email address. 162 * 163 * @access public 164 * @param string $address The full email address 165 * @return string The properly quoted email address 166 */ 167 function quoteSender($address) 168 { 169 if (strstr($address, '<')) { 170 $address = stripslashes($address); 171 $first_part = substr($address, 0, strrpos($address, '<') - 1); 172 $first_part = '"' . str_replace('"', '\"',($first_part)) . '"'; 173 $second_part = substr($address, strrpos($address, '<')); 174 $address = $first_part . ' ' . $second_part; 175 } 176 return $address; 177 } 178 179 180 /** 181 * Method used to remove any unnecessary quoting from an email address. 182 * 183 * @access public 184 * @param string $address The full email address 185 * @return string The email address without quotes 186 */ 187 function removeQuotes($address) 188 { 189 if (strstr($address, '<')) { 190 $address = stripslashes($address); 191 $first_part = substr($address, 0, strrpos($address, '<') - 1); 192 $second_part = substr($address, strrpos($address, '<')); 193 $address = $first_part; 194 } 195 if (preg_match('/^".*"/', $address)) { 196 $address = preg_replace('/^"(.*)"/', '\\1', $address); 197 } 198 if (!empty($second_part)) { 199 $address .= ' ' . $second_part; 200 } 201 return $address; 202 } 203 204 205 /** 206 * Method used to properly encode an email address. 207 * 208 * @access public 209 * @param string $address The full email address 210 * @return string The properly encoded email address 211 */ 212 function encodeAddress($address) 213 { 214 $address = MIME_Helper::removeQuotes($address); 215 if (Mime_Helper::is8bit($address)) { 216 // split into name and address section 217 preg_match("/(.*)<(.*)>/", $address, $matches); 218 $address = "=?" . APP_CHARSET . "?Q?" . 219 str_replace(' ', '_', trim(preg_replace('/([\x80-\xFF]|[\x21-\x2F]|[\xFC]|\[|\])/e', '"=" . strtoupper(dechex(ord(stripslashes("\1"))))', $matches[1]))) . "?= <" . $matches[2] . ">"; 220 return $address; 221 } else { 222 return MIME_Helper::quoteSender($address); 223 } 224 } 225 226 227 /** 228 * Decodes a quoted printable encoded address and returns the string. 229 * 230 * @param string $address The address to decode 231 * @return string The decoded address 232 */ 233 function decodeAddress($address) 234 { 235 if (preg_match("/=\?.+\?Q\?(.+)\?= <(.+)>/i", $address, $matches)) { 236 return str_replace("_", ' ', quoted_printable_decode($matches[1])) . " <" . $matches[2] . ">"; 237 } else { 238 return Mime_Helper::removeQuotes($address); 239 } 240 } 241 242 243 /** 244 * Returns if a specified string contains a quoted printable address. 245 * 246 * @param string $address The address 247 * @return boolean If the address is quoted printable encoded. 248 */ 249 function isQuotedPrintable($address) 250 { 251 if (preg_match("/=\?.+\?Q\?.+\?= <.+>/i", $address)) { 252 return true; 253 } else { 254 return false; 255 } 256 } 257 258 259 /** 260 * Determine if a string contains 8-bit characters. 261 * 262 * @access public 263 * 264 * @param string $string The string to check. 265 * 266 * @return boolean True if it does, false if it doesn't. 267 */ 268 function is8bit($string) 269 { 270 if (is_string($string) && preg_match('/[\x80-\xff]+/', $string)) { 271 return true; 272 } else { 273 return false; 274 } 275 } 276 277 278 /** 279 * Encode a string containing non-ASCII characters according to RFC 2047. 280 * 281 * @access public 282 * 283 * @param string $text The text to encode. 284 * @param string $charset (optional) The character set of the text. 285 * 286 * @return string The text, encoded only if it contains non-ASCII 287 * characters. 288 */ 289 function encode($text, $charset = APP_CHARSET) 290 { 291 /* Return if nothing needs to be encoded. */ 292 if (!MIME_Helper::is8bit($text)) { 293 return $text; 294 } 295 296 $charset = strtolower($charset); 297 $line = ''; 298 299 /* Get the list of elements in the string. */ 300 $size = preg_match_all("/([^\s]+)([\s]*)/", $text, $matches, PREG_SET_ORDER); 301 302 foreach ($matches as $key => $val) { 303 if (MIME_Helper::is8bit($val[1])) { 304 if ((($key + 1) < $size) && 305 MIME_Helper::is8bit($matches[$key + 1][1])) { 306 $line .= MIME_Helper::_encode($val[1] . $val[2], $charset) . ' '; 307 } else { 308 $line .= MIME_Helper::_encode($val[1], $charset) . $val[2]; 309 } 310 } else { 311 $line .= $val[1] . $val[2]; 312 } 313 } 314 315 return rtrim($line); 316 } 317 318 /** 319 * Internal recursive function to RFC 2047 encode a string. 320 * 321 * @access private 322 * 323 * @param string $text The text to encode. 324 * @param string $charset The character set of the text. 325 * 326 * @return string The text, encoded only if it contains non-ASCII 327 * characters. 328 */ 329 function _encode($text, $charset) 330 { 331 $char_len = strlen($charset); 332 $txt_len = strlen($text) * 2; 333 334 /* RFC 2047 [2] states that no encoded word can be more than 75 335 characters long. If longer, you must split the word. */ 336 if (($txt_len + $char_len + 7) > 75) { 337 $pos = intval((68 - $char_len) / 2); 338 return MIME_Helper::_encode(substr($text, 0, $pos), $charset) . ' ' . MIME_Helper::_encode(substr($text, $pos), $charset); 339 } else { 340 return '=?' . $charset . '?b?' . trim(base64_encode($text)) . '?='; 341 } 342 } 343 344 345 /** 346 * Method used to encode a given string in the quoted-printable standard. 347 * 348 * @access public 349 * @param string $hdr_value The string to be encoded 350 * @param string $charset The charset of the string 351 * @return string The encoded string 352 */ 353 function encodeValue($hdr_value, $charset = 'iso-8859-1') 354 { 355 preg_match_all('/(\w*[\x80-\xFF]+\w*)/', $hdr_value, $matches); 356 foreach ($matches[1] as $value) { 357 $replacement = preg_replace('/([\x80-\xFF])/e', '"=" . strtoupper(dechex(ord("\1")))', $value); 358 $hdr_value = str_replace($value, '=?' . $charset . '?Q?' . $replacement . '?=', $hdr_value); 359 } 360 return $hdr_value; 361 } 362 363 364 /** 365 * Given a string containing a header and body 366 * section, this function will split them (at the first 367 * blank line) and return them. 368 * 369 * @access public 370 * @param string $input Input to split apart 371 * @return array Contains header and body section 372 */ 373 function splitBodyHeader($input) 374 { 375 if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { 376 return array($match[1], $match[2]); 377 } 378 } 379 380 381 /** 382 * Parse headers given in $input and return 383 * as assoc array. 384 * 385 * @access public 386 * @param string $input Headers to parse 387 * @return array Contains parsed headers 388 */ 389 function getHeaderNames($input) 390 { 391 if ($input !== '') { 392 // Unfold the input 393 $input = preg_replace("/\r?\n/", "\r\n", $input); 394 $input = preg_replace("/\r\n(\t| )+/", ' ', $input); 395 $headers = explode("\r\n", trim($input)); 396 foreach ($headers as $value) { 397 $hdr_name = substr($value, 0, $pos = strpos($value, ':')); 398 $return[strtolower($hdr_name)] = $hdr_name; 399 } 400 } else { 401 $return = array(); 402 } 403 return $return; 404 } 405 406 407 /** 408 * Method used to get an unique attachment name for a given 409 * filename. This is specially useful for the emails that Microsoft 410 * Outlook sends out with several attachments with the same name 411 * when you embed several inline screenshots in the message 412 * 413 * @access public 414 * @param array $list The nested array of mime parts 415 * @param string $filename The filename to search for 416 * @return string The unique attachment name 417 */ 418 function getAttachmentName(&$list, $filename) 419 { 420 if (@in_array($filename, array_values($list))) { 421 // check if the filename even has an extension... 422 if (!strstr($filename, '.')) { 423 $first_part = $filename; 424 } else { 425 $first_part = substr($filename, 0, strrpos($filename, '.')); 426 } 427 // check if this is already named Outlook-2.bmp (or similar) 428 if (strstr($first_part, "-")) { 429 // if so, gotta get the number and increment it 430 $numeric_portion = substr($first_part, strrpos($first_part, "-")+1); 431 if (preg_match("/^[0-9]+$/", $numeric_portion)) { 432 $numeric_portion = intval($numeric_portion) + 1; 433 } 434 $first_part = substr($first_part, 0, strrpos($first_part, "-")); 435 } else { 436 $numeric_portion = 1; 437 } 438 if (!strstr($filename, '.')) { 439 $filename = $first_part . "-" . $numeric_portion; 440 } else { 441 $filename = $first_part . "-" . $numeric_portion . substr($filename, strrpos($filename, '.')); 442 } 443 return MIME_Helper::getAttachmentName($list, $filename); 444 } else { 445 return $filename; 446 } 447 } 448 449 450 /** 451 * Method used to check whether a given email message has any attachments. 452 * 453 * @access public 454 * @param mixed $message The full body of the message or parsed message structure. 455 * @return boolean 456 */ 457 function hasAttachments($message) 458 { 459 if (!is_object($message)) { 460 $message = Mime_Helper::decode($message, true); 461 } 462 $attachments = Mime_Helper::_getAttachmentDetails($message, TRUE); 463 if (count($attachments) > 0) { 464 return true; 465 } else { 466 return false; 467 } 468 } 469 470 471 /** 472 * Method used to parse and return the full list of attachments 473 * associated with a message. 474 * 475 * @access public 476 * @param mixed $message The full body of the message or parsed message structure. 477 * @return array The list of attachments, if any 478 */ 479 function getAttachments($message) 480 { 481 if (!is_object($message)) { 482 $message = Mime_Helper::decode($message, true); 483 } 484 return Mime_Helper::_getAttachmentDetails($message, TRUE); 485 } 486 487 488 /** 489 * Method used to parse and return the full list of attachment CIDs 490 * associated with a message. 491 * 492 * @access public 493 * @param mixed $message The full body of the message or parsed message structure. 494 * @return array The list of attachment CIDs, if any 495 */ 496 function getAttachmentCIDs($message) 497 { 498 if (!is_object($message)) { 499 $message = Mime_Helper::decode($message, true); 500 } 501 return Mime_Helper::_getAttachmentDetails($message, true); 502 } 503 504 505 function _getAttachmentDetails(&$mime_part, $return_body = FALSE, $return_filename = FALSE, $return_cid = FALSE) 506 { 507 $attachments = array(); 508 if (isset($mime_part->parts)) { 509 for ($i = 0; $i < count($mime_part->parts); $i++) { 510 $t = Mime_Helper::_getAttachmentDetails($mime_part->parts[$i], $return_body, $return_filename, $return_cid); 511 $attachments = array_merge($t, $attachments); 512 } 513 } 514 // FIXME: content-type is always lowered by PEAR class (CHECKME) and why not $mime_part->content_type? 515 $content_type = strtolower(@$mime_part->ctype_primary . '/' . @$mime_part->ctype_secondary); 516 if ($content_type == '/') { 517 $content_type = ''; 518 } 519 $found = 0; 520 // get the proper filename 521 $mime_part_filename = @$mime_part->ctype_parameters['name']; 522 if (empty($mime_part_filename)) { 523 $mime_part_filename = @$mime_part->d_parameters['filename']; 524 } 525 // hack in order to treat inline images as normal attachments 526 // (since Eventum does not display those embedded within the message) 527 if (@$mime_part->ctype_primary == 'image') { 528 // if requested, return only the details of a particular filename 529 if (($return_filename != FALSE) && ($mime_part_filename != $return_filename)) { 530 return array(); 531 } 532 // if requested, return only the details of 533 // a particular attachment CID. Only really needed 534 // as hack for inline images 535 if (($return_cid != FALSE) && (@$mime_part->headers['content-id'] != $return_cid)) { 536 return array(); 537 } 538 $found = 1; 539 } else { 540 if ((!in_array($content_type, Mime_Helper::_getInvalidContentTypes())) && 541 (in_array(@strtolower($mime_part->disposition), Mime_Helper::_getValidDispositions())) && 542 (!empty($mime_part_filename))) { 543 // if requested, return only the details of a particular filename 544 if (($return_filename != FALSE) && ($mime_part_filename != $return_filename)) { 545 return array(); 546 } 547 $found = 1; 548 } 549 } 550 if ($found) { 551 $t = array( 552 'filename' => $mime_part_filename, 553 'cid' => @$mime_part->headers['content-id'], 554 'filetype' => $content_type 555 ); 556 // only include the body of the attachment when 557 // requested to save some memory 558 if ($return_body == TRUE) { 559 $t['blob'] = &$mime_part->body; 560 } 561 $attachments[] = $t; 562 } 563 564 return $attachments; 565 } 566 567 568 /** 569 * Method used to get the encoded content of a specific message 570 * attachment. 571 * 572 * @access public 573 * @param mixed $message The full content of the message or parsed message structure. 574 * @param string $filename The filename to look for 575 * @param string $cid The content-id to look for, if any 576 * @return string The full encoded content of the attachment 577 */ 578 function getAttachment($message, $filename, $cid = FALSE) 579 { 580 $parts = array(); 581 if (!is_object($message)) { 582 $message = Mime_Helper::decode($message, true); 583 } 584 $details = Mime_Helper::_getAttachmentDetails($message, TRUE, $filename, $cid); 585 if (count($details) == 1) { 586 return array( 587 $details[0]['filetype'], 588 $details[0]['blob'] 589 ); 590 } else { 591 return array(); 592 } 593 } 594 595 596 /** 597 * Method used to decode the content of a MIME encoded message. 598 * 599 * @access public 600 * @param string $message The full body of the message 601 * @param boolean $include_bodies Whether to include the bodies in the return value or not 602 * @return mixed The decoded content of the message 603 */ 604 function decode($message, $include_bodies = FALSE, $decode_bodies = TRUE) 605 { 606 // need to fix a pretty annoying bug where if the 'boundary' part of a 607 // content-type header is split into another line, the PEAR library would 608 // not work correctly. this fix will make the boundary part go to the 609 // same line as the content-type one 610 if (preg_match('/^boundary=/m', $message)) { 611 $pattern = "#(Content-Type: multipart/.+); ?\r?\n(boundary=)$#im"; 612 $replacement = '$1; $2'; 613 $message = preg_replace($pattern, $replacement, $message); 614 } 615 616 $params = array( 617 'crlf' => "\r\n", 618 'include_bodies' => $include_bodies, 619 'decode_headers' => TRUE, 620 'decode_bodies' => $decode_bodies 621 ); 622 $decode = new Mail_mimeDecode($message); 623 return $decode->decode($params); 624 } 625 626 627 /** 628 * Method used to parse the decoded object structure of a MIME 629 * message into something more manageable. 630 * 631 * @access public 632 * @param object $obj The decoded object structure of the MIME message 633 * @param array $parts The parsed parts of the MIME message 634 * @return void 635 */ 636 function parse_output($obj, &$parts) 637 { 638 if (!empty($obj->parts)) { 639 for ($i = 0; $i < count($obj->parts); $i++) { 640 Mime_Helper::parse_output($obj->parts[$i], $parts); 641 } 642 } else { 643 $ctype = @strtolower($obj->ctype_primary.'/'.$obj->ctype_secondary); 644 switch($ctype){ 645 case 'text/plain': 646 if (((!empty($obj->disposition)) && (strtolower($obj->disposition) == 'attachment')) || (!empty($obj->d_parameters['filename']))) { 647 @$parts['attachments'][] = $obj->body; 648 } else { 649 @$parts['text'][] = $obj->body; 650 } 651 break; 652 case 'text/html': 653 if ((!empty($obj->disposition)) && (strtolower($obj->disposition) == 'attachment')) { 654 @$parts['attachments'][] = $obj->body; 655 } else { 656 @$parts['html'][] = $obj->body; 657 } 658 break; 659 // special case for Apple Mail 660 case 'text/enriched': 661 if ((!empty($obj->disposition)) && (strtolower($obj->disposition) == 'attachment')) { 662 @$parts['attachments'][] = $obj->body; 663 } else { 664 @$parts['html'][] = $obj->body; 665 } 666 break; 667 default: 668 // avoid treating forwarded messages as attachments 669 if ((!empty($obj->disposition)) && (strtolower($obj->disposition) == 'inline') && 670 ($ctype != 'message/rfc822')) { 671 @$parts['attachments'][] = $obj->body; 672 } elseif (stristr($ctype, 'image')) { 673 // handle inline images 674 @$parts['attachments'][] = $obj->body; 675 } elseif(strtolower(@$obj->disposition) == 'attachment') { 676 @$parts['attachments'][] = $obj->body; 677 } else { 678 @$parts['text'][] = $obj->body; 679 } 680 } 681 } 682 } 683 684 685 /** 686 * Given a quoted-printable string, this 687 * function will decode and return it. 688 * 689 * @access private 690 * @param string Input body to decode 691 * @return string Decoded body 692 */ 693 function _quotedPrintableDecode($input) 694 { 695 // Remove soft line breaks 696 $input = preg_replace("/=\r?\n/", '', $input); 697 698 // Replace encoded characters 699 $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); 700 701 return $input; 702 } 703 704 705 /** 706 * Returns the internal list of content types that we do not support as 707 * valid attachment types. 708 * 709 * @access private 710 * @return array The list of content types 711 */ 712 function _getInvalidContentTypes() 713 { 714 return array( 715 'message/rfc822', 716 'application/pgp-signature', 717 'application/ms-tnef', 718 ); 719 } 720 721 722 /** 723 * Returns the internal list of attachment dispositions that we do not 724 * support as valid attachment types. 725 * 726 * @access private 727 * @return array The list of valid dispositions 728 */ 729 function _getValidDispositions() 730 { 731 return array( 732 'attachment', 733 'inline' 734 ); 735 } 736 737 738 /** 739 * Splits the full email into headers and body 740 * 741 * @access public 742 * @param string $message The full email message 743 * @param boolean $unfold If headers should be unfolded 744 * @return array An array containing the headers and body 745 */ 746 function splitHeaderBody($message, $unfold = true) 747 { 748 if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $message, $match)) { 749 return array(($unfold) ? Mail_API::unfold($match[1]) : $match[1], $match[2]); 750 } 751 return array(); 752 } 753 } 754 755 // benchmarking the included file (aka setup time) 756 if (APP_BENCHMARK) { 757 $GLOBALS['bench']->setMarker('Included Mime_Helper Class'); 758 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Dec 19 21:21:33 2007 | Cross-referenced by PHPXref 0.7 |