<?php
/*!
* Jetpack CRM
* https://jetpackcrm.com
*
* Client Portal Module
*
*/
namespace Automattic\JetpackCRM;

defined( 'ZEROBSCRM_PATH' ) || exit( 0 );

require_once plugin_dir_path( __FILE__ ) . 'class-client-portal-endpoint.php';

/**
 * 
 * Client Portal Module class for Jetpack CRM.
 * To add a new endpoint use one of the existing endpoints located inside the 
 * './endpoints' folder.
 */
class Client_Portal {
	public $router    = null;
	public $render    = null;
	public $endpoints = null;

	/**
	 * The class constructor initializes the attribbutes and calls an init function.
	 * 
	 */
	public function __construct() {
		require_once plugin_dir_path( __FILE__ ) . 'class-client-portal-render-helper.php';
		require_once plugin_dir_path( __FILE__ ) . 'class-client-portal-router.php';

		$this->router = new Client_Portal_Router();
		$this->render = new Client_Portal_Render_Helper( $this );

		// Initializes it later. Priority 10
		add_action( 'init', array( $this, 'init' ) );
	}

	/**
	 * Initializes the Client Portal Module.
	 * Mainly sets ups all needed hooks.
	 *
	 */
	function init() {
		// Adding the shortcode function for the Client Portal.
		add_shortcode( 'jetpackcrm_clientportal', array( $this, 'client_portal_shortcode' ) );
		add_shortcode( 'zerobscrm_clientportal',  array( $this, 'client_portal_shortcode' ) );
		// Basic theme support (here for now, probs needs option).
		add_filter( 'body_class', array( $this, 'portal_theme_support' ) );
		// Fixes a bug when the Client Portal is set to the homepage (more info: gh-15).
		add_filter( 'redirect_canonical', array( $this, 'redirect_fix_portal_as_homepage' ), 10, 2 );
		// Hook used by our custom rewrite rules.
		add_filter( 'query_vars', array( $this, 'get_portal_query_vars' ), 0 );
		// Styles needed by the Client Portal.
		add_action( 'zbs_enqueue_scripts_and_styles', array( $this, 'portal_enqueue_scripts_and_styles' ) );
		// Custom login redirect hook (this one is in our $router).
		add_action( 'login_redirect', array( $this->router, 'redirect_contacts_upon_login' ), 10, 3 );
		// Initializes all endpoints (including the ones from external plugins).
		$this->init_endpoints();
		// this catches failed logins, checks if from our page, then redirs
		// From mr pippin https://pippinsplugins.com/redirect-to-custom-login-page-on-failed-login/
		add_action( 'wp_login_failed', array( $this, 'portal_login_fail_redirect' ) );  // hook failed login
	}

	/**
	 *
	 */
	public function add_endpoint_class_folder( $endpoint_folder_path ) {
		$endpoint_directory = glob( $endpoint_folder_path . '/class*endpoint.php' );
		foreach ( $endpoint_directory as $endpoint_file ) {
			require_once $endpoint_file;
			// Gets the filename without the ';php' suffix. e.g. 'class-single-invoice-endpoint'.
			$base_filename = basename( $endpoint_file, '.php' );
			// Turns the snake case filename into pascal case separated by '_'. e.g. 'Class_Single_Invoice_Endpoint'
			$pascal_case_filename = str_replace('-', '_', ucwords($base_filename, '-'));
			// Removes the 'Class' prefix and adds the hardcoded namespace. e.g. 'Automattic\JetpackCRM\SingleInvoiceEndpoint'
			$endpoint_class = 'Automattic\JetpackCRM\\' . str_replace('Class_', '', $pascal_case_filename);
			// Registers the endpoint
			$this->endpoints = $endpoint_class::register_endpoint($this->endpoints, $this);
		}
	}

	public function sort_endpoints_by_menu_order() {
		// Sort all endpoints by their order
		usort ( $this->endpoints, function( $endpoint_a, $endpoint_b ) {
			if ( $endpoint_a->menu_order == $endpoint_b->menu_order ) {
				return 0;
			} else {
    			return ( $endpoint_a->menu_order < $endpoint_b->menu_order ) ? -1 : 1;
			}
		} );
	}

