<?php

class ameRoleUtils {
	/**
	 * Retrieve a list of all known, non-meta capabilities of all roles.
	 *
	 * @param bool $include_multisite_caps
	 * @return array Associative array with capability names as keys
	 */
	public static function get_all_capabilities($include_multisite_caps = null) {
		if ( $include_multisite_caps === null ) {
			$include_multisite_caps = is_multisite();
		}

		//Cache the results.
		static $regular_cache = null, $multisite_cache = null;
		if ( $include_multisite_caps ) {
			if ( isset($multisite_cache) ) {
				return $multisite_cache;
			}
		} else if ( isset($regular_cache) ) {
			return $regular_cache;
		}

		$wp_roles = self::get_roles();
		$capabilities = [];

		//Iterate over all known roles and collect their capabilities
		foreach ($wp_roles->roles as $role) {
			if ( !empty($role['capabilities']) && is_array($role['capabilities']) ) { //Being defensive here
				//We use the "+" operator instead of array_merge() to combine arrays because we don't want
				//integer keys to be renumbered. Technically, capabilities should be strings and not integers,
				//but in practice some plugins do create integer capabilities.
				$capabilities = $capabilities + $role['capabilities'];
			}
		}
		$regular_cache = $capabilities;

		//Add multisite-specific capabilities (not listed in any roles in WP 3.0)
		if ( $include_multisite_caps ) {
			$multisite_caps = [
				'manage_sites'           => 1,
				'manage_network'         => 1,
				'manage_network_users'   => 1,
				'manage_network_themes'  => 1,
				'manage_network_options' => 1,
				'manage_network_plugins' => 1,
			];
			$capabilities = $capabilities + $multisite_caps;
			$multisite_cache = $capabilities;
		}

		return $capabilities;
	}

	/**
	 * Retrieve a list of all known roles and their names.
	 *
	 * @return array Associative array with role IDs as keys and role display names as values
	 */
	public static function get_role_names() {
		$wp_roles = self::get_roles();
		$roles = [];

		foreach ($wp_roles->roles as $role_id => $role) {
			$roles[$role_id] = $role['name'];
		}

		return $roles;
	}

	/**
	 * Get the global WP_Roles instance.
	 *
	 * @return WP_Roles
	 * @global WP_Roles $wp_roles
	 */
	public static function get_roles() {
		//Requires WP 4.3.0.
		return wp_roles();
	}

	/**
	 * Get the user object based on a user ID.
	 *
	 * In most cases, when this plugin needs to retrieve a user, it is the current user. This method
	 * attempts to make that common case faster and avoid creating duplicate WP_User instances.
	 *
	 * Note: This was originally a private method in the core class. It was moved here to make it
	 * available to other modules. Compatibility-related comments and special cases were kept.
	 *
	 * @param int $user_id
	 * @return WP_User|null
	 */
	public static function get_user_by_id($user_id) {
		static $isGettingCurrentUser = false;

		//Usually, pluggable functions will already be loaded by this point,
		//but there is at least one plugin that indirectly triggers this method
		//before wp_get_current_user() is available by checking user caps early.
		//
		//At least one plugin can enter infinite recursion if we call wp_get_current_user()
		//here. To prevent that, avoid nested calls and fall back to get_user_by().
		if ( function_exists('wp_get_current_user') && !$isGettingCurrentUser ) {
			$isGettingCurrentUser = true;
			try {
				$current_user = wp_get_current_user();
			} finally {
				$isGettingCurrentUser = false;
			}

			if ( $current_user && ($current_user->ID == $user_id) ) {
				return $current_user;
			}
		}

		if ( function_exists('get_user_by') ) {
			$user = get_user_by('id', $user_id);
			if ( $user === false ) {
				return null;
			} else {
				return $user;
			}
		}

		return null;
	}
}

class ameActorAccessCleaner {
	/**
	 * @var array<string, bool>
	 */
	private $userExistsCache = [];

	/**
	 * Remove non-existent actors from a dictionary where the keys are actor IDs.
	 *
	 * @param array|mixed $actorDictionary
	 * @return array|mixed
	 */
	public function cleanUpDictionary($actorDictionary) {
		if ( !is_array($actorDictionary) ) {
			return $actorDictionary;
		}
		if ( empty($actorDictionary) ) {
			return $actorDictionary;
		}

		$cleanedDictionary = [];
		foreach ($actorDictionary as $actorId => $value) {
			//Keep the entry if the actor exists. To reduce the risk of future bugs causing data
			//loss, avoid removing data associated with unknown/unsupported actor IDs.
			if ( $this->tryActorExists($actorId, true) ) {
				$cleanedDictionary[$actorId] = $value;
			}
		}
		return $cleanedDictionary;
	}

