<?php
/**
* @version	$Id: permissions_helper.php 15137 2012-03-04 08:06:21Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class kPermissionsHelper extends kHelper {

		/**
		 * Current set of permissions for group being edited
		 *
		 * @var Array
		 */
		var $Permissions = Array();

		function LoadPermissions($group_id, $cat_id, $type = 1, $prefix = '')
		{
			$perm_table = $this->Application->getUnitOption('perm', 'TableName');
			$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 <strong>' . $top_prefix . ' <- ' . $event->Prefix . ':' . $event->Name . '</strong>');
			}

			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;
				}

				$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);
			}
			$id_field = $this->Application->getUnitOption($prefix, 'IDField');
			$table_name = $this->Application->getUnitOption($prefix, 'TableName');
			$ci_table = $this->Application->getUnitOption('ci', 'TableName');

			if ($temp_mode) {
				$table_name = $this->Application->GetTempName($table_name, 'prefix:' . $prefix);
				$ci_table = $this->Application->GetTempName($ci_table, 'prefix:' . $prefix);
			}

			$owner_field = $this->Application->getUnitOption($prefix, 'OwnerField');
			if (!$owner_field) {
				$owner_field = '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'
			);

			$top_prefix = $event->getEventParam('top_prefix');
			$event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler');
			/* @var $event_handler kCatDBEventHandler */

			$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->getUnitOption($top_prefix, 'PermItemPrefix');
			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;
				}

				$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', urlencode('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);

				$event_handler = $this->Application->recallObject($prefix . '_EventHandler');
				/* @var $event_handler kEventHandler */

				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_field = $this->Application->getUnitOption($prefix, 'IDField');
			$table_name = $this->Application->getUnitOption($prefix, 'TableName');
			$id = $this->Application->GetVar($prefix.'_id');

			if (!$id) {
				return $this->Application->GetVar('m_cat_id');
			}

			$sql = 'SELECT ResourceId
					FROM '.$table_name.'
					WHERE '.$id_field.' = '.(int)$id;
			$resource_id = $this->Conn->GetOne($sql);

			$sql = 'SELECT CategoryId
					FROM '.$this->Application->getUnitOption('ci', 'TableName').'
					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' => urlencode('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 <a> 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 ( $user_id == USER_ROOT ) {
				// "root" is allowed anywhere
				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 ( 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;

				$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->getUnitOption('c', 'TableName') . '
							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->getUnitOption($prefix, 'PermItemPrefix');

			$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->getUnitOption($prefix, 'PermItemPrefix');

			$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->getUnitOption($prefix, 'PermItemPrefix');

			$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;
		}
	}