* @license GNU General Public License, version 2 (GPL-2.0) * * For full copyright and license information, please see * the docs/CREDITS.txt file. * */ namespace phpbb; /** * phpbb_visibility * Handle fetching and setting the visibility for topics and posts */ class content_visibility { /** * Database object * @var \phpbb\db\driver\driver_interface */ protected $db; /** * User object * @var \phpbb\user */ protected $user; /** * Auth object * @var \phpbb\auth\auth */ protected $auth; /** * phpBB root path * @var string */ protected $phpbb_root_path; /** * PHP Extension * @var string */ protected $php_ext; /** * Constructor * * @param \phpbb\auth\auth $auth Auth object * @param \phpbb\db\driver\driver_interface $db Database object * @param \phpbb\user $user User object * @param string $phpbb_root_path Root path * @param string $php_ext PHP Extension * @param string $forums_table Forums table name * @param string $posts_table Posts table name * @param string $topics_table Topics table name * @param string $users_table Users table name */ public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_interface $db, \phpbb\user $user, $phpbb_root_path, $php_ext, $forums_table, $posts_table, $topics_table, $users_table) { $this->auth = $auth; $this->db = $db; $this->user = $user; $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $php_ext; $this->forums_table = $forums_table; $this->posts_table = $posts_table; $this->topics_table = $topics_table; $this->users_table = $users_table; } /** * Can the current logged-in user soft-delete posts? * * @param $forum_id int Forum ID whose permissions to check * @param $poster_id int Poster ID of the post in question * @param $post_locked bool Is the post locked? * @return bool */ public function can_soft_delete($forum_id, $poster_id, $post_locked) { if ($this->auth->acl_get('m_softdelete', $forum_id)) { return true; } else if ($this->auth->acl_get('f_softdelete', $forum_id) && $poster_id == $this->user->data['user_id'] && !$post_locked) { return true; } return false; } /** * Get the topics post count or the forums post/topic count based on permissions * * @param $mode string One of topic_posts, forum_posts or forum_topics * @param $data array Array with the topic/forum data to calculate from * @param $forum_id int The forum id is used for permission checks * @return int Number of posts/topics the user can see in the topic/forum */ public function get_count($mode, $data, $forum_id) { if (!$this->auth->acl_get('m_approve', $forum_id)) { return (int) $data[$mode . '_approved']; } return (int) $data[$mode . '_approved'] + (int) $data[$mode . '_unapproved'] + (int) $data[$mode . '_softdeleted']; } /** * Create topic/post visibility SQL for a given forum ID * * Note: Read permissions are not checked. * * @param $mode string Either "topic" or "post" * @param $forum_id int The forum id is used for permission checks * @param $table_alias string Table alias to prefix in SQL queries * @return string The appropriate combination SQL logic for topic/post_visibility */ public function get_visibility_sql($mode, $forum_id, $table_alias = '') { if ($this->auth->acl_get('m_approve', $forum_id)) { return '1 = 1'; } return $table_alias . $mode . '_visibility = ' . ITEM_APPROVED; } /** * Create topic/post visibility SQL for a set of forums * * Note: Read permissions are not checked. Forums without read permissions * should not be in $forum_ids * * @param $mode string Either "topic" or "post" * @param $forum_ids array Array of forum ids which the posts/topics are limited to * @param $table_alias string Table alias to prefix in SQL queries * @return string The appropriate combination SQL logic for topic/post_visibility */ public function get_forums_visibility_sql($mode, $forum_ids = array(), $table_alias = '') { $where_sql = '('; $approve_forums = array_intersect($forum_ids, array_keys($this->auth->acl_getf('m_approve', true))); if (sizeof($approve_forums)) { // Remove moderator forums from the rest $forum_ids = array_diff($forum_ids, $approve_forums); if (!sizeof($forum_ids)) { // The user can see all posts/topics in all specified forums return $this->db->sql_in_set($table_alias . 'forum_id', $approve_forums); } else { // Moderator can view all posts/topics in some forums $where_sql .= $this->db->sql_in_set($table_alias . 'forum_id', $approve_forums) . ' OR '; } } else { // The user is just a normal user return $table_alias . $mode . '_visibility = ' . ITEM_APPROVED . ' AND ' . $this->db->sql_in_set($table_alias . 'forum_id', $forum_ids, false, true); } $where_sql .= '(' . $table_alias . $mode . '_visibility = ' . ITEM_APPROVED . ' AND ' . $this->db->sql_in_set($table_alias . 'forum_id', $forum_ids) . '))'; return $where_sql; } /** * Create topic/post visibility SQL for all forums on the board * * Note: Read permissions are not checked. Forums without read permissions * should be in $exclude_forum_ids * * @param $mode string Either "topic" or "post" * @param $exclude_forum_ids array Array of forum ids which are excluded * @param $table_alias string Table alias to prefix in SQL queries * @return string The appropriate combination SQL logic for topic/post_visibility */ public function get_global_visibility_sql($mode, $exclude_forum_ids = array(), $table_alias = '') { $where_sqls = array(); $approve_forums = array_diff(array_keys($this->auth->acl_getf('m_approve', true)), $exclude_forum_ids); if (sizeof($exclude_forum_ids)) { $where_sqls[] = '(' . $this->db->sql_in_set($table_alias . 'forum_id', $exclude_forum_ids, true) . ' AND ' . $table_alias . $mode . '_visibility = ' . ITEM_APPROVED . ')'; } else { $where_sqls[] = $table_alias . $mode . '_visibility = ' . ITEM_APPROVED; } if (sizeof($approve_forums)) { $where_sqls[] = $this->db->sql_in_set($table_alias . 'forum_id', $approve_forums); return '(' . implode(' OR ', $where_sqls) . ')'; } // There is only one element, so we just return that one return $where_sqls[0]; } /** * Change visibility status of one post or all posts of a topic * * @param $visibility int Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE} * @param $post_id mixed Post ID or array of post IDs to act on, * if it is empty, all posts of topic_id will be modified * @param $topic_id int Topic where $post_id is found * @param $forum_id int Forum where $topic_id is found * @param $user_id int User performing the action * @param $time int Timestamp when the action is performed * @param $reason string Reason why the visibility was changed. * @param $is_starter bool Is this the first post of the topic changed? * @param $is_latest bool Is this the last post of the topic changed? * @param $limit_visibility mixed Limit updating per topic_id to a certain visibility * @param $limit_delete_time mixed Limit updating per topic_id to a certain deletion time * @return array Changed post data, empty array if an error occurred. */ public function set_post_visibility($visibility, $post_id, $topic_id, $forum_id, $user_id, $time, $reason, $is_starter, $is_latest, $limit_visibility = false, $limit_delete_time = false) { if (!in_array($visibility, array(ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE))) { return array(); } if ($post_id) { if (is_array($post_id)) { $where_sql = $this->db->sql_in_set('post_id', array_map('intval', $post_id)); } else { $where_sql = 'post_id = ' . (int) $post_id; } $where_sql .= ' AND topic_id = ' . (int) $topic_id; } else { $where_sql = 'topic_id = ' . (int) $topic_id; // Limit the posts to a certain visibility and deletion time // This allows us to only restore posts, that were approved // when the topic got soft deleted. So previous soft deleted // and unapproved posts are still soft deleted/unapproved if ($limit_visibility !== false) { $where_sql .= ' AND post_visibility = ' . (int) $limit_visibility; } if ($limit_delete_time !== false) { $where_sql .= ' AND post_delete_time = ' . (int) $limit_delete_time; } } $sql = 'SELECT poster_id, post_id, post_postcount, post_visibility FROM ' . $this->posts_table . ' WHERE ' . $where_sql; $result = $this->db->sql_query($sql); $post_ids = $poster_postcounts = $postcounts = $postcount_visibility = array(); while ($row = $this->db->sql_fetchrow($result)) { $post_ids[] = (int) $row['post_id']; if ($row['post_visibility'] != $visibility) { if ($row['post_postcount'] && !isset($poster_postcounts[(int) $row['poster_id']])) { $poster_postcounts[(int) $row['poster_id']] = 1; } else if ($row['post_postcount']) { $poster_postcounts[(int) $row['poster_id']]++; } if (!isset($postcount_visibility[$row['post_visibility']])) { $postcount_visibility[$row['post_visibility']] = 1; } else { $postcount_visibility[$row['post_visibility']]++; } } } $this->db->sql_freeresult($result); if (empty($post_ids)) { return array(); } $data = array( 'post_visibility' => (int) $visibility, 'post_delete_user' => (int) $user_id, 'post_delete_time' => ((int) $time) ?: time(), 'post_delete_reason' => truncate_string($reason, 255, 255, false), ); $sql = 'UPDATE ' . $this->posts_table . ' SET ' . $this->db->sql_build_array('UPDATE', $data) . ' WHERE ' . $this->db->sql_in_set('post_id', $post_ids); $this->db->sql_query($sql); // Group the authors by post count, to reduce the number of queries foreach ($poster_postcounts as $poster_id => $num_posts) { $postcounts[$num_posts][] = $poster_id; } // Update users postcounts foreach ($postcounts as $num_posts => $poster_ids) { if (in_array($visibility, array(ITEM_REAPPROVE, ITEM_DELETED))) { $sql = 'UPDATE ' . $this->users_table . ' SET user_posts = 0 WHERE ' . $this->db->sql_in_set('user_id', $poster_ids) . ' AND user_posts < ' . $num_posts; $this->db->sql_query($sql); $sql = 'UPDATE ' . $this->users_table . ' SET user_posts = user_posts - ' . $num_posts . ' WHERE ' . $this->db->sql_in_set('user_id', $poster_ids) . ' AND user_posts >= ' . $num_posts; $this->db->sql_query($sql); } else { $sql = 'UPDATE ' . $this->users_table . ' SET user_posts = user_posts + ' . $num_posts . ' WHERE ' . $this->db->sql_in_set('user_id', $poster_ids); $this->db->sql_query($sql); } } $update_topic_postcount = true; // Sync the first/last topic information if needed if (!$is_starter && $is_latest) { if (!function_exists('update_post_information')) { include($this->phpbb_root_path . 'includes/functions_posting.' . $this->php_ext); } // update_post_information can only update the last post info ... if ($topic_id) { update_post_information('topic', $topic_id, false); } if ($forum_id) { update_post_information('forum', $forum_id, false); } } else if ($is_starter && $topic_id) { if (!function_exists('sync')) { include($this->phpbb_root_path . 'includes/functions_admin.' . $this->php_ext); } // ... so we need to use sync, if the first post is changed. // The forum is resynced recursive by sync() itself. sync('topic', 'topic_id', $topic_id, true); // sync recalculates the topic replies and forum posts by itself, so we don't do that. $update_topic_postcount = false; } $topic_update_array = array(); // Update the topic's reply count and the forum's post count if ($update_topic_postcount) { $field_alias = array( ITEM_APPROVED => 'posts_approved', ITEM_UNAPPROVED => 'posts_unapproved', ITEM_DELETED => 'posts_softdeleted', ITEM_REAPPROVE => 'posts_unapproved', ); $cur_posts = array_fill_keys($field_alias, 0); foreach ($postcount_visibility as $post_visibility => $visibility_posts) { $cur_posts[$field_alias[(int) $post_visibility]] += $visibility_posts; } $sql_ary = array(); $recipient_field = $field_alias[$visibility]; foreach ($cur_posts as $field => $count) { // Decrease the count for the old statuses. if ($count && $field != $recipient_field) { $sql_ary[$field] = " - $count"; } } // Add up the count from all statuses excluding the recipient status. $count_increase = array_sum(array_diff($cur_posts, array($recipient_field))); if ($count_increase) { $sql_ary[$recipient_field] = " + $count_increase"; } if (sizeof($sql_ary)) { $forum_sql = array(); foreach ($sql_ary as $field => $value_change) { $topic_update_array[] = 'topic_' . $field . ' = topic_' . $field . $value_change; $forum_sql[] = 'forum_' . $field . ' = forum_' . $field . $value_change; } $sql = 'UPDATE ' . $this->forums_table . ' SET ' . implode(', ', $forum_sql) . ' WHERE forum_id = ' . (int) $forum_id; $this->db->sql_query($sql); } } if ($post_id) { $sql = 'SELECT 1 AS has_attachments FROM ' . POSTS_TABLE . ' WHERE topic_id = ' . (int) $topic_id . ' AND post_attachment = 1 AND post_visibility = ' . ITEM_APPROVED . ' AND ' . $this->db->sql_in_set('post_id', $post_id, true); $result = $this->db->sql_query_limit($sql, 1); $has_attachment = (bool) $this->db->sql_fetchfield('has_attachments'); $this->db->sql_freeresult($result); if ($has_attachment && $visibility == ITEM_APPROVED) { $topic_update_array[] = 'topic_attachment = 1'; } else if (!$has_attachment && $visibility != ITEM_APPROVED) { $topic_update_array[] = 'topic_attachment = 0'; } } if (!empty($topic_update_array)) { // Update the number for replies and posts, and update the attachments flag $sql = 'UPDATE ' . $this->topics_table . ' SET ' . implode(', ', $topic_update_array) . ' WHERE topic_id = ' . (int) $topic_id; $this->db->sql_query($sql); } return $data; } /** * Set topic visibility * * Allows approving (which is akin to undeleting/restore) or soft deleting an entire topic. * Calls set_post_visibility as needed. * * Note: By default, when a soft deleted topic is restored. Only posts that * were approved at the time of soft deleting, are being restored. * Same applies to soft deleting. Only approved posts will be marked * as soft deleted. * If you want to update all posts, use the force option. * * @param $visibility int Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE} * @param $topic_id mixed Topic ID to act on * @param $forum_id int Forum where $topic_id is found * @param $user_id int User performing the action * @param $time int Timestamp when the action is performed * @param $reason string Reason why the visibilty was changed. * @param $force_update_all bool Force to update all posts within the topic * @return array Changed topic data, empty array if an error occured. */ public function set_topic_visibility($visibility, $topic_id, $forum_id, $user_id, $time, $reason, $force_update_all = false) { if (!in_array($visibility, array(ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE))) { return array(); } if (!$force_update_all) { $sql = 'SELECT topic_visibility, topic_delete_time FROM ' . $this->topics_table . ' WHERE topic_id = ' . (int) $topic_id; $result = $this->db->sql_query($sql); $original_topic_data = $this->db->sql_fetchrow($result); $this->db->sql_freeresult($result); if (!$original_topic_data) { // The topic does not exist... return array(); } } // Note, we do not set a reason for the posts, just for the topic $data = array( 'topic_visibility' => (int) $visibility, 'topic_delete_user' => (int) $user_id, 'topic_delete_time' => ((int) $time) ?: time(), 'topic_delete_reason' => truncate_string($reason, 255, 255, false), ); $sql = 'UPDATE ' . $this->topics_table . ' SET ' . $this->db->sql_build_array('UPDATE', $data) . ' WHERE topic_id = ' . (int) $topic_id; $this->db->sql_query($sql); if (!$this->db->sql_affectedrows()) { return array(); } if (!$force_update_all && $original_topic_data['topic_delete_time'] && $original_topic_data['topic_visibility'] == ITEM_DELETED && $visibility == ITEM_APPROVED) { // If we're restoring a topic we only restore posts, that were soft deleted through the topic soft deletion. $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true, $original_topic_data['topic_visibility'], $original_topic_data['topic_delete_time']); } else if (!$force_update_all && $original_topic_data['topic_visibility'] == ITEM_APPROVED && $visibility == ITEM_DELETED) { // If we're soft deleting a topic we only mark approved posts as soft deleted. $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true, $original_topic_data['topic_visibility']); } else { $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true); } return $data; } /** * Add post to topic and forum statistics * * @param $data array Contains information from the topics table about given topic * @param $sql_data array Populated with the SQL changes, may be empty at call time * @return null */ public function add_post_to_statistic($data, &$sql_data) { $sql_data[$this->topics_table] = (($sql_data[$this->topics_table]) ? $sql_data[$this->topics_table] . ', ' : '') . 'topic_posts_approved = topic_posts_approved + 1'; $sql_data[$this->forums_table] = (($sql_data[$this->forums_table]) ? $sql_data[$this->forums_table] . ', ' : '') . 'forum_posts_approved = forum_posts_approved + 1'; if ($data['post_postcount']) { $sql_data[$this->users_table] = (($sql_data[$this->users_table]) ? $sql_data[$this->users_table] . ', ' : '') . 'user_posts = user_posts + 1'; } set_config_count('num_posts', 1, true); } /** * Remove post from topic and forum statistics * * @param $data array Contains information from the topics table about given topic * @param $sql_data array Populated with the SQL changes, may be empty at call time * @return null */ public function remove_post_from_statistic($data, &$sql_data) { $sql_data[$this->topics_table] = ((!empty($sql_data[$this->topics_table])) ? $sql_data[$this->topics_table] . ', ' : '') . 'topic_posts_approved = topic_posts_approved - 1'; $sql_data[$this->forums_table] = ((!empty($sql_data[$this->forums_table])) ? $sql_data[$this->forums_table] . ', ' : '') . 'forum_posts_approved = forum_posts_approved - 1'; if ($data['post_postcount']) { $sql_data[$this->users_table] = ((!empty($sql_data[$this->users_table])) ? $sql_data[$this->users_table] . ', ' : '') . 'user_posts = user_posts - 1'; } set_config_count('num_posts', -1, true); } /** * Remove topic from forum statistics * * @param $topic_id int The topic to act on * @param $forum_id int Forum where the topic is found * @param $topic_row array Contains information from the topic, may be empty at call time * @param $sql_data array Populated with the SQL changes, may be empty at call time * @return null */ public function remove_topic_from_statistic($topic_id, $forum_id, &$topic_row, &$sql_data) { // Do we need to grab some topic informations? if (!sizeof($topic_row)) { $sql = 'SELECT topic_type, topic_posts_approved, topic_posts_unapproved, topic_posts_softdeleted, topic_visibility FROM ' . $this->topics_table . ' WHERE topic_id = ' . (int) $topic_id; $result = $this->db->sql_query($sql); $topic_row = $this->db->sql_fetchrow($result); $this->db->sql_freeresult($result); } // If this is an edited topic or the first post the topic gets completely disapproved later on... $sql_data[$this->forums_table] = (($sql_data[$this->forums_table]) ? $sql_data[$this->forums_table] . ', ' : '') . 'forum_topics_approved = forum_topics_approved - 1'; $sql_data[$this->forums_table] .= ', forum_posts_approved = forum_posts_approved - ' . $topic_row['topic_posts_approved']; $sql_data[$this->forums_table] .= ', forum_posts_unapproved = forum_posts_unapproved - ' . $topic_row['topic_posts_unapproved']; $sql_data[$this->forums_table] .= ', forum_posts_softdeleted = forum_posts_softdeleted - ' . $topic_row['topic_posts_softdeleted']; set_config_count('num_topics', -1, true); set_config_count('num_posts', $topic_row['topic_posts_approved'] * (-1), true); // Get user post count information $sql = 'SELECT poster_id, COUNT(post_id) AS num_posts FROM ' . $this->posts_table . ' WHERE topic_id = ' . (int) $topic_id . ' AND post_postcount = 1 AND post_visibility = ' . ITEM_APPROVED . ' GROUP BY poster_id'; $result = $this->db->sql_query($sql); $postcounts = array(); while ($row = $this->db->sql_fetchrow($result)) { $postcounts[(int) $row['num_posts']][] = (int) $row['poster_id']; } $this->db->sql_freeresult($result); // Decrement users post count foreach ($postcounts as $num_posts => $poster_ids) { $sql = 'UPDATE ' . $this->users_table . ' SET user_posts = 0 WHERE user_posts < ' . $num_posts . ' AND ' . $this->db->sql_in_set('user_id', $poster_ids); $this->db->sql_query($sql); $sql = 'UPDATE ' . $this->users_table . ' SET user_posts = user_posts - ' . $num_posts . ' WHERE user_posts >= ' . $num_posts . ' AND ' . $this->db->sql_in_set('user_id', $poster_ids); $this->db->sql_query($sql); } } } |ެ8KNM]ZȕgCD)wnV2cޫ2 Ո h C!5Ƥ Oz1~c>b7pu%Wo"ʵa>;ak'<c,SB?@-dc/,yoFQ+3JSl PL#*w-< :m6SݲoB,jp,PO ^;q8G i @c/'$}VL$a2$;DtQ^T7'Dvs`Ƶcf,"A:ȃ7.Eo@#ƚ ?E%_4"1%iR>ǜIqF)"ގc&Sw2\9Շk~jYՓ%Ro~&~1.ˍWQp{gXqJcjpe4=b(or"|uܨYRm]DY^3*h \\N/R&Gϝ`dEZpV PqX, *$êt_[.V>WYNNbmJE.YtJ1MC5+IJFqR/ '4F/{v]qI2_.Cm!8uTR| R'a 8ڳy! (ST#XVX nD'[#)hhS'54=3 54@@ҷ$/M-{Q猱hzY "_X&^w&dҟgJ&GM6UM Q2 ^$-8%Ha%-',Oa٥e8Da|%kˏɠ!ZkL8tyHG<>tΏ ru!7waՔe%aPR] ahtИ^81ߏSځffi# 1zlwdj]蒱3buwZ"tUp8kwoQ.20JNwλїn(Zt0X #(ֆ}Bk|& P=fASn5=i|y"+rGnfL.vpeS~iJ眝f¹ 7<$~HńTtH}?B-3b?.&xT;,nfb@Bv,_ x-xĽTr-s"bi寁27le4-(Pf"( Dpds}e;>a5$JD>}\D2G{lL"}%2[jڡ ˧\d[- H$HO˶N#>-  C;WE_ wbHO0ZqڋM_d)a3ńkF]?īOڎе%wΠj-QYoXRos۾ JWUZհ-j@S'm2R^ձ \s<:WnFTⓕ2v\Ew<"VbGFjqc4]KNc"uyd##{?V2ftWǛգL:N)y>%? "|| t17^&_H S/nLYԯF1Ü7[cKf=,K:!f#rW"z00'?h)ar֩5p(,*D>'p`rl&_)s!o'؇#ۮOp/.oUV3m6; v7/3, E7ZX V~)IB$fwT' \\ cx^% T]9V,'R(_{ӷIҹWMޛ^Oң2*J+ᢓg|-k^-=^BV? 0%UsBr)wf')'3ʫbB<)\{@lQȨ[ bpQ O)㣕, Kq5F~M|TD"9 dB\ N[1YPWL~ݐ5 K!B?mGM@I=P1@Fwn$☗y5.$Q( 2<ߪ%ZkWt1T3Κ?sl_F4;4{^Qgq(^x;7'[>{*&h:Vw< !H&lZĴ/P"ib?~9. Sd]R&/(̏R>D[SZl^ pJSP1@d,Z׽52b>hnGaZ~ļ+ږ}wgLLh?Eȭ̏VXdJvQh &$-b>:ZL!9JKap4(m_}k RQ'Ġ\C۵DXG4;'G]#=u8~L=c7qxſ#~ͅ7b Z =M=e66ĥ7lsX3Lʷھ$k\pm :"Ss46 8sƦKhnF~כj:Q,6ruGJT7Htff \Ũ<501_@ά9xIpLNzfct9xhZv'C ?V5G߈Tyw5(~!C;tnBwk=޹ ȂԺsħBɪ<|]s/"+wg<>j`(We͕ b'Euc%zpۥtk {GdA +dtCQZucCөw="a-0(誒*,m$%ڦxf4=T=i!',@&n'c#c&"/*K7 /"_-jz^aeoDŽ|6M (M+tY[6xei]'oHsD op(24jʛMuHRRx.۠?VY*PZ]|z\[q.e,J ܆/ w90I\H7(wu1ZJ y6ᨡس{mMK%.PyI.lTm6ШVOtrR[H?+ߕl1$~Ѳ׳.43 )}T6:gQˉa-,;aD@"{AZu݋[@YÏc\-[wJ}?J}:p׹h:Jj&c Y!:UyJp[]Fx> &.0bxaF$TO8~ʖ\*NV)ؚs>Ūf`7CsAX9A;je|켺U)mL7d 7U,w!R|?~zc`P[p6|b ,gxlއUI5V1Egbușq^ۨӔSO׍|jXnM+X$OZk2P?E_C;@M؋6S<L^eǗ\K9fkA KJGX]97d (s9ё#–&ZUcEme)i2Z5PF.Gusd-5*t [Zw!!҅6Sr3-ڬ`ZP \+1Cլ埯wUVK69i/͚' V0~_ ʌ{gd{$qBl 5]=3zrof;j_ңHM~Ő63NIv[QZv`yKuWM[g-Ni[@m^Y(ra6鶈V41 [&ZR\EP?a.Sdf LPWw];{E9\*W9R l9lQ/R`9 !3-u u#׾ \0*qv@U9gAoPrC (*z+H!CH"qp5p;%×Jl`-Ҋs11T<[ ];X{Rփ%M(x։ fNFluEEKvY D3W8t1!鹐>R-F2030J$#HshSxsSf5uy 3`|nВҙRd 4+C\W3 2֑GR?L%[;s4ˁ{3Ch+D`u*bG| ԟgh&F9]/;΅lE;ׁAϥ'<[X xWL{e 0y, ^JE (x-kzt8D/ GL(R).E@؅H#{Gصn(=!M*ds™7_YiùYy_p ؤ'ĮEm q?k]`ذ:_ʭe8q̴|m\o; `*,-PϑHx?"E ]MZuSqs7&IuͽS, MQ߇Mł#4.#ڗM//_@M8g#n8ĐѦ),t`xtNb s69y`J*c Bj 1xa5/d'l5}prm[,4Ħ>TH8H1T+1WwdMKW*EMJ 3fw\^?S,nѥOADYބk|rQˢ6y]#\Α>4׏y @L/`WPVMLK"4=]z#3K`;!M Ȧº$& S8%#CFcѤ-1ƅ `iǟ&~}^a]̳{,v0`X5 gg-r!6 >Opk4"44 `b^/)޹}9e~k3'u^*@C=֞Ll>VVp)u>jV2?;BoFCN.xZN+,"uq\}i ΡH0z%vL<-5xvF ~˷(PB%AŃ5UK>haK%?%V9yw '7n<$.}-FoCOpZV.>"q̒;`ݷ`L?HJ/x:YQ(+d19Wcť^ 6L|JĢj[k  )P<}G:~ЍԻD,R \xǑ (Aɻ,qF8Q%no?Q]v[ahr`?>xO^? J 9Jmn%/nHi":js`sQv{Dj\L<lpg8 HWU#bPC-gz.{zOd*Wa?]7 :b7=bqOAFAK M!ߐV{=L\n3fƝ3c`EwGXyvQTuمP:#Wq/y~0^ *qǺ[ 8pa"6S'8Hz\E˹.KD)V`#5x[S1rX:=@",T `>4Ut~%Iυ` A28+SA'!$XC|;:'p7t y)Uc y]A+=7q v\ ![q1AX>Algz͉mHӀ>Y2K$Y>iǤd׺D;+1m=O#wܞ3|, SŌ2&$^jQg8MӅ!0ҞRnݩͿVji0UcMtva]!l{r2C a8er_\O0u]-`E0hz; <ȗ!ԁ*2v۞ܰDEiڝ>W2%YY_ !T}yF#Qg2jn+ +bKqr|e>  NznXW0>sA_zV`#tPNZ3ÛϕDU[(n[o1>w߾pD JAқb왔)P I%B2*ڝK]w,Hv3{R!܇a!oG=KV?Kmބ@܊ᄨ` nt־4ܹUjP./DJ71p-$:L*rtuSFWawzLQ&~'Q ";|rVev e[ٺa$r⸐ R.S].8)%)õ+f8J~UQ)`W{[( Aix4' +)эeD4BW`po޹!EjGHjJ&u?Lhwϐ 0/nC#ydfM+W>'[~7%m/nZl!NTQah~ ] Y`77ߐQH=-'XWl/x9VZ(?,=wpn{nk9.`^x'7hr~H ?E q|AiP/z9h l1T ofb[o Lf 7@0 tUwVPr&a@xFD{ *,Dֶ;cu?eP,QahM O!aaY 'HM_CALQ %9O_ +׃B2#-6ՓD^gL8`3d Q8W=lO߲I "|Gr264Eg)" Y)dhoj$703`T0Bmb\=aFIF{V׃<0Bk)a*8)1-?B29Y"Rϒdm eIWŠ$H9DZ~xn8OQ&,nEZ4ٿO/M !R+-@S %zM]aQD*?x-<@ff_l.e<,O#А紝Cre+j 2BR[6`|7zii ſCN 2 oTO`΁8?eYrlU WO,~]) xs3rN'W.(S藕 'xU! ”{KҘcntIgĶDnklM#kApt۟]6_2t\ǝbKHYM ذyΪ6[t@ _s( ޙ 9 rYMՌv ]}#f$U|Rf?㶱vnZ2VU?S\ @8%6#^`;!"q$^y+i^&L-+Ѭg 4R4Q1̙}Ay|hEiMt:3,KDE=~I0x]9paZwTe*{)-Tq<# /Hi(Qiv5ĕY!"\{A/ɚN\`gw~݆Hu/4 l K}ʆT~;Tp vZ6%%{UX#4nIs=ra[9:6,5sv(>wcIC[E%tXug8]'\xvW~h˨+X礹#^pAn0;r˰u.hpN $ ld}6 dab;Lę"kVW*rjQDb%xQU͜.3BɛNG5g"h%=:#v4Y݇ճib6ڹ5M M m\؇Sȯe$-(S#F9oŦ7#O:'-znSGᙗt*2X쀯~8&vyïbOU\[xYl U~9x|@qЪ/%C7CouNDRZc %E͓p8;=k,\0}H d%'kO/jRd91`)v.6܁<=m]o-).pcif$xz)`lg*΁c/!Riq <MPVT +NѼʦѵk闊ZRf? (a[_}ٰ!L;z#dzTk!7 g1 mcԻ620~ 1*УP6ea;8,#Gev6_'|T膖(o PɊ~zQ1`#E\QcwB0lkK ETGaTK-y%PF\ТH$/B*$WSòxʰb`nOk/= 4vZϗ -o Lb-0K 9 Хq2 H'$#\bu$3doJmnyEF-qb;D CMgrc+.YHn,ʌ=IN 2h60KY/͐׶jP!juh`]!O ,9F?cj3{/:׹uw?cAOuÄ ``<pSM?X M\[VR–voBM}nG=Z3=!!@!vEt{8)Rd|C:\fDynͽPW'=}˔Hn:Qs2PS#g5p@Uw+Ve6zjX$QlPse$y7I񮛽+{Ѓ|K.V7 T !QP<=4m>'~y—PT:YKqp\Ec5|W;iK Eq3,<۽X 0Q3IF8;TوP0x:[xr5,l/XPUH$,Sx3d8-4WԎRomH8әqGs0DNO)hkۋSvLjaѵ,\@֧\Z\mӜVx,iD$=gXI;u{F ^hk0b}0Bڏ|LቚT^QߨMEjSo݈u˝w=9y)C| .# BzQs[ls<+t+t)c8UI 8[XyqVy[JQYL TҪM>ё_#O7kk d/hHY:5 iK[E\o/̳dT,CgMy1NeC%emYd1^gz8@!rny4^n*DWJ<'>ɒ)bu'(dTNESez%ٜ]&^L%s`̼P8ۺhtiS!೪@Sr$`׈)Q[ NIs?zcX[ϚNmǟrwxww_gBxgaeb_`Aԃ\_`M&Lb `)qUZv2e4 a>2O+i"ezn|p V' zG F)B B%N֍tlkh>S@P^/G0VuM_NH" NrNGoD#&q Bw;Cp48BT?Ie1-tT vR! -<@g̙' PONF_G|̢9%1n'@1vAtP H!$TPj)1#-/$fa4ⵇ5-mI䁒(G8Yhf+QP'd8Sl=UO_4K:\9=% r5[M%)Ku0wɪcy59Ti6U+kO*Ϭm0Bbgc'|Ԫ`{*9/. Nĕ]+;d|9ކ{SʈR罼_g'!wgĹ/X@Z' V !5Wb[Y3nL }'~ݐ*ϿiQPsq?j`Ue*߆lch! wv>tGPPh}Q7SR-E"o}E<ܹ*Ѵe dD[y MC͈`P$qb}v,TK*B@eq -| o힘`Q AKzG`PCl;T<j}w%b35u$eh+I|\ɭ*#V9tlIU҂ˋO$N=ڀ'SmK!ؙ,bMm#ZlϿ<$`s1a>0nV͜$?Y[!gsH}FdU { Z@PҲu؟(2GGuYPvtw7g<kXsQ6F:2LMmeb\HM\fguP S}ƐK.΋Fۢd}2BF/\ӰPXL;Q]̽%0%dlȻG"#p;wzo&T xwL6ui-m㬬cCV%C0l4 A{:"~-/ YVق;W=32Xt*O:?)sTX-Vl,r<%wjˆ5Vv͍/d|* koX$ DzCW =wȐ{ 8vV;N `mIڻ(J;bDŽ[l(rEd4GWʒR|Kcۖ\m#z,Qcnja]Q:g Leըqpvo$Gbμ%rC?QS&mm-6g*4JEr|RHTlT©'o&lݕҮ"729w:'oQ=h, 2Kf1;`xnY,~r> >3x4NBq1L}ɯ2y|i4"6TtNM;+F_TS:N|G՟G{856;{ Q,dDTA%~V,7fWi;֦p%m9o RlzQ=*Hhaا0.Zuunݚ]Vrd/V{l˞jIv)E[b &[?vs+kܟ "baweU D?i"뫧=91!z#mN`(X1~]Qj&p'8W0Upzb>DO3mkmLFoۓ kà,mmԋ<ד.2;KEbQGc+VO(c3tF85uIO+[ x6әX4ۧNJcBp_G7&q4:_EsqYpm. f r4$_ܢWRWjT mqU{xnSWV/RC3| ma&"՝5piIs){K;qNtMn<}rhXzz>uRAD+zE?J^hB'rõ}| 3BOA%r^xpޑmƔ\u`Ug]^,c D 2B=ٓr `:md&$]kZOnk! -at9J?J00 |j\ qHp#Ue,^lJ@ޏhI; 壬ƌLnҍu$*6:30&(U/53\j,7*gi*d g K$v[r&ÿ_T ܼ<`|?tH8( 9TSg d I.j#K_D qDX7Iu>~Ա i ɵ?t0Nβn%n=FVNJs\bIL,, >G|rpȂ=U[y:Y`q:y9MBef%};N!~}SVH߸?s~Qrl8ҖV-hs@5,S]n׈SrJGO)ǯи䑭=Kq^P;f:.vc`'0ʡOr+\Kmb].Q -K3bMA, 5 ЏB^tS&kpN୷QV^.D36|o1c8>MyDz:QMa<;COcGdF l$Ꙛ=]+<\پ:]N12-jSz^+!ʑEߺ҇p3 UȆȋ Qd(t{~_0$=}dW496Bދc!٨Y-Z^wg(Rkʣ;Pɢy؝'QvpE!`RH]+QZ?W QƬ <-uF)PDY{AIr8^K;q@HUD*8aw [l,TQ0tp$DleKx$Ys3L8lCK1|csъ6XR^֠)gjK[xtjW=`6s2Sy=o6U>.mt9_GGޛ51R;kf #(͒Z4\Ɵ9򨣶]YOCoCdg'P{UlTJpmÎcE_PDNvfޓgLQcbici9s|S \EKxKwJETC:ߺl$vQZynтGLshyT9;AofÎ3M0nDpr&jjRU[pP cKOQ`(aTXbιZf4-ܚI1a 0åY`PT0Cg1)~(q!@ff%`/}tM^ r'LW{0ty_2e:EC)r;Pl̈́ p;}#=ffڍ ';Ɛ~OIAz,sjrů%j 7 2<)XI^BYo5kP6C>b\._ˢԤ_ P`ܭR, Jx}"ꂃws92 1ٮN$l(A4;:Z|)wd4@8ޯ=?f8?N*xﻂPŅr~:kY>١"btYWjؼ m}6{v°??!k9-}KYZV2 E ăGr{sX/\ ` wk[1/;pQw] :-P%j1 %GJ݆ܽiaSyS$7 N4SLi_X-xV! *LZӥ>|BHMa+G!]DhI4ȷh ^A%>U"P3>%Ǔk}&C=Qj"IX& DP[Av5O24JTi ;p+2Qtaa7v_x u iOrF[8h`$`ӂΆ_v,WX—0P [Vz- ;b G"|(ip91c*T>́¾f?};Ve ^4vz/Z]6 MCdQ; .CqL?Z9h_fZOg'8 e+4Fj0EPIQ?0BJF2b{ĂY6qr5~\R  1{(#1:XŦr왗Їa5ǂ!bʀWEE2w)vT37pюb,YS'QuƋ&[9|:/!ߚg9|vԳiQT'L?t5kk'* 2~JE5gA={g J˕ڝcpFF㑴lSݣd)S+بv 'L(,ls=RM RMtyVI+N&P/e⾶1Q;H#w{ D M?rw͜"HX2)J4ME-:1s!FKTM}e{U 6Vy V]$!{Bjš2VQ.HR$F{_[gʬ&C!b)gv)7XR1G4 :hyxD}T/vڷ,0,j=fb>҂*{8Iaq1xf:">,Τ5 4e12u?pw@Aȷq,_3 !3 _b.9,v7N c'kUŢ Nf _rߗ 4Yjwtk)p|tF JAN(@ƾDp@>nH -ڭ9 $ti9R1U\!+U˟97Sd3FzUPK$Ƅ{R)y[˾ܬ >WtRJsd嚊",lX8j)\$Y31x]洺G"#w )"4Lcp{W2,fo >1Eld]ԾVS\W]b_sAJHX3;U٥kav,&| ks9+A$@$)4K&5\x|Mo|Xj($޴M]R*8ibsyoAְYsɱ]EAb@E[K1; ǚRƸ#~H2fcVT47{(6©xQl55zĿEc:Wb:9SD>ŷ ߢč_|?qNew4I^ ކEC_iˬ%l[:??yrSzrcTg!%5x/p {c8>dP쟰IR f~m{]b гJah7_U@jL@Fwyj!XjJ7#Vth=?lO+/H0dP|ue(ג6 *Hy=%`q&gm[|3|h;X#86j\҆qn_DD53"e8g~0>p -널4ɶ ~ Ra(˕3xii KBs+,'H'gG:/[ܭNLhpxLq`9[F 4j6O&Ν 20DL 鹁}%B-02uF]HxR =,K+mؚ v;D2{P YP? uAdMyhGM/qH.M"pmÂ`RUag}"a'} U.NyO8agJSOvj:6>Hf,rE"sff`Ev2)#jͯjS) Qd*4_ghؗ!]ފ936*#!jiUғOM7jEz==ok`RJK"G\ORT"hM`(1!ؿ M``2L`rծeL<9_@kL,bߊz Vow} UZ{&f-[;)5iq`dG@㗫lj}Di?Ϡj#} IT1 RQhfɚhSFhPlNwb/`M\$0" Ӄ,)WR5[aEbcy!n@~ AtCڕ?A* \+DRK-<^;dJ‡|o!uPXR5psLLEqsAϤ *oM ٓ_eWi!,Q(+z-74^9A/eU;[6^`4b/IsƉ P1:@2H }&Vp2sK LŌJ&Rͫ[`KAҎ/b#uy"|JIpk6HIQSqz%|6`lNČn]f$ 4L d/Ӷ8aL(E<,'A-Ȟ:I; kwۉ!ևKyP=W7i'BKz]boY60/8D>mYy1]'J'ڇ vG.DډQː f*t6<*] WBNїA~^>Jd{[w`wtAXFCkwС2(n3s_" һިdC!c:6#8kKoW~OOL6ngvW9S^Gd.bϹ-oʌR؆"ehouY~[uχZJ;uaKX (237<`+H;QzaKA=^iJ8 +6%0TUe5^銗nt]DU8ʟqF8ʇ.˂{^F\ؚa=p~0PAE.a x;P PԊ|ʒ~>Qj(ŐUrEVcJ7O IyMtbt 7ؕ/T5R\&o1:&~UᖈǬ 1tIA |ǿj%y : ǫPX2[U[).|y b׹H"to?.pm7K㇠!SK,*0'p^0^nNqb+#ǂ, Z&-NThnR Աoh@)) ʤ2Quk 8iHyK%\ҳ"_^G9s,}^O`=9]ϧ-犉h#":VpX\h|L J@Md#Ðu*\JM@ɜnϡEQt؍Y;GT352pFfH&bԿݯkw?[Q#[0b({w?=ʓAuDBN09iE]UG{ R](~N ?[vrI4rx@mhU+n.52 Ziy 䚊o(cv s:yQԂ6w_7XS]1]pX9-}1Y*A2jy~$;9ڇVso'Ja@\XcUke{s8QG˘iw> YBz2E!1; meW}"̆" +=׳,_IǕrźQPꤦ 򓇖Yl(E1߾wv{ϸ۪Ob <_eT1E+nIeAJ 5^蒵qIekkPݽG\,-d'=U jg6E'J6W ' UL.(9O[6le J)U'qG<[Tg_kbS\F䷻4zҿ[*XZrecbffO@zb(G󛔉._o‚4ts&f[ j!|lFuϺ VgXˮUĬ26H-xm IթhJ!}chi/Ճ ~Q/ʂuRۥLb=W(l% ^kQdu¡^zq<^G ۚ LI=@ZF͇{(HHfQfD>:~DyzzR)z oI=8ʖv^EE"buß;pEYjxn="CM"BYJjQHI-\9WTFG\dCgq"d&X^v)TٗemU7A϶Nfk M߁!~ys҂eՕz3ŷB[4OF r"~u=nPk],j- -S$t[OMnl!. \bg-'V[Y{tuuQ/Ɂ gR¹DZ%}*(x7>!d۱Y ( 9b1f9=s[NY["9@ HⰧZ-x<$±^rFi Yx{Ŧ*/MF]{҉Q/Her&) H/ff"sРl:x卲\><8Ƕ>W#2ܓR*fz810]N o{صfit ؊u_꼐]n}xﶖdŖ%k ;: m>gBYqNo|gW8(~S 7[df -ځm?o{,}T~"u^$N3WؑkX)-uNO]L:}I@1ƊU'"k %$+RdVCړ&.cb3- _HϨM1RЎ8ٍVߨ-.:T"Oᕙ}rWD D1qH`Œw`!b~?(#VT;RI3 b7{~ڷpH)d]Oܑ--`0Np)K],RPl:: 15}PJB(iL!@.t3pi{>ש)^8HLJgҎa51Rm GlDYXXz5v2MjޮFb4gw}^?>OQό0XF#kC,ldE.įcTp++x7fb=Dp҂3f2 %o^)L_"|oύҒ oX-;#ÑU#j&8 f3(5|u=G/, &v 2C[0]Z'xQ'B Z޺fQԐ RCߌb]<n|~oz{K_SW%.&8Pz0; 2ǩJJ4B5{lQ#3Pd1 e5f #GτMZҜߏY#čcJ#Z |U/yac(ֹ-8+hXB A$4! vk&#nwy}?k.7wctO.텱b?Rn.U Kl}raPuoUyM 8阘TeE:Wqcn1|SGOx%e1X)"]"^{'^Kd%xm/Y@/rWgE|q]X4ks1z?0KZd$m uxd&?<FvmL롁nӲŰiUK>kȶPXĹ(-2$RVzYpdi }f&HbƉyR@Ҹ7#3+)nRn*2o/|,ʰchHt@[&X^PN>ol4sg lM r{cAˊz:`d@ڭccb ,ku[l JT^DyaA!&0dHHǞDÌ ?V9Hު3[\6j0cےU_G99zjŝpU= Gի>1>zmO8 /ZKIK@gÍ64-$ƣ,I qCk>[:iK fnǸx87R8+OZ4It̓mnP q; >Ӳ|4e4[Ɵ#_tHf vWFHL:}FA)~lAoM}i u OIGjw#EϊIz-Meg27Ow\(bS:Vco)Fď'W:PMQR똄Nu FSɈŸD9 7?3إC Mv:M^>R\ej <.`Z˕zmxf #kw`Bp,ƽBphG ;L3e&PASBCuِʭ5a,,-'M bCl+ KҎ.|KxgXWtٿ'lz[jUˡ5cuU΀6@Kܳ*3 GBmsi˻)+qO1 i[MHvZ&XylͼzĤlޕ:!um!Ka{̟(Vcx&u1uk>G#1V5+=U{˜/9i,T{w͑lYV#^\  h*QV$5"[?*ͣ1.~/Uʩ b REQWBD! ^0"d [ЋK 1k|Hs|?*n7+B彺XVCIYLz ]-`}oxkBmcuG6t`uP6p:QÄn廭_A\7$)OiK< iCv>XlqC %ᬋbne$*\i8U:@ 56{xQ0me Iay\۱T]{K4^^u*Vζ[t9NXloMkS2& v͝ެn`Ղ _`)dw> /Xb !_dsF^./.>fG ~u &K6-s63<\j#ps (81jNbƂKzdM$\1Z):?؍7H$ _fH"0ecb_ڬ;<6y(F$i]읷,ZNFϓdL퓤Z]LY [YxT6CಝWC8sɕJ> pT>'UQrV %Iy=8'f9PݤzJ13_g΂1ZUW2ȷ@\wa8N1D\lIE}_#7&6U t?Ac$KW -r.M+{ 5+TZ,:*2)%ާ/ y7, ?>)uU#cg6m& y3IH[QJgwbnS+yM)j?zt{Zr HRMtTlB3I4^, tfs4crN`ZpZ5(4)CeaN.iɒC~Ns-SF}2D1V,6?}!^)2M5< T2ܴOQI/lKu3fr˓6F"})aafu~_ f1[Ʃ0&xyfUH1G`u?L++Bc-XɠTk~kC#AZ3*ˏ/.<&QC|7"HeJ?m20 Fbf#Isxc[YW T+M85 MB/t.#\kTڠGOZ}RY3' DC3uA~9['?l{{Ӗehlm#^] #%҂Gj_87\V/Z}N1/'9B +UlޟenE9>Nl34e=8z#T_Ɛ*6mrl3`c5GfEӀ#SըI%UH@Џ A${~Whm#hTc5Ɂ)hxs`_Ɓ=t9|S_%R|oNOmxh@R9H.']qX,ԟ|b}}\z^B8L+#\$\I''z$NA0[׸䡵[>cL fahsD[b[q$rz5"G',Pb43&ֺb~&ϲ .H!2Fn0uw/^=:j<~vE4/΂2Ɖ,N-: źZ;oܱ$Q |n RʲH|nNN$,$jr%R ӕ*&&/4VZI6~{QvYc#y$vhct| bK)̃ȓ.6beC2R6̽ 5WuNl~?VaAe:{6'z" iCF$K[eZSb)*O%8eg̰ޯGk' De&`1d/" K 4m`ZR|D;<8reM\wC&ekٹcӯ?(I'# '\óW4zc!=#SQI ;k>OCDόWiάި,HJ:uٳ"{Õh˷( $?RR~֒/J_FQEY$@S> 7e/a`wDdB /P] 栞3k!dE̔'W~!QGzdwq:f|o Ʃ8B'a Qo|'`qUM4̓)1d*`NҊ,@pE}ҷ*g![P`񐺾Pn͘ oxQZ%esF`3T}k#gfύ&}ɧcp8e9CŁWP#VE6K5-Ibr~ 1LN(h$@s L6'ۨ ]eb â&xSryd&캗v0i2ޣIQ,IlrNM>o2EMtiuzX@>. :FL|FV{yݴAYu]jp ىiNP[A%`%2 OY~x9p}K"{'(n~,c,ÿZdwZuFFG˛nSHZL: q A`=:s7rҌu>jt?xFN{ %ӨSiVSJJ#  PVv<c}Fe'%Md[m P(N־q5l`jeȭDRRxs!3X#qd@#ef}@tB FH8NfQ-_Ѻ$Y ؊YInS_dsOp  kK(=uCecnkNѴ( G\q,.tvq^>ҭ;oĝZ&8n$$(ɦBAƾmW'I&)]U#?E+mNv֗Δf}OT#j(kP)d.)!Ȑa*cY$ jmˬh|I* DUvAʍ"Md(Y~ʮ(|42nI"h]s=R7W\y"q] 1T%OPJ'26S5Ue+iwPIІ_иz:or(Y%ŵ#iAuY^Dj#{I׻\"snNtZ6 %!Հ:ݡPDB c€\l}(ZoE_F'<oZXl85"r}D!Φǭ1He@3$Sߥ}[aG\Aƍ^"G8+ @.߈P ctKɷ+&vRfKcɳ#9ts#ܸ^"Y l2u{ΜMc@ݜO;tέDߩf*"{678cN