<?php
/** 
 * @package     VikWP - Libraries
 * @subpackage  adapter.application
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * This adapter is required to wrap common wordpress functions
 * within the CMSApplication Joomla interface.
 * This is helpful to improve the portability between Joomla and Wordpress.
 *
 * @since 10.0
 */
class JApplication
{
	/**
	 * Input handler for REQUEST manipulation.
	 *
	 * @var JInput
	 */
	private $input = null;

	/**
	 * The application client name.
	 *
	 * @var string
	 */
	private $name = null;

	/**
	 * Flag to check if the redirect is going to be made via javascript.
	 *
	 * @var boolean
	 */
	private $jsRedirect = false;

	/**
	 * The HTTP header status code.
	 * 
	 * @var integer
	 * @since 10.1.40
	 */
	private $status = null;

	/**
	 * Class constructor.
	 *
	 * @param 	JInput 	$input 	The input handler.
	 */
	public function __construct($input = null)
	{
		// setup application input
		if (is_null($input))
		{
			$this->input = new JInput;
		}
		else
		{
			$this->input = $input;
		}

		// get current page
		global $pagenow;

		if (!$pagenow)
		{
			/**
			 * Current page not yet registered, we are probably under a multi-site,
			 * because the plugins_loaded hook may run before loading vars.php file.
			 *
			 * For this reason, we should auto-fill this global variable in advance
			 * by ourselves, simply by checking whether the request URL ends with
			 * admin-ajax.php
			 *
			 * @since 10.1.33
			 */
			$self = $this->input->server->getString('PHP_SELF');

			if (preg_match("/\/admin-ajax\.php$/i", $self))
			{
				// we reached the admin-ajax.php file, flag it
				$pagenow = 'admin-ajax.php';
			}
		}
		
		/**
		 * Set application client according to the side we
		 * are located. In case of AJAX end-point, we should
		 * fetch the client according to a reserved key
		 * that should be set in request.
		 *
		 * @since 10.1.31
		 */
		if ($pagenow !== 'admin-ajax.php')
		{
			// rely on the location of the file
			$this->name = is_admin() && $pagenow != 'admin-post.php' ? 'administrator' : 'site';
		}
		else
		{
			// rely on the AJAX reserved key
			$this->name = $this->input->get('vik_ajax_client', 'administrator');
		}
	}

	/**
	 * Magic method to access private properties.
	 *
	 * @param 	string 	$name 	The property to access.
	 *
	 * @return 	mixed 	The property.
	 */
	public function __get($name)
	{
		if ($name == 'input')
		{
			return $this->input;	
		}

		return null;
	}

	/**
	 * Returns a property of the object or the default value if the property is not set.
	 *
	 * @param 	string  $key 	The name of the property.
	 * @param 	mixed   $def 	The default value (optional) if none is set.
	 *
	 * @return  mixed   The value of the configuration.
	 */
	public function get($key, $def = null)
	{
		/**
		 * The configuration is now parsed within a separated object.
		 *
		 * @see 	JConfig
		 * @since 	10.1.4
		 */
		return JFactory::getConfig()->get($key, $def);
	}

	/**
	 * Returns a property of the object or the default value if the property is not set.
	 *
	 * @uses 	get()
	 */
	public function getCfg($key, $def = null)
	{
		return $this->get($key, $def);
	}

	/**
	 * Modifies a property of the object, creating it if it does not already exist.
	 *
	 * @param 	string 	$key 	  The name of the property.
	 * @param 	mixed 	$val 	  The value of the property to set (optional).
	 * @param 	mixed 	$network  An optional flag to check whether the option
	 * 							  should be updated for the current blog or for
	 * 							  a different one (@since 10.1.31).
	 * 							  - false  only the current blog will be updated;
	 * 							  - true   all the network blogs will be updated;
	 * 							  - int    only the specified blog will be updated.
	 *
	 * @return  mixed   Previous value of the property.
	 *
	 * @uses 	get()
	 */
	public function set($key, $val = null, $network = false)
	{
		$prev = $this->get($key);

		if ($network === false || !is_multisite())
		{
			// use default function
			update_option($key, $val);
		}
		else
		{
			if ($network === true)
			{
				// get all network sites
				$sites = array_map(function($site)
				{
					// take only the blog ID
					return $site->blog_id;
				}, get_sites());
			}
			else
			{
				// create a list with the specified blog ID
				$sites = array((int) $network);
			}

			// update all existing networks
			foreach ($sites as $blog_id)
			{
				// switch to blog
				switch_to_blog($blog_id);

				// update network option
				update_option($key, $val);

				// restore previous blog
				restore_current_blog();
			}
		}

		return $prev;
	}

