<?php

/* ======================================================================*\
  || #################################################################### ||
  || # vBulletin 5.0.0 Beta 10
  || # ---------------------------------------------------------------- # ||
  || # Copyright �2000-2012 vBulletin Solutions Inc. All Rights Reserved. ||
  || # This file may not be redistributed in whole or significant part. # ||
  || # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
  || # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
  || #################################################################### ||
  \*====================================================================== */

class vB_PermissionContext
{

	protected $primary_group_id;
	protected $secondary_group_ids;
	protected $infraction_group_ids;
	/**
	 *
	 * @var vB_Datastore
	 */
	protected $datastore;
	protected $group_ids = null;
	protected $permissions = null;
	protected $forumPerms = false;
	protected $channelPermissions;
	/**Once we've checked permissions let's retain that data. **/
	protected $perms = array();
	protected $permissionsFrom = array();
	public function __construct($datastore, $primary_group_id, $secondary_group_ids = array(), $infraction_group_ids = array())
	{
		$this->datastore = $datastore;

		// If $primary_group_id is 0, we assume that it's guest usergroup
		if (!$primary_group_id)
		{
			$primary_group_id = 1;
		}
		$this->primary_group_id = $primary_group_id;
		$this->secondary_group_ids = $secondary_group_ids;
		$this->infraction_group_ids = $infraction_group_ids;

		$this->group_ids = $this->getUserGroups();
		$this->buildBasicPermissions();
	}

	public function getPermission($group)
	{
		return $this->permissions[$group];
	}

	/**
	 * Returns permissions array. This is used for unit testing
	 *
	 * @param bool $get_bitmap
	 * @param bool $get_limit
	 * @return array
	 */
	public function getPermissions($get_bitmap = TRUE, $get_limit = TRUE)
	{
		$result = array();

		foreach ($this->permissions as $key=>$value)
		{
			if ($this->isLimitPermission($key))
			{
				if ($get_limit)
				{
					$result[$key] = $value;
				}
			}
			else
			{
				if ($get_bitmap)
				{
					$result[$key] = $value;
				}
			}
		}

		return $result;
	}

	public function isLimitPermission($permission)
	{
		$intperms = $this->datastore->get_value('bf_misc_intperms');
		return array_key_exists($permission, $intperms);
	}

	/**
	 * Return an array with usergroups that can provide permissions
	 * Extracted from includes/functions.php::cache_permissions
	 * @return array
	 */
	public function getUserGroups()
	{
		$usergroupcache = $this->datastore->get_value('usergroupcache');
		$bf_ugp_genericoptions = $this->datastore->get_value('bf_ugp_genericoptions');

		if (empty($this->secondary_group_ids) OR !($usergroupcache[$this->primary_group_id]['genericoptions'] & $bf_ugp_genericoptions['allowmembergroups']))
		{
			return array($this->primary_group_id);
		}
		else
		{
			return array_unique(array_merge(array($this->primary_group_id), (array)$this->secondary_group_ids));
		}
	}

	/**
	 * Sets the permissions attribute with basic permissions
	 * Adapted from includes/functions.php::cache_permissions
	 */
	protected function buildBasicPermissions()
	{
		//If we have infraction group let's get the overriding perm usergroup
		$assertor = vB::getDbAssertor();
		$groupIds = array();
		if (!empty($this->infraction_group_ids))
		{
			foreach ($this->infraction_group_ids as $group)
			{
				$groupInfo = $assertor->getRow('vBForum:infractiongroup', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
					'infractiongroupid' => $group ));

				if (!empty($groupInfo))
				{
					$groupIds[] = $groupInfo['orusergroupid'];
				}
			}

			$this->infraction_group_ids = array_unique($groupIds);
		}

		$usergroupcache = $this->datastore->get_value('usergroupcache');
		$bf_ugp = $this->datastore->get_value('bf_ugp');
		$bf_misc_intperms = $this->datastore->get_value('bf_misc_intperms');