	/**
	 * @param string $actorId
	 * @return bool
	 * @throws \ameUnsupportedActorIdException
	 */
	public function actorExists($actorId) {
		$parts = explode(':', $actorId, 2);
		if ( count($parts) !== 2 ) {
			throw new ameUnsupportedActorIdException('Actor ID must contain a colon character.');
		}

		switch ($parts[0]) {
			case 'user':
				return $this->userExists($parts[1]);
			case 'role':
				return $this->roleExists($parts[1]);
			case 'special':
				return $parts[1] == 'super_admin';
		}

		throw new ameUnsupportedActorIdException('Unsupported actor ID prefix "' . $parts[0] . '".');
	}

	/**
	 * Like actorExists(), but returns a default value if it can't parse the actor ID
	 * instead of throwing an exception.
	 *
	 * @param string $actorId
	 * @param bool $defaultResult
	 * @return bool
	 */
	public function tryActorExists($actorId, $defaultResult) {
		try {
			return $this->actorExists($actorId);
		} catch (ameUnsupportedActorIdException $e) {
			return $defaultResult;
		}
	}

	/**
	 * Check if a role exists on the current site.
	 *
	 * @param string $roleId
	 * @return bool
	 */
	public function roleExists($roleId) {
		$role = get_role($roleId);
		return !empty($role);
	}

	/**
	 * Check if a user exists in the database.
	 *
	 * @param $userLogin
	 * @return bool
	 */
	public function userExists($userLogin) {
		//The admin menu (or other configuration) can contain multiple references to the same user,
		//so we cache the results to avoid redundant trips the WP cache or, worse, the database.
		if ( !isset($this->userExistsCache[$userLogin]) ) {
			$user = get_user_by('login', $userLogin);
			$this->userExistsCache[$userLogin] = !empty($user);
		}
		return $this->userExistsCache[$userLogin];
	}
}

class ameUnsupportedActorIdException extends Exception {
}

abstract class ameAccessEvaluatorConfigFields {
	/**
	 * @var \WPMenuEditor
	 */
	protected $menuEditor = null;

	/**
	 * @var bool|null
	 */
	protected $superAdminDefaultAccess = null;

	/**
	 * @var bool|null
	 */
	protected $roleDefaultAccess = null;

	/**
	 * @var array<string, bool>
	 */
	protected $perRoleDefaultAccess = [];

	/**
	 * @var bool
	 */
	protected $defaultEvaluationResult = false;

	public function configToJs() {
		$roleDefault = $this->roleDefaultAccess;
		if ( ($roleDefault === null) && !empty($this->perRoleDefaultAccess) ) {
			$roleDefault = $this->perRoleDefaultAccess;
		}

		return [
			'noValueDefault'      => $this->defaultEvaluationResult,
			'roleDefault'         => $roleDefault,
			'superAdminDefault'   => $this->superAdminDefaultAccess,
			'roleCombinationMode' => 'Some',
		];
	}
}

class ameAccessEvaluatorBuilder extends ameAccessEvaluatorConfigFields {
	public function __construct($menuEditor) {
		$this->menuEditor = $menuEditor;
	}

	public static function create($menuEditor) {
		return new ameAccessEvaluatorBuilder($menuEditor);
	}

	public function superAdminDefault($defaultValue) {
		$this->superAdminDefaultAccess = $defaultValue;
		return $this;
	}

	public function roleDefault($defaultValue) {
		$this->roleDefaultAccess = $defaultValue;
		return $this;
	}

	public function roleDefaultFor($roleId, $defaultValue) {
		$this->perRoleDefaultAccess[$roleId] = $defaultValue;
		return $this;
	}

	public function defaultResult($result) {
		$this->defaultEvaluationResult = $result;
		return $this;
	}

	/**
	 * @return \ameActorAccessEvaluator
	 */
	public function build() {
		return new ameActorAccessEvaluator($this);
	}

	/**
	 * @param WP_User $user
	 * @return \ameAccessEvaluatorWithUser
	 */
	public function buildForUser($user) {
		return new ameAccessEvaluatorWithUser($this, $user);
	}
}