	/**
	 * Is admin interface?
	 *
	 * @return  boolean  True if this application is administrator.
	 *
	 * @uses 	isClient()
	 */
	public function isAdmin()
	{
		return $this->isClient('administrator');
	}

	/**
	 * Is site interface?
	 *
	 * @return  boolean  True if this application is site.
	 *
	 * @uses 	isClient()
	 */
	public function isSite()
	{
		return $this->isClient('site');
	}

	/**
	 * Check the client interface by name.
	 *
	 * @param   string   $identifier  String identifier for the application interface.
	 *
	 * @return  boolean  True if this application is of the given type client interface.
	 */
	public function isClient($identifier)
	{
		return $this->name === $identifier;
	}

	/**
	 * Forces the application to run under the specified client.
	 *
	 * @param   string   $identifier  String identifier for the application interface.
	 *
	 * @return  void
	 * 
	 * @since 	10.1.39
	 */
	public function setClient($identifier)
	{
		$this->name = $identifier;
	}

	/**
	 * Gets a user state.
	 *
	 * @param 	string 	$key 	  The key of the user state.
	 * @param 	mixed 	$default  The default value for the state if not found.
	 *
	 * @return  mixed 	The user state.
	 *
	 * @since 	10.1.15
	 */
	public function getUserState($key, $default = null)
	{
		$session = JFactory::getSession();

		// extract user state from session
		return $session->get($key, $default, 'jsession.userstate');
	}

	/**
	 * Sets the value of a user state variable.
	 *
	 * @param   string  $key    The key of the user state.
	 * @param   mixed   $value  The value of the variable.
	 *
	 * @return  mixed   The previous state, if one existed.
	 *
	 * @since   10.1.15
	 */
	public function setUserState($key, $value)
	{
		// get previous state, if any
		$old = $this->getUserState($key);

		$session = JFactory::getSession();

		// update session with specified state
		$session->set($key, $value, 'jsession.userstate');

		// return previous state
		return $old;
	}

	/**
	 * Gets the value of a user state variable.
	 *
	 * @param 	string 	$key 	  The key of the user state variable.
	 * @param 	string 	$request  The name of the variable passed in a request.
	 * @param 	mixed 	$default  The default value for the variable if not found.
	 * @param 	string 	$type 	  Filter for the variable.
	 *
	 * @return  mixed 	The request user state.
	 *
	 * @uses 	getUserState()
	 * @uses 	setUserState()
	 */
	public function getUserStateFromRequest($key, $request, $default = null, $type = 'none')
	{
		// try to get value from the request
		$val = $this->input->get($request, null, $type);

		if (!is_null($val))
		{
			// the value exists, register the user state for later use and return it
			$this->setUserState($key, $val);

			return $val;
		}

		// Otherwise try to access the current user state.
		// Returns default value if user state was not previously set.
		return $this->getUserState($key, $default);
	}

	/**
	 * Enqueue a system message.
	 *
	 * @param   string  $msg   The message to enqueue.
	 * @param   string  $type  The message type (success, notice, warning or error).
	 *
	 * @return  void
	 */
	public function enqueueMessage($msg, $type = 'success')
	{
		$session = JFactory::getSession();

		// use a different namespace for each application client
		$namespace = 'jsession.' . $this->name . '.system';

		// create system message object
		$obj = new stdClass;
		$obj->message = $msg;
		$obj->type 	  = $type;

		// get queue from the session (an empty array if not set)
		$queue = $session->get('messagesqueue', array(), $namespace);

		// push the object only if it is not already in the queue
		if (!in_array($obj, $queue))
		{
			$queue[] = $obj;
			$session->set('messagesqueue', $queue, $namespace);
		}
	}

	/**
	 * Returns the queue containing the system messages.
	 *
	 * @return 	array 	The messages list.
	 */
	public function getMessagesQueue()
	{
		$session = JFactory::getSession();

		// use a different namespace for each application client
		$namespace = 'jsession.' . $this->name . '.system';

		// get queue from the session (an empty array if not set)
		$queue = $session->get('messagesqueue', array(), $namespace);

		// flush the system queue to avoid displaying duplicated messages
		$session->clear('messagesqueue', $namespace);

		return $queue;
	}

	/**
	 * Returns the application JMenu object.
	 *
	 * @param   string  $name     The name of the application/client.
	 * @param   array   $options  An optional associative array of configuration settings.
	 *
	 * @return  JMenu|null
	 *
	 * @since   10.1.19
	 */
	public function getMenu($name = null, array $options = array())
	{
		if (!isset($name))
		{
			$name = $this->name;
		}

		// inject this application object into the JMenu tree if one isn't already specified
		if (!isset($options['app']))
		{
			$options['app'] = $this;
		}

		try
		{
			// load JMenu file
			JLoader::import('adapter.menu.menu');

			// try to obtain a valid menu instance
			$menu = JMenu::getInstance($name, $options);
		}
		catch (Exception $e)
		{
			return null;
		}

		return $menu;
	}

