* Licensed under the GNU GPL. For full terms see the file COPYING. * * @package plugins * @subpackage html_mail * */ /** * Load configuration file */ function hm_get_config() { $one = @include_once(SM_PATH . 'plugins/html_mail/config.php.sample'); $two = @include_once(SM_PATH . 'plugins/html_mail/config.php'); if (!$one && !$two) { echo sprintf(_("ERROR (%s): can't load config file"), 'html_mail'); exit(1); } } /** * Inserts controls on the compose page that let the user * switch between HTML and text on the fly. */ function html_mail_choose_type_on_the_fly() { sq_bindtextdomain('html_mail', SM_PATH . 'locale'); textdomain('html_mail'); if (html_area_is_on_and_is_supported_by_users_browser()) { echo '
 '; global $comp_in_html; sqgetGlobalVar('comp_in_html', $comp_in_html, SQ_FORM); if ($comp_in_html) { echo ''; } } textdomain('squirrelmail'); } function html_mail_choose_type_on_the_fly_important() { global $javascript_on, $PHP_SELF, $comp_in_html; if(!$javascript_on || !html_area_is_supported_by_users_browser()) return; sq_bindtextdomain('html_mail', SM_PATH . 'locale'); textdomain('html_mail'); sqgetGlobalVar('comp_in_html', $comp_in_html, SQ_FORM); if(!html_compose_is_on()) { echo ''; } textdomain ('squirrelmail'); } /** * "Turns on" this plugin if the compose page is currently * being shown */ function html_mail_header_do() { global $PHP_SELF; if (stristr($PHP_SELF, 'compose.php')) { html_mail_turn_on_htmlarea(); } } /** * Do the actual insertion of the enhanced text editor * * Also check that this plugin is in the correct order in $plugins array */ function html_mail_turn_on_htmlarea() { global $plugins, $color, $customStyle, $use_spell_checker, $fully_loaded, $username, $data_dir; // list of plugins that should come BEFORE this plugin (any that will // modify outgoing messages on the compose_send hook) // $check_for_previous_plugins = array( 'gpg', // is this one necessary? oh well, no reason we can't play it safe 'hancock', 'taglines', 'quote_tools', 'email_footer', ); // now just make sure html_mail comes after all those plugins listed above $my_plugin_index = array_search('html_mail', $plugins); foreach ($check_for_previous_plugins as $plug) { $i = array_search($plug, $plugins); if (is_numeric($i) && $i > $my_plugin_index) { // array_search returns NULL before PHP 4.2.0, FALSE after that sq_bindtextdomain('html_mail', SM_PATH . 'locale'); textdomain('html_mail'); echo "\n\n

" . sprintf(_("FATAL: HTML_Mail plugin must come AFTER %s in plugins array. Please modify plugin order using conf.pl or by editing config/config.php"), $plug) . '

'; exit; } } hm_get_config(); if (html_area_is_on_and_is_supported_by_users_browser()) { echo ''; } } /** * Inserts extra JavaScript at bottom of compose page * that is needed by the enhanced editor * * @todo Enable spellchecker tinyMCE plugin for IE. */ function html_mail_footer() { global $username, $data_dir; hm_get_config(); if (html_area_is_on_and_is_supported_by_users_browser()) { // replace newlines with
's in body // (comment out these three lines if you // want to do this in html_mail_compose_form_do() // and miss automated signatures) echo '"; global $squirrelmail_language, $editor_height, $editor_size, $default_html_editor_height, $reply_focus; if (!$editor_height) $editor_height = $default_html_editor_height; $lang = substr($squirrelmail_language, 0, strpos($squirrelmail_language, '_')); if (empty($lang) || !file_exists(SM_PATH . 'plugins/html_mail/tiny_mce/langs/' . $lang . '.js')) $lang = 'en'; $ua = html_mail_browser_info(); if(isset($ua['msie'])) { $msie = true; } else { $msie = false; } echo ' '; global $html_mail_display_hint; if(isset($html_mail_display_hint)) { html_mail_display_attachment_hint(); } /* if(tinyMCE != undefined) { tinyMCE.execCommand('mceFocus',false,'body'); } else { $("#body")[0].focus(); } */ } } function html_mail_display_attachment_hint() { global $compose_new_win, $color; if ($compose_new_win == '1') { echo ''."\n" ; } else { echo '
' . "\n"; } echo ''; echo '
'; echo _("Note: If you are resuming a draft, forwarding a message or editing a message as new, it might be advisable to delete unwanted attachments above."); echo '

'; } /** * Turns off squirrelspell when the user is composing * HTML-formatted email, since squirrelspell will * choke on the HTML. This function also reformats the * message body as needed (such as getting the HTML * part to edit if user settings demand it). * */ function html_mail_compose_form_do() { global $squirrelmail_plugin_hooks; if (html_area_is_on_and_is_supported_by_users_browser()) { if (!empty($squirrelmail_plugin_hooks['compose_button_row']['squirrelspell'])) unset($squirrelmail_plugin_hooks['compose_button_row']['squirrelspell']); // need to encode body text so > signs and other stuff don't // get interpreted incorrectly as HTML entities // // but only need to do this once; don't repeat if user just // clicked to add a signature or upload a file or add addresses, etc // global $sigappend, $from_htmladdr_search, $restrict_senders_error_no_to_recipients, $restrict_senders_error_too_many_recipients; sqgetGlobalVar('sigappend', $sigappend, SQ_FORM); sqgetGlobalVar('from_htmladdr_search', $from_htmladdr_search, SQ_FORM); sqgetGlobalVar('restrict_senders_error_too_many_recipients', $restrict_senders_error_too_many_recipients, SQ_FORM); sqgetGlobalVar('restrict_senders_error_no_to_recipients', $restrict_senders_error_no_to_recipients, SQ_FORM); if ($sigappend != 'Signature' && $from_htmladdr_search != 'true' && $restrict_senders_error_no_to_recipients != 1 && $restrict_senders_error_too_many_recipients != 1 && empty($_FILES['attachfile'])) { global $username, $key, $imapServerAddress, $imapPort, $imapConnection, $mailbox, $uid_support, $messages, $passed_id, $data_dir, $passed_ent_id, $smaction, $color, $wrap_at, $body; $aggressive_reply = getPref($data_dir, $username, 'html_mail_aggressive_reply', 0); $aggressive_reply_with_unsafe_images = getPref($data_dir, $username, 'html_mail_aggressive_reply_with_unsafe_images', 0); sqgetGlobalVar('messages', $messages, SQ_SESSION); sqgetGlobalVar('smaction', $smaction, SQ_FORM); sqgetGlobalVar('HTTP_REFERER', $referer, SQ_SERVER); sqgetGlobalVar('key', $key, SQ_COOKIE); if ($smaction == 'reply' || $smaction == 'reply_all' || $smaction == 'forward' || $smaction == 'draft' || $smaction == 'edit_as_new') { // we can skip all this code that tries to get a HTML part // if user doesn't want it anyway // $treatAsPlainText = TRUE; if ($aggressive_reply || (!empty($referer) && strpos($referer, 'view_as_html=1') !== FALSE) || getPref($data_dir, $username, 'show_html_default') ) { $treatAsPlainText = FALSE; $imapConnection = sqimap_login($username, $key, $imapServerAddress, $imapPort, 0); $mbx_response = sqimap_mailbox_select($imapConnection, $mailbox, false, false, true); $uidvalidity = $mbx_response['UIDVALIDITY']; if (!isset($messages[$uidvalidity])) $messages[$uidvalidity] = array(); // grab message from session cache or from IMAP server if (!isset($messages[$uidvalidity][$passed_id]) || !$uid_support) { $message = sqimap_get_message($imapConnection, $passed_id, $mailbox); $message->is_seen = true; $messages[$uidvalidity][$passed_id] = $message; } else { $message = $messages[$uidvalidity][$passed_id]; } if($smaction == 'forward' || $smaction == 'draft' || $smaction == 'edit_as_new') { for($i=0; $i < sizeof($message->entities); $i++) { if($message->entities[$i]->type0 == 'text' && $message->entities[$i]->type1 =='html') { global $html_mail_display_hint; $html_mail_display_hint = true; // TODO: this should be handled in compose.php logic probably; remove the unwanted // message entity altogether. } } } $orig_header = $message->rfc822_header; // is there an html part? if so, use it to redefine $body // $ent_ar = $message->findDisplayEntity(array(), array('text/html'), TRUE); if (!empty($ent_ar)) { // from compose.php (mutilated and modified...) global $languages, $squirrelmail_language, $default_charset; set_my_charset(); $unencoded_bodypart = mime_fetch_body($imapConnection, $passed_id, $ent_ar[0]); $body_part_entity = $message->getEntity($ent_ar[0]); $bodypart = decodeBody($unencoded_bodypart, $body_part_entity->header->encoding); // handle HTML // // do this after we call magicHTML()... $bodypart = str_replace("\n", ' ', $bodypart); // TODO: next line won't make a difference in ultimate result, // although we should keep an eye on it if problems arise, $bodypart = str_replace(array(' ','>','<'),array(' ','>','<'),$bodypart); // TODO: we can't strip out tags, cuz we want the tags! // but we don't want to be indescriminate, so we // use magicHTML() below... it is possible that // some people with extensive HTML mails will complain // about what magicHTML() does to their mail... //$bodypart = strip_tags($bodypart); // trick magicHTML() if needed by injecting info into $_GET // if ($aggressive_reply_with_unsafe_images || (!empty($referer) && strpos($referer, 'view_unsafe_images=1') !== FALSE)) { global $_GET; if (!check_php_version(4,1)) { global $HTTP_GET_VARS; $_GET = $HTTP_GET_VARS; } $_GET['view_unsafe_images'] = 1; } $bodypart = magicHTML($bodypart, $passed_id, $message, $mailbox, FALSE); // last param added in 1.5.1 $bodypart = str_replace("\n", ' ', $bodypart); if (isset($languages[$squirrelmail_language]['XTRA_CODE']) && function_exists($languages[$squirrelmail_language]['XTRA_CODE'])) { if (mb_detect_encoding($bodypart) != 'ASCII') $bodypart = $languages[$squirrelmail_language]['XTRA_CODE']('decode', $bodypart); } // charset encoding in compose form stuff if (isset($body_part_entity->header->parameters['charset'])) $actual = $body_part_entity->header->parameters['charset']; else $actual = 'us-ascii'; if ( $actual && is_conversion_safe($actual) && $actual != $default_charset) { $bodypart = charset_convert($actual,$bodypart,$default_charset,false); } // end of charset encoding in compose $body = $bodypart; // NOTE: here is the alternative code we still need if we don't use the above // if ($smaction == 'forward') { $body = getforwardHeader($orig_header) . $body; $body = '
' . $body; } if ($smaction == 'reply' || $smaction == 'reply_all') { $from = (is_array($orig_header->from)) ? $orig_header->from[0] : $orig_header->from; $body = getReplyCitation($from , $orig_header->date) . $body; } } else $treatAsPlainText = TRUE; } // plain text messages: need to make sure HTML entities // don't get interpreted incorrectly... if ($treatAsPlainText) { // email addresses in the form "name"
in // the original message lose the address since it // is mistaken for a HTML tag $body = htmlspecialchars($body); } } // for some strange reason, the subject line // doesn't get a
before it in the forward header // unless there is a space after the newline. // argh! From: suffers from the same problem $body = preg_replace(array('/-----\s' . _("Subject") . '/', '/\s' . _("From") . '/'), array("-----\n" . _("Subject"), "\n" . _("From")), $body); } } } /** * @return boolean */ function html_compose_is_on() { global $username, $data_dir, $javascript_on, $comp_in_html; sqgetGlobalVar('comp_in_html', $comp_in_html, SQ_FORM); $type = getPref($data_dir, $username, 'compose_window_type', ''); return ($javascript_on && ($type == 'html' || $comp_in_html)); } /** * @return boolean */ function html_area_is_supported_by_users_browser() { static $html_area_is_supported_by_users_browser = -1; if($html_area_is_supported_by_users_browser == 1) { return true; } elseif($html_area_is_supported_by_users_browser == 0) { return false; } $ua = html_mail_browser_info(); $ret = ( (isset($ua['msie']) && $ua['msie'] >= 6.0) || (isset($ua['firefox'])) || (isset($ua['safari'])) || (isset($ua['webkit'])) || (isset($ua['opera']) && $ua['opera'] >= 9.5) || (isset($ua['netscape'])) || (isset($ua['konqueror'])) || (isset($ua['gecko']) && $ua['gecko'] >= 20030624) ); return $ret; } /** * @return boolean */ function html_area_is_on_and_is_supported_by_users_browser() { return (html_compose_is_on() && html_area_is_supported_by_users_browser() ); } /** * Show user configuration items * */ function html_mail_display($hookName) { // 1.4.x - 1.5.0: options go on display options page // 1.5.1 and up: options go on compose options page // if (check_sm_version(1, 5, 1) && $hookName[0] != 'options_compose_inside') return; if (!check_sm_version(1, 5, 1) && $hookName[0] != 'options_display_inside') return; global $username, $data_dir, $email_type, $html_mail_aggressive_reply, $default_aggressive_html_reply, $html_mail_aggressive_reply_with_unsafe_images, $default_aggressive_reply_with_unsafe_images; hm_get_config(); $email_type = getPref($data_dir, $username, 'compose_window_type', ''); $html_mail_aggressive_reply = getPref($data_dir, $username, 'html_mail_aggressive_reply', $default_aggressive_html_reply); $html_mail_aggressive_reply_with_unsafe_images = getPref($data_dir, $username, 'html_mail_aggressive_reply_with_unsafe_images', $default_aggressive_reply_with_unsafe_images); sq_bindtextdomain('html_mail', SM_PATH . 'locale'); textdomain('html_mail'); echo html_tag( 'tr', "\n". html_tag( 'td', '' . _("Compose in Rich Text (HTML)") . '' , 'center' ,'', 'valign="middle" colspan="2" nowrap' ) ) ."\n"; // email_type echo '
' . _("Default Email Composition Format:") . "\n" . '
\n" . '    \n". '' . "\n"; // html_mail_aggressive_reply echo '' . _("Only Reply In HTML When Viewing HTML Format:") . "\n" . '
' . '\n" . '
\n". '' . "\n"; // html_mail_aggressive_reply_with_unsafe_images echo '' . _("Only Allow Unsafe Images In HTML Replies When Viewing Unsafe Images:") . "\n" . '
' . '\n" . '
\n". '' . "\n"; textdomain ('squirrelmail'); } /** * Save user configuration items */ function html_mail_save($hookName) { if (check_sm_version(1, 5, 1) && $hookName[0] != 'options_compose_save') return; if (!check_sm_version(1, 5, 1) && $hookName[0] != 'options_display_save') return; global $username, $data_dir, $email_type, $html_mail_aggressive_reply, $html_mail_aggressive_reply_with_unsafe_images; hm_get_config(); sqgetGlobalVar('email_type', $email_type, SQ_FORM); sqgetGlobalVar('html_mail_aggressive_reply', $html_mail_aggressive_reply, SQ_FORM); sqgetGlobalVar('html_mail_aggressive_reply_with_unsafe_images', $html_mail_aggressive_reply_with_unsafe_images, SQ_FORM); setPref($data_dir, $username, 'html_mail_aggressive_reply_with_unsafe_images', $html_mail_aggressive_reply_with_unsafe_images); setPref($data_dir, $username, 'html_mail_aggressive_reply', $html_mail_aggressive_reply); setPref($data_dir, $username, 'compose_window_type', $email_type); } /** * Changes outgoing message format to include multipart html and text parts if * needed */ function html_mail_alter_type_do(&$argv) { // change outgoing encoding if supported/turned on if (html_area_is_on_and_is_supported_by_users_browser()) { $message = &$argv[1]; global $strip_html_send_plain, $base_uri; sqgetGlobalVar('strip_html_send_plain', $strip_html_send_plain, SQ_FORM); $serverAddress = get_location(); if (strpos($serverAddress, '/') !== FALSE) $serverAddress = substr($serverAddress, strpos($serverAddress, '/') + 2); if (strpos($serverAddress, '/') !== FALSE) $serverAddress = substr($serverAddress, 0, strpos($serverAddress, '/')); // user wants to send this one in plain text, // so we have to: // 1) convert

and
into newlines // 2) strip the HTML out // 3) drop comments generated by html-stripping mechanism // if ($strip_html_send_plain) { if (is_array($message->entities) && sizeof($message->entities) > 0) { $msg = str_replace(array('', ''), '', sq_sanitize( preg_replace('/(|)/i', "\n", $message->entities[0]->body_part), array(TRUE), array(), array(), array(), array(), array(), array(), array(), array(), array())); // decode special chars.. $message->entities[0]->body_part = my_html_entity_decode($msg); } else { $msg = str_replace(array('', ''), '', sq_sanitize( preg_replace('/(|)/i', "\n", $message->body_part), array(TRUE), array(), array(), array(), array(), array(), array(), array(), array(), array())); // decode special chars.. $message->body_part = my_html_entity_decode($msg); } } else { // otherwise, set the outgoing content type correctly and add a // text/plain mime part, which means non-multipart messages // need to be converted to multipart... // figure out how images should be linked (HTTP/HTTPS) global $outgoing_image_uri_https; hm_get_config(); if ($outgoing_image_uri_https == 1) { $http = 'http'; } else if ($outgoing_image_uri_https == 2) { $http = 'https'; } else { if (isset($_SERVER['SERVER_PORT'])) $serverPort = $_SERVER['SERVER_PORT']; else $serverPort = 0; $http = ($serverPort == $outgoing_image_uri_https ? 'https' : 'http'); } // already multipart; change original message part to // multipart/alternative and add a plain text and html // part therein // if (is_array($message->entities) && sizeof($message->entities) > 0) { $plainText = str_replace(array('', ''), '', sq_sanitize( preg_replace('/(|)/i', "\n", $message->entities[0]->body_part), array(TRUE), array(), array(), array(), array(), array(), array(), array(), array(), array())); $plainText = my_html_entity_decode($plainText); // convert relative URIs to absolute; also remove URIs // to download.php (embedded images, etc) until we // find the time to code a way to forward on those images // $message->entities[0]->body_part = preg_replace(array('|src=(["\'])' . $base_uri . '|si', '||si'), array('src=\1' . $http . '://' . $serverAddress . $base_uri, '[IMAGE REMOVED]'), $message->entities[0]->body_part); $message->entities[0]->mime_header->type1 = 'html'; $htmlTextPart = $message->entities[0]; // break connection between $htmlTextPart and $message unset($message->entities[0]); // create new message part in place of removed one $message->entities[0] = new Message(); $message->entities[0]->mime_header = new MessageHeader(); // tag that message part as multipart alternative $message->entities[0]->mime_header->type0 = 'multipart'; $message->entities[0]->mime_header->type1 = 'alternative'; $message->entities[0]->mime_header->encoding = ''; $message->entities[0]->mime_header->parameters = array(); $message->entities[0]->body_part = ''; // gets us a different message boundary $message->entities[0]->entity_id = 'usf' . mt_rand(1000, 9999); // create new plaintext message $plainTextPart = new Message(); $plainTextPart->body_part = $plainText; $mime_header = new MessageHeader; $mime_header->type0 = 'text'; $mime_header->type1 = 'plain'; $mime_header->encoding = $message->entities[0]->mime_header->encoding; $mime_header->parameters = $message->entities[0]->mime_header->parameters; $plainTextPart->mime_header = $mime_header; // add plain text and html entities to multipart/alternative message $message->entities[0]->addEntity($plainTextPart); $message->entities[0]->addEntity($htmlTextPart); } else { // not multipart; convert to multipart, change original message // to html and add text/plain part $plainText = str_replace(array('', ''), '', sq_sanitize( preg_replace('/(|)/i', "\n", $message->body_part), array(TRUE), array(), array(), array(), array(), array(), array(), array(), array(), array())); $plainText = my_html_entity_decode($plainText); // convert relative URIs to absolute; also remove URIs // to download.php (embedded images, etc) until we // find the time to code a way to forward on those images $message->body_part = preg_replace(array('|src=(["\'])' . $base_uri . '|si', '||si'), array('src=\1' . $http . '://' . $serverAddress . $base_uri, '[IMAGE REMOVED]'), $message->body_part); $htmlTextPart = new Message(); $htmlTextPart->body_part = $message->body_part; $htmlPartMime_header = new MessageHeader; $htmlPartMime_header->type0 = 'text'; $htmlPartMime_header->type1 = 'html'; $htmlPartMime_header->encoding = $message->rfc822_header->encoding; $htmlPartMime_header->parameters = $message->rfc822_header->content_type->properties; $htmlTextPart->mime_header = $htmlPartMime_header; $plainTextPart = new Message(); $plainTextPart->body_part = $plainText; $plainPartMime_header = new MessageHeader; $plainPartMime_header->type0 = 'text'; $plainPartMime_header->type1 = 'plain'; $plainPartMime_header->encoding = $message->rfc822_header->encoding; $plainPartMime_header->parameters = $message->rfc822_header->content_type->properties; $plainTextPart->mime_header = $plainPartMime_header; // clear out some parts of the original non-multipart message // $message->rfc822_header->encoding = ''; $message->rfc822_header->content_type->type0 = 'multipart'; $message->rfc822_header->content_type->type1 = 'alternative'; $message->rfc822_header->content_type->properties = array(); $message->body_part = ''; $message->entities = array($plainTextPart, $htmlTextPart); } } return $message; } } function my_html_entity_decode($text) { if (function_exists('html_entity_decode')) return html_entity_decode($text); // copied from http://us3.php.net/preg-replace $search = array ("']*?>.*?'si", // Strip out javascript "'<[\/\!]*?[^<>]*?>'si", // Strip out html tags "'([\r\n])[\s]+'", // Strip out white space "'&(quot|#34);'i", // Replace html entities "'&(amp|#38);'i", "'&(lt|#60);'i", "'&(gt|#62);'i", "'&(nbsp|#160);'i", "'&(iexcl|#161);'i", "'&(cent|#162);'i", "'&(pound|#163);'i", "'&(copy|#169);'i", "'&#(\d+);'e"); // evaluate as php $replace = array ("", "", "\\1", "\"", "&", "<", ">", " ", chr(161), chr(162), chr(163), chr(169), "chr(\\1)"); return preg_replace ($search, $replace, $text); } /** * Wraps text at $wrap characters while preserving HTML tags * * Has a problem with special HTML characters, so call this before * you do character translation. * * Specifically, ' comes up as 5 characters instead of 1. * This should not add newlines to the end of lines. */ function sqHTMLWordWrap(&$line, $wrap) { global $languages, $squirrelmail_language; if (isset($languages[$squirrelmail_language]['XTRA_CODE']) && function_exists($languages[$squirrelmail_language]['XTRA_CODE'])) { if (mb_detect_encoding($line) != 'ASCII') { $line = $languages[$squirrelmail_language]['XTRA_CODE']('wordwrap', $line, $wrap); return; } } ereg("^([\t >]*)([^\t >].*)?$", $line, $regs); $beginning_spaces = $regs[1]; if (isset($regs[2])) { $words = explode(' ', $regs[2]); } else { $words = ''; } // pull words back together if they have split up a tag // $newWords = array(); $newWord = ''; foreach ($words as $word) { $newWord .= ' ' . $word; // this is a bit simplistic; a message might have // a less than sign without it being an opening // tag marker, but we'll go with this since grepping // for all possible tags is much more trouble and // there are probably not many messages where this // will be a problem // // this won't work: if tag doesn't end in next word segment... //if (strpos($word, '<') === FALSE) // $LTcount = preg_match_all('//', $newWord, $junk); if ($LTcount == $GTcount) { $newWords[] = $newWord; $newWord = ''; } } $words = $newWords; $i = 0; $line = $beginning_spaces; while ($i < count($words)) { /* Force one word to be on a line (minimum) */ $line .= $words[$i]; $line_len = strlen($beginning_spaces) + strlen(strip_tags($words[$i])) + 2; if (isset($words[$i + 1])) $line_len += strlen(strip_tags($words[$i + 1])); $i ++; /* Add more words (as long as they fit) */ while ($line_len < $wrap && $i < count($words)) { $line .= ' ' . $words[$i]; $i++; if (isset($words[$i])) $line_len += strlen(strip_tags($words[$i])) + 1; else $line_len += 1; } /* Skip spaces if they are the first thing on a continued line */ while (!isset($words[$i]) && $i < count($words)) { $i ++; } /* Go to the next line if we have more to process */ if ($i < count($words)) { $line .= "
"; } } } /** * User-Agent parsing; mainly used for detection of IE (what else?) * * @return array Something like Array( [safari] => 532.8) or * Array( [msie] => 8.0) * @see http://www.php.net/manual/en/function.get-browser.php#92310 */ function html_mail_browser_info($agent=null) { // Declare known browsers to look for $known = array('msie', 'firefox', 'safari', 'webkit', 'opera', 'netscape', 'konqueror', 'gecko'); // Clean up agent and build regex that matches phrases for known browsers // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor // version numbers. E.g. "2.0.0.6" is parsed as simply "2.0" $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']); $pattern = '#(?P' . join('|', $known) . ')[/ ]+(?P[0-9]+(?:\.[0-9]+)?)#'; // Find all phrases (or return empty array if none found) if (!preg_match_all($pattern, $agent, $matches)) return array(); // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase, // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one // in the UA). That's usually the most correct. $i = count($matches['browser'])-1; return array($matches['browser'][$i] => $matches['version'][$i]); }