Application->getUnitConfig('perm')->getTableName(); $perm_table = $this->Application->GetTempName($perm_table, 'prefix:'.$prefix); $sql = 'SELECT * FROM '.$perm_table.' WHERE (GroupId = '.$group_id.') AND (CatId = '.$cat_id.') AND (Type = '.$type.')'; $permissions = $this->Conn->Query($sql, 'Permission'); $this->Permissions = Array(); foreach ($permissions as $perm_name => $perm_options) { $perm_record['value'] = $perm_options['PermissionValue']; $perm_record['id'] = $perm_options['PermissionId']; $this->Permissions[$perm_name] = $perm_record; } } function getPermissionValue($perm_name) { return isset($this->Permissions[$perm_name]) ? $this->Permissions[$perm_name]['value'] : 0; } function getPermissionID($perm_name) { return isset($this->Permissions[$perm_name]) ? $this->Permissions[$perm_name]['id'] : 0; } /** * This is old permission like ADMIN or LOGIN * * @param string $section_name * @param string $perm_name * @return bool */ function isOldPermission($section_name, $perm_name) { return $section_name == 'in-portal:root' && $perm_name != 'view'; } /** * Returns permission names to check based on event name and item prefix (main item or subitem) * * @param kEvent $event * @param Array $perm_mapping * @return Array */ function getPermissionByEvent($event, $perm_mapping) { $top_prefix = $event->getEventParam('top_prefix'); $prefix_type = ($top_prefix == $event->Prefix) ? 'self' : 'subitem'; $perm_mapping = getArrayValue($perm_mapping, $event->Name); if (!$perm_mapping[$prefix_type]) { throw new Exception('Permission mappings not defined for event ' . $top_prefix . ' <- ' . $event->Prefix . ':' . $event->Name . ''); } if ($perm_mapping[$prefix_type] === true) { // event is defined in mapping but is not checked by permissions return true; } return explode('|', $perm_mapping[$prefix_type]); } /** * Common event permission checking method * * @param kEvent $event * @param Array $perm_mapping * @return bool */ function CheckEventPermission($event, $perm_mapping) { $section = $event->getSection(); if (preg_match('/^CATEGORY:(.*)/', $section)) { return $this->CheckEventCategoryPermission($event, $perm_mapping); } $top_prefix = $event->getEventParam('top_prefix'); $check_perms = $this->getPermissionByEvent($event, $perm_mapping); if ($check_perms === true) { // event is defined in mapping but is not checked by permissions return true; } $perm_status = false; foreach ($check_perms as $perm_name) { // check if at least one of required permissions is set if ($perm_name == 'debug' && $this->Application->isDebugMode(false)) { // universal "debug" permission return true; } elseif ( $perm_name == 'admin' && $this->Application->isAdminUser ) { // any logged-in admin user will suffice return true; } $perm_name = $section.'.'.$perm_name; $perm_status = $this->CheckPermission($perm_name, 1); if (($perm_name == $section.'.add') && $perm_status && ($top_prefix == $event->Prefix)) { // main item, add permission allowed, but ID is > 0, then deny permission // how to get id here } if ($perm_status) { return $perm_status; } } return $this->finalizePermissionCheck($event, $perm_status); } /** * Returns owner + primary category for each item (used for permission checking) * * @param string $prefix * @param string $ids * @param bool $temp_mode * @return Array * @author Alex */ function GetCategoryItemData($prefix, $ids, $temp_mode = false) { if (is_array($ids)) { $ids = implode(',', $ids); } $config = $this->Application->getUnitConfig($prefix); $id_field = $config->getIDField(); $table_name = $config->getTableName(); $ci_table = $this->Application->getUnitConfig('ci')->getTableName(); if ($temp_mode) { $table_name = $this->Application->GetTempName($table_name, 'prefix:' . $prefix); $ci_table = $this->Application->GetTempName($ci_table, 'prefix:' . $prefix); } $owner_field = $config->getOwnerField('CreatedById'); $sql = 'SELECT item_table.'.$id_field.', item_table.'.$owner_field.' AS CreatedById, ci.CategoryId FROM '.$table_name.' item_table LEFT JOIN '.$ci_table.' ci ON ci.ItemResourceId = item_table.ResourceId WHERE item_table.'.$id_field.' IN ('.$ids.') AND (ci.PrimaryCat = 1)'; return $this->Conn->Query($sql, $id_field); } /** * Check category-based permissions for category items * * @param kEvent $event * @param Array $event_perm_mapping * @return bool */ function _frontCheckEventCategoryPermission($event, $event_perm_mapping) { // mapping between specific permissions and common permissions static $perm_mapping = Array( 'add' => 'ADD', 'add.pending' => 'ADD.PENDING', 'edit' => 'MODIFY', 'edit.pending' => 'MODIFY.PENDING', 'delete' => 'DELETE', 'view' => 'VIEW', 'debug' => 'DEBUG', 'admin' => 'ADMIN', ); $top_prefix = $event->getEventParam('top_prefix'); /** @var kCatDBEventHandler $event_handler */ $event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler'); $raise_warnings = $event->getEventParam('raise_warnings'); $event->setEventParam('raise_warnings', 0); if ( $event->Prefix != $top_prefix ) { $top_event = new kEvent($top_prefix . ':' . $event->Name); $id = $event_handler->getPassedID($top_event); } else { $id = $event_handler->getPassedID($event); } $event->setEventParam('raise_warnings', $raise_warnings); $owner_id = USER_ROOT; // owner is root if not detected if ( !$id ) { // item being created -> check by current (before editing started, saved in OnPreCreate event) category permissions // note: category in session is placed on catalog data import start $category_id = $this->Application->isAdmin ? $this->Application->RecallVar('m_cat_id') : $this->Application->GetVar('m_cat_id'); } elseif ( $top_prefix == 'c' || $top_prefix == 'st' ) { $category_id = $id; } else { // item being edited -> check by it's primary category permissions $items_info = $this->GetCategoryItemData($top_prefix, $id); if ( $items_info ) { $category_id = $items_info[$id]['CategoryId']; $owner_id = $items_info[$id]['CreatedById']; } else { // item wasn't found in database $category_id = $this->Application->GetVar('m_cat_id'); } } // specific permission check for pending & owner permissions: begin $uploader_events = Array ('OnUploadFile', 'OnDeleteFile', 'OnViewFile'); if ( in_array($event->Name, $uploader_events) ) { // don't recall target object during uploader-related, because OnItemLoad will use incorrect // $user_id in Firefox (during Flash problems session will be used from Internet Exploere) $new_item = false; } else { $new_item = $this->Application->isAdminUser && $event_handler->isNewItemCreate($event) ? true : false; $check_status = $this->checkCombinedPermissions($event, $owner_id, (int)$category_id, $new_item); } if ( isset($check_status) ) { return $this->finalizePermissionCheck($event, $check_status); } // specific permission check for pending & owner permissions: end $perm_status = false; $check_perms = $this->getPermissionByEvent($event, $event_perm_mapping); if ( $check_perms === true ) { // event is defined in mapping but is not checked by permissions return true; } $item_prefix = $this->Application->getUnitConfig($top_prefix)->getPermItemPrefix(); foreach ($check_perms as $perm_name) { // check if at least one of required permissions is set if ( !isset($perm_mapping[$perm_name]) ) { // not mapped permission (e.g. advanced:approve) -> skip continue; } if ( $perm_name == 'debug' && $this->Application->isDebugMode(false) ) { // universal "debug" permission return true; } elseif ( $perm_name == 'admin' && $this->Application->isAdminUser ) { // any logged-in admin user will suffice return true; } $perm_name = $item_prefix . '.' . $perm_mapping[$perm_name]; $perm_status = $this->CheckPermission($perm_name, 0, (int)$category_id); if ( $perm_status ) { return $perm_status; } } return $this->finalizePermissionCheck($event, $perm_status); } /** * Finalizes permission checking (with additional debug output, when in debug mode) * * @param kEvent $event * @param bool $perm_status * @return bool */ function finalizePermissionCheck($event, $perm_status) { if (!$perm_status) { if (MOD_REWRITE) { // $event->SetRedirectParam('m_cat_id', 0); // category means nothing on admin login screen $event->SetRedirectParam('next_template', 'external:' . $_SERVER['REQUEST_URI']); } else { $event->SetRedirectParam('next_template', $this->Application->GetVar('t')); } if ($this->Application->isDebugMode()) { // for debugging purposes $event->SetRedirectParam('section', $event->getSection()); $event->SetRedirectParam('main_prefix', $event->getEventParam('top_prefix')); $event->SetRedirectParam('event_name', $event->Name); } $event->status = kEvent::erPERM_FAIL; } return $perm_status; } /** * Allows to check combined permissions (*.owner, *.pending) for add/modify/delete operations from admin & front-end * * @param kEvent $event * @param int $owner_id * @param int $category_id * @param bool $new_item * @return mixed */ function checkCombinedPermissions($event, $owner_id, $category_id, $new_item = false) { $ret = null; // true/false when used, null when not used $top_prefix = $event->getEventParam('top_prefix'); // check admin permission if (substr($event->Name, 0, 9) == 'OnPreSave') { if ($new_item) { $ret = $this->AddCheckPermission($category_id, $top_prefix); } else { // add & modify because $new_item is false, when item is aready created & then saved in temp table (even with 0 id) $ret = $this->AddCheckPermission($category_id, $top_prefix) || $this->ModifyCheckPermission($owner_id, $category_id, $top_prefix); } } // check front-end permissions switch ($event->Name) { case 'OnCreate': $ret = $this->AddCheckPermission($category_id, $top_prefix); break; case 'OnUpdate': $ret = $this->ModifyCheckPermission($owner_id, $category_id, $top_prefix); break; case 'OnDelete': case 'OnMassDelete': $ret = $this->DeleteCheckPermission($owner_id, $category_id, $top_prefix); break; } if ($ret === 0) { // permission check failed (user has no permission) $event->status = kEvent::erPERM_FAIL; } return $ret; } /** * Simplified permission check for category items, when adding/editing them from advanced view. * * @param kEvent $event * @param Array $event_perm_mapping * @return mixed */ function CheckEventCategoryPermission($event, $event_perm_mapping) { if (!$this->Application->isAdmin) { // check front-end permission by old scheme return $this->_frontCheckEventCategoryPermission($event, $event_perm_mapping); } if (substr($event->Name, 0, 9) == 'OnPreSave') { // check separately, because permission mapping is not defined for OnPreSave* events $check_perms = Array ('add', 'edit'); } else { $check_perms = $this->getPermissionByEvent($event, $event_perm_mapping); } if ($check_perms === true) { // event is defined in mapping but is not checked by permissions return true; } // 1. most of events does require admin login only $perm_status = $this->Application->isAdminUser; // 2. in case, when event require more, then "view" right, then restrict it to temporary tables only if (!in_array('view', $check_perms)) { $perm_status = $perm_status && $this->Application->IsTempMode($event->Prefix, $event->Special); } return $this->finalizePermissionCheck($event, $perm_status); } function TagPermissionCheck($params, $is_owner = false) { $perm_prefix = getArrayValue($params, 'perm_prefix'); $perm_event = getArrayValue($params, 'perm_event'); $permission_groups = getArrayValue($params, 'permissions'); $check_admin = isset($params['admin']) && $params['admin']; if ($permission_groups && !$perm_event) { // check permissions by permission names in current category $permission_groups = explode('|', $permission_groups); $group_has_permission = false; $perm_category = isset($params['cat_id']) ? $params['cat_id'] : $this->Application->GetVar('m_cat_id'); if ($perm_prefix) { // use primary category of item with id from {perm_prefix}_id as base for permission checking $perm_category = $this->getPrimaryCategory($perm_prefix); } $is_system = isset($params['system']) && $params['system'] ? 1 : 0; foreach ($permission_groups as $permission_group) { $has_permission = true; $permissions = explode(',', $permission_group); if ( $check_admin ) { foreach ($permissions as $permission) { $owner_checked = (strpos($permission, '.OWNER.') !== false) ? $is_owner : true; $has_permission = $has_permission && $this->CheckAdminPermission($permission, $is_system, $perm_category) && $owner_checked; } } else { foreach ($permissions as $permission) { $owner_checked = (strpos($permission, '.OWNER.') !== false) ? $is_owner : true; $has_permission = $has_permission && $this->CheckPermission($permission, $is_system, $perm_category) && $owner_checked; } } $group_has_permission = $group_has_permission || $has_permission; if ($group_has_permission) { return true; } } return false; } elseif ($perm_event) { // check permission by event name list ($prefix, ) = explode(':', $perm_event); /** @var kEventHandler $event_handler */ $event_handler = $this->Application->recallObject($prefix . '_EventHandler'); return $event_handler->CheckPermission( new kEvent($perm_event) ); } return true; } /** * Returns item's primary category (get item_id from request) * * @param string $prefix * @return int */ function getPrimaryCategory($prefix) { $id = $this->Application->GetVar($prefix.'_id'); if (!$id) { return $this->Application->GetVar('m_cat_id'); } $config = $this->Application->getUnitConfig($prefix); $sql = 'SELECT ResourceId FROM '. $config->getTableName() .' WHERE '. $config->getIDField() .' = '.(int)$id; $resource_id = $this->Conn->GetOne($sql); $sql = 'SELECT CategoryId FROM '.$this->Application->getUnitConfig('ci')->getTableName().' WHERE ItemResourceId = '.$resource_id.' AND PrimaryCat = 1'; return $this->Conn->GetOne($sql); } /** * Returns no permission template to redirect to * * @param Array $params * @return Array */ function getPermissionTemplate($params) { $t = $this->Application->GetVar('t'); $next_t = getArrayValue($params, 'next_template'); if ( $next_t ) { $t = $next_t; } $redirect_params = $this->Application->HttpQuery->getRedirectParams(true); if (array_key_exists('pass_category', $params)) { $redirect_params['pass_category'] = $params['pass_cateogry']; } if (MOD_REWRITE) { // TODO: $next_t variable is ignored !!! (is anyone using m_RequireLogin tag with "next_template" parameter?) $redirect_params = Array ( 'm_cat_id' => 0, // category means nothing on admin login screen 'next_template' => 'external:' . $_SERVER['REQUEST_URI'], ); } else { $redirect_params['next_template'] = $t; } if ($this->Application->isAdmin) { $redirect_params['m_wid'] = ''; // remove wid, otherwise parent window may add wid to its name breaking all the frameset (for targets) $redirect_params['pass'] = 'm'; // don't pass any other (except "m") prefixes to admin login template } if (!$this->Application->LoggedIn()) { $redirect_template = array_key_exists('login_template', $params) ? $params['login_template'] : ''; if (!$redirect_template && $this->Application->isAdmin) { $redirect_template = 'login'; } } else { if (array_key_exists('no_permissions_template', $params)) { $redirect_template = $params['no_permissions_template']; } else { $redirect_template = $this->Application->isAdmin ? 'no_permission' : $this->Application->ConfigValue('NoPermissionTemplate'); } if ($this->Application->isDebugMode()) { $redirect_params['from_template'] = 1; $redirect_params['perms'] = $params[ isset($params['permissions']) ? 'permissions' : 'perm_event' ]; } } if (isset($params['index_file']) && $params['index_file']) { $redirect_params['index_file'] = $params['index_file']; } return Array ($redirect_template, $redirect_params); } /** * Check current user permissions based on it's group permissions in specified category (for non-system permissions) or just checks if system permission is set * * @param string $name permission name * @param int $cat_id category id, current used if not specified * @param int $type permission type {1 - system, 0 - per category} * @return int */ function CheckPermission($name, $type = 1, $cat_id = null) { $user_id = $this->Application->RecallVar('user_id'); return $this->CheckUserPermission($user_id, $name, $type, $cat_id); } /** * Check current admin permissions (when called from Front-End) based on it's group permissions in specified category (for non-system permissions) or just checks if system permission is set * * @param string $name permission name * @param int $cat_id category id, current used if not specified * @param int $type permission type {1 - system, 0 - per category} * @return int */ function CheckAdminPermission($name, $type = 1, $cat_id = null) { if ( $this->Application->isAdmin ) { return $this->CheckPermission($name, $type, $cat_id); } $user_id = $this->Application->RecallVar('admin_user_id'); return $this->CheckUserPermission($user_id, $name, $type, $cat_id); } function CheckUserPermission($user_id, $name, $type = 1, $cat_id = null) { $user_id = (int)$user_id; if ( $this->Application->permissionCheckingDisabled($user_id) ) { return substr($name, -5) == '.deny' || $name == 'SYSTEM_ACCESS.READONLY' ? 0 : 1; } if ( !isset($cat_id) ) { $cat_id = $this->Application->GetVar('m_cat_id'); } if ( $type == 1 ) { // "system" permission are always checked per "Home" category (ID = 0) $cat_id = 0; } elseif ( "$cat_id" === "0" ) { $cat_id = $this->Application->getBaseCategory(); } // perm cache is build only based on records in db, that's why if permission is not explicitly denied, then // that (perm cache creator) code thinks that it is allowed & adds corresponding record and code below will // return incorrect results if ( $user_id == $this->Application->RecallVar('user_id') ) { $groups = $this->Application->RecallVar('UserGroups'); } else { // checking not current user $groups = $this->Application->RecallVar('UserGroups:' . $user_id); if ( $groups === false ) { // die('me'); $sql = 'SELECT GroupId FROM '.TABLE_PREFIX.'UserGroupRelations WHERE (PortalUserId = '.$user_id.') AND ( (MembershipExpires IS NULL) OR ( MembershipExpires >= UNIX_TIMESTAMP() ) )'; $groups = $this->Conn->GetCol($sql); array_push($groups, $this->Application->ConfigValue('User_LoggedInGroup') ); $groups = implode(',', $groups); $this->Application->StoreVar('UserGroups:' . $user_id, $groups); } } $groups = explode(',', $groups); $cache_key = $name . '|' . $type . '|' . $cat_id . '|' . implode(',', $groups); $perm_value = $this->Application->getCache('permissions[%' . ($type == 1 ? 'G' : 'C') . 'PermSerial%]:' . $cache_key); if ( $perm_value !== false ) { return $perm_value; } if ( preg_match('/(.*)\.VIEW$/', $name) && ($type == 0) ) { // cached view permission of category: begin if ( $this->Application->ConfigValue('CheckViewPermissionsInCatalog') ) { if ( strpos($cat_id, '|') !== false ) { $category_path = explode('|', substr($cat_id, 1, -1)); $cat_id = end($category_path); } $sql = 'SELECT PermissionConfigId FROM ' . TABLE_PREFIX . 'CategoryPermissionsConfig WHERE PermissionName = ' . $this->Conn->qstr($name); $perm_id = $this->Conn->GetOne($sql); $sql = 'SELECT PermId FROM ' . TABLE_PREFIX . 'CategoryPermissionsCache WHERE (PermId = ' . $perm_id . ') AND (CategoryId = ' . (int)$cat_id . ')'; $view_filters = Array (); foreach ($groups as $group) { $view_filters[] = 'FIND_IN_SET(' . $group . ', ACL)'; } $sql .= ' AND (' . implode(' OR ', $view_filters) . ')'; $perm_value = $this->Conn->GetOne($sql) ? 1 : 0; } else { $perm_value = 1; } $this->Application->setCache('permissions[%CPermSerial%]:' . $cache_key, $perm_value); return $perm_value; // cached view permission of category: end } if ( is_numeric($cat_id) && $cat_id == 0 ) { $cat_hierarchy = Array (0); } else { if ( strpos($cat_id, '|') !== false ) { $cat_hierarchy = $cat_id; } else { $sql = 'SELECT ParentPath FROM ' . $this->Application->getUnitConfig('c')->getTableName() . ' WHERE CategoryId = ' . $cat_id; $cat_hierarchy = $this->Conn->GetOne($sql); if ( $cat_hierarchy === false ) { // category was deleted, but reference to it stays in other tables -> data integrity is broken $cat_hierarchy = '|' . $this->Application->getBaseCategory() . '|'; } } $cat_hierarchy = explode('|', substr($cat_hierarchy, 1, -1)); $cat_hierarchy = array_reverse($cat_hierarchy); array_push($cat_hierarchy, 0); } $perm_value = 0; $groups = implode(',', $groups); foreach ($cat_hierarchy as $category_id) { $sql = 'SELECT SUM(PermissionValue) FROM ' . TABLE_PREFIX . 'Permissions WHERE Permission = "' . $name . '" AND CatId = ' . $category_id . ' AND GroupId IN (' . $groups . ') AND Type = ' . $type; $res = $this->Conn->GetOne($sql); if ( $res !== false && !is_null($res) ) { $perm_value = $res ? 1 : 0; break; } } $this->Application->setCache('permissions[%' . ($type == 1 ? 'G' : 'C') . 'PermSerial%]:' . $cache_key, $perm_value); return $perm_value; } /** * Returns categories, where given permission is set to "1" * * @param string $permission_name * @return Array */ function getPermissionCategories($permission_name) { $groups = $this->Application->RecallVar('UserGroups'); // get categories, where given permission is explicitely defined $sql = 'SELECT SUM(PermissionValue), CatId FROM ' . TABLE_PREFIX . 'Permissions WHERE Permission = "' . $permission_name . '" AND GroupId IN (' . $groups . ') AND Type = 0 GROUP BY CatId'; $permissions = $this->Conn->GetCol($sql, 'CatId'); // get all categories along with their parent path $sql = 'SELECT ParentPath, CategoryId FROM ' . TABLE_PREFIX . 'Categories'; $parent_paths = $this->Conn->GetCol($sql, 'CategoryId'); foreach ($parent_paths as $category_id => $parent_path) { if (array_key_exists($category_id, $permissions)) { // permission for given category is set explicitly continue; } $perm_value = 0; $parent_path = explode('|', substr($parent_path, 1, -1)); $parent_path = array_reverse($parent_path); array_push($parent_path, 0); foreach ($parent_path as $parent_category_id) { if (array_key_exists($parent_category_id, $permissions)) { $perm_value = $permissions[$parent_category_id] ? 1 : 0; break; } } $permissions[$category_id] = $perm_value; } // remove categories, where given permissions is denied foreach ($permissions as $category_id => $perm_value) { if (!$perm_value) { unset($permissions[$category_id]); } } return array_keys($permissions); } /** * Allows to check MODIFY & OWNER.MODFY +/- PENDING permission combinations on item * * @param int $owner_id user_id, that is owner of the item * @param int $category_id primary category of item * @param string $prefix prefix of item * @return int {0 - no MODIFY permission, 1 - has MODIFY permission, 2 - has MODIFY.PENDING permission} */ function ModifyCheckPermission($owner_id, $category_id, $prefix) { $perm_prefix = $this->Application->getUnitConfig($prefix)->getPermItemPrefix(); $live_modify = $this->CheckPermission($perm_prefix.'.MODIFY', ptCATEGORY, $category_id); if ($live_modify) { return 1; } else if ($this->CheckPermission($perm_prefix.'.MODIFY.PENDING', ptCATEGORY, $category_id)) { return 2; } if ($owner_id == $this->Application->RecallVar('user_id')) { // user is item's OWNER -> check this permissions first $live_modify = $this->CheckPermission($perm_prefix.'.OWNER.MODIFY', ptCATEGORY, $category_id); if ($live_modify) { return 1; } else if ($this->CheckPermission($perm_prefix.'.OWNER.MODIFY.PENDING', ptCATEGORY, $category_id)) { return 2; } } return 0; } /** * Allows to check DELETE & OWNER.DELETE permission combinations on item * * @param int $owner_id user_id, that is owner of the item * @param int $category_id primary category of item * @param string $prefix prefix of item * @return int {0 - no DELETE permission, 1 - has DELETE/OWNER.DELETE permission} */ function DeleteCheckPermission($owner_id, $category_id, $prefix) { $perm_prefix = $this->Application->getUnitConfig($prefix)->getPermItemPrefix(); $live_delete = $this->CheckPermission($perm_prefix.'.DELETE', ptCATEGORY, $category_id); if ($live_delete) { return 1; } if ($owner_id == $this->Application->RecallVar('user_id')) { // user is item's OWNER -> check this permissions first $live_delete = $this->CheckPermission($perm_prefix.'.OWNER.DELETE', ptCATEGORY, $category_id); if ($live_delete) { return 1; } } return 0; } /** * Allows to check ADD +/- PENDING permission combinations on item * * @param int $category_id primary category of item * @param string $prefix prefix of item * @return int {0 - no ADD permission, 1 - has ADD permission, 2 - has ADD.PENDING permission} */ function AddCheckPermission($category_id, $prefix) { $perm_prefix = $this->Application->getUnitConfig($prefix)->getPermItemPrefix(); $live_add = $this->CheckPermission($perm_prefix.'.ADD', ptCATEGORY, $category_id); if ($live_add) { return 1; } else if ($this->CheckPermission($perm_prefix.'.ADD.PENDING', ptCATEGORY, $category_id)) { return 2; } return 0; } }