		if (count($this->group_ids) == 1)
		{
			$permissions = $usergroupcache[$this->group_ids[0]];
		}
		else
		{
			// initialise fields to 0
			foreach ($bf_ugp AS $dbfield => $permfields)
			{
				$permissions["$dbfield"] = 0;
			}

			// return the merged array of all user's membergroup permissions
			foreach ($this->group_ids AS $usergroupid)
			{
				foreach ($bf_ugp AS $dbfield => $permfields)
				{
					//Some permissions- initially the create permissions- are only available at the channel level.
					if (isset($usergroupcache["$usergroupid"]["$dbfield"]))
					{
						$permissions["$dbfield"] |= $usergroupcache["$usergroupid"]["$dbfield"];
					}
				}
				foreach ($bf_misc_intperms AS $dbfield => $precedence)
				{
					// put in some logic to handle $precedence
					if (!isset($intperms["$dbfield"]))
					{
						$intperms["$dbfield"] = $usergroupcache["$usergroupid"]["$dbfield"];
					}
					else if (!$precedence)
					{
						if ($usergroupcache["$usergroupid"]["$dbfield"] > $intperms["$dbfield"])
						{
							$intperms["$dbfield"] = $usergroupcache["$usergroupid"]["$dbfield"];
						}
					}
					else if ($usergroupcache["$usergroupid"]["$dbfield"] == 0 OR (isset($intperms["$dbfield"]) AND $intperms["$dbfield"] == 0))
					{
						// Set value to 0 as it overrides all
						$intperms["$dbfield"] = 0;
					}
					else if ($usergroupcache["$usergroupid"]["$dbfield"] > $intperms["$dbfield"])
					{
						$intperms["$dbfield"] = $usergroupcache["$usergroupid"]["$dbfield"];
					}
				}
			}
			$permissions = array_merge($permissions, $intperms);
		}

		if (!empty($this->infraction_group_ids))
		{
			foreach ($this->infraction_group_ids AS $usergroupid)
			{
				foreach ($bf_ugp AS $dbfield => $permfields)
				{
					if (isset($usergroupcache["$usergroupid"]["$dbfield"]))
					{
						$permissions["$dbfield"] &= $usergroupcache["$usergroupid"]["$dbfield"];
					}
				}
				foreach ($bf_misc_intperms AS $dbfield => $precedence)
				{
					if (!$precedence)
					{
						if ($usergroupcache["$usergroupid"]["$dbfield"] < $permissions["$dbfield"])
						{
							$permissions["$dbfield"] = $usergroupcache["$usergroupid"]["$dbfield"];
						}
					}
					else if ($usergroupcache["$usergroupid"]["$dbfield"] < $permissions["$dbfield"] AND $usergroupcache["$usergroupid"]["$dbfield"] != 0)
					{
						$permissions["$dbfield"] = $usergroupcache["$usergroupid"]["$dbfield"];
					}
				}
			}
		}