abstract class ameBaseActorAccessEvaluator extends ameAccessEvaluatorConfigFields {
	/**
	 * @param \ameAccessEvaluatorBuilder $builder
	 */
	public function __construct($builder) {
		$this->superAdminDefaultAccess = $builder->superAdminDefaultAccess;
		$this->roleDefaultAccess = $builder->roleDefaultAccess;
		$this->perRoleDefaultAccess = $builder->perRoleDefaultAccess;
		$this->defaultEvaluationResult = $builder->defaultEvaluationResult;
		$this->menuEditor = $builder->menuEditor;
	}

	/**
	 * @param array<string,bool> $enabledForActor
	 * @param string $userActorId
	 * @param bool $isSuperAdmin
	 * @param array<string,string> $roleActors Role ID => Actor ID
	 * @return bool
	 */
	protected function evaluate($enabledForActor, $userActorId, $isSuperAdmin, $roleActors) {
		//User-specific settings have the highest priority.
		if ( isset($enabledForActor[$userActorId]) ) {
			return $enabledForActor[$userActorId];
		}

		//Super Admin is next.
		if ( $isSuperAdmin ) {
			if ( isset($enabledForActor['special:super_admin']) ) {
				return $enabledForActor['special:super_admin'];
			} else if ( $this->superAdminDefaultAccess !== null ) {
				return $this->superAdminDefaultAccess;
			}
		}

		//Finally, allow access if at least one role has access.
		$hasAccess = null;
		foreach ($roleActors as $roleId => $roleActorId) {
			$roleHasAccess = null;

			if ( isset($enabledForActor[$roleActorId]) ) {
				$roleHasAccess = $enabledForActor[$roleActorId];
			} else if ( $this->roleDefaultAccess !== null ) {
				$roleHasAccess = $this->roleDefaultAccess;
			} else if ( isset($this->perRoleDefaultAccess[$roleId]) ) {
				$roleHasAccess = $this->perRoleDefaultAccess[$roleId];
			}

			if ( $roleHasAccess !== null ) {
				if ( $hasAccess === null ) {
					$hasAccess = $roleHasAccess;
				} else {
					$hasAccess = $hasAccess || $roleHasAccess;
				}
			}
		}

		if ( $hasAccess !== null ) {
			return $hasAccess;
		}
		return $this->defaultEvaluationResult;
	}

	/**
	 * @param WP_User $user
	 * @return array<string,string> Role ID => Actor ID
	 */
	protected function getUserRoleActors($user) {
		$roles = $this->menuEditor->get_user_roles($user);
		$roleActors = [];
		foreach ($roles as $roleId) {
			$roleActors[$roleId] = 'role:' . $roleId;
		}
		return $roleActors;
	}
}

class ameActorAccessEvaluator extends ameBaseActorAccessEvaluator {
	/**
	 * @param \WP_User $user
	 * @param array<string,bool> $enabledForActor
	 * @return bool
	 */
	public function isEnabledForUser($user, $enabledForActor) {
		return $this->evaluate(
			$enabledForActor,
			'user:' . $user->user_login,
			is_multisite() && is_super_admin($user->ID),
			$this->getUserRoleActors($user)
		);
	}

	/**
	 * Alias for isEnabledForUser().
	 *
	 * @param \WP_User $user
	 * @param array<string,bool> $enabledForActor
	 * @return bool
	 */
	public function userHasAccess($user, $enabledForActor) {
		return $this->isEnabledForUser($user, $enabledForActor);
	}
}

class ameAccessEvaluatorWithUser extends ameBaseActorAccessEvaluator {
	/**
	 * @var WP_User
	 */
	private $user;

	private $userActorId;
	private $isSuperAdmin;
	private $roleActors;

	/**
	 * @param \ameAccessEvaluatorBuilder $builder
	 * @param \WP_User $user
	 */
	public function __construct($builder, $user) {
		parent::__construct($builder);
		$this->user = $user;

		$this->userActorId = 'user:' . $user->user_login;
		$this->isSuperAdmin = is_multisite() && is_super_admin($user->ID);
		$this->roleActors = $this->getUserRoleActors($user);
	}

	/**
	 * @param array<string,bool> $enabledForActor
	 * @return bool
	 */
	public function isEnabled($enabledForActor) {
		return $this->evaluate(
			$enabledForActor,
			$this->userActorId,
			$this->isSuperAdmin,
			$this->roleActors
		);
	}

	/**
	 * Alias for isEnabled().
	 *
	 * @param array<string,bool> $enabledForActor
	 * @return bool
	 */
	public function userHasAccess($enabledForActor) {
		return $this->isEnabled($enabledForActor);
	}

	/**
	 * Get the user for which this evaluator was created.
	 *
	 * @return WP_User
	 */
	public function getUser() {
		return $this->user;
	}
}