	/**
	 * Returns the application JPathway object.
	 *
	 * @param   string  $name     The name of the application.
	 * @param   array   $options  An optional associative array of configuration settings.
	 *
	 * @return  JPathway|null
	 *
	 * @since   10.1.19
	 */
	public function getPathway($name = null, $options = array())
	{
		if (!isset($name))
		{
			$name = $this->name;
		}

		try
		{
			// load JPathway file
			JLoader::import('adapter.pathway.pathway');

			// try to obtain a valid pathway
			$pathway = JPathway::getInstance($name, $options);
		}
		catch (Exception $e)
		{
			return null;
		}

		return $pathway;
	}

	/**
	 * Returns the application JRouter object.
	 *
	 * @param   string   $name     The name of the application.
	 * @param   array    $options  An optional associative array of configuration settings.
	 *
	 * @return  JRouter  A JRouter object.
	 *
	 * @throws  Exception
	 *
	 * @since 	10.1.19
	 */
	public static function getRouter($name = null, array $options = array())
	{
		try
		{
			// load JRouter file
			JLoader::import('adapter.router.router');

			// try to obtain a valid router, if any
			$router = JRouter::getInstance($name ?: 'site', $options);
		}
		catch (Exception $e)
		{
			return null;
		}

		return $router;
	}

	/**
	 * Registers a handler to a particular event group.
	 *
	 * @param   string    $event    The event name.
	 * @param   callable  $handler  The handler, a function or an instance of an event object.
	 *
	 * @return  self  	  This object to support chaining.
	 *
	 * @since   10.1.30
	 */
	public function registerEvent($event, $handler)
	{
		// proxy for JEventDispatcher
		return JEventDispatcher::getInstance()->register($event, $handler);
	}

	/**
	 * Calls all handlers associated with an event group.
	 *
	 * @param   string  $event  The event name.
	 * @param   array   $args   An array of arguments (optional).
	 *
	 * @return  array   An array of results from each function call, or null if no dispatcher is defined.
	 *
	 * @since   10.1.30
	 */
	public function triggerEvent($event, ?array $args = null)
	{
		// proxy for JEventDispatcher
		return JEventDispatcher::getInstance()->trigger($event, $args);
	}

	/**
	 * Redirect to another URL.
	 *
	 * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently"
	 * or "303 See Other" code in the header pointing to the new location. If the headers have already been
	 * sent this will be accomplished using a JavaScript statement.
	 *
	 * @param   string   $url     The URL to redirect to. Can only be http/https URL.
	 * @param   integer  $status  The HTTP 1.1 status code to be provided. 303 is assumed by default.
	 *
	 * @return  void
	 *
	 * @uses 	shouldRedirect()
	 *
	 * @link 	https://developer.wordpress.org/reference/functions/wp_redirect/
	 */
	public function redirect($url, $status = 303)
	{
		if ($this->isAdmin())
		{
			// if the URL starts with index.php, replace it into admin.php
			if (strpos($url, 'index.php?') === 0)
			{
				$url = str_replace('index.php?', 'admin.php?', $url);
			}
			// else if the URL starts with ?, prepend admin.php
			else if (strpos($url, '?') === 0)
			{
				$url = 'admin.php' . $url;
			}

			// change end-point in case we are doing AJAX
			if (wp_doing_ajax())
			{
				/**
				 * @note 	this is not really used for AJAX calls,
				 * 			but for redirects between "iframe" pages
				 * 			(contents rendered via AJAX in modal boxes).
				 */
				$url = str_replace('admin.php', 'admin-ajax.php', $url);
			}
		}
		else
		{
			// if the URL is "index.php", we probably need to visit the home page
			if ($url == 'index.php')
			{
				$url = JUri::root();
			}

			// we don't need to route URLs that start with "index.php" because
			// we are (probably) already under a rewritten page and this means
			// that the plugin is going to be processed correctly.
		}

		// redirect is allowed only if the headers haven't been sent yet
		if (!headers_sent())
		{
			wp_redirect($url, $status);
			exit;
		}

		// JS redirect only once
		if (!$this->shouldRedirect())
		{
			// register JS redirect
			$this->jsRedirect = true;

			// otherwise redirect using javascript
			echo "<script>document.location.href='" . str_replace("'", '&apos;', $url) . "';</script>\n";
		}
	}

	/**
	 * Checks if the application is going to do a JS redirect.
	 * The javascript redirect is applied when the headers have been already sent.
	 *
	 * @return 	boolean  True if JS redirect, otherwise false.
	 */
	public function shouldRedirect()
	{
		return $this->jsRedirect;
	}

