* @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')) { // The following lines are for extensions which include message_parser.php // while $phpbb_root_path and $phpEx are out of the script scope // which may lead to the 'Undefined variable' and 'failed to open stream' errors if (!isset($phpbb_root_path)) { global $phpbb_root_path; } if (!isset($phpEx)) { global $phpEx; } 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)) { if (is_callable($replacement)) { $this->message = preg_replace_callback($regexp, $replacement, $this->message); } else { $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; $bbcode_class = $this; // 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. // // To perform custom validation in extension, use $this->validate_bbcode_by_extension() // method which accepts variable number of parameters $this->bbcodes = array( 'code' => array('bbcode_id' => 8, 'regexp' => array('#\[code(?:=([a-z]+))?\](.+\[/code\])#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_code($match[1], $match[2]); } )), 'quote' => array('bbcode_id' => 0, 'regexp' => array('#\[quote(?:="(.*?)")?\](.+)\[/quote\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_quote($match[0]); } )), 'attachment' => array('bbcode_id' => 12, 'regexp' => array('#\[attachment=([0-9]+)\](.*?)\[/attachment\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_attachment($match[1], $match[2]); } )), 'b' => array('bbcode_id' => 1, 'regexp' => array('#\[b\](.*?)\[/b\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_strong($match[1]); } )), 'i' => array('bbcode_id' => 2, 'regexp' => array('#\[i\](.*?)\[/i\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_italic($match[1]); } )), 'url' => array('bbcode_id' => 3, 'regexp' => array('#\[url(=(.*))?\](?(1)((?s).*(?-s))|(.*))\[/url\]#uiU' => function ($match) use($bbcode_class) { return $bbcode_class->validate_url($match[2], ($match[3]) ? $match[3] : $match[4]); } )), 'img' => array('bbcode_id' => 4, 'regexp' => array('#\[img\](.*)\[/img\]#uiU' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_img($match[1]); } )), 'size' => array('bbcode_id' => 5, 'regexp' => array('#\[size=([\-\+]?\d+)\](.*?)\[/size\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_size($match[1], $match[2]); } )), 'color' => array('bbcode_id' => 6, 'regexp' => array('!\[color=(#[0-9a-f]{3}|#[0-9a-f]{6}|[a-z\-]+)\](.*?)\[/color\]!uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_color($match[1], $match[2]); } )), 'u' => array('bbcode_id' => 7, 'regexp' => array('#\[u\](.*?)\[/u\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_underline($match[1]); } )), 'list' => array('bbcode_id' => 9, 'regexp' => array('#\[list(?:=(?:[a-z0-9]|disc|circle|square))?].*\[/list]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_parse_list($match[0]); } )), 'email' => array('bbcode_id' => 10, 'regexp' => array('#\[email=?(.*?)?\](.*?)\[/email\]#uis' => function ($match) use($bbcode_class) { return $bbcode_class->validate_email($match[1], $match[2]); } )), 'flash' => array('bbcode_id' => 11, 'regexp' => array('#\[flash=([0-9]+),([0-9]+)\](.*?)\[/flash\]#ui' => function ($match) use($bbcode_class) { return $bbcode_class->bbcode_flash($match[1], $match[2], $match[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') . '$#iu', $in) && !preg_match('#^' . get_preg_expression('www_url') . '$#iu', $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']) { $imagesize = new \FastImageSize\FastImageSize(); $size_info = $imagesize->getImageSize(htmlspecialchars_decode($in)); if ($size_info === 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'] < $size_info['height']) { $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'] < $size_info['width']) { $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') . '$#iu', $in) && !preg_match('#^' . get_preg_expression('www_url') . '$#iu', $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) { $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_callback('#quote="(.*?)"\]#i', function ($match) { return 'quote="' . str_replace(array('[', ']', '\\\"'), array('[', ']', '\"'), $match[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']++; 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) { $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') . '$#iu', $url) || preg_match('#^' . get_preg_expression('www_url') . '$#iu', $url) || preg_match('#^' . preg_quote(generate_board_url(), '#') . get_preg_expression('relative_url') . '$#iu', $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; /** * 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, $user, $phpbb_dispatcher, $phpbb_container; $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(); } // 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; } } /** * This event can be used for additional message checks/cleanup before parsing * * @event core.message_parser_check_message * @var bool allow_bbcode Do we allow BBCodes * @var bool allow_magic_url Do we allow magic urls * @var bool allow_smilies Do we allow smilies * @var bool allow_img_bbcode Do we allow image BBCode * @var bool allow_flash_bbcode Do we allow flash BBCode * @var bool allow_quote_bbcode Do we allow quote BBCode * @var bool allow_url_bbcode Do we allow url BBCode * @var bool update_this_message Do we alter the parsed message * @var string mode Posting mode * @var string message The message text to parse * @var string bbcode_bitfield The bbcode_bitfield before parsing * @var string bbcode_uid The bbcode_uid before parsing * @var bool return Do we return after the event is triggered if $warn_msg is not empty * @var array warn_msg Array of the warning messages * @since 3.1.2-RC1 * @change 3.1.3-RC1 Added vars $bbcode_bitfield and $bbcode_uid */ $message = $this->message; $warn_msg = $this->warn_msg; $return = false; $bbcode_bitfield = $this->bbcode_bitfield; $bbcode_uid = $this->bbcode_uid; $vars = array( 'allow_bbcode', 'allow_magic_url', 'allow_smilies', 'allow_img_bbcode', 'allow_flash_bbcode', 'allow_quote_bbcode', 'allow_url_bbcode', 'update_this_message', 'mode', 'message', 'bbcode_bitfield', 'bbcode_uid', 'return', 'warn_msg', ); extract($phpbb_dispatcher->trigger_event('core.message_parser_check_message', compact($vars))); $this->message = $message; $this->warn_msg = $warn_msg; $this->bbcode_bitfield = $bbcode_bitfield; $this->bbcode_uid = $bbcode_uid; if ($return && !empty($this->warn_msg)) { return (!$update_this_message) ? $return_message : $this->warn_msg; } // Get the parser $parser = $phpbb_container->get('text_formatter.parser'); // Set the parser's options ($allow_bbcode) ? $parser->enable_bbcodes() : $parser->disable_bbcodes(); ($allow_magic_url) ? $parser->enable_magic_url() : $parser->disable_magic_url(); ($allow_smilies) ? $parser->enable_smilies() : $parser->disable_smilies(); ($allow_img_bbcode) ? $parser->enable_bbcode('img') : $parser->disable_bbcode('img'); ($allow_flash_bbcode) ? $parser->enable_bbcode('flash') : $parser->disable_bbcode('flash'); ($allow_quote_bbcode) ? $parser->enable_bbcode('quote') : $parser->disable_bbcode('quote'); ($allow_url_bbcode) ? $parser->enable_bbcode('url') : $parser->disable_bbcode('url'); // Set some config values $parser->set_vars(array( 'max_font_size' => $config['max_' . $this->mode . '_font_size'], 'max_img_height' => $config['max_' . $this->mode . '_img_height'], 'max_img_width' => $config['max_' . $this->mode . '_img_width'], 'max_smilies' => $config['max_' . $this->mode . '_smilies'], 'max_urls' => $config['max_' . $this->mode . '_urls'] )); // Parse this message $this->message = $parser->parse(htmlspecialchars_decode($this->message, ENT_QUOTES)); // Remove quotes that are nested too deep if ($config['max_quote_depth'] > 0) { $this->remove_nested_quotes($config['max_quote_depth']); } // Check for "empty" message. We do not check here for maximum length, because bbcode, smilies, etc. can add to the length. // The maximum length check happened before any parsings. if ($mode === 'post' && utf8_clean_string($this->message) === '') { $this->warn_msg[] = $user->lang['TOO_FEW_CHARS']; return (!$update_this_message) ? $return_message : $this->warn_msg; } // Remove quotes that are nested too deep if ($config['max_quote_depth'] > 0) { $this->message = $phpbb_container->get('text_formatter.utils')->remove_bbcode( $this->message, 'quote', $config['max_quote_depth'] ); } // Check for errors $errors = $parser->get_errors(); if ($errors) { foreach ($errors as $i => $args) { // Translate each error with $user->lang() $errors[$i] = call_user_func_array(array($user, 'lang'), $args); } $this->warn_msg = array_merge($this->warn_msg, $errors); return (!$update_this_message) ? $return_message : $this->warn_msg; } if (!$update_this_message) { unset($this->message); $this->message = $tmp_message; return $return_message; } $this->message_status = 'parsed'; return false; } /** * Formatting text for display */ function format_display($allow_bbcode, $allow_magic_url, $allow_smilies, $update_this_message = true) { global $phpbb_container, $phpbb_dispatcher; // If false, then the parsed message get returned but internal message not processed. if (!$update_this_message) { $tmp_message = $this->message; $return_message = &$this->message; } $text = $this->message; $uid = $this->bbcode_uid; /** * Event to modify the text before it is parsed * * @event core.modify_format_display_text_before * @var string text The message text to parse * @var string uid The bbcode uid * @var bool allow_bbcode Do we allow bbcodes * @var bool allow_magic_url Do we allow magic urls * @var bool allow_smilies Do we allow smilies * @var bool update_this_message Do we update the internal message * with the parsed result * @since 3.1.6-RC1 */ $vars = array('text', 'uid', 'allow_bbcode', 'allow_magic_url', 'allow_smilies', 'update_this_message'); extract($phpbb_dispatcher->trigger_event('core.modify_format_display_text_before', compact($vars))); $this->message = $text; $this->bbcode_uid = $uid; unset($text, $uid); // NOTE: message_status is unreliable for detecting unparsed text because some callers // change $this->message without resetting $this->message_status to 'plain' so we // inspect the message instead //if ($this->message_status == 'plain') if (!preg_match('/^<[rt][ >]/', $this->message)) { // Force updating message - of course. $this->parse($allow_bbcode, $allow_magic_url, $allow_smilies, $this->allow_img_bbcode, $this->allow_flash_bbcode, $this->allow_quote_bbcode, $this->allow_url_bbcode, true); } // There's a bug when previewing a topic with no poll, because the empty title of the poll // gets parsed but $this->message still ends up empty. This fixes it, until a proper fix is // devised if ($this->message === '') { $this->message = $phpbb_container->get('text_formatter.parser')->parse($this->message); } $this->message = $phpbb_container->get('text_formatter.renderer')->render($this->message); $text = $this->message; $uid = $this->bbcode_uid; /** * Event to modify the text after it is parsed * * @event core.modify_format_display_text_after * @var string text The message text to parse * @var string uid The bbcode uid * @var bool allow_bbcode Do we allow bbcodes * @var bool allow_magic_url Do we allow magic urls * @var bool allow_smilies Do we allow smilies * @var bool update_this_message Do we update the internal message * with the parsed result * @since 3.1.0-a3 */ $vars = array('text', 'uid', 'allow_bbcode', 'allow_magic_url', 'allow_smilies', 'update_this_message'); extract($phpbb_dispatcher->trigger_event('core.modify_format_display_text_after', compact($vars))); $this->message = $text; $this->bbcode_uid = $uid; if (!$update_this_message) { unset($this->message); $this->message = $tmp_message; return $return_message; } $this->message_status = 'display'; return false; } /** * Decode message to be placed back into form box */ function decode_message($custom_bbcode_uid = '', $update_this_message = true) { // If false, then the parsed message get returned but internal message not processed. if (!$update_this_message) { $tmp_message = $this->message; $return_message = &$this->message; } ($custom_bbcode_uid) ? decode_message($this->message, $custom_bbcode_uid) : decode_message($this->message, $this->bbcode_uid); if (!$update_this_message) { unset($this->message); $this->message = $tmp_message; return $return_message; } $this->message_status = 'plain'; return false; } /** * Replace magic urls of form http://xxx.xxx., www.xxx. and xxx@xxx.xxx. * Cuts down displayed size of link if over 50 chars, turns absolute links * into relative versions when the server/script path matches the link */ function magic_url($server_url) { // We use the global make_clickable function $this->message = make_clickable($this->message, $server_url); } /** * Parse Smilies */ function smilies($max_smilies = 0) { global $db, $user; static $match; static $replace; // See if the static arrays have already been filled on an earlier invocation if (!is_array($match)) { $match = $replace = array(); // NOTE: obtain_* function? chaching the table contents? // For now setting the ttl to 10 minutes switch ($db->get_sql_layer()) { case 'mssql': case 'mssql_odbc': case 'mssqlnative': $sql = 'SELECT * FROM ' . SMILIES_TABLE . ' ORDER BY LEN(code) DESC'; break; // LENGTH supported by MySQL, IBM DB2, Oracle and Access for sure... default: $sql = 'SELECT * FROM ' . SMILIES_TABLE . ' ORDER BY LENGTH(code) DESC'; break; } $result = $db->sql_query($sql, 600); while ($row = $db->sql_fetchrow($result)) { if (empty($row['code'])) { continue; } // (assertion) $match[] = preg_quote($row['code'], '#'); $replace[] = '' . $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; global $phpbb_container; $error = array(); $num_attachments = sizeof($this->attachment_data); $this->filename_data['filecomment'] = $request->variable('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 = $request->variable('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)) { /** @var \phpbb\attachment\manager $attachment_manager */ $attachment_manager = $phpbb_container->get('attachment.manager'); $filedata = $attachment_manager->upload($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_callback('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#', function ($match) { return '[attachment='.($match[1] + 1).']' . $match[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->variable('delete_file', array(0 => 0))); $index = (!empty($index)) ? $index[0] : false; if ($index !== false && !empty($this->attachment_data[$index])) { /** @var \phpbb\attachment\manager $attachment_manager */ $attachment_manager = $phpbb_container->get('attachment.manager'); // 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) { $attachment_manager->unlink($row['physical_filename'], 'file'); if ($row['thumbnail']) { $attachment_manager->unlink($row['physical_filename'], 'thumbnail'); } $db->sql_query('DELETE FROM ' . ATTACHMENTS_TABLE . ' WHERE attach_id = ' . (int) $this->attachment_data[$index]['attach_id']); } } else { $attachment_manager->delete('attach', $this->attachment_data[$index]['attach_id']); } unset($this->attachment_data[$index]); $this->message = preg_replace_callback('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#', function ($match) use($index) { return ($match[1] == $index) ? '' : (($match[1] > $index) ? '[attachment=' . ($match[1] - 1) . ']' . $match[2] . '[/attachment]' : $match[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)) { /** @var \phpbb\attachment\manager $attachment_manager */ $attachment_manager = $phpbb_container->get('attachment.manager'); $filedata = $attachment_manager->upload($form_name, $forum_id, false, '', $is_message); $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_callback('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#', function ($match) { return '[attachment=' . ($match[1] + 1) . ']' . $match[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; global $request; $this->filename_data['filecomment'] = $request->variable('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 $user, $config; $poll_max_options = $poll['poll_max_options']; // Parse Poll Option text $tmp_message = $this->message; $poll['poll_options'] = explode("\n", trim($poll['poll_option_text'])); $poll['poll_options_size'] = sizeof($poll['poll_options']); foreach ($poll['poll_options'] as &$poll_option) { $this->message = $poll_option; $poll_option = $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'); } unset($poll_option); $poll['poll_option_text'] = implode("\n", $poll['poll_options']); // Parse Poll Title $this->message = $poll['poll_title']; 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']; } } 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']); $this->message = $tmp_message; } /** * Remove nested quotes at given depth in current parsed message * * @param integer $max_depth Depth limit * @return null */ public function remove_nested_quotes($max_depth) { global $phpbb_container; if (preg_match('#^<[rt][ >]#', $this->message)) { $this->message = $phpbb_container->get('text_formatter.utils')->remove_bbcode( $this->message, 'quote', $max_depth ); return; } // Capture all [quote] and [/quote] tags preg_match_all('(\\[/?quote(?:="(.*?)")?:' . $this->bbcode_uid . '\\])', $this->message, $matches, PREG_OFFSET_CAPTURE); // Iterate over the quote tags to mark the ranges that must be removed $depth = 0; $ranges = array(); $start_pos = 0; foreach ($matches[0] as $match) { if ($match[0][1] === '/') { --$depth; if ($depth == $max_depth) { $end_pos = $match[1] + strlen($match[0]); $length = $end_pos - $start_pos; $ranges[] = array($start_pos, $length); } } else { ++$depth; if ($depth == $max_depth + 1) { $start_pos = $match[1]; } } } foreach (array_reverse($ranges) as $range) { list($start_pos, $length) = $range; $this->message = substr_replace($this->message, '', $start_pos, $length); } } /** * 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; } /** * Function to perform custom bbcode validation by extensions * can be used in bbcode_init() to assign regexp replacement * Example: 'regexp' => array('#\[b\](.*?)\[/b\]#uise' => "\$this->validate_bbcode_by_extension('\$1')") * * Accepts variable number of parameters * * @return mixed Validation result */ public function validate_bbcode_by_extension() { global $phpbb_dispatcher; $return = false; $params_array = func_get_args(); /** * Event to validate bbcode with the custom validating methods * provided by extensions * * @event core.validate_bbcode_by_extension * @var array params_array Array with the function parameters * @var mixed return Validation result to return * * @since 3.1.5-RC1 */ $vars = array('params_array', 'return'); extract($phpbb_dispatcher->trigger_event('core.validate_bbcode_by_extension', compact($vars))); return $return; } } qwT #}6 $h 2 x ~%]`Yq)iG:D>h(N̸c"gK d9}{ӟq19uw'WS}ڝM$E2Rt37\?c('NMA3r$tJ_ϼ"q\Z5q:K'wzmHB=H<lNpS!r{:/z.p!|U˟N\I~.0bqUhIw' ȞSs:fk@Z{+P:!#UPy66;Z~~y.X^yfDX9~ROr30՘|3i۟7$o]3PmP8gM"RVIm)ԭ~o<.},-Z6>.p̠3> //@DN h) O#6٦-{]{Q(?־فߝZ" <{ci'?؞?_:;| MA^ot5UxPe4}2LleI+e5;zC5w/LC&bTgOSȣpƩ 7 6j(k1BXQ$bt# pm~O҃q*/(v#D W#&o|)MbC,!MuaWŜ@k?% 6KŅ-;>VQ, VB=@ ߡ=2|FVF3`&>жh)wt2h#Rj6+zgrq&>6"﷾iٗ.KuC VmBAܼ}"mcJvn*^+خn~;͍G~,%OSl/0}y7ҤT"GEq=N,!jR$ (8-Qs0ΆùΜQH9,6UURMkxۺJ l#,W9KաFdzX0 bɛWʨK#& OHOvA 98A|nft#=5p `8x3bЄ}QѺ@=S(>n%w׍e`^ɿXx01OFnlR`Bvxz?N7¯] `ȕ|C{ [UOibiHk4S҈y#ى\ou,3Ϲ5_T}4܏LICTXtT" 2Mʍrt0mh0P3;qYB2o'ylpAvəsfA):iTIcg A_D2?OC FD gN>>(NĻ4VlrFVL2jN>ң}Y kRkaEKj(E\JSJ<_7?}C(4lg+$:%Iq1ͩ0RHMАc*<ߘ^0._ @*_8y8x <%/{|HxuB@UWܛ; u0!l]Rlu`ok9O=OP3)z=_f2xb᱁{)_Dڽ>ev=A&=E 3Vgܩl}iq+F;V`*~/q5 \jZq.u<,`&iuKQލ Cz HzM#. kƹLG n)kءܸ7t ؔ묛 MT_a P2X lkWIN[$^wӔX(?8Ξ:>0T ;'jē6UUJy+<\.sx "b0"P,7ەUAVN&6MCUbFMkLeƫ-X(3W굂]RՆT"Ihp~UIN</N_)4C#Q/brNc0ft,\'Ar+#2!%]+tktڈvjQZ.5z: ,W6^8Sˁ|4~d a S?="Pn"~( s,LѐJ5 3 jF2O ‘;D.t[b r(xDFQ$^g7Ӵ@1-6Yx2X@s{H-=l1זΝ%.N*?ĝx>NR f7ӯfmAq0 dcf5~O0:q>sN j38P 7Huہn^PKsVo#GV ^ZccaJb~,1="(i}60KC~v@M] ۲ ( eE ie9\$䭳~ ΛP p&ܴ.t(/R'hu21CC$r~α|p/`Wqp5m~^PÛ%ꮋu#C33-=Jm6qCaW fyd, e0}p鯐gasy3DjI#}F'SSfA31! ױ"\~e/ԕ sDr,GImhrW4_R#`0;kbrܚsҽ4&}dt](6Ctb 4*iP-ǀųiV[`,` ~>yEG hU_>%_ tz% V;E?xm [2q* 9]evNȱ{\΄OAo'dXAX t"|q6pKuX7f%!ULjр= 3~Ay"ズ55%Ōi tʕۼ [ˋ:o̍i,Qej@3:p#SO;(C%1HL_x[}gReZtZngZ5>\Jrs^ tn "pOځ57. ho歹Y"p@Wm?5Wv`F\7;CW{c6%Q^C{u.>Lܚb۪`l isGW'/ꞴM1nX8`:B&'6 9Yu- e8gd l~6 RnK;gj:(P!Sw hx̓gù')|v NoD?B.?r)ŧ`~ pi6jCB(W$-!>qA"";PKRw*=FtYl[\=4HĤcƙ慵^9|5cwu;I E`])栛g| oLd7U%n[f\l` +qb @[.Q2l_x*jQ CE4FƴH=b,蜑nqAO?v7kUZqvĆ&=Z=RNIcZn\ 3z]&eç/ћAsz\"Ul36cSF[ _JKÚq 撲 =6 hQM+9uժc耩fUt݅1BilP?hzlɾ]kF6; )V =:E=W+2dI6/ݜɩiz"53|rY@9gw6+9e̼ =Ԛqӹ4鿰qsqBQB^u[*n Z['.Pgz[jλ=|[vd&DcjnGbJS[ׂ*rd+sY]LA^4VNMwocS1z snn<^5R\zyUfMdBW~rU0e2r҈s춆dX UƹSShZ4M7lB<{lںKc9ɡ͍y&:S2) ړ [W4'"O'D AU 77Т #v0H&5(:/S8dK2Ȟ}^/-!yo75-|/ZD"iH_?'v ^jqǓK(O/AjT7$8'VvtWlz"NiQX,.oW3ԒsXo衖?juaT(\$a3T۴#yHR 8-F?S2ҝ,A{ua- sGY)kZ$_P -LB`ϣd"Ĉa ݔq̽N3@rKx/Hx!$E+-iHƻXhs'tg2zyFgSsmVYmm BI+;c2/~kU`j#v b9:J]]votߤb\f_?6]pr[9Pp@S>L'G|Nq:\YUv[6v!YZEo Qү W gc7״SdL cPβ 3\ +:\ԬZMjy`V|K~ُk0\L72av žJPTɧI'F +! ڐ ;PPh}fr?a0?. b .%)SMe]vc1`9w5Mp2ӢF#ޘVNY{ɤhDC0⿵!G QΈ@NTw&^ݢ$LiѲ?7~*NK?>'*ު!@[~립!Wr䩽u$l73~X}j`״2Ȫ֝+Vyc!?!jd#xyN=NA>ƚt1SX"!^\ZFxfb"ՋNʔ*IQD1esZ' 76q^YȅT"ơJzfꕙ@|Ѩ|<53 /ZȵڟyEv$,A[i4ߠh!#@>!p Qa9NeHnTe h/(ȿqaO _Zx39F[*rA&n/Dz768vv SAdgd~]RTd,E:?}g)^VRD0DVvcte9"$V[7 U&"aWmg&g.^)F`ْ'uI{g -@Iq+F/ZQ5ܺlaIƠ=./u`##8cgPbךq}>\ʹuT9o#Qdan'B859Ԉ+)#`v⍇QX7urVani+`\6]{ 欘%XRsg :,@ry G?*%H seU?4Yw_lOڕy[,< }MpŞO{Bjy/ߓfׯjmZeN((Cni 0^7~T/ѺCoԚ2<*FC]ݴ*dSۙ1F|.Gf2 Mz@"~lwYoCsϞ#snZA{bUy"TF?%t&v<]6<fz'H86<7Bh^>8 溑2{B.%]s *7fWH|î[>9Q'a+ްM{6'#tnG!Y.uyN3W?K ޫ.-3V&{k})p@YWE 016:kW8v?A,q*1Ovp<ɪfQ ";Ti@/ ӕ0"׈ H D}8 ^420bq! E\_y{ʗ'#d`W)@>]>mڨ87ޏH?m g1_U{,cH >}}?ڣ?Jz5؛io2q ~| }k̑Xqd86@x?۝!Tb~-_!ͩr]yCJOxgȐ8t%>fQfvon'}Jr3kz&qL]#tA0}Ω,gDP+7I b+ĨlTUT~R*& ; OԔZoA#~<[JCkn:)7j*>P:+t.u%!]"PQ6I5u2/ gEh=$ X qEc,vƛ|V<72gAMw;]FLAJA ykMLJ&3/#v;ib3H;HNb 5=rJ}>Į.t:6Z >Uҋ_B(b>➜zca@oigԋڲ\ &TR|4R<I(0XGtjfeM,!.@dr"pHD|zHjq ne/!.<`˽_ NTlj.EnX*@o)6[R@)^r(9\C;^ Ŷ<6tk_ɩK-lp3Uoj]O"ᴛ.417s)EPWāW+ki)2#yD"e"az:z_|d@N;VhĮšy0btG@luzcJqhy))r"}#Ryq,3o)uЯ)1)<[rLׄe_ $}#mzHF*Uׂz(W` m&T5%J6^mrm?l$Y(%`C^Xn$m\>a6 g\,к}A" dIqt%Hz^hqK'W`\UbhcfvQz޾$3SEHErS챖/#1hmRv3Y uW*Fu0hǪH6Ţ |sWݓ4-lBwˠ61kjN}K`i=Ew5Vx^ #.YCpXi Io9 @-d֗"MmMٓ{mvV'qUtS dCJa.ZӋ/F,1Fr8Oy"|ǀ3"@g;]A1V9. R#8=gOȿ0#&[1{JHc.znio#aB\7B- MyGT. UIN.>{$g7,춪3:@/HKKjm1#b QGeըèxȑWu@G-N>+ <j1Kԇ 0:3C6JЦ_>A.)rd[a%Ypl?7Sy7V LbSCo]86.3OVrNV`(v+#7N@/ &?guzV/(240RT0:Jk#(WOH쀵_#^-c5^I޺|{^PG?+8TFʽ;051%Hi[~OE ڥU S8vR,o'#[.,s;t L R>cV)(tS˩'yT1Q\dZY CwlՌIyr,2_ɓ9Lj+1>cݳo塤Upj[ɭ6Z 6U 㖕1mм󓪄ƃ,i'cI4aԔG_obqa^lVV tyKvWdD~v:Aٺ!@:i-j44݌BB#O[Mx@LJG;\𒞠b}sJʴ D# a*/ D\B&IQ0khÈM͇eM-D/{Q>;?iF7dhލ x㔪8S|NqO%/5 V^%ԇ!Bv^  y*[1[B_ ]C_騮܁2։R﫧&)7,.ě׬ Xq. 8C*k@D +j 1)w(jA ~6,?ƣ]y_8-[j!"n9@6LZB61uD{vX^%e!3@8XY{n_i,9N(iEx(Y+][ W!`{E;Z+XV̭CI[G FyAY.Eв\dv浆4&&󜋗`uf]x P>pqOwm"8@bs&1zq.+aX$#dc148*L$u:ѳN++T9kl[Im$9t)D.)Xd@:$]`,?F+oWc7n$0@XY.R `(P8G>])pEw,51q@PF?jiƓnsnؐ3lOv3܉(ذ戜8HDf`͖%QRPGQ~!mlqc#찄QpDF'f-:uMv6I?vsBSӟ_5E1n($硎1 gxt 5q+j=` s#leb?@CDf˹&e%[2'u̝[H/3]FYR~8 r_xsA$ca=;~Q&>@XGSʵÄC0-w%/Kߥ1z 逕:}y p㫦?pG <S|& ݨ7#~y_n`2÷E2SdMpb8g*.tPw1I3]@TgY*|w^m KdP*1Aߜ 4AG&a?1L>W |U? e˵V ut2+O ƒCn1dig1+R?"[(E^qr"۵X5iG22c(2z7:_)86;0!_IemEѴ-@$O*~IQ].yT2LJ{Th*,LzƃiBN8Z=끫pC䪜A'P#="7z"(/yN,_۷'r_{<1LL6_of5ec^+ V]a@T)`Tw渋%"3]8C:-@E&5nέсL,V4r:G85W,;[z%˷f4Mcf_8ll7$vPkRS۔ ٦G R|2,fІx$}oKqX$ΈR˰A%`r0 ԨT(;7dG!O{e.W;յL_/e;5B9-=s !TRrS`m!Zˤ^Vϧ; 6D:ԡgXb" !cd$(t]Uj1ҹJsJFfWK$%ir̢cIHup^,Eធ|_̍Jod6h* bϻ;eD@5㶎)Yz!XOI&(ح$مH&tOMX.fFe$`8 jZo0.LOcX0[w(#ɕ(hy-ͼ̟ )I?6sN̐^1!FWsqn R5"DѺ199$6.Ct gA:ӿkxЕ|-~]'lvd+r'B;M@eL{f EZhblo%j$^Nd@W%>SB9x-bJ1\<^ŏv0萞7jp0~WeTCYK:^Jf|\4,t3,-hC.P@~[bF ǢѫnHnPؕZkФ:l0ACkQʹ]*;MkSDGt}WeV.c_]໗m(6M(7˻K{pg^l1}pEZFZ0D6-| fbe- \0fqBmZN5LCObM ׅM<EmшآGO堅21KJcN46 ԘPBHS_&&um'U1]bSSsO1TŲT:)o?jblmN9 b/ж=O韈=q# ЖuY'完EU4;ڷ&ۃӌ@ &8 *>]vq2&Va8¶{E!1МalN6yֵ`W;e4@M3pޢ/' D>] /8ϲK1ٽ^&f1 5o%aIDq*6B 8ØE堿?up$!`gumDuE,+ JAs1$$č gcWOcwU7GD_רiʷ2-uTQ@(QM it 2Y#B-#Dms#%N[HYލ3= &މF}bzmԸug6u+5'^5uWnHbo-YTԢlCb)zX\s-+[/ &5(hF[}Ijc{h,:~dvKc/Fs/EIQ9#6/ ,q3v]NfC{؞+(wt#֨1b@^4SPeS{wQ&w Yr[.O%?0|KŃ:AN65p'\1*6dPXs ۄ&XڰИkQ=0PPP~%?ts alEZz+pwi&ݼ[,', H\}=uVx$lxP 8zJǤulyF軈FgIV"Yus_qJ=N6 lն:),+JYӴK/hPI3GO+!],qznH@ ao\|B;(ch*ϑЊ]wF~9pX uX}FPmB {iv5:yauOi_c fb̹5mkbW _ղӊʨmwJ^kJ*3@n_*oo/f4Tz,OPc8*[JVefFxP[ hïwbTB&ylA86Qs1@8bL[KV};+ʦ~ qV{`#q~ Udfp|92@ J&@'J$bjS;I ^DҖC9ゝI>Ktĺ 7(0^|sd(?M4g7ު&GL^G*чW7Ie^q5tg4S߭)+xr`L!|28߮d(68rtsxm:bE̡ƂgGւ3ڌLZ$lV-A.ިXj+czcbo'3K9ۊIAˮ24q Co]bwS qup|(hlDZrrH9vTJlCt֕+< &GR/S}";-8NWU` |AF BXu.n@:ѠPCCX.F|W E7Ȥ "+ u=yi4?Y>O7Q9wr`] H[>AGU[0hխn 9Xe#YB>j#p-kJYEwFPߊzN-4CxNJEA*ia|] Kܵ4'v[*F2A9$Wd% K#6{\K'mpm0dPdhr6찳vz%ܧU|FfՑNNr($P;~$QqQPISuLۧNQ+}8 /Q$TG'ZOv@񐊚CrУ MWmYO[ot}^-IsxRPP1TW oܶh~[Oշ~}E.$^qyAmAI3uQQ 0,ᆸb,J6#l *,+og\+FJoH;;DT pèrX:|}=nz&7pzWruK R5Pz2 Z=}X0?oe Ɇ%;i=ġOo:~%SE .!Z IX $!^LY6rdOV޸OG}tUCz%6K)0;W*m2-fFԦNd9"BE-B.vX:(`O m]xٮAan<[Cԗ8h꥿lx;Dgb$ Mk _F1b? Cep@ @ zKWJ~| ^,zt?@ߥa ƹ7c".Nmyo ʃ]Gf+w,dj g;]yIdQXZ"ՅkRjp nB9+CdʖvA}{x!5.;zKx:5ieX_9xuOfx|I6@:c2jpzqV~ͣ7Qv+ԧaWyd: KYr*/,5`ΓQ$ISI.*#BNi{ q~|4`Jj.;~!~z*/0&Vhoj-mɚ"E/ŗ0-r5?+sM ^CZi  vTVWOgɂ΂{d [IqS\ S5`1| πck*@QL[p)zeyk>0&f""I0#ޝ D@*TO^m6@Ekʜ(s(=XEu4A#:gڌ8 TKji4cTBuä,g겱;C\l31\o܁ԢB-[pDosq9W^׽saK ֍q&'>kI#iQp6QoӍ_#ITZ'x7]Hd$*Si2z9wQ|kF녫m^r AH\wPE5j|PDǿ!-yg|i* lita\ :]HccK[(c-@Ep}ey151)L<M4Z,)-_nI<=BTIѹz#,9kw ^| z1tvWjί)$L{WٯX9g}}QKN&צKB@12{&"3NDa*pĘl:(j[=F{ ٯEωU2*o` A|HkTTVUzu0d7DlpRK )f7v $u8Ƙ^ce:4۳@xo#~/:Fuy&*S 9 v[ smx, 0Dx( )U]=jȉk5%YffJB|?bRBHw't*qͱ,OU>҃-rAs=gz*z'~:% uw);ƞ}4?J,Z}LyhAஹJHcE XNJlWx^< +T93H}o,aR``0[a|p[†$:Oi~0.tu>iUu`c? i" %N`qn P`iji5ϼy UOf_&|U~$ ' \ɜ!G'PbZ!x5Ba?)A6Rn=z9]dh. G؅|❛j(]r3+SlI<0ĆC~\տq0O|2ǥ!6>p':`'y6[OS,S{4|Ois00]^N@Β:so]0%7Igc0 p}ahICܣKL|x˚%WYcڬԲNlHۊeى";kY?`8Az*z-聉w}05u|1U E? "zHu`GGSLp`Rk?tQu6WeX.{@y-ojڠmT5н^3-c&bEyKqwGMfiT*Y}5cZV~۰y3X&~f)cIBw.Q^-7pm/-|L*vpF휗fE)ϬB-@; gD ybjh/5\ [!3[Xw4qi?+X{7m}J/Breה7cB3ep҈ <'6"7:?>,6 6詐܆3Pv~;mz,7 #Z;NB Itt[ cN e`Xgr<k"_E0,7 yd6+X$82?&/?"#?nOÝo'XN0kbnCV, )Eܥ e,*܃=동HdToAt,w' Uwȧ#6C3¨Qn7ZJ~P `] 74q3mWQ kүIa.L(G/w L$}KOS:۔U{7k^2a3 5Es&}*ND$T`W@A':p ޑ^>kb-PgɆYJGw% ˡ}FY0D7IL-HGGonE̔@lp}~}s*5_Um.H)b?ܽ Kp3B (>Q+.@|32!c|-,>ѧCzg݌RȮMʺ؈#;u]ݯP]uoR(B'|GB8 raA4@lL:U@Rʞk^Hv4iWhC/VCd(u"$AJ`a]cڎҗol lq/z\N0vK89#FL_e#y0[?Z*<Ǹvg)'ЇV>v+Qz_>13(ӱ᩻E-dW( x(k;5ĉ{?ӧf ȫTұ$T>XSu7Ta%3k<}$\FO64ijpygie1Qd KLƐs/3Lƽ_V @`Ĝ&pw#[zݡ-8^fYߟڣxYI34ekɔ8u/d< GDsHufU47X+ɓ4;(>٪;ik&KD)UA٠N=+AzY=(\9>Dz1`A ,yP 2s'nwTg]k"[?]ĕ*)qs9Wo+"((ݸxW<[#0 n}ޚMhJB7 3HEBc @V>ƯUE` R䆙Mr/.9cFڦ BdV76nGxHiJHz"ѵsp6XW-E}6F0/CDצѦIѐ@~N*ͮ[_~k{Ed(It6"U0rۀ[2izbRIEmjVW"(c&X,!i9;eH}n]ncMW,[.Xsm*٘qX ^ ]B~X[2d۰OW Mfۅ {s%3l-@sU_"AHd2ܡDw'GBfkvu̺#]/pN( `oٜ-n{Yc[QyN i*؊c񵸭rtǐY7^Tuh˫7~T[  n'XmEi+1q)Ӎw d_ڹzTHCшpD^fIM&'K-0U:ReGРg,wAg}ToG.Q$ OԪ$kS#VS A<O|Yb2/Jw(;H,7%ڑgJ;IeE`{F*k#(^/Lzi9&tǒUZSIW]ۻ}HV0{:[2Չ{_ 퐛|g)owD=NX Vz=F[]`tp 3)#C-f}8VRɦFKj4 loFW`#7OuF ˞ *0xgא}S6Wl-`xD?Nk@ qPeUDzZG6d.qpUԬDC.>,̀ռ*b5XBPOkM4;ęsVX (:TRo5 Y`[3`.-u3ʀ7n 2Cz+hS7a5aR"7v<(U D^u@` ()nW܎w"2Q+cr3Y8' [x҈g:l3Ͷr6)qm = F?XǘL?*:gR|9naT!qI_V6m 'EX r)!:Ǥ{UGlkK)H=f<-ZMQ(b94X޻[ֺ_h>!qZb0 ZFlT\/Dr& k 9oJiu0m1ٵ*[H 4nzS n) q|Զ#&H5 ܘE^I@[xLw gb1aV40'JuR](G5b9 m<{ؚށv#% t@ *;:iڑr ڵmHg|/(T7C=[0\*ߔ\8_m') 13L,#c ΠWZ S%f>v%߁z  gS7v+ϫQԌsOxI|0:IAsO.]|UP"xvކd_~Wj;Dqo&[0q* ^'{z!^RԔY"7$֣8 Nm:W9-DL[|>U^ E+@Khp qd~ rc!D2'Ӕtnj,~!A挒XJķ TGnm@miB AS9!8~>8xAjT<5JX^Эw&wl1aV!QR`Qzn"`ßBuc"u_ rx<%3.LՌpFsYNkA.iI= Y _53qA_U'./bx s?GS`wc̔ |2̀1X=ag|.N3uhn j{ʷQv:(h0Le|P RI'/?8|34~9&l h~wVFp0- 'ܯ- ]%G6cMftbɥǙ:dSEр(c%svpDsϧNr0/+ r6v؈>IW透.^UZ8JtkFI[cw:'yeB 7<%Ae$ʲRVM;A6X[6 ajCfngI4\<c P3XHd3EV_ @NUNJЩ,Z큀tjJ~⒉\xC6=*0X7+ iB LvaZ?bf:»9V}J_z^xQ TBt AOLK5ݨaQ|4z2/l6Nn< 6~kgF NP=M]#ͧϫg 5y'eIUF=4ј 4^TA4j9 &6w ΑH9 FG\_'!t&ϻ0[&b5FᶡT'oo)TȓI; ?f_)~u4)hW;{-Ȳ3jؗeϽl-֒v&5Pqx}'#\Lk%ݠp8O mkF֎Rs zG y.ih؜ ssUCH篙֢d-(S%i5O3&lse+'a's%ި.3M< So=%Yuc#Z U".Xu8]RVK)6m҄8x7_%tC]x4%?'DJ{k S7m~kP^B CE,¸6W]4dF5.~͠iM`P7W3D:%"FΤh `qE7zת] ?S\VoA;ACT[_q1zrveI2VP\9zٌݨg!UH|;K8ŝA'}FPІZ3!þfzD$"9>vΡNaDZB8EÛIA bW0+kV i\9we`%uYg3hRqׯ \) C^1= KͫgZX]B:;ZUBb)EFo#G jc+*rrki#{<+ljí$EXN̅Lc~NKR/0pҡ=4u8="%׫FkGXyy5Sd4OuO;D}50[lR?/ir#q2b"` vY,I󘇤Uwj 8=X=p E:HQ 2{)$$LߖZoz#0mIjkN6?1"7@ |ޟZ;AJBo z&O䆣˚HV֨l53ʫfq|o-/ngRv/fv07r,#IqyIކLG:B wReU(u\ccaj}Tnųp2w$8?֕8/a/6^|L5աF$W0k`Ƃ\J2W}_wWa"t\Gz͑< m`}R@yO^{ u`pXq O T@g*;W_z8q3ET2e|fcw/ pA\S'ƯlU[fu 0]~ C\[)h$IO yP]DGAuR+)Dզod[3X&/}pȜVɊu4,uGەoY\Rb_Ps??O(;GNXҍL1`yѦ%ZZўo~j4WeA!;`O #Mtx%WgE7jNȧ 0N ރ?p{d; tQ@Qbh0O#>cqGB~VIn~aA: Ѧ$Lڟl]U|h;M-8*!ڏ=rN@\xwVza SI\!>`s .(Ua+(?xU4j1x`<b5He?}g*gy;L5W(ՉE?b*5ۺN1"\ĘcX|gL< AA[c,f^Dtzo#>$|+0VBZj8G8]5b(^(Ѓ/lGn/x:G2K졎0w+NX 9Zj q'8#YӃW7#IE腶U &>RQ7!odt+RVÏn Rv%*'tÖPS^}U%9ʂ^֒yOmWhKjJ(:+촖]"0 EMlwJ'?^?¼̫8cW~6dJfVMQlNxğcotRi[ D)*kS96#U*&Pk i.$-gip8 ]Ul<S0197A'/! y@:O>Bb- @0 #уϢNM:>̝ō ;UɧXfHRFG۰7VO;I|qٯO#_hLgcۥ# HtUWEIB>ot@Qv -:"h7vh!;r]{%+uDϒKIKw i&|ߪ+qmqT(Rؽo6JGwKA+Vb?`@;#]ApC"Gd.(6ڪ6w;6ByvX+D3o q(^j{x+(FVKn-3bjyXsA)Ӹ*bLOCc3H~ K&UgΪg[L_x6c0.L~-_U) D2zOx\Y$boBZx8A0䆑 D#awUTzeqI|-.N7r\RjnL1İI⺿2c>6*e,/D5"Ft8(=UɞW|؛qdF<f ci=GzL6I(]tAW9>WQXܯs|nX]5+,Mn] 7Ȗo !kFhS{rd`^39XAfА;[<\q@28Pܑbcq:|Y`A GvZ6N^X5 5| ^̼WXǦVƓY{=- qa"BrR^n^zM'̝ly: q d7K0gTLt {D7;щ*`Hw/VZو״Y ۡא}mQ1̒r)&5ԝ#dh50Lk6E2;ӺtPj{CAHѨׄ?ާs#L"P9`@]Vhq4gD(?)kݖ̳Oquj[d:krPp*HV\ߍh9FR?9khLf\9[2nư3L|ODŽ=l"S"dn]Zk/m(${rH2TM u?K Bp#I ?/pKwn^goԥ{&$}JL K+4Y"a+74݂^#OLgpR ~5rr |&dLhy|ܧa1`YSPx0hqRwFl?3`ltBjTNeaqg~ }R+Fk.;NU-)j|HOW*DC>r`U'I]L%yL ~+xfB@hZR1l]O5 Y.Q#K1x\܄+bJn=w}4*i_B= 5>MByq!xfNFd~5e'ܓ0fQnrwlR%*hy' rs%x<4}WzŢQa)sĀt^L/7#UgVSs}yɥS]kۑ]9m,$"߬G8r5/~H4<8uW'5 R˘t'nJyی{MCxNJV!VXN2ϡ(}v$e"vPv][o'C6N)2 Q$=tx2v>*IGGˆ3oC^LDsuNBzrnpW~`Y?LK=U&cƊOW0?ЩU%4}I4>m*Y x尹fx['jwCfrh+)6e/P` ]4)p1aRpE(2/(e2h1=&rH(xݤ"ˤ.>*gR)S*0|,yW*}>Ez5 <+4/ D M1/h棫EK7 IĽߟja3u aӧno`^GS_%†PeWV Z;Y|NeI-T8UTΛ\<9Gַ/FKQbc"o%蹫aBn+V`$ljQE5,J<3:>8h 3nwlO86%lY {aUP/ iuLO%-KFjaqaR'CJ[BxJ]ug=#A Dg;8#Krq uXyD<81A-^ǵS\Ň&)'fN|mHSFT~M?1a6Yi7!k ͍ᩙ,2Vڨ ːMIuR6ʏNO%cv'*Dp@kL5`s4`_V,.{g\7/݃Y'V>Ҵ&kcn an|`2~kۇCoׂ4 R1qe|0U+)ZBDX}s Z?{81cbe xIf:AZm-P䟼ճ88_xTtM :,M#eq8&/e!ť| {ϜkIcyO@s=coGP q R+LnQe^Zz܉p1y9L1e^nHFGH؜ͺk5!5'23ųBH|0Bj>Bk""zV[SO{!{-hOȕoߍ utn ь'"Avo'kRdGx-Se ؕaa)D0bh`T5o<:+J\d pu7 _ڏT^IYLL2f]na^L%~>BT_E #$I}MC\h-HieSAB g{x@c_Bl>A%;+jVļ{F&M^" *V]B銊PRz}~ **H :.kl~>wtZڊLnAnܛ4kȮu{~Zzn1*. e-xXHoU{]9P@ê 5VuWKX?4M$ #@,kR7&%sc Zt8ZϤ% DZJҼrVh+E99*nD[L"0M*Y+*(a.O?Cs;MG6)URU#zEϑLV hX:[ OI&x8bV$͚m΁EZ1 w֛f&M/;$OM:?wgH3FE/SpQnx$ؚU O3 gWWhUqzl,W\ 녬d=7(c3\x(y uGRDvcB"*BA]{ȜuckJO)bDޑ)nRjJ K/BV%70^[ rzho88bLZ|[\kϟmJGO*ӮB#~tz4Nq5jpķ1G~[7gQf;./ )ͯmŽ}F=[<}pDp қw|%`gu=A_R[+eP0UgջNEqfo1iABH YvK쌄 {& ,I{?yobRZlqaJRa dzE}2a8hS%jm䳽躪sdMAdTEº ;Hwmu^#ZL_1f,lLYq£i&%7QC.a oژH!3 W~Үcf 5\0((t?5Hq8ݷqRzAOZ7˰qQm1XPe+tQѭHD/kKɧ,Fva#87 %CtD1 R9՝ͯ#>slS~R%D cXdg-0M?v edATxH9zpIɩ h<䏓St)|lPF\DcayGCsV.!X̛a7\G9hOYn%r&dmO]PMz ݊b _M'pv90MGL|o~(lWMXh"jgDYuX-NnvmZ}<]a~(gxjwC1#=ۺCxb td ޒ'o+);j(^Pmd@pik.U2/[eMO`4;&T)V+Ĕ TE m6wrT*xCa>1g+ 5-cj#G' p R;ib Zf3qM|'D@&ϞCLő4sSe$sQ[oq ̓j$ =x`w4+ n;IC |;T;nᑟ"' pA?܁\|q.UÖd#w?&/7Ją ԋU)H7!a$%ZɄ{(sS˲morIkxGs5BFRLW&쫔w^'7YElC]kE<NwBN8tk8|,81X-r  L7ce @L86vAVF1qcF tN_{=4V]*2uz+~ĘfEF`%P6sZ?L,[~'wfHpPl^> vy K,-8R2ˁٝJJWq -ϑ6ؗG Fre$=8 JyF#ƸCt4H/EI#1`$}ms!g3T9[ZzD94oJ3?B-𞗆4LcLa ggEHx$d䭦|n[g򼝒]$.ܞpҊ躽0&%·Sיb4}ZҪ#h΀uA'&W-5:i'r)-j@o)RD8_&z"֏GvYKCl1ƅ^@]L;:?ƃݘQ OɶYX2!" %[k{Ǐ:ͽ`bP!W 3gķt]Ra·@Ia).XPpp+,A_{8 }ABfJ,\"MT 3M91u>޵Me**|}Zs7*ϒ{k O˩w\G0do1~豏&*ƊJNMKBoIЃ*9\4V] y.SvU*s_߄(G>/RTЙ ޿A$Y,do2zzDӘ?}1B]X,͏s; uɷMJ3c*m%Ba;7?Ħ8 %%Y:sCK+0лq*SQq8qϖľZ$6<k,.jjIVU+qS[MC]Ҷ(BXcvʾn3I. %.a¡pxmȌyXf `b-O->V}(CTJ/ݮ=_U' N)/v{)70Pz<޶CD` Lo"gEg-RwOf(a_L+I5dLGOr{͡m%]s+^][A1 H#yFJt0\$9C]GʯDlDdL_yr4a* o#{ Mx0Wrޤ$ȵNYr/MMPR5xWC-ԭI$@Y a\uyۥcHCiA`e V8)U=S2"E) *0} @9ITm@E)&bGo(Oݘb"e>4Y] *])c RChny~&C'wW UCh(hŝ y{8B{ؽy1hGQw~pPMC֣TN$8GC$vhA0TX;?ɳ 6^%rҎss Q` .@vOw0 >q{$+q2Gd$>q~RWtTK'[*[qɽ,4UBfT*HAH6"F4GwD]ʓ8ݕjGeFNSͳ*-n*Tk,O^yöQ{5lĽkN['ޏ yR.}Ғ?AhEV:T/5$g1 YScKgT&N-gM1EX풎IacRdSk[<1oC_md%}84I+ٞXJwԠf(,f/Qj<+F#b[/Wx 9)@c@"QV#yt;xnsUt]HVmw`R RDj ۱MAlX>so_ e2э/VBOzD9L軬+I;ǁǗRt$U/HuA4 u"@Ֆ=qB4q'iVn}FԛHwu5,S0 =5{&lt̮҇rը) _ᑝծC6(RD麠PYčX0v'I֕pgnyѮ)dX"0,gϤqgv⽤4ƿ@[jM+ם8xftrX(SnH AU |d]>DfCJȥ;?ĸ0f،p;9J8Ix dJ-8Wܦm_0u"]$"zdrҦ=Zӫ]RfVF&죩UpdTZ61!"eڇrօ1rz gj17&Zs ~PIh-iW3++^iV"Htg,6*?yǯ3D;~-7$!d ?Prlu\߷BgDI@]LvM"3z.ԄKH^U,˪V;:c"om_ϘK#(`yC!R,(-|RS Ԇ`BKttCDӷmWk^aS6ס4ᄞ7]g5tw<"qȧ&jv9]8қEg .zlT_Ǧk6SlqO(<:m=}8G(my*`mQp:d$s:uNW@@4++yg9߿'-!%,S-s'c!I|e ?]0!Yƀ1KFqJj+psd3]([2.I2,K$&tֽS Hi%澥\ʱ]v%U6KMxIyhƟi W~CHʓݽ3 ~3O/xhT l14K7% gxy ·oɻŃdiphFm^) H=P?3RUؿ%~.-Co)k9f a~+v^C9uG IrϮ䮦3fn-g@Z|C0fy |*Іx_#怱aJk淋6CSTE*{Gha<_mo'd a2?^%z(Pᶒ~\fJv+ҌVedBz!0(gƵݿt~8( \&#u5x}F ſa;VV6âpLBݺA>{M/{`+7 ZY:Q`;lWGX36`ǫ*O[,%HFy WA}rqO>VE:X!z|C[ZO炤H?Kt6ߠm" y'I+&辪VϵCRq}0&E,vn5"eh({z`i\\#T?s=TK+;h5{ 3C3O„O # }0ʐ!1爛Tee 6$'m|a ST Lc}XVCB'LN23߬GϛhtO!_+LEmCeYR( WȞ)&NT3|%ʟ8Bq+{mD2[6q71MPsF[@3U-эNlĜwёSXt]py +WRx : ,gxqL:~ˁX4}<"s'՝F LQ+k> Tѳ,׭& ĀA"7O"iQv.BK^ϸX>Rs bCg Z5T3,* 7sM5I;rcTaL2"QWU} 6q9D,HBUiw1 u\9P- &ٺlE]!+px1`)e.Vq~9cv,-Ѝ1a#~AsKCܻs(ת ~IC]F 9NwB7$Yy0[3rle {C!oD]hVи"Ij\CbeGmWަeT7Au0g;O ]>WVR@Is'UgZb>9~^~RU P\17\[w:U FI^9rSWNz^&>i)s ~Th?L(gpc(Kk-WUYsebND *^d9"ǙTl]Y.VӁ3P?Wyd y$(]_^GNx-v Kw M(m ɹu:L$&ͯwg a LƠ3ل.!\}"lX8.ƹzWD#}Oo׌rnCX15NkIS-0&wn ݠznxlO=4b䪧4Kh&O. pãEz)ФH oJ8c-O }Z~ibN: YS[O7CjFQ DpErJI(Ytn'kJ].]$[+g c錉{Wg SM2dfHBL_)No'UY#ؿ- fIߣJ7.ChoTtGeo6[8"s 3>DeT]3onhXR @' y96Q޴bc_X"_anC UsIRh&[8Kʑrn˰.]Ha2/hN}Z{00 ܶЧ" X2_ },?]3i?ٓS-b -B̙]lݯ")LE0d]-rr={Ke26#q,Gn>;򛢣>f$5TIN %>7ӸRtS;v-%q vE b.-#%]U]-,_@=kO^CL⇩<=q}FLnVGP^ [ޖgb]MUQ Eiiטq'52>\YHlGN^lSJ syỦ :NOv̌3wg2ؾl2q?γ>3 N\_4Xn3(wnYxD {C).H`6ʰu~֙5v) *Un$ K1yw# @7@5ŽZ]Sҭ^,}ϴ("oh8c8Q),Gted֩f1efҕ{Al qojO!*Fc'24WQt; Qe7] ;M&O{8w'sPqHkM⃻9di-=ٿd '"+Y *<"֝;z4<}«-/_2d';ghMF"d I_b1.ȅԖ&RrkF-N͒Q-k-[2/Cl4sX\~Ҕ٢^De9ߛxVftj"TYj2pN }YM?不p %GҺt7ꥤ2B1S/>_ z+HZ[ 7G3LټQrE$LucȤ]bh/Rxo "g_l}1yvԽjGo3CsX@ ư C QHa!W-yc챩>)``GB!NO - z*{``DRΪrg?I пQr=&Q\ `r0ɿ. i,P<yr뿶Mt(6UGQ.Ņ#+sQ/k\@ ط) Q)BsR r/0 9p1'-TfZ{]L4g-Yd[BsIɭXFBZ/M5k'[q)o-w_!ΧzҞ=+ V 7 X=ϲ=K2%xT0"͡nYL'W7Axy1!pӴJ+dD-,,1se}z;x+`\Ӥ:6=t"U&!M,[֩#TUa=^ #V[S(oۅ*8REC K$038rItE9 ^W+W\ Xq@ !h/a[pl4ԃsfTw:i_ƓxiIn.>fZuzdlZ@Yg]_c YgRy%tB!JͰFxN_&=hL(ze{0:Z +F6,j.[Q0+NؓlFvGCMw&g;Qd]EdBwQd!r6ex(Cj*PٿYyۣS^֧U,̛cߏӑa +X}@>2ag*$L"E/ %^Cƚ{NG= 0Ǒv?mYLIH(wFx( Ebťʵ r *i[% >HLaL^cpk/XFPb|#dQ <̏"փps:\OsrW?a[hUps盖A%sYQa;JZO68 -\Zŗș05ǽ:^TϹ#iB Ot:C884-<~ Wc4 "_ud\4_0un)Ś7| ` S#>~,4[?dAsa<ΆUNn0x'ZDԼ̞ x{W36~ t**o"Y-gx:yy$.8P&#Q"6SQbt?v= 8yc\(8˾¼2!,XPzd<ϛxH a`3'hAfܡMiN@/%eTՊp}GEytvd~~kz4pzqxRmiYo65s9$;ӠNϹ}Om6 AJX8s9m̐6<ӷY&ͫZ#ʇGNݨ;ג= -w>R  1*Y #)۳[U7.sx!-UF :E n>4_N Xt ށ  KQ3U}Iҙy `,,MlpL<.2+]wղ.Û_>]KT'+,J m]oPr?rfO5?Ŕs5AlYpϽPzc#wޝ>duě2{G vLÿh5䲹^b?=u".Tc%`>'1$VlfΔ t L&@{[]ϳXq6\$*3ۑ9$u PwTgmSCa+^jZҰ^Rt(k%Mּ v81pYaZb& =;Ea* /kUb REڶ%4Gud"EZb K)ci;8$I\1fTgKMۚ*k7gr eaE33({yJ^WW|5pS0_>I0#J>yTIbmwje;ڪur8~P2DբyI* b V$k6@Wm7mTӒ(q5U<~jaFWqy_Ad EEՊTZtEuOSrW#^5=Rp?Q!H^r0iYV2Qyj 7z;LPmHǍxlaʴۘ˗~0[caKvim`y-NtD$1ٶPxm>TPۍR8>T ~H*z$1y'B3则byk3 M{7'l+ŁhbٻHB}d,Vn] Ag!C0"|wY.U~B'xRF*aNΦ+C!vEheNyIP[] l9K_|oKy'P in)r뒺xgc1-HxVuV Md8+n~Y7.E@X2btUMR!78J#@hfukAY1hJ<,v=eɍKǥ ^!oi xc yx9ӳ\̲wМ%7HSF"Ȁ[/2j45o}12i~5;.۸vx-X:٘x\?_oCgeY ' TKẌlXJeBkA”# шè֜czSclD;Me"o!O8sYhJ =¾?%(`-T4rb&qhׂ4-}5C焕Ptuن*9S@<ZSv(o_Y6^aacr 2$͸䪴*⒫>^H9CЯ-ԩOe߻ *-PoeѝyrXBq|k#+k bT //Iy?%5ȩz]qZ4+XWl,hfk?JzOtZ9AYr'_k*e^7mooSXFFG\bz6=ޒԗY'CaǰB#|2/2HROB߶)t `w@g=:xQj?* [FynKƍxrZ'buK* ݈CA JS[*=ض7 i;}6qWs# h"Bx-1s, 4|rX˼tt#iBW>۾l_]_xoƠMjpꁒb//rz.Gdq}{@fh96@ _#` [qi/obḂfb\5jWſ W4rYR>Vy[Xsxim&7~&bƒU]8̀[w v_$bDZx@A y*SR_@Ӛ@<8Iz(d̺cwz{~j)±lr8m=jxr?*,'\N;sЕ* 6>IWGDɱ/C :i&#ǵc9E'40@l[ cqsWt? c`yGU~/Got#[UDg'ՀNl?(6%]ܙ {*J[Q~D目V=#-<žmZO ?hc`6#.n74&\ ^!rCKpQ)\q%þv Vf=Ҧsc7@iWuدrvAW. p19LGG玘1'V,-GYf.!OEVtV|c_26(dhzTηqLD41Lb_uмg>Оq"݊Y4'^Dc;l(I#w1=R{nOx{xD3:,Hy':^M} A )<2q❙ҊM-R֓MN Ί R dN`l2 RZ0^a>*W*cjg$A X,]8 SH<7kmBadT۪~E,i\M7 oٗ2zkpǒtItZ78|)EgbjJ.I1RmeQv2]{X 8|fg0SkX8b9rd]-~)ݚzLF-FӓőYQwŒgq!hq !~|鶃ōlj((qױ ƶ/Q  +,?/Ɛ̀H{TD[ft04, nZKlQbPz*sSJ@P[pR͕=5沶i>EH>E,&3%uS<]չN"sY"3y?^!@]=U??k+|1HNڢHOfjKܘ1~ 1HÌMp^v KvA[tbE A{mPl=stn-xCfreϸ;>ƖkHQ<kҧX65"4ߘ zx(:f8UaW]]l"1rhe)SO1硝ϚN2K>uBChuWcEBjXqbSxOoul)C{7mَdyΙ0j٪4og 3ND2Օ5=)D7a]LiA>`4 qGdYI07w=*Ǡ8LF !+s21iL/+&B*fXe'hZmpZfo+W$@>YUa!wju7px-/a:rg1Bj0U]Jp;o-)*h;CKai3yHbiON't^֡p+6BQkFIC"*,A3A<eQn2otz*89e"e)Ӛ7&צ|x@Uu6hpQ,kpaZ]Aa.z@C/l -G(81߭ FR(<=[zu'lϗ#~}I}x&yE,H)4XG$\( 3NL|in$Al!8M3/riڡ0sl)R%LM7 YmcmgˤLmiInjƙظN}WU`QWrp!,1]CWf)Jׅ៓Aʡ^P/Kuc>tgª)> (sA/BhcC:YBƉ9迿#1˜oB3 }?(VnZl?e.oq$}`4>>X6=L*Wcnt<Fߨ ᣅ4%{5 <9ܮG 'z5{'aY#[|;ƐaPĖU}_PrEO'tvי&YfW3l&c(qNŽ[n~J:˓gSKB /Ir=v@4Ra(`\q >͝ ^D Q)Nx"6hPReF(DI;uk:k}R]a?@=A̤$lJ;oAȮml[~!JGA%{pSG{t~@}ajk'J!EO#04<)^8u$a#7qq}ǯ+Zِ 2.l^;eatw 1x@bS:zͰTh%)(bkѼO 'NjݦGr[ǻ,`Lw. -l9>m]%"Utܠi1{O7b?l|IxXO3OtA".vͺwAym7djE'gg{E"Ʉw2ϒi-;VҬ %.C9ꈮ !H}sbwx皕B~2$g7Mĭhv>+"rRKm^5g7F!\5^+ر6rm?OOJ77pWefpC:&=%Ȁ9}(*\u6 0>QrJXPd+b3K|(\gcSļ5 O)V D'VlJ4΢ğ:un4X3(Jll8e>**]HƓ5UdtD Feq; 1 ҽhjTOTش[K82>zMBPGlZD=6ˏg6.b=i (`iI/k%m͸Dj L;TGάByn}NK/um+!㘆]dE~KB؂<׮+` g{ZFG`aο7 8"j{ p=@`]٩/U1X{L&ԀXB0ם8XtĘ;LXÏhC?L8oFe^'DHn[ӸA{-<fwUH_*je{Y?(wH 42;Ws ߻ J t'Z8ax81[k9Iaۄ) GDd[MwUWg ҟd=4M2DŽ .ʣ@8o-UL9\r'\ YA^b{ȅ޷W#?Yil `\iɧ.d侤&Y8[?.ݥn-uLl4NZ'`,\1*75l4 u(H}xN!5NF X`|XhBU/г6CiGnԇϋ/lKyBqnm)•gw1x aj փJNcX`__C'?ibfؓ8*kcYH>FS[;McŠ8J^lZ[ %r1{r|XS↣m'#$Z] ČMA?T2#H(}W&= {(޸J|&s Pp5. 0 [4MzS*xG 8s@!_CZcJ*M0W_0}, 1Pd%8fYo#a}#gPjԌn΃,Q(6?;W娔rǭdk& >]lQe4) 4+.aFl<Ѷyc "]'=&+ybWvFtKȖ,N?vdA| {p7{Fi_m7-/A~R z9{ v%_Bcٰ3D;/Sx rF.{SQc͛c#B #l{SFGS_}IVw^yt^P,2mOQ%wKH#]? ec/_M\966 #?hOJZڦVƢUu*OD,ox_@VojqlP|4_xӛ@Q)5ys%D%|ҾEqo! i$FOgZ`7W cE\%'e4ű >'KNֺ1Jyg&v{ΏL:>\胩J}H; GЭsh")~*h9ɧ4X ^KDbq #7WB*dZQmj?R%vxcGwF%gaM[~HW|G;op`iJ"! Ur: k}義 p-7KnN D[͟JI _}w/V|l\vX9TpOr}_-xASfuekKWD hK?&2K_⚑F=,7, ed9M\MFܒ=f{<?bjEsfEd~\0aC.$Wr<'~x̀D%p*܈$$Q3@0@E*1S˴fj]R_k('`8\vem1Pc@6DXOpg >O)7/=Q OxdwktVZiS9r^Exߕ&?%J0"~NpRpQpL'of/u^=p c7vwb(kȝ_:/MB{VϖvY\Ι7}Uߗ H| ^I3/h1~qw)t;m v3p:$ ozT>c[Up }Wq"/N_jhr|MBIy&,xEfL!moVୡ2x˜y s^ͺ (Σ\IZ4Кȓj{Ldٞ bߨ<ج͌18Z4$[SӞ.g椫I&—#AblPIXA5\צKT9 {hُ$+^6jϢ@2PI/f"9Nfj=Q2\X 5]GRD~)-IpO+Ё<G'Ɏd$ks$cMGH؛e'uvY|$EekX'I6Rt-{R݆<e+#$-x}n/ Jpm[Dj I7[0;eMjÒN퍘L;7IFv9x _{O( #"JS?M5H/'{NA)q q#j{3H&K~O.J:rlwbqz~tq:wrl0u$ "izz(2= n㩻X-G+Y GC1| JxP8ifC7h*NhJf8rg7XU vJMiV" EcWrhɯ9"&bQ]{_=~әVXn]46MHU-͋!6Zp)| `!;4l4YwS17fY|7_"B_U)gGC )hJxJx)G_aEYJgD" C"〓 ڿV*^jlTnn * $Kǵ7kB9UIoJ53Ȭ>'U&3%E6%?J<3ǁc8uXҿDԓ4R K0-H( deR˴FT,)?JR>פ;u$&FNkZJWQ菻D tLJA-ҒPh99,~n0Ϥ@o|@y>t >ହ0L K^U?d ľ JL3,(x< >m.1_a՝?ɛ Y-LP^lZ2Q i震ŲFh8 S^06nCIGJ 3rxi%PVc Is 17ߘU$mԍK9~P!mO6]ϑ?^T<o䯗%C ۔αݎ專cZ0C`zfy_`ġs;Aㄸ<Y +MLfHJDԶwM܎?(^R[3ơYA>-]ro7פGb;-j';yB+tZO2UY<#cD2[ͧMoɶ Tŗt[k>]%upCR:"=<"M.ȨMmbrӖxր5/o=vڍgr5%~rרl0akcw_1g^زB.N j M7Ti$<OujJUh6SEs=ʭc9+,*vf?X/3lk爗[EqT' HF:!=kC4?2|/e\%e6ڠͧyD@^X}Cclڌb( Pؤ<y6 Ol2BǶ2Hs$%nQO8R' :c|EhC+ȈPu }CG6k2| ;WD)=&&ŏ#Gl9 twѢ5'-8L+TVP~_D5csm옴B/bi$DESBԟOqq_\;P7r:oڏeûo#7 y\)9OU"