	/**
	 *  Initializes all the endpoints for the Client Portal
	 */
	public function init_endpoints() {
		// Since this is the init function, we should start with an empty array.
		$this->endpoints = array();
		// By default we load all classes in the endpoints folder.
		$this->add_endpoint_class_folder( plugin_dir_path( __FILE__ ) . 'endpoints' );
		// Allowing plugins to declare their endpoint classes.
		do_action( 'jpcrm_client_portal_register_endpoint', $this );

		do_action( 'jpcrm_client_portal_post_init_endpoints', $this );

		$this->sort_endpoints_by_menu_order();
		$this->add_all_rewrite_endpoints();
	}

	/**
	 * Sorts out the stylesheet includes.
	 *
	 */
	function portal_enqueue_scripts_and_styles() {
		global $zbs;

		wp_enqueue_style( 'zbs-portal', plugins_url( '/css/jpcrm-public-portal' . wp_scripts_get_suffix() . '.css', __FILE__ ), array(), $zbs::VERSION );
		wp_enqueue_style( 'zbs-fa', ZEROBSCRM_URL . 'build/lib/font-awesome/css/font-awesome.min.css', array(), $zbs::VERSION );

		// This do_action call was left here for compatibility purposes (legacy).
		do_action('zbs_enqueue_portal', 'zeroBS_portal_enqueue_stuff');
		// This new action should be used for newer implementations.
		do_action('jpcrm_enqueue_client_portal_styles');
	}

	/**
	 * Function used to offer css support for some themes.
	 */
	function portal_theme_support( $classes = array() ) {
		$theme_slug = get_stylesheet();

		switch( $theme_slug ) {
			case 'twentyseventeen':
				$classes[] ='zbs-theme-support-2017';
				break;
			case 'twentynineteen':
				$classes[] = 'zbs-theme-support-2019';
				break;
			case 'twentytwenty':
				$classes[] = 'zbs-theme-support-2020';
				break;
			case 'twentytwentyone':
				$classes[] = 'zbs-theme-support-2021';
				break;
			case 'twentytwentytwo':
				$classes[] = 'zbs-theme-support-2022';
				break;
		}
		return $classes;
	}

	/**
	* Locate template.
	*
	* Locate the called template.
	* Search Order:
	* 1. /themes/theme/zerobscrm-plugin-templates/$template_name
	* 2. /themes/theme/$template_name
	* 3. /plugins/portal/v3/templates/$template_name.
	*
	* @since 1.2.7
	*
	* @param string $template_name Template to load.
	* @param string $string $template_path Path to templates.
	* @param string $default_path Default path to template files.
	* @return string Path to the template file.
	*/
	function locate_template( $template_name, $template_path = '', $default_path = '' ) {
		// Set variable to search in zerobscrm-plugin-templates folder of theme.
		if ( ! $template_path ) :
			$template_path = 'zerobscrm-plugin-templates/';
		endif;
		// Set default plugin templates path.
		if ( ! $default_path ) :
			$default_path = ZEROBSCRM_PATH . 'modules/portal/templates/'; // Path to the template folder
		endif;
		// Search template file in theme folder.
		$template = locate_template( array(
			$template_path . $template_name,
			$template_name
		) );
		// Get plugins template file.
		if ( ! $template ) :
			$template = $default_path . $template_name;
		endif;
		return apply_filters( 'locate_template', $template, $template_name, $template_path, $default_path );
	}