	/**
	 * Login authentication function.
	 *
	 * @param   array 	 $credentials  Array('username' => string, 'password' => string)
	 * @param   array 	 $options      Array('remember' => boolean)
	 *
	 * @return  boolean  True on success, false if failed.
	 */
	public function login($credentials, $options = array())
	{
		$login = array();
		$login['user_login'] 	= $credentials['username'];
		$login['user_password'] = $credentials['password'];
		$login['remember']		= isset($options['remember']) ? $options['remember'] : false;

		$options['redirect']	= isset($options['redirect']) ? $options['redirect'] : '';

		// direct login only if the headers haven't been sent
		if (!headers_sent())
		{
			$res = wp_signon($login);

			return $res instanceof WP_User;
		}
		// otherwise use <form> workaround to dispatch wp-login.php
		else
		{
			$url = wp_login_url($options['redirect']);
			$url .= (strpos($url, '?') !== false ? '&' : '?') . 'action=login';

			?>

			<form action="<?php echo $url; ?>" method="post" name="loginform" id="loginform">
				<input type="hidden" name="log" value="<?php echo $login['user_login']; ?>" />
				<input type="hidden" name="pwd" value="<?php echo $login['user_password']; ?>" />
				<input type="hidden" name="rememberme" value="<?php echo $login['remember'] ? '1' : ''; ?>" />
			</form>

			<script>
				document.loginform.submit();
			</script>

			<?php
		}
	}

	/**
	 * Log the current user out, by destroying the current user session.
	 * It takes a non-used parameter for compatibility with other CMS.
	 * 
	 * @param 	int 	$uid 		the id of the current user logged in
	 *
	 * @return 	void
	 * 
	 * @since 	10.1.5
	 */
	public function logout($uid)
	{
		wp_logout();
	}

	/**
	 * Method to close the application.
	 *
	 * @param   integer  $code  The exit code (optional; default is 0).
	 *
	 * @return  void
	 *
	 * @since   10.1.33
	 */
	public function close($code = 0)
	{
		exit($code);
	}

	/**
	 * Method to set a response header. If the replace flag is set then all headers
	 * with the given name will be replaced by the new one. The headers are stored
	 * in an internal array to be sent when the site is dispatched to the browser.
	 *
	 * @param   string   $name     The name of the header to set.
	 * @param   string   $value    The value of the header to set.
	 * @param   boolean  $replace  True to replace any headers with the same name.
	 *
	 * @return  self     This object to support chaining.
	 *
	 * @since   10.1.33
	 */
	public function setHeader($name, $value, $replace = false)
	{
		/**
		 * We have to treat the status in a slightly different way because WordPress is unable
		 * to properly send this header. All the headers registered through the "wp_headers"
		 * hook are always sent with the "{KEY}: {VALUE}" format, while the status should be
		 * sent as {PROTOCOL} {CODE} {DESCRIPTION}.
		 * 
		 * @since 10.1.40
		 */
		if (strtolower($name) === 'status')
		{
			if (is_null($this->status) || $replace)
			{
				// update HTTP status
				$this->status = (int) $value;
			}
		}
		else
		{
			// register filter to attach the given header into the WP pool
			add_filter('wp_headers', function($headers) use ($name, $value, $replace)
			{
				if (!isset($headers[$name]) || $replace)
				{
					// set/replace header with the given value
					$headers[$name] = $value;
				}

				return $headers;
			});
		}

		return $this;
	}

	/**
	 * Method to get the array of response headers to be sent when
	 * the response is dispatched to the client.
	 *
	 * @return  array
	 *
	 * @since   10.1.33
	 */
	public function getHeaders()
	{
		global $wp;

		$headers = [];

		if (!is_null($this->status))
		{
			// manually register the status header within the pool
			$headers['status'] = $this->status;
		}

		/**
		 * Filters the HTTP headers before they're sent to the browser.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $headers Associative array of headers to be sent.
		 * @param WP       $this    Current WordPress environment instance.
		 */
		return apply_filters('wp_headers', $headers, $wp);
	}

	/**
	 * Method to clear any set response headers.
	 *
	 * @return  self  This object to support chaining.
	 *
	 * @since   10.1.33
	 */
	public function clearHeaders()
	{
		// remove all the callbacks assigned to this hook
		remove_filter('wp_headers');

		// clear the previously registered HTTP status code too
		$this->status = null;

		return $this;
	}

	/**
	 * Sends the response headers.
	 *
	 * @return  self  This object to support chaining.
	 *
	 * @since   10.1.33
	 */
	public function sendHeaders()
	{
		global $wp;

		// send headers through WP
		$wp->send_headers();

		/**
		 * In case we have a registered HTTP status code, send this header.
		 * 
		 * @since 10.1.40
		 */
		if (!is_null($this->status))
		{
			status_header($this->status);
		}

		return $this;
	}
}
