* @license GNU General Public License, version 2 (GPL-2.0) * * For full copyright and license information, please see * the docs/CREDITS.txt file. * */ /** * @ignore */ if (!defined('IN_PHPBB')) { exit; } if (!class_exists('bbcode')) { include($phpbb_root_path . 'includes/bbcode.' . $phpEx); } /** * BBCODE FIRSTPASS * BBCODE first pass class (functions for parsing messages for db storage) */ class bbcode_firstpass extends bbcode { var $message = ''; var $warn_msg = array(); var $parsed_items = array(); /** * Parse BBCode */ function parse_bbcode() { if (!$this->bbcodes) { $this->bbcode_init(); } global $user; $this->bbcode_bitfield = ''; $bitfield = new bitfield(); foreach ($this->bbcodes as $bbcode_name => $bbcode_data) { if (isset($bbcode_data['disabled']) && $bbcode_data['disabled']) { foreach ($bbcode_data['regexp'] as $regexp => $replacement) { if (preg_match($regexp, $this->message)) { $this->warn_msg[] = sprintf($user->lang['UNAUTHORISED_BBCODE'] , '[' . $bbcode_name . ']'); continue; } } } else { foreach ($bbcode_data['regexp'] as $regexp => $replacement) { // The pattern gets compiled and cached by the PCRE extension, // it should not demand recompilation if (preg_match($regexp, $this->message)) { $this->message = preg_replace($regexp, $replacement, $this->message); $bitfield->set($bbcode_data['bbcode_id']); } } } } $this->bbcode_bitfield = $bitfield->get_base64(); } /** * Prepare some bbcodes for better parsing */ function prepare_bbcodes() { // Ok, seems like users instead want the no-parsing of urls, smilies, etc. after and before and within quote tags being tagged as "not a bug". // Fine by me ;) Will ease our live... but do not come back and cry at us, we won't hear you. /* Add newline at the end and in front of each quote block to prevent parsing errors (urls, smilies, etc.) if (strpos($this->message, '[quote') !== false && strpos($this->message, '[/quote]') !== false) { $this->message = str_replace("\r\n", "\n", $this->message); // We strip newlines and spaces after and before quotes in quotes (trimming) and then add exactly one newline $this->message = preg_replace('#\[quote(=".*?")?\]\s*(.*?)\s*\[/quote\]#siu', '[quote\1]' . "\n" . '\2' ."\n[/quote]", $this->message); } */ // Add other checks which needs to be placed before actually parsing anything (be it bbcodes, smilies, urls...) } /** * Init bbcode data for later parsing */ function bbcode_init($allow_custom_bbcode = true) { global $phpbb_dispatcher; static $rowset; // This array holds all bbcode data. BBCodes will be processed in this // order, so it is important to keep [code] in first position and // [quote] in second position. // To parse multiline URL we enable dotall option setting only for URL text // but not for link itself, thus [url][/url] is not affected. $this->bbcodes = array( 'code' => array('bbcode_id' => 8, 'regexp' => array('#\[code(?:=([a-z]+))?\](.+\[/code\])#uise' => "\$this->bbcode_code('\$1', '\$2')")), 'quote' => array('bbcode_id' => 0, 'regexp' => array('#\[quote(?:="(.*?)")?\](.+)\[/quote\]#uise' => "\$this->bbcode_quote('\$0')")), 'attachment' => array('bbcode_id' => 12, 'regexp' => array('#\[attachment=([0-9]+)\](.*?)\[/attachment\]#uise' => "\$this->bbcode_attachment('\$1', '\$2')")), 'b' => array('bbcode_id' => 1, 'regexp' => array('#\[b\](.*?)\[/b\]#uise' => "\$this->bbcode_strong('\$1')")), 'i' => array('bbcode_id' => 2, 'regexp' => array('#\[i\](.*?)\[/i\]#uise' => "\$this->bbcode_italic('\$1')")), 'url' => array('bbcode_id' => 3, 'regexp' => array('#\[url(=(.*))?\](?(1)((?s).*(?-s))|(.*))\[/url\]#uiUe' => "\$this->validate_url('\$2', ('\$3') ? '\$3' : '\$4')")), 'img' => array('bbcode_id' => 4, 'regexp' => array('#\[img\](.*)\[/img\]#uiUe' => "\$this->bbcode_img('\$1')")), 'size' => array('bbcode_id' => 5, 'regexp' => array('#\[size=([\-\+]?\d+)\](.*?)\[/size\]#uise' => "\$this->bbcode_size('\$1', '\$2')")), 'color' => array('bbcode_id' => 6, 'regexp' => array('!\[color=(#[0-9a-f]{3}|#[0-9a-f]{6}|[a-z\-]+)\](.*?)\[/color\]!uise' => "\$this->bbcode_color('\$1', '\$2')")), 'u' => array('bbcode_id' => 7, 'regexp' => array('#\[u\](.*?)\[/u\]#uise' => "\$this->bbcode_underline('\$1')")), 'list' => array('bbcode_id' => 9, 'regexp' => array('#\[list(?:=(?:[a-z0-9]|disc|circle|square))?].*\[/list]#uise' => "\$this->bbcode_parse_list('\$0')")), 'email' => array('bbcode_id' => 10, 'regexp' => array('#\[email=?(.*?)?\](.*?)\[/email\]#uise' => "\$this->validate_email('\$1', '\$2')")), 'flash' => array('bbcode_id' => 11, 'regexp' => array('#\[flash=([0-9]+),([0-9]+)\](.*?)\[/flash\]#uie' => "\$this->bbcode_flash('\$1', '\$2', '\$3')")) ); // Zero the parsed items array $this->parsed_items = array(); foreach ($this->bbcodes as $tag => $bbcode_data) { $this->parsed_items[$tag] = 0; } if (!$allow_custom_bbcode) { return; } if (!is_array($rowset)) { global $db; $rowset = array(); $sql = 'SELECT * FROM ' . BBCODES_TABLE; $result = $db->sql_query($sql); while ($row = $db->sql_fetchrow($result)) { $rowset[] = $row; } $db->sql_freeresult($result); } foreach ($rowset as $row) { $this->bbcodes[$row['bbcode_tag']] = array( 'bbcode_id' => (int) $row['bbcode_id'], 'regexp' => array($row['first_pass_match'] => str_replace('$uid', $this->bbcode_uid, $row['first_pass_replace'])) ); } $bbcodes = $this->bbcodes; /** * Event to modify the bbcode data for later parsing * * @event core.modify_bbcode_init * @var array bbcodes Array of bbcode data for use in parsing * @var array rowset Array of bbcode data from the database * @since 3.1.0-a3 */ $vars = array('bbcodes', 'rowset'); extract($phpbb_dispatcher->trigger_event('core.modify_bbcode_init', compact($vars))); $this->bbcodes = $bbcodes; } /** * Making some pre-checks for bbcodes as well as increasing the number of parsed items */ function check_bbcode($bbcode, &$in) { // when using the /e modifier, preg_replace slashes double-quotes but does not // seem to slash anything else $in = str_replace("\r\n", "\n", str_replace('\"', '"', $in)); // Trimming here to make sure no empty bbcodes are parsed accidently if (trim($in) == '') { return false; } $this->parsed_items[$bbcode]++; return true; } /** * Transform some characters in valid bbcodes */ function bbcode_specialchars($text) { $str_from = array('<', '>', '[', ']', '.', ':'); $str_to = array('<', '>', '[', ']', '.', ':'); return str_replace($str_from, $str_to, $text); } /** * Parse size tag */ function bbcode_size($stx, $in) { global $user, $config; if (!$this->check_bbcode('size', $in)) { return $in; } if ($config['max_' . $this->mode . '_font_size'] && $config['max_' . $this->mode . '_font_size'] < $stx) { $this->warn_msg[] = $user->lang('MAX_FONT_SIZE_EXCEEDED', (int) $config['max_' . $this->mode . '_font_size']); return '[size=' . $stx . ']' . $in . '[/size]'; } // Do not allow size=0 if ($stx <= 0) { return '[size=' . $stx . ']' . $in . '[/size]'; } return '[size=' . $stx . ':' . $this->bbcode_uid . ']' . $in . '[/size:' . $this->bbcode_uid . ']'; } /** * Parse color tag */ function bbcode_color($stx, $in) { if (!$this->check_bbcode('color', $in)) { return $in; } return '[color=' . $stx . ':' . $this->bbcode_uid . ']' . $in . '[/color:' . $this->bbcode_uid . ']'; } /** * Parse u tag */ function bbcode_underline($in) { if (!$this->check_bbcode('u', $in)) { return $in; } return '[u:' . $this->bbcode_uid . ']' . $in . '[/u:' . $this->bbcode_uid . ']'; } /** * Parse b tag */ function bbcode_strong($in) { if (!$this->check_bbcode('b', $in)) { return $in; } return '[b:' . $this->bbcode_uid . ']' . $in . '[/b:' . $this->bbcode_uid . ']'; } /** * Parse i tag */ function bbcode_italic($in) { if (!$this->check_bbcode('i', $in)) { return $in; } return '[i:' . $this->bbcode_uid . ']' . $in . '[/i:' . $this->bbcode_uid . ']'; } /** * Parse img tag */ function bbcode_img($in) { global $user, $config; if (!$this->check_bbcode('img', $in)) { return $in; } $in = trim($in); $error = false; $in = str_replace(' ', '%20', $in); // Checking urls if (!preg_match('#^' . get_preg_expression('url') . '$#i', $in) && !preg_match('#^' . get_preg_expression('www_url') . '$#i', $in)) { return '[img]' . $in . '[/img]'; } // Try to cope with a common user error... not specifying a protocol but only a subdomain if (!preg_match('#^[a-z0-9]+://#i', $in)) { $in = 'http://' . $in; } if ($config['max_' . $this->mode . '_img_height'] || $config['max_' . $this->mode . '_img_width']) { $stats = @getimagesize(htmlspecialchars_decode($in)); if ($stats === false) { $error = true; $this->warn_msg[] = $user->lang['UNABLE_GET_IMAGE_SIZE']; } else { if ($config['max_' . $this->mode . '_img_height'] && $config['max_' . $this->mode . '_img_height'] < $stats[1]) { $error = true; $this->warn_msg[] = $user->lang('MAX_IMG_HEIGHT_EXCEEDED', (int) $config['max_' . $this->mode . '_img_height']); } if ($config['max_' . $this->mode . '_img_width'] && $config['max_' . $this->mode . '_img_width'] < $stats[0]) { $error = true; $this->warn_msg[] = $user->lang('MAX_IMG_WIDTH_EXCEEDED', (int) $config['max_' . $this->mode . '_img_width']); } } } if ($error || $this->path_in_domain($in)) { return '[img]' . $in . '[/img]'; } return '[img:' . $this->bbcode_uid . ']' . $this->bbcode_specialchars($in) . '[/img:' . $this->bbcode_uid . ']'; } /** * Parse flash tag */ function bbcode_flash($width, $height, $in) { global $user, $config; if (!$this->check_bbcode('flash', $in)) { return $in; } $in = trim($in); $error = false; // Do not allow 0-sizes generally being entered if ($width <= 0 || $height <= 0) { return '[flash=' . $width . ',' . $height . ']' . $in . '[/flash]'; } $in = str_replace(' ', '%20', $in); // Make sure $in is a URL. if (!preg_match('#^' . get_preg_expression('url') . '$#i', $in) && !preg_match('#^' . get_preg_expression('www_url') . '$#i', $in)) { return '[flash=' . $width . ',' . $height . ']' . $in . '[/flash]'; } // Apply the same size checks on flash files as on images if ($config['max_' . $this->mode . '_img_height'] || $config['max_' . $this->mode . '_img_width']) { if ($config['max_' . $this->mode . '_img_height'] && $config['max_' . $this->mode . '_img_height'] < $height) { $error = true; $this->warn_msg[] = $user->lang('MAX_FLASH_HEIGHT_EXCEEDED', (int) $config['max_' . $this->mode . '_img_height']); } if ($config['max_' . $this->mode . '_img_width'] && $config['max_' . $this->mode . '_img_width'] < $width) { $error = true; $this->warn_msg[] = $user->lang('MAX_FLASH_WIDTH_EXCEEDED', (int) $config['max_' . $this->mode . '_img_width']); } } if ($error || $this->path_in_domain($in)) { return '[flash=' . $width . ',' . $height . ']' . $in . '[/flash]'; } return '[flash=' . $width . ',' . $height . ':' . $this->bbcode_uid . ']' . $this->bbcode_specialchars($in) . '[/flash:' . $this->bbcode_uid . ']'; } /** * Parse inline attachments [ia] */ function bbcode_attachment($stx, $in) { if (!$this->check_bbcode('attachment', $in)) { return $in; } return '[attachment=' . $stx . ':' . $this->bbcode_uid . ']' . trim($in) . '[/attachment:' . $this->bbcode_uid . ']'; } /** * Parse code text from code tag * @access private */ function bbcode_parse_code($stx, &$code) { switch (strtolower($stx)) { case 'php': $remove_tags = false; $str_from = array('<', '>', '[', ']', '.', ':', ':'); $str_to = array('<', '>', '[', ']', '.', ':', ':'); $code = str_replace($str_from, $str_to, $code); if (!preg_match('/\<\?.*?\?\>/is', $code)) { $remove_tags = true; $code = ""; } $conf = array('highlight.bg', 'highlight.comment', 'highlight.default', 'highlight.html', 'highlight.keyword', 'highlight.string'); foreach ($conf as $ini_var) { @ini_set($ini_var, str_replace('highlight.', 'syntax', $ini_var)); } // Because highlight_string is specialcharing the text (but we already did this before), we have to reverse this in order to get correct results $code = htmlspecialchars_decode($code); $code = highlight_string($code, true); $str_from = array('', '', '','[', ']', '.', ':'); $str_to = array('', '', '', '[', ']', '.', ':'); if ($remove_tags) { $str_from[] = '<?php '; $str_to[] = ''; $str_from[] = '<?php '; $str_to[] = ''; } $code = str_replace($str_from, $str_to, $code); $code = preg_replace('#^()\n?(.*?)\n?()$#is', '$1$2$3', $code); if ($remove_tags) { $code = preg_replace('#()?\?>()#', '$1 $2', $code); } $code = preg_replace('#^(.*)#s', '$2', $code); $code = preg_replace('#(?:\s++| )*+$#u', '', $code); // remove newline at the end if (!empty($code) && substr($code, -1) == "\n") { $code = substr($code, 0, -1); } return "[code=$stx:" . $this->bbcode_uid . ']' . $code . '[/code:' . $this->bbcode_uid . ']'; break; default: return '[code:' . $this->bbcode_uid . ']' . $this->bbcode_specialchars($code) . '[/code:' . $this->bbcode_uid . ']'; break; } } /** * Parse code tag * Expects the argument to start right after the opening [code] tag and to end with [/code] */ function bbcode_code($stx, $in) { if (!$this->check_bbcode('code', $in)) { return $in; } // We remove the hardcoded elements from the code block here because it is not used in code blocks // Having it here saves us one preg_replace per message containing [code] blocks // Additionally, magic url parsing should go after parsing bbcodes, but for safety those are stripped out too... $htm_match = get_preg_expression('bbcode_htm'); unset($htm_match[4], $htm_match[5]); $htm_replace = array('\1', '\1', '\2', '\1'); $out = $code_block = ''; $open = 1; while ($in) { // Determine position and tag length of next code block preg_match('#(.*?)(\[code(?:=([a-z]+))?\])(.+)#is', $in, $buffer); $pos = (isset($buffer[1])) ? strlen($buffer[1]) : false; $tag_length = (isset($buffer[2])) ? strlen($buffer[2]) : false; // Determine position of ending code tag $pos2 = stripos($in, '[/code]'); // Which is the next block, ending code or code block if ($pos !== false && $pos < $pos2) { // Open new block if (!$open) { $out .= substr($in, 0, $pos); $in = substr($in, $pos); $stx = (isset($buffer[3])) ? $buffer[3] : ''; $code_block = ''; } else { // Already opened block, just append to the current block $code_block .= substr($in, 0, $pos) . ((isset($buffer[2])) ? $buffer[2] : ''); $in = substr($in, $pos); } $in = substr($in, $tag_length); $open++; } else { // Close the block if ($open == 1) { $code_block .= substr($in, 0, $pos2); $code_block = preg_replace($htm_match, $htm_replace, $code_block); // Parse this code block $out .= $this->bbcode_parse_code($stx, $code_block); $code_block = ''; $open--; } else if ($open) { // Close one open tag... add to the current code block $code_block .= substr($in, 0, $pos2 + 7); $open--; } else { // end code without opening code... will be always outside code block $out .= substr($in, 0, $pos2 + 7); } $in = substr($in, $pos2 + 7); } } // if now $code_block has contents we need to parse the remaining code while removing the last closing tag to match up. if ($code_block) { $code_block = substr($code_block, 0, -7); $code_block = preg_replace($htm_match, $htm_replace, $code_block); $out .= $this->bbcode_parse_code($stx, $code_block); } return $out; } /** * Parse list bbcode * Expects the argument to start with a tag */ function bbcode_parse_list($in) { if (!$this->check_bbcode('list', $in)) { return $in; } // $tok holds characters to stop at. Since the string starts with a '[' we'll get everything up to the first ']' which should be the opening [list] tag $tok = ']'; $out = '['; // First character is [ $in = substr($in, 1); $list_end_tags = $item_end_tags = array(); do { $pos = strlen($in); for ($i = 0, $tok_len = strlen($tok); $i < $tok_len; ++$i) { $tmp_pos = strpos($in, $tok[$i]); if ($tmp_pos !== false && $tmp_pos < $pos) { $pos = $tmp_pos; } } $buffer = substr($in, 0, $pos); $tok = $in[$pos]; $in = substr($in, $pos + 1); if ($tok == ']') { // if $tok is ']' the buffer holds a tag if (strtolower($buffer) == '/list' && sizeof($list_end_tags)) { // valid [/list] tag, check nesting so that we don't hit false positives if (sizeof($item_end_tags) && sizeof($item_end_tags) >= sizeof($list_end_tags)) { // current li tag has not been closed $out = preg_replace('/\n?\[$/', '[', $out) . array_pop($item_end_tags) . ']['; } $out .= array_pop($list_end_tags) . ']'; $tok = '['; } else if (preg_match('#^list(=[0-9a-z]+)?$#i', $buffer, $m)) { // sub-list, add a closing tag if (empty($m[1]) || preg_match('/^=(?:disc|square|circle)$/i', $m[1])) { array_push($list_end_tags, '/list:u:' . $this->bbcode_uid); } else { array_push($list_end_tags, '/list:o:' . $this->bbcode_uid); } $out .= 'list' . substr($buffer, 4) . ':' . $this->bbcode_uid . ']'; $tok = '['; } else { if (($buffer == '*' || substr($buffer, -2) == '[*') && sizeof($list_end_tags)) { // the buffer holds a bullet tag and we have a [list] tag open if (sizeof($item_end_tags) >= sizeof($list_end_tags)) { if (substr($buffer, -2) == '[*') { $out .= substr($buffer, 0, -2) . '['; } // current li tag has not been closed if (preg_match('/\n\[$/', $out, $m)) { $out = preg_replace('/\n\[$/', '[', $out); $buffer = array_pop($item_end_tags) . "]\n[*:" . $this->bbcode_uid; } else { $buffer = array_pop($item_end_tags) . '][*:' . $this->bbcode_uid; } } else { $buffer = '*:' . $this->bbcode_uid; } $item_end_tags[] = '/*:m:' . $this->bbcode_uid; } else if ($buffer == '/*') { array_pop($item_end_tags); $buffer = '/*:' . $this->bbcode_uid; } $out .= $buffer . $tok; $tok = '[]'; } } else { // Not within a tag, just add buffer to the return string $out .= $buffer . $tok; $tok = ($tok == '[') ? ']' : '[]'; } } while ($in); // do we have some tags open? close them now if (sizeof($item_end_tags)) { $out .= '[' . implode('][', $item_end_tags) . ']'; } if (sizeof($list_end_tags)) { $out .= '[' . implode('][', $list_end_tags) . ']'; } return $out; } /** * Parse quote bbcode * Expects the argument to start with a tag */ function bbcode_quote($in) { global $config, $user; $in = str_replace("\r\n", "\n", str_replace('\"', '"', trim($in))); if (!$in) { return ''; } // To let the parser not catch tokens within quote_username quotes we encode them before we start this... $in = preg_replace('#quote="(.*?)"\]#ie', "'quote="' . str_replace(array('[', ']', '\\\"'), array('[', ']', '\"'), '\$1') . '"]'", $in); $tok = ']'; $out = '['; $in = substr($in, 1); $close_tags = $error_ary = array(); $buffer = ''; do { $pos = strlen($in); for ($i = 0, $tok_len = strlen($tok); $i < $tok_len; ++$i) { $tmp_pos = strpos($in, $tok[$i]); if ($tmp_pos !== false && $tmp_pos < $pos) { $pos = $tmp_pos; } } $buffer .= substr($in, 0, $pos); $tok = $in[$pos]; $in = substr($in, $pos + 1); if ($tok == ']') { if (strtolower($buffer) == '/quote' && sizeof($close_tags) && substr($out, -1, 1) == '[') { // we have found a closing tag $out .= array_pop($close_tags) . ']'; $tok = '['; $buffer = ''; /* Add space at the end of the closing tag if not happened before to allow following urls/smilies to be parsed correctly * Do not try to think for the user. :/ Do not parse urls/smilies if there is no space - is the same as with other bbcodes too. * Also, we won't have any spaces within $in anyway, only adding up spaces -> #10982 if (!$in || $in[0] !== ' ') { $out .= ' '; }*/ } else if (preg_match('#^quote(?:="(.*?)")?$#is', $buffer, $m) && substr($out, -1, 1) == '[') { $this->parsed_items['quote']++; // the buffer holds a valid opening tag if ($config['max_quote_depth'] && sizeof($close_tags) >= $config['max_quote_depth']) { if ($config['max_quote_depth'] == 1) { // Depth 1 - no nesting is allowed $error_ary['quote_depth'] = $user->lang('QUOTE_NO_NESTING'); } else { // There are too many nested quotes $error_ary['quote_depth'] = $user->lang('QUOTE_DEPTH_EXCEEDED', (int) $config['max_quote_depth']); } $out .= $buffer . $tok; $tok = '[]'; $buffer = ''; continue; } array_push($close_tags, '/quote:' . $this->bbcode_uid); if (isset($m[1]) && $m[1]) { $username = str_replace(array('[', ']'), array('[', ']'), $m[1]); $username = preg_replace('#\[(?!b|i|u|color|url|email|/b|/i|/u|/color|/url|/email)#iU', '[$1', $username); $end_tags = array(); $error = false; preg_match_all('#\[((?:/)?(?:[a-z]+))#i', $username, $tags); foreach ($tags[1] as $tag) { if ($tag[0] != '/') { $end_tags[] = '/' . $tag; } else { $end_tag = array_pop($end_tags); $error = ($end_tag != $tag) ? true : false; } } if ($error) { $username = $m[1]; } $out .= 'quote="' . $username . '":' . $this->bbcode_uid . ']'; } else { $out .= 'quote:' . $this->bbcode_uid . ']'; } $tok = '['; $buffer = ''; } else if (preg_match('#^quote="(.*?)#is', $buffer, $m)) { // the buffer holds an invalid opening tag $buffer .= ']'; } else { $out .= $buffer . $tok; $tok = '[]'; $buffer = ''; } } else { /** * Old quote code working fine, but having errors listed in bug #3572 * * $out .= $buffer . $tok; * $tok = ($tok == '[') ? ']' : '[]'; * $buffer = ''; */ $out .= $buffer . $tok; if ($tok == '[') { // Search the text for the next tok... if an ending quote comes first, then change tok to [] $pos1 = stripos($in, '[/quote'); // If the token ] comes first, we change it to ] $pos2 = strpos($in, ']'); // If the token [ comes first, we change it to [ $pos3 = strpos($in, '['); if ($pos1 !== false && ($pos2 === false || $pos1 < $pos2) && ($pos3 === false || $pos1 < $pos3)) { $tok = '[]'; } else if ($pos3 !== false && ($pos2 === false || $pos3 < $pos2)) { $tok = '['; } else { $tok = ']'; } } else { $tok = '[]'; } $buffer = ''; } } while ($in); $out .= $buffer; if (sizeof($close_tags)) { $out .= '[' . implode('][', $close_tags) . ']'; } foreach ($error_ary as $error_msg) { $this->warn_msg[] = $error_msg; } return $out; } /** * Validate email */ function validate_email($var1, $var2) { $var1 = str_replace("\r\n", "\n", str_replace('\"', '"', trim($var1))); $var2 = str_replace("\r\n", "\n", str_replace('\"', '"', trim($var2))); $txt = $var2; $email = ($var1) ? $var1 : $var2; $validated = true; if (!preg_match('/^' . get_preg_expression('email') . '$/i', $email)) { $validated = false; } if (!$validated) { return '[email' . (($var1) ? "=$var1" : '') . ']' . $var2 . '[/email]'; } $this->parsed_items['email']++; if ($var1) { $retval = '[email=' . $this->bbcode_specialchars($email) . ':' . $this->bbcode_uid . ']' . $txt . '[/email:' . $this->bbcode_uid . ']'; } else { $retval = '[email:' . $this->bbcode_uid . ']' . $this->bbcode_specialchars($email) . '[/email:' . $this->bbcode_uid . ']'; } return $retval; } /** * Validate url * * @param string $var1 optional url parameter for url bbcode: [url(=$var1)]$var2[/url] * @param string $var2 url bbcode content: [url(=$var1)]$var2[/url] */ function validate_url($var1, $var2) { global $config; $var1 = str_replace("\r\n", "\n", str_replace('\"', '"', trim($var1))); $var2 = str_replace("\r\n", "\n", str_replace('\"', '"', trim($var2))); $url = ($var1) ? $var1 : $var2; if ($var1 && !$var2) { $var2 = $var1; } if (!$url) { return '[url' . (($var1) ? '=' . $var1 : '') . ']' . $var2 . '[/url]'; } $valid = false; $url = str_replace(' ', '%20', $url); // Checking urls if (preg_match('#^' . get_preg_expression('url') . '$#i', $url) || preg_match('#^' . get_preg_expression('www_url') . '$#i', $url) || preg_match('#^' . preg_quote(generate_board_url(), '#') . get_preg_expression('relative_url') . '$#i', $url)) { $valid = true; } if ($valid) { $this->parsed_items['url']++; // if there is no scheme, then add http schema if (!preg_match('#^[a-z][a-z\d+\-.]*:/{2}#i', $url)) { $url = 'http://' . $url; } // Is this a link to somewhere inside this board? If so then remove the session id from the url if (strpos($url, generate_board_url()) !== false && strpos($url, 'sid=') !== false) { $url = preg_replace('/(&|\?)sid=[0-9a-f]{32}&/', '\1', $url); $url = preg_replace('/(&|\?)sid=[0-9a-f]{32}$/', '', $url); $url = append_sid($url); } return ($var1) ? '[url=' . $this->bbcode_specialchars($url) . ':' . $this->bbcode_uid . ']' . $var2 . '[/url:' . $this->bbcode_uid . ']' : '[url:' . $this->bbcode_uid . ']' . $this->bbcode_specialchars($url) . '[/url:' . $this->bbcode_uid . ']'; } return '[url' . (($var1) ? '=' . $var1 : '') . ']' . $var2 . '[/url]'; } /** * Check if url is pointing to this domain/script_path/php-file * * @param string $url the url to check * @return true if the url is pointing to this domain/script_path/php-file, false if not * * @access private */ function path_in_domain($url) { global $config, $phpEx, $user; if ($config['force_server_vars']) { $check_path = $config['script_path']; } else { $check_path = ($user->page['root_script_path'] != '/') ? substr($user->page['root_script_path'], 0, -1) : '/'; } // Is the user trying to link to a php file in this domain and script path? if (strpos($url, ".{$phpEx}") !== false && strpos($url, $check_path) !== false) { $server_name = $user->host; // Forcing server vars is the only way to specify/override the protocol if ($config['force_server_vars'] || !$server_name) { $server_name = $config['server_name']; } // Check again in correct order... $pos_ext = strpos($url, ".{$phpEx}"); $pos_path = strpos($url, $check_path); $pos_domain = strpos($url, $server_name); if ($pos_domain !== false && $pos_path >= $pos_domain && $pos_ext >= $pos_path) { // Ok, actually we allow linking to some files (this may be able to be extended in some way later...) if (strpos($url, '/' . $check_path . '/download/file.' . $phpEx) !== 0) { return false; } return true; } } return false; } } /** * Main message parser for posting, pm, etc. takes raw message * and parses it for attachments, bbcode and smilies */ class parse_message extends bbcode_firstpass { var $attachment_data = array(); var $filename_data = array(); // Helps ironing out user error var $message_status = ''; var $allow_img_bbcode = true; var $allow_flash_bbcode = true; var $allow_quote_bbcode = true; var $allow_url_bbcode = true; var $mode; /** * The plupload object used for dealing with attachments * @var \phpbb\plupload\plupload */ protected $plupload; /** * The mimetype guesser object used for attachment mimetypes * @var \phpbb\mimetype\guesser */ protected $mimetype_guesser; /** * Init - give message here or manually */ function parse_message($message = '') { // Init BBCode UID $this->bbcode_uid = substr(base_convert(unique_id(), 16, 36), 0, BBCODE_UID_LEN); $this->message = $message; } /** * Parse Message */ function parse($allow_bbcode, $allow_magic_url, $allow_smilies, $allow_img_bbcode = true, $allow_flash_bbcode = true, $allow_quote_bbcode = true, $allow_url_bbcode = true, $update_this_message = true, $mode = 'post') { global $config, $db, $user; $this->mode = $mode; foreach (array('chars', 'smilies', 'urls', 'font_size', 'img_height', 'img_width') as $key) { if (!isset($config['max_' . $mode . '_' . $key])) { $config['max_' . $mode . '_' . $key] = 0; } } $this->allow_img_bbcode = $allow_img_bbcode; $this->allow_flash_bbcode = $allow_flash_bbcode; $this->allow_quote_bbcode = $allow_quote_bbcode; $this->allow_url_bbcode = $allow_url_bbcode; // If false, then $this->message won't be altered, the text will be returned instead. if (!$update_this_message) { $tmp_message = $this->message; $return_message = &$this->message; } if ($this->message_status == 'display') { $this->decode_message(); } // Do some general 'cleanup' first before processing message, // e.g. remove excessive newlines(?), smilies(?) $match = array('#(script|about|applet|activex|chrome):#i'); $replace = array("\\1:"); $this->message = preg_replace($match, $replace, trim($this->message)); // Store message length... $message_length = ($mode == 'post') ? utf8_strlen($this->message) : utf8_strlen(preg_replace('#\[\/?[a-z\*\+\-]+(=[\S]+)?\]#ius', ' ', $this->message)); // Maximum message length check. 0 disables this check completely. if ((int) $config['max_' . $mode . '_chars'] > 0 && $message_length > (int) $config['max_' . $mode . '_chars']) { $this->warn_msg[] = $user->lang('CHARS_' . strtoupper($mode) . '_CONTAINS', $message_length) . '
' . $user->lang('TOO_MANY_CHARS_LIMIT', (int) $config['max_' . $mode . '_chars']); return (!$update_this_message) ? $return_message : $this->warn_msg; } // Minimum message length check for post only if ($mode === 'post') { if (!$message_length || $message_length < (int) $config['min_post_chars']) { $this->warn_msg[] = (!$message_length) ? $user->lang['TOO_FEW_CHARS'] : ($user->lang('CHARS_POST_CONTAINS', $message_length) . '
' . $user->lang('TOO_FEW_CHARS_LIMIT', (int) $config['min_post_chars'])); return (!$update_this_message) ? $return_message : $this->warn_msg; } } // Prepare BBcode (just prepares some tags for better parsing) if ($allow_bbcode && strpos($this->message, '[') !== false) { $this->bbcode_init(); $disallow = array('img', 'flash', 'quote', 'url'); foreach ($disallow as $bool) { if (!${'allow_' . $bool . '_bbcode'}) { $this->bbcodes[$bool]['disabled'] = true; } } $this->prepare_bbcodes(); } // Parse smilies if ($allow_smilies) { $this->smilies($config['max_' . $mode . '_smilies']); } $num_urls = 0; // Parse BBCode if ($allow_bbcode && strpos($this->message, '[') !== false) { $this->parse_bbcode(); $num_urls += $this->parsed_items['url']; } // Parse URL's if ($allow_magic_url) { $this->magic_url(generate_board_url()); if ($config['max_' . $mode . '_urls']) { $num_urls += preg_match_all('#\' . $row['code'] . ''; } $db->sql_freeresult($result); } if (sizeof($match)) { if ($max_smilies) { // 'u' modifier has been added to correctly parse smilies within unicode strings // For details: http://tracker.phpbb.com/browse/PHPBB3-10117 $num_matches = preg_match_all('#(?<=^|[\n .])(?:' . implode('|', $match) . ')(?![^<>]*>)#u', $this->message, $matches); unset($matches); if ($num_matches !== false && $num_matches > $max_smilies) { $this->warn_msg[] = sprintf($user->lang['TOO_MANY_SMILIES'], $max_smilies); return; } } // Make sure the delimiter # is added in front and at the end of every element within $match // 'u' modifier has been added to correctly parse smilies within unicode strings // For details: http://tracker.phpbb.com/browse/PHPBB3-10117 $this->message = trim(preg_replace(explode(chr(0), '#(?<=^|[\n .])' . implode('(?![^<>]*>)#u' . chr(0) . '#(?<=^|[\n .])', $match) . '(?![^<>]*>)#u'), $replace, $this->message)); } } /** * Parse Attachments */ function parse_attachments($form_name, $mode, $forum_id, $submit, $preview, $refresh, $is_message = false) { global $config, $auth, $user, $phpbb_root_path, $phpEx, $db, $request; $error = array(); $num_attachments = sizeof($this->attachment_data); $this->filename_data['filecomment'] = utf8_normalize_nfc(request_var('filecomment', '', true)); $upload = $request->file($form_name); $upload_file = (!empty($upload) && $upload['name'] !== 'none' && trim($upload['name'])); $add_file = (isset($_POST['add_file'])) ? true : false; $delete_file = (isset($_POST['delete_file'])) ? true : false; // First of all adjust comments if changed $actual_comment_list = utf8_normalize_nfc(request_var('comment_list', array(''), true)); foreach ($actual_comment_list as $comment_key => $comment) { if (!isset($this->attachment_data[$comment_key])) { continue; } if ($this->attachment_data[$comment_key]['attach_comment'] != $actual_comment_list[$comment_key]) { $this->attachment_data[$comment_key]['attach_comment'] = $actual_comment_list[$comment_key]; } } $cfg = array(); $cfg['max_attachments'] = ($is_message) ? $config['max_attachments_pm'] : $config['max_attachments']; $forum_id = ($is_message) ? 0 : $forum_id; if ($submit && in_array($mode, array('post', 'reply', 'quote', 'edit')) && $upload_file) { if ($num_attachments < $cfg['max_attachments'] || $auth->acl_get('a_') || $auth->acl_get('m_', $forum_id)) { $filedata = upload_attachment($form_name, $forum_id, false, '', $is_message); $error = $filedata['error']; if ($filedata['post_attach'] && !sizeof($error)) { $sql_ary = array( 'physical_filename' => $filedata['physical_filename'], 'attach_comment' => $this->filename_data['filecomment'], 'real_filename' => $filedata['real_filename'], 'extension' => $filedata['extension'], 'mimetype' => $filedata['mimetype'], 'filesize' => $filedata['filesize'], 'filetime' => $filedata['filetime'], 'thumbnail' => $filedata['thumbnail'], 'is_orphan' => 1, 'in_message' => ($is_message) ? 1 : 0, 'poster_id' => $user->data['user_id'], ); $db->sql_query('INSERT INTO ' . ATTACHMENTS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary)); $new_entry = array( 'attach_id' => $db->sql_nextid(), 'is_orphan' => 1, 'real_filename' => $filedata['real_filename'], 'attach_comment'=> $this->filename_data['filecomment'], 'filesize' => $filedata['filesize'], ); $this->attachment_data = array_merge(array(0 => $new_entry), $this->attachment_data); $this->message = preg_replace('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#e', "'[attachment='.(\\1 + 1).']\\2[/attachment]'", $this->message); $this->filename_data['filecomment'] = ''; // This Variable is set to false here, because Attachments are entered into the // Database in two modes, one if the id_list is 0 and the second one if post_attach is true // Since post_attach is automatically switched to true if an Attachment got added to the filesystem, // but we are assigning an id of 0 here, we have to reset the post_attach variable to false. // // This is very relevant, because it could happen that the post got not submitted, but we do not // know this circumstance here. We could be at the posting page or we could be redirected to the entered // post. :) $filedata['post_attach'] = false; } } else { $error[] = $user->lang('TOO_MANY_ATTACHMENTS', (int) $cfg['max_attachments']); } } if ($preview || $refresh || sizeof($error)) { if (isset($this->plupload) && $this->plupload->is_active()) { $json_response = new \phpbb\json_response(); } // Perform actions on temporary attachments if ($delete_file) { include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx); $index = array_keys(request_var('delete_file', array(0 => 0))); $index = (!empty($index)) ? $index[0] : false; if ($index !== false && !empty($this->attachment_data[$index])) { // delete selected attachment if ($this->attachment_data[$index]['is_orphan']) { $sql = 'SELECT attach_id, physical_filename, thumbnail FROM ' . ATTACHMENTS_TABLE . ' WHERE attach_id = ' . (int) $this->attachment_data[$index]['attach_id'] . ' AND is_orphan = 1 AND poster_id = ' . $user->data['user_id']; $result = $db->sql_query($sql); $row = $db->sql_fetchrow($result); $db->sql_freeresult($result); if ($row) { phpbb_unlink($row['physical_filename'], 'file'); if ($row['thumbnail']) { phpbb_unlink($row['physical_filename'], 'thumbnail'); } $db->sql_query('DELETE FROM ' . ATTACHMENTS_TABLE . ' WHERE attach_id = ' . (int) $this->attachment_data[$index]['attach_id']); } } else { delete_attachments('attach', array(intval($this->attachment_data[$index]['attach_id']))); } unset($this->attachment_data[$index]); $this->message = preg_replace('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#e', "(\\1 == \$index) ? '' : ((\\1 > \$index) ? '[attachment=' . (\\1 - 1) . ']\\2[/attachment]' : '\\0')", $this->message); // Reindex Array $this->attachment_data = array_values($this->attachment_data); if (isset($this->plupload) && $this->plupload->is_active()) { $json_response->send($this->attachment_data); } } } else if (($add_file || $preview) && $upload_file) { if ($num_attachments < $cfg['max_attachments'] || $auth->acl_gets('m_', 'a_', $forum_id)) { $filedata = upload_attachment($form_name, $forum_id, false, '', $is_message, false, $this->mimetype_guesser, $this->plupload); $error = array_merge($error, $filedata['error']); if (!sizeof($error)) { $sql_ary = array( 'physical_filename' => $filedata['physical_filename'], 'attach_comment' => $this->filename_data['filecomment'], 'real_filename' => $filedata['real_filename'], 'extension' => $filedata['extension'], 'mimetype' => $filedata['mimetype'], 'filesize' => $filedata['filesize'], 'filetime' => $filedata['filetime'], 'thumbnail' => $filedata['thumbnail'], 'is_orphan' => 1, 'in_message' => ($is_message) ? 1 : 0, 'poster_id' => $user->data['user_id'], ); $db->sql_query('INSERT INTO ' . ATTACHMENTS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary)); $new_entry = array( 'attach_id' => $db->sql_nextid(), 'is_orphan' => 1, 'real_filename' => $filedata['real_filename'], 'attach_comment'=> $this->filename_data['filecomment'], 'filesize' => $filedata['filesize'], ); $this->attachment_data = array_merge(array(0 => $new_entry), $this->attachment_data); $this->message = preg_replace('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#e', "'[attachment='.(\\1 + 1).']\\2[/attachment]'", $this->message); $this->filename_data['filecomment'] = ''; if (isset($this->plupload) && $this->plupload->is_active()) { $download_url = append_sid("{$phpbb_root_path}download/file.{$phpEx}", 'mode=view&id=' . $new_entry['attach_id']); // Send the client the attachment data to maintain state $json_response->send(array('data' => $this->attachment_data, 'download_url' => $download_url)); } } } else { $error[] = $user->lang('TOO_MANY_ATTACHMENTS', (int) $cfg['max_attachments']); } if (!empty($error) && isset($this->plupload) && $this->plupload->is_active()) { // If this is a plupload (and thus ajax) request, give the // client the first error we have $json_response->send(array( 'jsonrpc' => '2.0', 'id' => 'id', 'error' => array( 'code' => 105, 'message' => current($error), ), )); } } } foreach ($error as $error_msg) { $this->warn_msg[] = $error_msg; } } /** * Get Attachment Data */ function get_submitted_attachment_data($check_user_id = false) { global $user, $db, $phpbb_root_path, $phpEx, $config; global $request; $this->filename_data['filecomment'] = utf8_normalize_nfc(request_var('filecomment', '', true)); $attachment_data = $request->variable('attachment_data', array(0 => array('' => '')), true, \phpbb\request\request_interface::POST); $this->attachment_data = array(); $check_user_id = ($check_user_id === false) ? $user->data['user_id'] : $check_user_id; if (!sizeof($attachment_data)) { return; } $not_orphan = $orphan = array(); foreach ($attachment_data as $pos => $var_ary) { if ($var_ary['is_orphan']) { $orphan[(int) $var_ary['attach_id']] = $pos; } else { $not_orphan[(int) $var_ary['attach_id']] = $pos; } } // Regenerate already posted attachments if (sizeof($not_orphan)) { // Get the attachment data, based on the poster id... $sql = 'SELECT attach_id, is_orphan, real_filename, attach_comment, filesize FROM ' . ATTACHMENTS_TABLE . ' WHERE ' . $db->sql_in_set('attach_id', array_keys($not_orphan)) . ' AND poster_id = ' . $check_user_id; $result = $db->sql_query($sql); while ($row = $db->sql_fetchrow($result)) { $pos = $not_orphan[$row['attach_id']]; $this->attachment_data[$pos] = $row; $this->attachment_data[$pos]['attach_comment'] = $attachment_data[$pos]['attach_comment']; unset($not_orphan[$row['attach_id']]); } $db->sql_freeresult($result); } if (sizeof($not_orphan)) { trigger_error('NO_ACCESS_ATTACHMENT', E_USER_ERROR); } // Regenerate newly uploaded attachments if (sizeof($orphan)) { $sql = 'SELECT attach_id, is_orphan, real_filename, attach_comment, filesize FROM ' . ATTACHMENTS_TABLE . ' WHERE ' . $db->sql_in_set('attach_id', array_keys($orphan)) . ' AND poster_id = ' . $user->data['user_id'] . ' AND is_orphan = 1'; $result = $db->sql_query($sql); while ($row = $db->sql_fetchrow($result)) { $pos = $orphan[$row['attach_id']]; $this->attachment_data[$pos] = $row; $this->attachment_data[$pos]['attach_comment'] = $attachment_data[$pos]['attach_comment']; unset($orphan[$row['attach_id']]); } $db->sql_freeresult($result); } if (sizeof($orphan)) { trigger_error('NO_ACCESS_ATTACHMENT', E_USER_ERROR); } ksort($this->attachment_data); } /** * Parse Poll */ function parse_poll(&$poll) { global $auth, $user, $config; $poll_max_options = $poll['poll_max_options']; // Parse Poll Option text ;) $tmp_message = $this->message; $this->message = $poll['poll_option_text']; $bbcode_bitfield = $this->bbcode_bitfield; $poll['poll_option_text'] = $this->parse($poll['enable_bbcode'], ($config['allow_post_links']) ? $poll['enable_urls'] : false, $poll['enable_smilies'], $poll['img_status'], false, false, $config['allow_post_links'], false, 'poll'); $bbcode_bitfield = base64_encode(base64_decode($bbcode_bitfield) | base64_decode($this->bbcode_bitfield)); $this->message = $tmp_message; // Parse Poll Title $tmp_message = $this->message; $this->message = $poll['poll_title']; $this->bbcode_bitfield = $bbcode_bitfield; $poll['poll_options'] = explode("\n", trim($poll['poll_option_text'])); $poll['poll_options_size'] = sizeof($poll['poll_options']); if (!$poll['poll_title'] && $poll['poll_options_size']) { $this->warn_msg[] = $user->lang['NO_POLL_TITLE']; } else { if (utf8_strlen(preg_replace('#\[\/?[a-z\*\+\-]+(=[\S]+)?\]#ius', ' ', $this->message)) > 100) { $this->warn_msg[] = $user->lang['POLL_TITLE_TOO_LONG']; } $poll['poll_title'] = $this->parse($poll['enable_bbcode'], ($config['allow_post_links']) ? $poll['enable_urls'] : false, $poll['enable_smilies'], $poll['img_status'], false, false, $config['allow_post_links'], false, 'poll'); if (strlen($poll['poll_title']) > 255) { $this->warn_msg[] = $user->lang['POLL_TITLE_COMP_TOO_LONG']; } } $this->bbcode_bitfield = base64_encode(base64_decode($bbcode_bitfield) | base64_decode($this->bbcode_bitfield)); $this->message = $tmp_message; unset($tmp_message); if (sizeof($poll['poll_options']) == 1) { $this->warn_msg[] = $user->lang['TOO_FEW_POLL_OPTIONS']; } else if ($poll['poll_options_size'] > (int) $config['max_poll_options']) { $this->warn_msg[] = $user->lang['TOO_MANY_POLL_OPTIONS']; } else if ($poll_max_options > $poll['poll_options_size']) { $this->warn_msg[] = $user->lang['TOO_MANY_USER_OPTIONS']; } $poll['poll_max_options'] = ($poll['poll_max_options'] < 1) ? 1 : (($poll['poll_max_options'] > $config['max_poll_options']) ? $config['max_poll_options'] : $poll['poll_max_options']); } /** * Setter function for passing the plupload object * * @param \phpbb\plupload\plupload $plupload The plupload object * * @return null */ public function set_plupload(\phpbb\plupload\plupload $plupload) { $this->plupload = $plupload; } /** * Setter function for passing the mimetype_guesser object * * @param \phpbb\mimetype\guesser $mimetype_guesser The mimetype_guesser object * * @return null */ public function set_mimetype_guesser(\phpbb\mimetype\guesser $mimetype_guesser) { $this->mimetype_guesser = $mimetype_guesser; } } ̦## ȞYF6~"@C#Mt@`;"o**y1KkcR:׳ej}ՑQ/eH`o0FppNQe2uhbE>7JNi 9J:5QW#7 fwsj-ؼnN;jp븡%Q_OeQZ}-?=\WFIB~\37tm>xVnswgaXCXڵu^x|e\2ʎٳӴ[9WkGjN{;J=˿;B`xP T|a#<){l]ܤi>Z|@E`f10$l cknKs ~Z.ZxB~Kˋ9lJ_s/K_p{ 4 TA|*̬4"[C7pp.6َq?3iD>`ʄúoLIO֦w/{7/vD}uS=ZDcЅ$1˯q5 )BN?O$w(03~.֟.,d ?f7ovsڽ-YsDN=4\llh7TGV&#`Ù@?P&O%QKe6"$?iU6>kBXKn 88XJ:ۆcWaYgF Ƒn]k/  \z[B݉rrQR|aZ%HaEXa4t͒Q?%.ܞ,Bh33dP' wr«,?GmZg-\rhGhkVc!#1*T 0h0X>bUQYe!I|áof{f-#!LDc6z9{Y8WRHMx!% >9{S]SޗI2/[ NXHz{ͩ#B{|-|b}|7/Ve{zq s0#*^z(ENP|S7(%j; mIJigm?Jt]^|TlF~])z/I|yBX#'yN*qfc5ŅaKJB?O>3x>z7Gsᆬ^l门N (#K"ϑN0Xzw*gkF9fa,=la2BŘ59r+'flZ'?4~.SonU46Q\ "Bh(qyS$":{55$;aȢ,acs&rUƅ߯ek|sM(ugxpN̸- ~:<%H. .RCuI[Q_c~lJKPgeGBϣTжTJT0cF%˓[ ESl}q|J y`ir|r 3:'<-5tr<򚈆}c=~e.3#! xA&m)Ӕd37ss_ODEq< n|nڍ:;_] QȰ=d#\u9eKdz5Tqғv>զ/ 6*][yGA=TgUoxQk ÂA5)ΝӁ{UNwA~۾Y ͽgڻYq&קZ;woqA|VBOU^8m:wxAGAA+1srIJIL 6`S9ɎՖM'%&7[_),nA\gxMiao L;)pgW0$WLGarpyBT [3J;qBYNt ;Ǯ -c5L{CJ$%Lo.^!&5Y9~z~!M12(-hWgQ7\Dؗ[lr\2luM$#hMfPE-plB̵pid^тTvr-RB^.@t+Kfʄ.>V b#^ެe"j,{Ka(-[-SqOR<;E2K0pgӹTa^m3= ;A9%A9#~[o]=x0"{s6^ ʭ^iD9R} kk֤5-m mVGykx`mj~&ؓ#|+ܿHl$ip95kJ  4\?}o&9 (@879 =fAZcl1scZ& <:W@XF0ΆfE Kڗ0r C?#)8}+oOF3]k $z?{=tߥJq,RǽzZx|trZ;EO*nIoضz7<4ZO {Tnpۃ /Җ S6$ZVF6%ل *m@Tv& Mȧ,t?ؤ6|lBdږ̴Q^Bhz[MR#4V^M(b.߃3Ի /_|M2,eb8z2uS,vs53"rW_Qư3ڛr1fB⌋4mdI!o94:j8uem#1cGkGgschs#.lj5n)g^`-K$]4= \_0n6G:4D®ԧ,Pɦlx[] KG|[Ƿ{]kű+o"|,!R IؑG[ɂAnǀ`E䢨Ii0쓯kEl?OD6;G|nBy-#|TseQ@5UqTgYʙ:?Fgq5ȿW6j0D3M@HZ Yį R ʰhiIwW赟.. <_"!S.4¹\,Vmu[,/|t+ru;p#ٻ,:0I(ޙ+ݖD J#` =(1&VJ(J0v ~.ucН27mFbκa=g庒],9nc.RGs8t.;DVuU3/w_ٓ4":nu 92Sw +gWЉ@k_V-qn@Ca78:9%-ڐxσrK۪Ca\Ek2 r0^G;- 58QN!S赾$AY:`t')34$:jޛI"ĐŞba9C< dŭlvމ,Zo(Mkg$hc+kŸ3 U"`J};3e',Ejl<黛Pf6׸k ;iF(.V3 ~ס RJ?4XPNܰxM}{n]{qzU=.GrN(aΐqD >=Oo8ݲsT ]"b K@˚YȾ/|a_Y|w\Œz"x#J 1ZTiQ!:x5px|]2k,jHrs7b&5!$]Yb}]\f%y+ Z!2`Zs'i3LIͳZ&u3PztpN ?-/cyrqA5< yIYOc<<}lu1AZ84)1eds6;f'UፕkEs,n)} )7Ubp!1Җ8an"9hټ,;=(+٨8{cI13-,l/(6J+;(1\k͓QwqC{e@ ~̔ k6FdgTcΊ:92SEGj$gғjr!yh}-wu>6ִkB=`@-䆝#[e'SGoy|{^hqfR==9eNq*RAd!pU6B'}#[[I1oƛo7^n1?yFzz=xMW)RSIZ DJ:|0|s|I<[Oopqr)%E\%NKްT%˹#{ D$2%;7щfUԢ{PE;|+!,M=]knE368f Re=`BTqqRwIjHp0ږizڑ_&\O X1w Rb;X<ʕ!%K:fþKKKUhuؙDPv]о=J+x)ޡe3u! AAg=g}c} 7|sWnN%n}S^}[ u[@Ph CޘxI8 ľv0=LxAx/\47j - f`G!lNpg,DX/H%5]6s`YfjXf29@KeA&pدӝMF|Sd5Mb~*szx{N4, 9t&" Xgxł-/>dUEHݰ ezr$axqtlf-fR[r-FX+xr<^jʯA< *F`?szQ?O07.WtxI7}-M͂@r#& \UErʾWQYC딤M԰C'QC*2@"=I}zRS NHxM ?ÚȞ,c~h621̭FI4 ~ޝzO?C,B1~U4}jlFPCAgF2Satnf4V.iiE' j Ϣ>uo ZYn\vhR8'fga/6z/9w * 7F#( #ELRB%I2(UU-B$xt'ɠOKҩ0|Wl4mg2+x**}Q˳/z$LǺ7Mq A];We 8@K'cUt:n`<3Cm~h/>yL}5AXEHUC]mWl[[rQּH.9x-mS'))7r*81 /wS]o9{!i^.fxMHm8ccEg~JGR!O14)q<t #ƃh{3uՑF}nNPj9NYl{g^Tz+48lzϩ ܪojaoc{ScS]!(Пg6nIHoߡsn5oP*w.Ҭ0cwJμx/ԃVa3[[ř OUŽ'Ma~6g+0~N: @Ih0ḫaP%G+HxNtag2݀*~|W'y8cV{|ŧ8鱭['SZQ%{tAПocnFk%M#nkh:yfgn3bHxY+\qrE1SDSBD@nT/V:0T 9s{SiQK;n89`?(:>'Q]eXH/պdjP?8h&[vׅf.yEtH\ۄ\Rwnj4qmm-/)f1 `q[H3>fs}D7Pg6qZ2J$?x3|vxчp{%Xᬕ#&$6L:`e]vqM *f Bw]Ō$;6~ߘN9l89ʾslh$=j_l>lbz,cl -&wΏ[oi{^1.@pLC"z"rjMmI<+.TW#r=(&=fL,^ݻ܃9~)ِ~_p4:#ztK޽ov^*T?.l iଊ= ʆ:}96TUrgQB"q1d:qң5mEF֎z 1ubVp2 8ރa'`"C Nhx^妼x/ Jr5NzwKs?6Tܖ-Ð/IŗȻދSVs 9:Cи;DASNŘ2y`pWkSbAydx c':RlX7kBO-Ek߫XbƪAug_u'@oHwSRs (U(dS!u6wN\W^l["}djcľߚ&ӓk^Bpl/@V |/%jO͑sƨ7^<[Y@ʶy&!_~y/C[52ٵSQ83Xyn|gnrx~ LR~}!JAXo,4Ebfϡ- Dl׆YZ#0^(0:iL@*"2YEc>˨ #oמom9C;[Z;&7o޾x~vokkx6.on?=yN8 =yMMm[ׯWwǫ5R6BD)4H+ivƩ>v%  +pLW:yZ3C1'n;;*X5J$J*\LVmZ"chF37c%v:=oz5z|$yFB<,}mQ2tJm`%-ݶ }c74djzj1@K $(u#굃sO:rvW]D5m rqtf'!iV>T^)<^ln!Csף]nKUdIR isMG^R++f[|lt0(_lyQ-Ft+ p7T"ܞ:rt1 ˉ= FVe,>0puƐSWxv/A颟 q`ENjתH Z]C -͛Z]ןH,9B/.t`?$xLRGKޮj_u#87r)q'5G^\˫s|Y'en.`JIayNb7| K.c}L-ؓzY G}ƒ3b*S4Ԏ}Օ?}~JPGt7T~75JAyFWx0A(l. mST({()ưZ"zXЪ*UMR n ń/ ΕM6@1UrGحvxG" J |ϱRs |EWxh.MXM3(-(z 0RcdWAl11YԑQe2(1[q_V5(^ȶCD9:yb۰D.ͮAdZ%}ᢐ #IB$CT},ư۶mK<\Zwpqy7-xRQp&pGS}_~K{K3Y O0sySsABMsbIp-V1@MnFeMc#cZw2pwA?jJG4뗷芦<3A e~ ū 5x  %p,BPxϦ`SΝ* 3 !J:m"!aPƩ;\ܹs"k9ro> 2 ϰ+,Wq$i|BRԔ] ~ '1t:vʹ\L壼WH9ip Lqk#^:@s"' &?1LUf &LstO>OCV|9M4Ӱ > ^_$ܟ K!u?0sNnUsSVid(̢^ J*>r;W°S6xIdLp=.5¼OvD rjͩm$ ҝwc)gkepfIP"GV,8`Sۺ_ :0s'vLbӸ)=rIJj3 N 3ڃ$:>9?ԿA/FE;S!}];2\z._I@>J,hZp\88IqZji{Qm1Ь D8OdܷK nkZ4 h-qp:8D7TG2"f7 DtԒ䘓Ek.n'- ]ں9pK ;I&&'6QZ_,m\Y[gUirLk%r٨,1XOa es\ӐQV¾܄3BP4JeЪGH(%SPIL OpsZqѢ9A4Bמ%/nL̩.9)Ve.$ѓtB=MDMV+q߃[b6Vn}ͥ?oM37P Uex~hpEy+A܃ɪmWP]Ȇ lsTzё?ü[&tzopDD&,VoAs#q=WP ǽz&Maض)f7}~R~[u^ZdU{i:Pp5mOu+CO|B-O}1.Gka3Cq2.kg=o0 i͔h(5RE5l]Y\9ϔ;0%X1@ vt6Bt]$u\RM Kqi=:bbvN?e"-&}&O'F*ЖX:cG=[[Q&K"8 դɉzu/[ʘr&{>OotO駹:qFdc;$ǚNv0b6Bȳ|Q 1FϰM5?6g<_X#qsXN[UIlfgi6g1Bp{e;Ǔ1gZl 5qP'~vOɔ"ey5QikJ~n8.[eNV^:Y2Iur 4^:NKJocu:@zоբ"c P ٰg=qY+iM.\Wc_O`?=iE.Tzqq-Dη/66 }zHU/6z-*7{tǝ.VTtM1?Y9ؿ@DO)=+eɞn&Sn UK|mvƜՂQqH{}:9N&`cb]K{*ΆDWƂ,:9;UC?+GF6PH[mVM3DȠ^f dK /m߶z͊R)+SmJwv&+yR[Y jᤓ:] }Z:IŶ#AH/:{~,y .33Oztv i7oBeS5P8lMU\%>b5lC}y'S̞lj~`ܙ`X:-E߳KyYSp!ӉA Ȧꊵ\}[HE `uJDa %v(9j*#Ḍ0l/m&hQssI<s?$l8#'0pĚ z~mmat̅&{gtixA/V#VLڈV{~QA6thQ>yɖaԄ'j3/eS O/`V=r+Dp S++, I jx8M}v͘ۈ63pXĵ=pLr#_]?yѲK}e.",5VL K&ΰN[Bg5WQ@;`tsUL_T9A-崌}jnɖ[g=ZeXcFԮ`7iu獨ᕽ^ݺ,_hf# f25Vtdsc/LǼv< ndE}ヮP~ 'Q\Ұ0 oH/kj4WuMRHeB}e/qCafIQKIç RN{;v(җx;u!_bwX.jn:4q˗ <[j{uEKѱQ r7[7pե1{;άL\N x xz$x sXSk5u1ĖtQl!t \3):sN m[J"&2E1>G3H<\N kK@@T(&e * qh˚ڰ99gS2X.7ۭK\f#C|B0̆#=u`D~}sv;=>Ɨ/fs9g#zb\rփAO%NnY\ WY2ےx ;ɖoͭ}rg4 f.MTeSJ^G?f˔<LG&:v.MiQ 9^JJ2q*důQ龴nŻQ҆@d e٨"U(^j7ʢ 0͵\0):mE p<|T^>Ͳ 4{,ҹ= ‰-̓VN$[\vo[$".|ˁ!if(Մɏ%֜|K'2C^ߕPL|&&َh%u»5>̞%dz>8t{!]c*p<'ln5睼{g4~/ZMUYdX-i)0ʦKbp֮hKtFp;'Nv{0HMqK8t83˺eMQKsIKA[rJʉ?U{{SN t`ƚ~UX)@7YF?pjNuk.`UWC)Z:S2;P,\,U9of18ZK)V2|=)ѱu܎4/"Rv! F'F@ֈh R ~7L ~E͕G1+7zq~nR)h^ p'S76&o>U{v^lx8r6ttFS>)zW;/@hǏݍv6_w;n~nW??wA_OZAw+yFe[T)<1MO+&ndZ@K5xiz12'dxY_X!U [qG~xxʕsIDj'E(Ӌ:^>6Cyˏ969zw"g0(+ri5/wIp2'D(#NDV ҧ5GP>!|{v'Q4Q{#(,LQ46hd,+jp(# a=^a.\I-%c&oIz/~NbVnߑxL<&Ԥ'R4?f}Vu~2^1ﺢ~sqWyL|nߪU@pxz\+tOb- *@-DtkW@[Em V,xTy٦ {{hnRF<Q1///kVKcԒw#"(*zCa3bk5ր ]ߴ8iqrYW`u/|}/Ɖtn4Z;Gk4?1TeVkq@w_-$\Kp/3?-c-pF⣜ڙ:j?.8d5]XMpRq޾zR-ŰI9%X,٦}OuE6Dc(:v'2â2.1I{P'b^E٧Nث4,JXa͙fi4FhfucB`V = cyMЄzj͠p$AU8cq?(@\28W,Bp eVl H&xZUBBwE룙uA9rn&~g7t$Zu0VY B!soʞhԖ۹|TvH0 A0=2*XM't o h9gXb O?f:RQ$aC0jP@!2A-s_2~i=5,w+fEɄ.3,4boT2B > H[*"L^5qB;=̩AO̤95D8j 6-h(2b%7=e8#`9!AA92#BN)5.N[)QP!DU2t)#7HĔ΁HxI m#E h\ԇ~FxX )$>pkbVr7f)S TZ("k*_XZXf< LI4UA}4X0$#|UEhixbh3 T|[L ZI RȲB4QFCiHOD.U%pU񑒒bw%$ 4r*^<۫sBSPr 1N: CIUUN20>#S&N,2k^ۻp3] ZgO+Obu`aUJ$6k"Aww/>5Wo&SA6g+hOځOqTZDqGGJD8R<[mӿ4$h SR u>-}9 #G sAknB+'Z<@8S*zif FzT5ƶ 'It,IwxP-L!X4 SU~3ٍu  ! u/nf Bw+(T*1Z5wC*7>R"ռ1gw:w!|No#SD.s0`r_or0U4|<=bBv5{a*?A`N`HiQy4$ _1 #5wīQ1X }w= ڤKܙ9\A__()t vzj\h٢y@Z/4zMTP{`N:#gpAJV;p:uAJb&19ۏe>] TٲIQIs"¢'SבJ܌L0PECT&`*ȀH8<>Snn_*2cBK[KwY eX>O70j#hlx9P 6dbIQisW ,Qי*T4I\SSCWCAM@ HZ.eMrEaRn:b~LO]xSYM1:]ݣ~X19FM*kS.uE֖rc2gLbcH6(**+z B0 #si [Rlqh09Dǜ£`Q!vilfh\F\8KuƩQL#<4%dLM*c)9l7 4[iG}}+v  G$0M뒔0U5}1Gi.EƟML\k3Zd ,9@"g'-Q+dцZκ'߇!_Zx;n# $QaӷPtth>ޖ# _o>4SJK7)/aS=q8cY֮sDI.+.\$<>f]p!q d}Y̶-R&1a(O\^qP }lB)AD]GI<:QnR[$$cs}pd\Ywva&rBkg$7%@ [P` OEм G:ag˘H@%k9vљDi=x9՗p4KD5mNslgtqȥ>PC Vm\p2tE`W/=K v;@,;ڬ,ZZ}F =٤|PDʘ#շ^q^GI ;0ê&_d1B2ib8pA R/Mi񇬸2} b88ƍ2";Fic\'U"K͡p8{>= mށ?i3 ۏ ek ѤO)4D|TDa8X`?&v[ )ur:ʪe/+LjdmpL3XmK+hΤԦL3јo԰:-kφjM.oiuA 5 )@U9amtb!46+gür^i2Qg".ɗʳ}D w"8% 4D:srXo)8`E/J,rLe+SGU*OTߚ 5j&ި5 bI(#NorK]BT ʔNTPݩiKSiq AuRU}[Lo*"ic35HVF}$v[v~h'- nz˜X*r]Y{qRS͕zoN}C3ى^ʹFk'x]N>yj~$=<*}Cדz{e=>zf,wtϓo$f[$@~kl97?q$7׃ҘH?rev8)p߉UH: 1`H/NW2 ɼ5)u-tL˯IR_&MM@*i&oؾeRӀv]wT(ZԹвFh_h|˰o'pS3Y"E&B<kx0.a È3:^/#%7#NOW3*TA= - ||G}ڧsW#1dHhL6[Q{X]/K :2W{:]/W{@C-~3D|L ٿFRWxЈ3K +'ɓi\/M>5kǫPjhǵXÂq{+Հj|lp\eq|mheFn߆ćq#g=nhݿߍ}{è_B4[}P@ +_퇂c=QFy}Ry~O۵boNIs_" CEŌU҆<V:eZ\P.)A%--IaXW-pHU>)`}WFrNwΤm~ի`۽|+Ӓ}m_g8UVP{]26HN/%"gj6cjnZq4^T%TxmA?V3¹ ٴ[w'1;Fj4B^'ʓN!sc]0e$rH.bPwggl_8t2g󊺴`klHݐ3'\XO~(@>~Z1 Ҵ;.BT‹G+&dHI kqdDAÕCoĹ-˭_vJj^JSiɌIJLbe 6IiHD稓 i׊t$88I6H>嶓D#,HU(7WZX@2*VX"n:2CBr7+Xvk@Pk [&uMe<`rc6o:UU}\9! o*=F>ѧp77IpN<ňK$`t`mTp _ۡhĐå7B7o;;^Ep=$$9a+?/0}mWׯa:9mu &n> l}|z$fɚbC ~m x෍7X3z3(4oE:kyEJU6m>&b~bg/v7}ߪ~*2wqƪV ڌ ID]Z V*E=h:]*!q^ PY,ׂZ< E_o+fּc5xQĈ*&ȞjMC )^lz>T*JPQ:ӒxZmpi߃_%\xLʃS0&V//t|L;lHL rn= oT6L- u1h<1h)5n?uřܐ%}WoirLg i'/ņDM:QO?RDIVe[be`<|_ѭf]Xe^٨/2n-`<mNdmj/Þc^BSʋ(9PW$mqؓcGMI+5AJ, 0B+Xv.AraT{H[Ji! ZP֑YkE>M4CnJ[IwNzjٌ*hfE5CRdv Eq4nTԼ)?45i\;\onRyn~{wy&R^&9䉻yX;PVjgD΃XN뮇o뷮2`S-POb"-x3iݏASc6gycS(AdZE)qgjDr-wDV&1bR8&`)1Mn J`A5r[n;7W&3ŏ ^ ^BC[_M.{M]#jJ sz Us<=9 *yAC0B׳j GcP*^ 1?OW0_P/8T+I@[9@yCl @x~spc=)p!+^ou 0JY㱭nSp63P!3p}1Կ+7(Q*~_JH1*th cE0Mϒ}A_T͕_&Q1vG+~_ O}U$Ws}lM~>_}ryYg>̘=`頙 ˁ5& - {$͇Wϒ;AmyʤEKR^UWJ|ez≯Ih2= ;W3}p` W>GAB,6V^l5&FSq+Sz=X1V}}a?JEYVaG+7V?X#נV_ź8T>U6r$MB=.FJkmȚ):G\uVi%Zd1"8+W^Vw@4L՟u<~_`*fr cQ 6(uP X W Hqk6!aA@S o!6eGAelJ2t;Tem۶%rnrRg3@J8ȯl?lq2N4nShks>{X;[o6v^߷kĐ`0^)zs*76) eKy1MQT&2Azvl]Ƚtr/fjxʛgy.8%Uw*j~n`vI?_2s*h٤o;{{vש?[znhlNff6(uJU!7v˸W ڶL~~G\o )wcp^UEP xA# y|SYoj*[N`Vп-2JLY0ÑkD ej!h0 b$)?f zӿrtONշє(W''ܟ.J #DQ 'PbͦH G;o4'883'=_& J:X)̈9i &aiZb3%dX.٘G GT1,wh_h|hfm8G 5;ˠUt"jYrL<9"Bф-2Aʈp|QtVhX"ԷOQXu1V I"TN-Qrf OQ/AI1o@#WcoC@tHJ{l1R;SʞzOinmE#8>w\ l8=!%cJT"o^oYܛ?W@7D&c%Y详ָ[IQAOhAjQCKZW;kn3p^ڠ2aP%`/=% m'$*CA&9+:RdAw[V X"|㊉. $i ݮ}qDZkpj~׃mzPb+2_.Ω¼UBW_M3[(ʽv:nOm:v9Ј؅u;R>lmX=!'7>^.hb`3 (3#ItKw̪Hֲ[o`% jͺ32;~n-fסB_ڞN?̒bpg$&PՆUy{>c4C缙1@LsVYͦ,?*.ZݷP |ڠij32 .1Qpi9i:1T7W>̙_VOzI$_Jp:hHݺVmSU\ZQ |CKY^8d`Wス; wqU{pܘp^`O>cc}C-DgJ-X$ g9:b@"K`I!0(ƦN(NfQ:O͟Wgk1fGGhvb!)l.,-A:hlXi,}15J !bB >(jIBqp٠9yT~; !y1zJp7h>j[𼰤kaX!~j̕YO/E}\:W>4_geVmN_ǔz?)fao7aQ{oc2mWViРz_¶T 'R.PWĕ! ^ʫ8|? QN4}W̿:ڔd'ZBU5C8ԤaU--cd09#k+/k21r{~5j|Rt@%6}t,X 6ӛ<16#<8fvɣ4 iX8jی*mF8gC{Kb.Pą vOJhP3Q2;KvDfUlAif~#~UUA╨;d{ROS~ûuswc5s rw2<}_n=!e.|tOݔuP76m3* _;txC9.΃BG KGZIՕ|nN[q9W_6Y%H+nMN3êpsyeMV_7s# g@@ Y UQxJw5*krOk@&/s(n`N_b81^a5ۚ썒w׸*3`͑3U) Eˮ^߀XрGJ Gs-~3 ƷJ<)7)6h 7k'zcu+*uEnq hcN&aIǭ1g 4-~/쬾3\:%ށ|s:4b{b/t=H)WR^QɏHxMz j!UbyG`RSdPzw oKtO{\OtJDXNMh߄* V]n-֌pi(!<4R/i~zB El}!\A7(2Q'[X 'nX'5Hl2^4(: SPgQ9sj& n'"ŕiCPӀծieW6up#کLgn^a/^,ө#8YD3O_Q+!V&F-.WԵN].*N';:Ι߷)UT%:̒G4)ũ<Ddic'r}>t:ւ́:)`JH;|1Srw+˟?s$!|ڗ*PR:E`6YT-T Z׷(Pl$ bXBT9Vs6d^Q]dZ~Za~5ݨ%{ɂ\\$lnE" 4-=8#H#Mf\bbN_e"܏ZxbRG:,UF XG?-Cħu|F2)Kd9SpU-JB3qAKVb"YnxDѽ ;grp$atgB DKNyhM2x6M^Qno$\n? gۻfsD$$ytT֮wڧt<=-Փn!T̚,>P& L ~E.G`!2Çglb9R2C+5vȱH;}jxn=͐J>*GF3˒>{-_L/!n ̂Z6#.&#1Ro>j/ جpt <^1kFlj;`$oij{ef:=.󴘛h@ku9K^fctPo4ƘFCԒ8F,7W󞌾x%<%-jS ? umɊ9!bpZncEl=@ːE.0N{1#iF#nOSVgY }umvtYITwc$ʁ'Kϓ66vXv.'H-S!q! y\]Ӣ? Gs٨,$`E O!2| E]+`,qQ{4J1 5{ߝ=ڒ .GP ꪍh"Иe5ޙUlY"\ms:9CD҂A4(, A5 8caJ#g4w'AAp ܣ˥ZeuAq,3wNگx|KP 'cP1BDME-| [#MW6`gU۳|Oػ<惇?f?wJ$9^==l;hl;yf26zꕗAtak6RQaimoԐ=%j.ohy$WU%/锗:RۓYN)7LS:'ɺUrCP/Fp{o݋!;AAaH2a"edg }"l I*-M5+{5##) 2iDzԈ?8XIҟR7@ПHDt7ϊzo8%%]r>}Q5@h.p8 u&4nuĄI=wMK1E`^*G>w4p%B\@(k|ubEKi;wjUS{ M8`Ud*uR8YVF3s 8NIEi#3jA$zl,"gߊb9R926gҎwCS1O*krRtyC$e~MN^&0jZ.mEBm}QU2UIVs6% t|zaOU6PLY]`x|I%$ IdSQܝj`{Ow  i1Uj7ٟă_nL[-=zf$ckfn P,z25THA~-dS6OTvWbhZZ>T i)B Ȣ6Ԇ#mQ/m. K!YxҍY_z_܇{zWWV^?^ ~Xu*WW\=CX`Q"i_O 9Jڜ s=헦Z-+ZϺ[/el]mZW14?iD2Β際44K|[H1PN0*q4M@ֺQڊFp_Ns!rF|6~~LLh[L{^8_փ8w}" Ud``L mNb 7X1J*uG묁˻4;a*P'f (\Bő`Dc$^BXSWmy[1h 9_r#HΖu6EnWxG獊_ꆭ'ʇKb?-]%D 9RBs]Ƿ %u)B &U+.ãK)>?d÷   +#,8niwva39q qV8Ⱦ-8OBSjM؊1!;R8yu.X>f! "qU3rQuzkǕ;,%r2w!g/? <]'gan9OGV(<$|7^~Xm)Fvs٫9OI%3:]62Ș U={|ulo^s]-և_?+/K$_Hs(tmNC kq" GiwAl,PM, C(4cUeN&,?hr4| b2ԄB}.d2r.?TLO墷Ux=6a# Kd5Sš%n %XEƈqI ;2Ke ?TfcNric#?J1 !ij񌌌R ճ0Om-!s'qđpLJ69naLX6J/ {~*x,qyyW(CX16qJiD 6g9^x>S}q%p}rFv䡒(&'cՆӄz贡LIY{2S;ÐHMMQlx'覅FD sk>"Vv?FIrJOHE+GvOb,j $ޤoDu:'+5c!x.۲h%i˗ ʼndxJ!5 WɩƇ#H,ҮN\V^Qԡ\˾K3M͜(/%LG3lzGtؠ I0+ 6km譛4*U#-+DgзT~X{6Y,aDX.(l)meE=].hlsm6=T3T%ʢ d VrɯČ(ڷtNEξ D[AO=꨿AP՘^I|5Ѧ 'K9枵2W'#y/3VQ%Bu5NnZNWuaMQ<ߥH)щqbOP( f͕k\d&J`kDDs,{Ozlg5r~2Xc OI,7ٕ]`@ECO}\WiӳoeEJa- Hag=}ޛï+o%NFg?']^nVpcSϤ^zy6,{(,JDxAJJ+ǹ"ZMak z~4]3O<#"wքwӟ}!X%e>?*i.򋪫8^6Ï%7"󕡛$͟Ώ?=Td6d34-уO~Z|T?xMl6IHA90c}::Gt`#7OK8wB883ڈ N@(y zwY]D^\h B~$q+D'"nΩV#g~I P9(& lĪZϹr(*GUFU_(].+eIRշ,nW9gTdfދ oVH3 (_y2w^)[i $_Q'hYFCSr7jR6sN "G`cirS Sǃbk[nEAA!3^^=g E˥ IX;síRɲIia9KgKZtp˧ms-83H 9\'+%`as)Ao,tX 7 S] &iЙ-TX4!9<{kkAu%H50\hhgk r>Sk0_t9Ţ~dq'D^(N"Cڥ2pH9>#:i2K!LPDa 7R{~ waUTWik ڲ 즶R{Iϱյ/ e_IQՊ߼U{b+ϦD_o@Ռ"L;lHC=C5x_ZKVpjť};վ.st}6 ,Qs>K冱?`}q:fb.Af.d!ʆFtr[\$0 E =,-s^X!'4賶DŽ=!VU򢄉5xH&:Usd1'q|+ᢚP:na ΁&!OXhX,* qWx|%tiR:='X!qSI*k٢ȉ+xy 3z}ziع'H 5t?&,M/w' =K jpi9D'Ws38ʓsc~ݷoP ڦ]tTT\>o WI/I,ѳ,\ݩX{%} u=9?LDTᦃ3JX<(v0;Xsxx qxs >F$R{ةݵ@)/!6Nr%g#BRHB{nvd&[ΐUJ~㯶,񝧙ۜˀsnX)|q$pZ99qiڨ,>!۽W{[/{xo^Z E&lM5NEBQ vbQ5E)(*9AjPW[ rxA=bBx.w?1I!\"G#_7Pm/1RA? QnBe,Ɯ謭.cu gSˁ12 fA%?8"VsF8%٭dS\F8/**i<"6<3m@A5>K߀^hߊ[ͼPR;֢[9PTM|Ԋhx߉19|H~hDDT !&d7Qtt9UCT K9%5;wXa51J,-eNk\0:Ex- "ѴLIꑰ@}\_$ 6lQ*;s#V2NkZ7.޻KD :S?vz> `+ /]cT%~f뽁8wM²zp)ӄ`<^A3t`{OuC]\Ni$6 )砬k^|Xa?$1Uk !2(?fJXٛ&c$0+Eta>=,UQ%TbVŸ%puݚܢG1(αOxeJb7Chy8ok0rCsyP8ʠ|ػ˷(d]Z s˫A(<^[1WE׋ʤDQQUm4U Րz!TžzqQ3AY!W'ߴɋ.5yaE՛]`:o6*Y UO琺Wf)xዾ5f5#(&Rô*Iv! + 9BÇ8l_y9(g4 Ft / R9O\-|=*!d(295b# &kI86&}?LgI~\oGZ] h ccI^>s Jg<NL0qaNP톱 /Eb_NמR4Pb^g֋~ꉎǐB I=0*;3qmJx6nxzwwD6f>99ؙx(qe*WAIeY4_ԶaK"v+dNn;qwHufur:m{ 6(LYpʼ+Og\ יnbuA ?@8']7Pht9.G=ܚ6`I^3H`BuhLTbZ ށ*͍T#fv4թ,e~yV" V/g~hbIo|P.QqJ!@WA`4Vq Hek]%%E4;s .̎xYt['_ӭєZ1 ^ TTXq ~Ul,/Oj=*23_6cigN|}V|AU_1Md}$'Vv,{=Ή͟A='cVriד"# ,9Ϸ7P[ =0qϷ s{E1VV9B:<<տ!]*2WnL'YTnQ6175RzAGn5 Sҥ/ >9Ij4L/!ZF7cB7Ew|>> fQ/8`z32QYII/{9*n$طiUM-3=S&BU\RxSP|NZTe$O^nB]('5lq:VpkѲx5_X2DӠ*:V)֨OUT<]eZj3ߖ7:7L;` Z[@r0B0+T~;J ?iKvx%z N"-_Nb(6֜eV2BR(N䖥EЄ)&=/bj*u6/ii3ݘGFE؂+M5ʻJctG Ӓ걇CX^3YlŶ,B\m gK%'c"9 j5@rI LeG<}fcdoɛNun"bэ%4`:T$Vqz_qV(Xp{h%8$Yu N1ðy9!v:Sה'M5uL:fSqAoolY\I "r34e./s0$J[X3s2p: $ZQ1JtVd8f>E"j3 :[%EdbVY&ɿutѫt1X߉]@qr&s8בEO.HI=^T? 3} ʢR_?W.//K[Rdg&%g`FͻG9ݸGQ n)57mh.Gsɯi=}Ru<akNm-r w$o nׅ6 z\Psr%" ?*gg>rZY0Kd%鰓^[D 0vsF^_-T.>k!0Hnk1qNrRO[hԏxsyYPSf.}OD`5 TVHRa?AƓEt9^]:=a 6){,o%iOy)`!ˍq/4XA`2:LnkrCT=RzMBin@Į|?Mx /1!#dJMƵ*3wO=y sy&_hIwk jC_QۧsH,;`@6D:=$/Q{Ҏ .&pzvoj[E/9*ۅYo"ڙ>!TͺHRsh "5^VIPVÅs<"9e|>eo:op6u7ۿb8u`]i>|z;_ɍ.|t{|?ʥn_1t|MUatMLd_\14_HTTqE\Gf9 ď_I[gܪOtX[nW:j܍9~-`&0=JN9Q*T9&75vl ?xspc=-|ɛg6L F&jE ,EIUOdZQ?׃+x]Fג_UhJݒDi]"^L_ר\Y3\-,ǶGrOXđ~Na?w  fap4 x0|-.7;ם 6kH[Tߍ&A؆='bq͟Y,Žs.W x=~Saxݻg!?Wם\=w/z參 ql$؃`?>` o9t̠Gvo׊~1xD\`,)ӯr˃{K*Ӄ6ǜ ܮ;+'d܋.*¦XxpnߡEp^w _ڮ_8W 8UxAw97.UfwWO-|Yw~N/.|,E-;EQ|bt0U Ǻo;plM;n{WS5e?j\Z3ÈlAw*W޹V89۽p5{Et (){iK |™Q\ߧ5ȾS8]jt0H"iX04庴[8;t·sN^~,/)g/9ޥZ [˙L[Q\r5{iNy²_P){+ {%_{]0w y: {m@tUՠ.3bP!tJO`|e{k/xz< `|Uw$ۻnRBN0bayEn+ >8Ҝq)+eN:֊2 3!N8OC7l3y Õ|l+qqW~UÉYnq 2VV-Zؓw6$>U~x*C6pr24Sk~ vF>=ɽ0)VTklYɱ;wE'bڧOLteOZMqBP޷@ )Oؑ,!w%_ CYR/b%;WHZVͅ޻/ĩ$kLLµE|C&L-G"gR&-ڇQ?%֠3@Sڮ:qU(:ntΤtQ|PVV_~ 5-F AH'h\ڐx x4^ʑt:Hhap/{a!ޒͰml,uz:<V<hBcbҜ Ͷ~Q6 @o쳶b^ok2mzeUirya74qkWV{*SY \Y?Юذjx-$Z7E5?Fbd=6.7`,Ө['ո5aV(XV4Sbu:5c-_(%'6/hːez5m`qFE,YreƢhN",?hsr& ΍iZ-}o%jaIq!fuΟ (ǠLSH7Y_d oyIFtdEdžO2BJG7vjy+p8t>籽kp~o[ib:Y2{_$]~Zd\Vte;V-caU O9<ܧ^M1IHgeX5娶:0M@F1)UѕMSr۝br290䰘{l_:C)"yK|ʲCa z5 d< uQaƧ#u(6l:S?UX%Hu;=:4!o^-瘦mO*5G ̗ F' ^?D]CK&Y h-IxW-rVZgW9=WsӴHDM" +3ǯBbj33lg6BAدw'`pԙTTYBFj =F\Nym eSB3J5R%3jDpZ5Hr$jֲ~q&3z}&ga{8FWߋhHFS۷aj3\IZQ19o.yfB“DKSd8(QNm` ذ"!-- gmu!H =->bZֱ w؞\q$hkE#MJ+5wK_L02$sV. gi4%]d%[Suh$gk_p.kifVcpJ͛gyox9  \=+uB!s| ,dwH$U+C!L@xeU>.TNa 0vdﭻ CME4C=n+vl_INgք=نFV[1&љN ߰rבTғ4 RG.2}8i0¤ћ(߉t,S Ke. . siG[P/!`Rh"-+QI&{䃕N]]?bllC10 :cg:a,+!RλdPqxIط2b.}b%,cVTyn{g!~o.XSi#19B9'W.r#܌=bW`B쯆,ڒ\1.;Z4+UM18zPVۧ5VIfw#pv=^+gĖj; Xర$*VsHO!%*]tMd4 >]o@ɅsQ<9Mh8L,J2UdQx R$!Uy_Q6C\ǣ8):.S_Jj:/'ev:G8CE@/溂C@>1(2^BdDSV-OU~sޔ9dE9z.0*re ֮\n룹DV + ԱH`HE`JJpY1& e= zo׀l8+ei^uYch{@;pI?R7Ԉ5 iiyblX,vuZj(l ,/nyF_Y.5T̗F(BE_~;.wjTl;t ^vyN_$!{PU<|]:E9 /pw9&}MN9eJ d_'&T}8м;a$d5슨ʪTa@I}N\b Y:́$_*bD5-ɮ:|? !굅_^i.cq+2Om w]Xp1WbּRҷJPYs_Zw< V_33\tFjjV74I!"7 |ƫ&@Ij$f4B_ZXZܥ#gzXJ[J(t^$ܦ_ufm&JӃ9UZ ?O|aдo< B")6ɪm_>ך~nfƔ;ctp9{0xAL(\PN߈`5k! zYۼ?S̸>6RӢw M͐͵Cf/k#pYgJ몌rQ 6Jk|zչpδ{›"pҙ1!Yݚj( T[.Z\[lU!a~:IQ3M&->U" [TJ̍ssc,O<$ \\b !L0Nc$=(Cmam0^t`^G~ɈEW[2@gd[{L'r9U޲+=g40Wߊ^?L..Q E>|뜥)ov^o=k~c_֫헿ͩ qŤx~w07pRQYg|(j6"/?I QjNX`TE:Vp&fYP̨?/(^\Zg|]KcI13G?($BQ%_|1(0TdNNR)L1*)5Cn1g,[J5\PE!/aev}λLvVm):7t˸Fo]`"m۠I£7m͍ZŒ%e(q1Q g=WuTYqvNE'cnʂDk눘"T GxE^Cӝ9)Œ5; |;s)OX htw3JCj) _x! ;!TX+.E$+?Y$l|ƪVlK[DK$}Pw, uzt-hkk+ћ8/{P#" Bx4}=/Umt`(wAKe!rdDOMkqXyW\*rKd(º\0k٤@1P7Lʚų%Hhjn7rk2b:VLXyZ㼳Y/ϙ:Zy,*'GwaVd!+..ŖI$mmI--^2pqf ǶLO?t^bθLc򹡇6 8,.0=Jut?_9!sFs=Bd\K4vX %)5{jYudX}e΃Z< &wZj3c{<"I>4C?1~Ya7.뇔eUyikɘ6n9r.PT-m{PLa<)lҫǧ/YF+O]uuc#ko =iE2e>7|г xAB  sܧ)>T K! ːFVDEDcӋNqAv&s`s}DzHzDz)mjiQb(jE#n@j= va JW2M3逃+M7MhIy'B*OI `t@^t'?n&%mRp A/d*GY3C#;'(XD=I ) T1I7Zw'q^UzɉF\I# ~rȱި1z4K; ٸDE`Ɯ6z=6BN"LeoQN7Ŀ2M|ـ%Px;1y_ziYrQ7]TzuO./ZRZˍuv!9o6NʿWcN+E=W1JϚ;VviB f(Bt.qa>=6ߩ#oL4 Mjz^V @{=Q9*p:;EНH xq֘):EciE!Dѩi) MQX2F("GQ"auu A)@B%栌AmFI/A, )5̂^7TqTvjK$\Bgd!;@D7 ua= L8.t %04_6U`$} (ɀ_ȫ}Jpp*X 5g,{wO .M{50͝{vOX*E#AoOrՆ-]ozѶ+5E+,La7G@E(YVf8ޘƒu8ּ\4 ='y-Feylo޹r]\LBD/ idsɠʕ,tȬ0pWsֽȊ)tBi^V>@e6p+_89AQ.iUg)cWԑ[& z~#*̓"[^x!}۪G,YȏM\KS/|1]7 FKw(-ްl&_S .^}뭹%倵^e&IXXE: *q{aSo4)=Zh,a&<`0l w#Ϯ;w[GyXeH̝ \))@̙T7+ntZV)x^h6,du1RS6 ۬6d0;fhh]#hpQ.HZO+goZlgc0i4a\qdmT2{=rsG.Mb6 cDp΁[UCcYט=GO:EE=TْNrUaWsQ)B i%DjHW2hkdxqL?|RB%" +3.MS97G)(9'])ia =Ue\&+f3Y&9vm`nnn,k\1dpL[g1l{X-4s (> ;'-՛0$/y; P-%Jaͬ,GߜvNԠ;<MztߚNĒ&OM둿-nHKoIE7 vT~<xfDVsC0Tܶ1+$:+ oz>F"9ގini@F\8UR)Vd4 ѹAшŐҋQID ǡ 'F)~%Rs=e*иC s&G5ꆐԉ.xiz918iqVIG:4@͐l3tB᷸k a'\HG⌍T °\V;48B !F~ǧ~ :)Q1EQ!^oZmJՓ|A n̛anSB}DJ ;Ĥx6u4my"`mpBkP^;N3jTV1)AXy߹XЁH2-k6R^8YC3POF?#NyicU+)ϭܓ~Яc/Y?!OSV?|uEQ.It8ӻsGG&@$wm>(&!&їrq%a[Ovb.*:'FmvoV# ߈Hz i/$'&hBփ]iw~fෝmy9GR3@ͦJNE\_F@ ɶw஺qee5Uj3̨0@n 2u(HMc_N~#Zz) Z6z*M|%AfPt Vъ,Y/h(uqF/svV/h} 3 2y=u T_mї2, |<*]9EFUQ{i? Dַw7674v;byܙg_C'9Ā.s ~; Ǭ3#_ϖ(VQ?G|9q6K+qGO4}~ǣ#5u|OҒt\Wp8nRVB:+_w`FU\҅4 U&B5:?8i`,gITLZRsٗp|늃ɹI^W02J~xz],hbE7,]֩AU"R f.]"55X#V ێK,;Į:|̾adR0+jyWεV[V@}.3!Ll!+?d͆G0Jx.E^$ۛ:IBkf(…R@wݡ"qwwcWKX` jHNO9~|P}oR5hsʍڇ*Z}obZW;9OK)LS. R)1k5eWƉ=59W`6nG7b=["+65ЏC „W2+Z]dFg@3)2#!<@3FJe"mxf̓TɇC*5$gD~{kQVcQ, п}G_*פ7rPjWhd5+!_ etA-N:2̱ҽJrw$-y_)oUwzYrBMJA j2\_*wy$Ǩ0|;|veyX+ˬ?`FYA|njңwD59I|i[u4MQ0`xz/!+zI0Jq$@A VAhvAxe8XN8W C߂\o"+mGT`εFzcUV7Ƭeؖ]*3G⨻dZK)c]!nv>FOMfQqP1J@fQq䩊Zis埥F*Yz=8L"0O2HS،r|7; otÑ\rQ6A!4; |84/U;4mg5,L$ڰIr3٪f A W!8OX|>RDnC0Eg>,ivFP $<:@iw!V 5w+*M8oA7`#Z|Q.}UUFl9Gxf=9V? ٣pd-jq} V"dÒڟ+۵lCr0Dx.s(G4zم2\[ЗJ KF5 Qla, َ7L ¹W)7yYr!s,biE Z|*;택֫Vp<c8KnGuzP$z}1[btŎn2(3bzQrz=kEւA<GvOT@KmV"l]遍'S;.>i,zWhIv&B/gFuH T5;A='U&S«W;hʱ#JZ rhLa\[aBخKS>jdAB#LbӨ;m4$ m`TŠ$g7}_8$h0fAKbwrkYГS]n.A,>Qs_T3H͌Ӂc˺O&8nF7aj$Ĭ5o(YSesl~ k>M NL0_=h\fk,zR Mjm_]kq(\`ӱl;TvD@"xOn8 t6~fó!_a,Y5gDhbP=Ʋ#U⭎fO*j^#e_a[CH9J;0׵IYSRV܄KDr*GNPdWϚ #]2mbyqKKC;dVc!H[2iiH魼8Ov!A&ѝ.Ϻjb˻u($:Utk٢: ۜaOaP'_BLE |YMt3, qpF$L\q~鰔1ctuuMʜ'pQ&Ra*n ߒ'V-f|"I׍Vqj׋ :K X8{ܘ|WAMXܡyKލt}a^ yll4 x_\!r뱿9޻ .K%\J,0fc\a쩭Ž;sz=!<*$rᧈc|>(s襧mʠ(I(/HX8)w**&^Id$ 9rG:+6`Y$vYL⧌bḛ*/64r9ܞ_@js155' k2W;:;ODJ@O~,Ơ0nTYQ^oqW 8Y/n*Z3jLuV da/[OAx댎]G+l^N͢jF5CG'*!:ﲲ\}R9hv T殧r?El;xgwՓeQ m&{z3(QE@K Q;_XXD;A|Du`X?eig fZX& -Z6T1Wrc4:Dni^ͯKM4jgtfK10oI,^>mP΄9<IPktͲƕ۳ @nY1-8cQL WM4js.d6E[VթbJX*4FjK \"d af٠r,%04 8-.QQ$"PR)lpq ;<"H' PuswcƸe3^LfXWv Y, 7TdJlQNnD]m GR9Pp~ 5s8bś70ӓIى;n9G cL#p^t הu0Sh*0!L<ڄ|k}x:%o]R]=h+"nyblpˇ{O׍h} t9"zQ .WMoq 2֛_o=~Wƃ!ކ.s6J=yOѹLQ*XN3x*Qz)gr, pɵ1)_a8$ׅ.'nG UAS-qx {E~ȣ_z';ݚwC;ز|'bblmVEHQQi