	/**
	* Get template.
	*
	* Search for the template and include the file.
	*
	* @since 1.2.7
	*
	* @see get_template()
	*
	* @param string $template_name Template to load.
	* @param array $args Args passed for the template file.
	* @param string $string $template_path Path to templates.
	* @param string $default_path Default path to template files.
	*/
	function get_template( $template_name, $args = array(), $tempate_path = '', $default_path = '' ) {

		if ( is_array( $args ) && isset( $args ) ) :
			extract( $args );
		endif;	
		$template_file = $this->locate_template( $template_name, $tempate_path, $default_path );
		if ( ! file_exists( $template_file ) ) :
			_doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $template_file ) ), '1.0.0' );
			return;
		endif;
		include_once $template_file;
	}

	// this handles contact detail updates via $_POST from the client portal
	// this is a #backward-compatibility landmine; proceed with caution (see gh-1642)
	function jpcrm_portal_update_details_from_post($cID=-1 ){

		global $zbs, $zbsCustomerFields;

		/**
		* This gets fields hidden in Client Portal settings.
		* Eventually we should expand this to preprocess and filter
		* the following fields altogether if disabled:
		*   - countries: zeroBSCRM_getSetting('countries')
		*   - second addresses: zeroBSCRM_getSetting('secondaddress')
		*   - all addresses: zeroBSCRM_getSetting('showaddress')
		*   - not sure what this is: $zbs->settings->get('fieldhides')
		*/
		$hidden_fields    = $zbs->settings->get( 'portal_hidefields' );
		$hidden_fields    = !empty( $hidden_fields ) ? explode( ',', $hidden_fields ) : array();
		$read_only_fields = $zbs->settings->get( 'portal_readonlyfields' );
		$read_only_fields = !empty( $read_only_fields ) ? explode( ',', $read_only_fields ) : array();

		// get existing contact data
		$old_contact_data = $zbs->DAL->contacts->getContact( $cID );

		// downgrade to old-style second address keys so that field names match the object generated by zeroBS_buildContactMeta()
		$key_map = array(
			'secaddr_addr1' => 'secaddr1',
			'secaddr_addr2' => 'secaddr2',
			'secaddr_city' => 'seccity',
			'secaddr_county' => 'seccounty',
			'secaddr_country' => 'seccountry',
			'secaddr_postcode' => 'secpostcode'
		);
		foreach ( $key_map as $newstyle_key => $oldstyle_key ) {
			if ( isset( $old_contact_data[$newstyle_key] ) ){
				$old_contact_data[$oldstyle_key] = $old_contact_data[$newstyle_key];
				unset($old_contact_data[$newstyle_key]);
			}
		}

		// create new (sanitised) contact data from $_POST
		$new_contact_data = zeroBS_buildContactMeta( $_POST, $old_contact_data ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		// process fields
		$fields_to_change = array();
		foreach ( $new_contact_data as $key => $value ) {
			// check for hidden or read only field groups
			$is_hidden_or_readonly_field_group = false;
			if ( isset( $zbsCustomerFields[$key] ) && isset( $zbsCustomerFields[$key]['area'] ) ) {
				$area_key = ( $zbsCustomerFields[$key]['area'] == "Main Address" ) ? 'jpcrm-main-address' : '';
				$area_key = ( $zbsCustomerFields[$key]['area'] == "Second Address" ) ? 'jpcrm-main-address' : $area_key;
				if ( in_array( $area_key, $hidden_fields ) || in_array( $area_key, $read_only_fields ) ) {
					$is_hidden_or_readonly_field_group = true;
				}
			}

			// if invalid or unauthorised field, keep old value
			if ( !isset( $zbsCustomerFields[$key] ) || in_array( $key, $hidden_fields ) || in_array( $key, $read_only_fields) || $is_hidden_or_readonly_field_group ) {
				$new_contact_data[$key] = $old_contact_data[$key];
			}

			// collect fields that changed
			elseif ( $old_contact_data[$key] != $value ) {
				$fields_to_change[] = $key;
			}
		}
		// update contact if fields changed
		if ( count( $fields_to_change ) > 0 ) {

			$cID = $zbs->DAL->contacts->addUpdateContact(
				array(
					'id'    =>  $cID,
					'data'  => $new_contact_data,
					'do_not_update_blanks' => false
				)
			);


			// update log if contact update was successful
			if ( $cID ){

				// build long description string for log
				$longDesc = '';
				foreach ( $fields_to_change as $field ) {
					if ( !empty( $longDesc ) ) {
						$longDesc .= '<br>';
					}
					$longDesc .= sprintf( '%s: <code>%s</code> → <code>%s</code>', $field, $old_contact_data[$field], $new_contact_data[$field]);
				}

				zeroBS_addUpdateLog(
					$cID,
					-1,
					-1,
					array(
						'type' => __( 'Details updated via Client Portal', 'zero-bs-crm' ),
						'shortdesc' => __( 'Contact changed some of their details via the Client Portal', 'zero-bs-crm' ),
						'longdesc' => $longDesc,
					),
					'zerobs_customer'
				);

				echo "<div class='zbs_alert'>" . esc_html__( 'Details updated.', 'zero-bs-crm') . "</div>";

			}
			else {
				echo "<div class='zbs-alert-danger'>" . esc_html__( 'Error updating details!', 'zero-bs-crm' ) . "</div>";
			}
		}

		return $cID;
	}

	/**
	 * Checks if a user has "enabled" or "disabled" access.
	 * 
	 * @return bool True if the user is enabled in the Client Portal.
	 */
	function is_user_enabled() {
		// cached?
		if (defined('ZBS_CURRENT_USER_DISABLED')) return false;

		global $wpdb;
		$uid = get_current_user_id();
		$cID = zeroBS_getCustomerIDFromWPID($uid);

		// these ones definitely work
		$uinfo = get_userdata( $uid );
		$potentialEmail = ''; if (isset($uinfo->user_email)) $potentialEmail = $uinfo->user_email;
		$cID = zeroBS_getCustomerIDWithEmail($potentialEmail);

		$disabled = zeroBSCRM_isCustomerPortalDisabled($cID);

		if (!$disabled) return true;

		// cache to avoid multi-check
		define('ZBS_CURRENT_USER_DISABLED',true);
		return false;

	}

	/**
	 * Fixes a bug when the Client Portal is set to the homepage.
	 */
	function redirect_fix_portal_as_homepage( $redirect_url, $requested_url ) {
		// When the Client Portal is set to the homepage we have to allow the slug
		// to be used for the child pages. We have to do this because WordPress will
		// redirect child pages to the root (e.g. '/clients/invoices' to '/invoices')
		// when the Client Portal is set to the homepage. This will fix it.
		if ( $this->is_a_client_portal_endpoint() ) {
			return $requested_url;
		}

		return $redirect_url;
	}

	function add_all_rewrite_endpoints() {
		foreach ( $this->endpoints as $endpoint ) {
			if ( $endpoint->add_rewrite_endpoint ) {
				$slug = $endpoint->slug;
				// TODO: remove reliance on Client Portal Pro from Core
				if ( function_exists( 'zeroBSCRM_clientPortalgetEndpoint' ) ) {
					$slug = zeroBSCRM_clientPortalgetEndpoint( $slug );
				}
				add_rewrite_endpoint( $slug, EP_ROOT | EP_PAGES );
			}
		}
		jpcrm_client_portal_flush_rewrite_rules_if_needed();
	}

	/**
	 * Returns the query vars associated with the Client Portal.
	 *
	 * @return array The list of the query vars associated with the Client Portal.
	 */
	function get_portal_query_vars( $vars ) {
		foreach ( $this->endpoints as $endpoint ) {
			if ( $endpoint->add_rewrite_endpoint ) {
				$slug = $endpoint->slug;
				// TODO: remove reliance on Client Portal Pro from Core
				if ( function_exists( 'zeroBSCRM_clientPortalgetEndpoint' ) ) {
					$slug = zeroBSCRM_clientPortalgetEndpoint( $slug );
				}
				$vars[] = $slug;
			}
		}
		return $vars;
	}

	/**
	 * Lets us check early on in the action stack to see if page is ours.
	 * Only works after 'wp' in action order (needs wp_query->query_var)
	 * Is also used by zeroBSCRM_isClientPortalPage in Admin Checks 
	 * (which affects force redirect to dash, so be careful).
	 * 
	 * @return bool Returns true if the current page is a portal page.
	 */
	function is_portal_page() {
		return ! is_admin() && $this->is_a_client_portal_endpoint();
	}

	/**
	 * Checks if is a child, or a child of a child, of the client portal main page.
	 *
	 * @return bool Returns true if is a child, or a child of a child, of the client portal main page.
	 */
	function is_child_of_portal_page() {
		global $post; 
		
		if (!is_admin() && function_exists('zeroBSCRM_getSetting') && zeroBSCRM_isExtensionInstalled('portal')){

			$portalPage = (int)zeroBSCRM_getSetting('portalpage');
			
			if ($portalPage > 0 && isset($post) && is_object($post)){

				if ( is_page() && ($post->post_parent == $portalPage) ) {
						return true;
				} else { 

					// check 1 level deeper
					if ($post->post_parent > 0){

						$parentsParentID = (int)wp_get_post_parent_id($post->post_parent);
						
						if ($parentsParentID > 0 && ($parentsParentID == $portalPage) ) return true;

					}
					return false; 
				}
			}
		}
		return false;

	}
	
	/**
	 * Only works after 'wp' in action order (needs $wp_query->post).
	 *
	 *	@return bool If current page loaded has an endpoint that matches ours returns true. False otherwise.
	 */
	function is_a_client_portal_endpoint() {
		global $wp_query;
		// We get the post id (which will be the page id) + compare to our setting.
		$portalPage = zeroBSCRM_getSetting('portalpage');
		if (
			! empty( $portalPage ) &&
			$portalPage > 0 &&
			isset( $wp_query->post ) &&
			gettype( $wp_query->post ) == 'object' &&
			isset( $wp_query->post->ID ) &&
			$wp_query->post->ID == $portalPage
		) {
			return true;
		} else {
			return $this->is_child_of_portal_page();
		}
	}

	/**
	 * This is the shortcode function for the Client Portal. 
	 *
	 */
	function client_portal_shortcode() {
		// This function is being called by a shortcode (add_shortcode) and should never return any output (e.g. echo).
		// The implementation is old and removing all the output requires a lot of work. This is a quick workaround to fix it.
		ob_start();
		// this checks that we're on the front-end
		// ... a necessary step, because the editor (wp) now runs the shortcode on loading (probs gutenberg)
		// ... and because this should RETURN, instead it ECHO's directly
		// ... it should not run on admin side, because that means is probs an edit page!
		if ( !is_admin() ) {
			global $wp_query;

			// Setting the default endpoint to be the dashboard.
			// This could be customizable by the user if we want to.
			$endpoints_slug_array_column = array_column($this->endpoints, null, 'slug');
			// Let the default endpoint to be overriden by plugins.
			$default_endpoint_slug = apply_filters( 'jpcrm_client_portal_default_endpoint_slug', 'dashboard', $this );
			$endpoint = $endpoints_slug_array_column[$default_endpoint_slug];
			$portal_query_vars = $this->get_portal_query_vars( $wp_query->query_vars );

			foreach( $portal_query_vars as $var_key => $var_value ) {
				foreach ( $this->endpoints as $endpoint_search ) {
					if ( $endpoint_search->slug === $var_key ) {
						$endpoint = $endpoint_search;
						$endpoint->param_value = $var_value;
						break 2; // Breaks this loop and the outer loop, hence 2.
					}
				}
			}

			// allows one to tweak endpoint properties as needed before running endpoint actions
			$endpoint->before_endpoint_actions();
			$endpoint->perform_endpoint_action();
		}

		$result = ob_get_contents();
		ob_end_clean();
		return $result;
	}

	/**
	 * This catches failed logins, checks if from our page, then redirs
	 * From mr pippin https://pippinsplugins.com/redirect-to-custom-login-page-on-failed-login/
	 *
	 */
	function portal_login_fail_redirect( $username ) {
		$referrer = '';
		if(array_key_exists('HTTP_REFERER', $_SERVER)){
			$referrer = $_SERVER['HTTP_REFERER'];  // where did the post submission come from?
		}

		// if there's a valid referrer, and it's not the default log-in screen + it's got our post
		if ( !empty($referrer) && !strstr($referrer,'wp-login') && !strstr($referrer,'wp-admin') && isset($_POST['fromzbslogin'])) {
				wp_redirect(zeroBS_portal_link('dash') . '?login=failed' );  // let's append some information (login=failed) to the URL for the theme to use
				exit( 0 );
		}
	}

	/**
	 * Gets client portal endpoint name for a given object type.
	 *
	 * @param int $obj_type_id  object type ID.
	 *
	 * @return string|bool endpoint name or false if endpoint is not supported
	 */
	function get_endpoint( $obj_type_id ) {
		return $this->router->get_endpoint( $obj_type_id );
	}

	/**
	* Returns bool if current portal access is provided via easy-access hash
	* 
	* @return	bool - true if current access is via hash
	*/
	function access_is_via_hash( $obj_type_id ){
		return $this->router->access_is_via_hash( $obj_type_id );
	}

	/**
	 * Gets current object ID based on portal page URL.
	 *
	 * @param int $obj_type_id  object type ID.
	 *
	 * @return int|false Object ID or false if invalid object, bad permissions, or any other failure
	 */
	function get_obj_id_from_current_portal_page_url( $obj_type_id ) {
		return $this->router->get_obj_id_from_current_portal_page_url( $obj_type_id );
	}	
}