		$this->permissions = & $permissions;
	}

	/**
	 * Does this group have the requested system permissions
	 * Adapted from vB_Legacy_CurrentUser::hasPermission
	 *
	 * @param string $group Permission group the permission is in
	 * @param string $permission Name of permission
	 * @return boolean
	 */
	public function hasPermission($group, $permission)
	{
		if (!$this->isLimitPermission($permission))
		{
			$bf_ugp = $this->datastore->get_value('bf_ugp_' . $group);
			if (isset($this->permissions[$group]) AND isset($bf_ugp[$permission]))
			{
				return (bool) ($this->permissions[$group] & $bf_ugp[$permission]);
			}
			else
			{
				return false;
			}
		}
		else
		{
			// todo: throw an exception?
			return false;
		}
	}

	public function getLimit($permission)
	{
		if ($this->isLimitPermission($permission))
		{
			return $this->permissions[$permission];
		}
		else
		{
			// this is not an intperm, throw an exception?
			return -1;
		}
	}


	/**** Returns the available permissions
	 *
	 *	@return 	mixed
	 **/
	public function getForumPerms()
	{
		if ($this->forumPerms === false)
		{
			$this->forumPerms = $this->datastore->get_value('bf_ugp');
		}
		return $this->forumPerms;
	}

	/**** Returns a list of forums this usergroup can view.
	 *
	 *	@param	integer	the usergroup id
	 *
	 *	@return 	array		an array of 'canRead' and 'cantRead', each an array
	 **/
	public function getForumAccess($usergroupid)
	{
		//canview is defined as 1
		$canview = 1;
		//see if we already have a record
		if (isset($this->perms[$usergroupid]) && $this->perms[$usergroupid])
		{
			return $this->perms[$usergroupid];
		}

		$hashkey = 'vb_readperms';
		//See if we have a cached value
		$perms = vB_Cache::instance()->read($hashkey);
		if (isset($perms[$usergroupid]))
		{
			$this->perms = $perms;
			return $perms[$usergroupid];
		}

		//we need to build the permissions. First let's do the CMS.
		$perms = $this->buildPerms();
		return $perms[$usergroupid];
	}

	/**** Returns a list of forums this usergroup can view.
	 *
	 *	@param	integer	the usergroup id
	 *
	 *	@return 	array		an array of nodeids which might contains canRead, cantRead, canModerate, cantModerate
	 **/
	public function getSingleForumAccess($usergroupid, $nodeid = false)
	{
		//see if we already have a record
		if (isset($this->perms[$usergroupid]))
		{
			$perms = $this->perms;
		}
		else
		{
			$hashkey = 'vb_readperms';
			//See if we have a cached value
			$perms = vB_Cache::instance()->read($hashkey);

			if (isset($perms[$usergroupid]))
			{
				$this->perms = $perms;
			}
			else
			{
				$perms = $this->buildPerms();
			}
		}

		if (empty($perms[$usergroupid]))
		{
			return array();
		}
		else
		{
			if ($nodeid)
			{
				//we want for a single node
				$result = array($nodeid => array());
				foreach ($perms[$usergroupid]['canRead'] AS $permNodeid)
				{
					if ($permNodeid == $nodeid)
					{
						$result[$nodeid][] = 'canRead';
						break;
					}
				}

				//If we have canRead we don't have to check cantRead
				if (empty($perms['canRead']))
				{
					foreach ($perms[$usergroupid]['cantRead'] AS $permNodeid)
					{
						if ($permNodeid == $nodeid )
						{
							$result[$nodeid][] = 'cantRead';
							break;
						}
					}
				}

				foreach ($perms[$usergroupid]['canModerate'] AS $permNodeid)
				{
					if ($permNodeid == $nodeid )
					{
						$result[$nodeid][] = 'canModerate';
						break;
					}
				}

				//If we have canModerate we don't have to check cantModerate
				if (empty($perms['canModerate']))
				{
					foreach ($perms[$usergroupid]['cantModerate'] AS $permNodeid)
					{
						if ($permNodeid == $nodeid )
						{
							$result[$nodeid][] = 'cantModerate';
							break;
						}
					}
				}

				// perm missing
				if (
					(!in_array('canModerate', $result[$nodeid]) AND !in_array('cantModerate', $result[$nodeid]))
																OR
					(!in_array('canRead', $result[$nodeid]) AND !in_array('cantRead', $result[$nodeid]))
				)
				{
					// let's check if we can grab it from $permissionsFrom
					if (isset($this->permissionsFrom[$usergroupid][$nodeid]) AND $this->permissionsFrom[$usergroupid][$nodeid])
					{
							if ((bool)$this->getChannelPerm($usergroupid, 'moderatorpermissions', 'canmoderateposts', $this->permissionsFrom[$usergroupid][$nodeid]))
							{
								$result[$nodeid][] = 'canModerate';
							}
							else
							{
								$result[$nodeid][] = 'cantModerate';
							}

							if ((bool)$this->getChannelPerm($usergroupid, 'forumpermissions', 'canview', $this->permissionsFrom[$usergroupid][$nodeid]))
							{
								$result[$nodeid][] = 'canRead';
							}
							else
							{
								$result[$nodeid][] = 'cantRead';
							}
					}
					else
					{
						// get perms from parents...
						$parentage = vB_Library::instance('node')-> fetchClosureParent($nodeid);

						$parents = array();
						foreach ($parentage as $parent)
						{
							$parents[] = $parent['parent'];
						}

						$parentsFrom = array();
						foreach (array("canmoderateposts" => "moderatorpermissions", "canview" => "forumpermissions") AS $permission => $permgroup)
						{
							$permSet = false;
							foreach ($parents AS $parent)
							{
								if ($this->getChannelPermSet($usergroupid, $parent))
								{
									if ((bool)$this->getChannelPerm($usergroupid, $permgroup, $permission, $parent))
									{
										$permName = ($permission == 'canmoderateposts') ? 'canModerate' : 'canRead';
									}
									else
									{
										$permName = ($permission == 'canmoderateposts') ? 'cantModerate' : 'cantRead';
									}

									$result[$nodeid][] = $permName;
									$permSet = true;
									$this->permissionsFrom[$usergroupid][$nodeid] = $parent;
									break;
								}

								$parentsFrom[] = $parent;
							}

							// store value
							if (!$permSet)
							{
								$result[$nodeid][] = ($permission == 'canmoderateposts') ? 'cantModerate' : 'cantRead';
								$this->permissionsFrom[$usergroupid][$nodeid] = false;
							}
						}

						// and let's store info in permissionsFrom
						foreach ($parentsFrom AS $parent)
						{
							$this->permissionsFrom[$usergroupid][$parent] = $this->permissionsFrom[$usergroupid][$nodeid];
						}
					}
				}
			}
			else
			{
				//We didn't get a nodeid.
				//We need to convert the four permission arrays into
				// $node=>array Of Permissions
				$result = array();
				foreach ($perms[$usergroupid] AS $permType => $nodes)
				{
					foreach ($nodes AS $permNodeid)
					{
						if (empty($result[$permNodeid]))
						{
							$result[$permNodeid] = array();
						}
						$result[$permNodeid][] = $permType;
					}
				}

			}

			return $result;
		}
	}

	/** Build and caches the permissions array
	*
	*/
	protected function buildPerms()
	{
		//we need to build the permissions.
		//Most of the time the user will have access to the root. If that's the case
		// then we can ignore all the other read access values.
		$perms = array();
		//let's get a list of all the groups and their CMS access
		$assertor = vB::getDbAssertor();
		$groupquery = $assertor->assertQuery('usergroup', array(vB_dB_Query::TYPE_KEY =>  vB_dB_Query::QUERY_SELECT));
		$groups = array();

		$sections = array();
		$parents = array();

		if ($groupquery AND $groupquery->valid())
		{
			$groupinfo = $groupquery->current();
			while($groupquery->valid())
			{
				$perms[$groupinfo['usergroupid']] = array('canRead' => array(), 'cantRead' => array(),
					'canModerate' => array(), 'cantModerate' => array());
				$groupinfo = $groupquery->next();
			}
		}

		$bf_ugp = $this->datastore->get_value('bf_ugp_forumpermissions');
		$bf_mod = $this->datastore->get_value('bf_misc_moderatorpermissions');

		//Now get the permissions.
		$permquery = $assertor->assertQuery('vBForum:permission', array(vB_dB_Query::TYPE_KEY =>  vB_dB_Query::QUERY_SELECT));

		if ($permquery AND $permquery->valid())
		{
			$permission = $permquery->current();
			while($permquery->valid())
			{
				if ($permission['forumpermissions'] & $bf_ugp['canview'])
				{
					$perms[$permission['groupid']]['canRead'][] = $permission['nodeid'];
				}
				else
				{
					$perms[$permission['groupid']]['cantRead'][] = $permission['nodeid'];
				}

				if ($permission['moderatorpermissions'] & $bf_mod['canmoderateposts'])
				{
					$perms[$permission['groupid']]['canModerate'][] = $permission['nodeid'];
				}
				else
				{
					$perms[$permission['groupid']]['cantModerate'][] = $permission['nodeid'];
				}
				$permission = $permquery->next();
			}
		}

		$this->perms = $perms;

		$hashkey = 'vb_readperms';
		vB_Cache::instance()->write($hashkey, $perms, 1440, 'perms_changed');
		return $perms;
	}


	/** This checks to see if a specific permission has been set for a specific usergroup in a channel
	 *
	 *	@param	int	usergroup id
	 * 	@param	int	channel id
	 *
	 ***/
	public function getChannelPermSet($usergroupid, $channelid)
	{
		//make sure permissions are set
		if (empty($this->channelPermissions[$usergroupid]))
		{
			$this->loadChannelPermissions($usergroupid);
			$this->validateInfractionPermissions($usergroupid, $channelid);
		}
		return array_key_exists($channelid, $this->channelPermissions[$usergroupid]);

	}

	/*** Does the actual bitmap check for a single usergroup in a channel, after we have all the data loaded
	 *
	 * @param integer	the usergroup id
	 * @param string	the name of the permission group
	 *	@param string	the name of the permission
	 * @param int		the channel to check. Note that we may inherit from a parent.
	 *
	 *	@return	boolean
	 *
	 ***/
	public function getChannelPerm($usergroupid, $permissiongroup, $permission, $channelid)
	{
		if ($permissiongroup == 'moderatorpermissions')
		{
			$bf_ugp = $this->datastore->get_value('bf_misc_' . $permissiongroup);
		}
		else
		{
			$bf_ugp = $this->datastore->get_value('bf_ugp_' . $permissiongroup);
		}

		if (empty($this->channelPermissions[$usergroupid]))
		{
			$this->loadChannelPermissions($usergroupid);
			$this->validateInfractionPermissions($usergroupid, $channelid);
		}

		if (isset($this->channelPermissions[$usergroupid][$channelid]) AND
				isset($this->channelPermissions[$usergroupid][$channelid][$permission]))
		{
			return $this->channelPermissions[$usergroupid][$channelid][$permission];
		}

		switch($permissiongroup)
		{
			case 'forumpermissions':
				return $bf_ugp[$permission] & $this->channelPermissions[$usergroupid][$channelid]['forumpermissions'];
				break;
			case 'createpermissions':
				//We might have gotten an integer;
				if (is_numeric($permission))
				{
					$permission = strtolower(vB_Types::instance()->getContentTypePackage($permission) . '_' .
						vB_Types::instance()->getContentTypeClass($permission) );
				}
				else
				{
					$permission = strtolower($permission);
				}

				return $bf_ugp[$permission] & $this->channelPermissions[$usergroupid][$channelid]['createpermissions'];
				break;
			case 'moderatorpermissions':
				return $bf_ugp[$permission] & intval($this->channelPermissions[$usergroupid][$channelid]['moderatorpermissions']);
				break;
			default:
				return false
				;
		} // switch

	}


	/** Clears all existing permissions. Needed primarily in test.
	*
	* 	@param	int		optional usergroup
	*
	**/
	public function clearChannelPermissions($usergroupid = false)
	{
		if ($usergroupid)
		{
			unset($this->channelPermissions[$usergroupid]);
			unset($this->perms[$usergroupid]);
		}
		else
		{
			$this->channelPermissions = false;
		}

		$this->permissionsFrom = false;
		$this->permissions = false;
	}


	/***** This loads a permission group from the database and caches the results
	 *
	 *		@param 	int		$usergroupid
	 *
	 ***/
	public function loadChannelPermissions($usergroupid)
	{
		$hashkey = 'channelperms_' . $usergroupid;
		$writelock = true;
		$this->channelPermissions[$usergroupid] = vB_Cache::instance()->read($hashkey, $writelock, false);
		//See if we got a result.
		if (!$this->channelPermissions[$usergroupid])
		{
			$this->channelPermissions[$usergroupid] = vB::getDbAssertor()->getRows('vBForum:permission', array('groupid' => $usergroupid), false, 'nodeid');

			vB_Cache::instance()->write($hashkey, $this->channelPermissions[$usergroupid], 1440, 'perms_changed');
		}
	}

	protected function validateInfractionPermissions($usergroupid, $channelid)
	{
		/** Check infraction groups */
		if (!empty($this->infraction_group_ids))
		{
			/** Temp array for infraction perms */
			$iPerms = array("permissionid" => $this->channelPermissions[$usergroupid][$channelid]["permissionid"], "nodeid" => $this->channelPermissions[$usergroupid][$channelid]["nodeid"], "groupid" => $this->channelPermissions[$usergroupid][$channelid]["groupid"]);
			foreach ($this->infraction_group_ids as $usergroup)
			{
				$this->loadChannelPermissions($usergroup);
				foreach ($this->channelPermissions[$usergroup][$channelid] as $permGroup => $permVal)
				{
					if (!in_array($permGroup, array('nodeid', 'permissionid', 'groupid')))
					{
						if (!in_array($permGroup, array('forumpermissions', 'moderatorpermissions', 'createpermissions')))
						{
							$iPerms[$permGroup] = (isset($iPerms[$permGroup]))
								? min($iPerms[$permGroup], $permVal)
								: min($this->channelPermissions[$usergroupid][$channelid][$permGroup], $permVal);
						}
						else
						{
							$iPerms[$permGroup] = (isset($iPerms[$permGroup]))
								? ($iPerms[$permGroup] & $permVal)
								: $permVal;
						}
					}
				}
			}
			$this->channelPermissions[$usergroupid][$channelid] = $iPerms;
		}
	}

	/** Gets any admin usergroups. This is defined as one that can administer permissions
	 *
	 *	@return		mixed		array of integers
	 **/
	public static function getAdminUsergroups($all = true)
	{
		$usergroupcache = vB::getDatastore()->getValue('usergroupcache');
		$bf_ugp = vB::getDatastore()->get_value('bf_ugp');
		$adminMask = $bf_ugp['adminpermissions']['cancontrolpanel'];

		$group = 0;
		$groups = array();
		foreach($usergroupcache AS $usergroup)
		{
			if ($usergroup['adminpermissions'] & $adminMask)
			{
				if ($all)
				{
					$groups[] = $usergroup['usergroupid'];
				}
				else
				{
					$group = ($group ? $group : $usergroup['usergroupid']);
				}
			}
		}

		return ($all ? $groups : $group);
	}


	/** Gets any super moderator usergroups. This is defined as one that has the ismoderator permission
	 *
	 *	@return		mixed		array of integers
	 **/
	public static function getModeratorUsergroups($all = true)
	{
		$usergroupcache = vB::getDatastore()->getValue('usergroupcache');
		$bf_ugp = vB::getDatastore()->get_value('bf_ugp');
		$modMask = $bf_ugp['adminpermissions']['ismoderator'];

		$group = 0;
		$groups = array();
		foreach($usergroupcache AS $usergroup)
		{
			if ($usergroup['adminpermissions'] & $modMask)
			{
				if ($all)
				{
					$groups[] = $usergroup['usergroupid'];
				}
				else
				{
					$group = ($group ? $group : $usergroup['usergroupid']);
				}
			}
		}

		return ($all ? $groups : $group);
	}


	/** Gets an admin user. This is defined as one that can administer the admincp
	 *
	 *	return	integer		a userid with admin permissions
	 **/
	public static function getAdminUser()
	{
		//First see if we have somebody configured as superadmin.
		$config = vB::getConfig();
		if (!empty($config['SpecialUsers']['superadministrators']))
		{
			$superAdmins = explode(",", $config['SpecialUsers']['superadministrators']);
			return current($superAdmins);
		}

		//See if we have somebody with an admin user as primary usergroup
		$usergroups = self::getAdminUsergroups();

		if (empty($usergroups))
		{
			return false;
		}

		$user = vB::getDbAssertor()->getRow('user', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			vB_dB_Query::COLUMNS_KEY => 'userid', 'usergroupid' => $usergroups));

		if (!empty($user))
		{
			return $user['userid'];
		}
		//Last chance- see if we have somebody who has one of the admin groups as a secondary id. This is potentially very expensive.
		//so it's a real last resort.
		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			vB_dB_Query::CONDITIONS_KEY => array(array('field' => 'membergroupids', vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_INCLUDES)));
		$assertor = vB::getDbAssertor();
		foreach($usergroups AS $usergroup)
		{
			$data[vB_dB_Query::CONDITIONS_KEY][0]['value'] = $usergroup;
			$test = $assertor->assertQuery('user', $data);
			//There's a dangerous condition we need to check. Let's say a site has more than eleven usergroups, and usergroup 2 has the admin permission. We need to
			//may have gotten usergroup 12 or 20.
			if ($test->valid())
			{
				foreach($test as $user)
				{
					$memberGroups = explode(',', $user['membergroupids']);
					if (in_array($usergroup,$memberGroups))
					{
						return $user['userid'];
					}
				}
			}
		}
		//out of things to try;
		return false;
	}
}

