<?php 
/*!
 * Jetpack CRM
 * https://jetpackcrm.com
 *
 * OAuth connection handler. Requires PHP 7.3+
 *
 */
namespace Automattic\JetpackCRM;
use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Grant\RefreshToken;
use Google\Client;
use Google\Service\Gmail;

// block direct access
defined( 'ZEROBSCRM_PATH' ) || exit;

class Oauth_Handler {

	/* 
	* Enabled (we disable oauth here if less than PHP <7.3 (req version))
	*/
	private $enabled = true;

	/* 
	* Debug output?
	*/
	private $debug = true;

	/**
	 * Callback url base (url which oauth bounces back to) (has provider attached via `get_callback_url()`)
	 */
	private $providers = array(

		'google_mail' => array(
			'name'      => 'Google Mail',
			'provider'  => 'google_mail', // means we'll use 'provider_google_mail()' instead of 'provider_generic()'
			'docs_url'  => 'https://kb.jetpackcrm.com/knowledge-base/using-gmail-with-jetpack-crm-mail-delivery-system/#setting-up-gmail-oauth-connection-and-mail-delivery-method',

			// required for refreshing tokens generically:
			// https://github.com/thephpleague/oauth2-google/blob/main/src/Provider/Google.php#L39
        	'url_authorize'               => 'https://accounts.google.com/o/oauth2/v2/auth',
        	'url_access_token'            => 'https://oauth2.googleapis.com/token',
        	'url_resource_owner_details'  => 'https://openidconnect.googleapis.com/v1/userinfo',

        	// required scopes
			/* Scopes: https://developers.google.com/gmail/api/auth/scopes

				https://www.googleapis.com/auth/gmail.compose
				https://www.googleapis.com/auth/gmail.send
				Google_Service_Gmail::GMAIL_READONLY
			*/
        	'scopes'                      => array( 'https://www.googleapis.com/auth/gmail.send' )
		)

	);


	/**
	 * Init
	 */
	public function __construct() {

		global $zbs;

		// require PHP 7.3
		if ( $zbs->has_min_php_version( '7.3' ) ) {

			// Add our action to the stack
			add_filter( 'jpcrm_listener_actions', array( $this, 'add_listener_action' ), 1 );

			// set action for endpoint listener to fire, so we can catch oauth requests (if any)
			add_action( 'jpcrm_listener_oauth', array( $this, 'catch_oauth_request'), 10 );

		} else {

			$this->enabled = false;

		}

	}


	/**
	 * Uses package installer to ensure packages are installed
	 */
	public function ensure_packages_installed() {

		global $zbs;

		$zbs->load_package_installer();
		$zbs->ensure_package_installed( 'oauth_dependencies', 1.0 );

	}



	/**
	 * Return enabled state
	 */
	public function enabled() {

		return $this->enabled;

	}


	/**
	 * Include league/oauth2-client, (well, in fact, autoload /vendor)
	 * Either: league/oauth2-client
	 * https://oauth2-client.thephpleague.com/usage/
	 * Or: league/oauth2-google
	 * https://github.com/thephpleague/oauth2-google
	 * 
	 */


	/**
	 * Returns a provider class (typically generated by `provider_generic()`, but allows exceptions, (e.g. `provider_google_mail()`))
	 */
	public function provider( $provider_key, $args=array() ){

		// see if we have a unique provider class (e.g. google_mail)
		$provider_info = $this->get_provider( $provider_key );

		if ( isset( $provider_info['provider'] ) && method_exists( $this, 'provider_' . $provider_info['provider'] ) ){

			// use custom provider
			return call_user_func_array( array( $this, 'provider_' . $provider_info['provider'] ), $args );

		} else {

			// use generic provider
			return $this->provider_generic( $args );

		}

	}


	/**
	 * Returns a generic provider instance
	 */
	public function provider_generic( $args=array() ){

		global $zbs;

        // ============ LOAD ARGS =============
        $default_args = array(

        	'id'                      => '',
        	'secret'                  => '',
        	'redirect_uri'            => '',

        	'urlAuthorize'            => '',
        	'urlAccessToken'          => '',
        	'urlResourceOwnerDetails' => ''

        ); foreach ($default_args as $argK => $argV){ $$argK = $argV; if (is_array($args) && isset($args[$argK])) {  if (is_array($args[$argK])){ $newData = $$argK; if (!is_array($newData)) $newData = array(); foreach ($args[$argK] as $subK => $subV){ $newData[$subK] = $subV; }$$argK = $newData;} else { $$argK = $args[$argK]; } } }
        // ============ / LOAD ARGS =============

		// make sure our dependencies are installed
		$this->ensure_packages_installed();

		// build provider
		return new \League\OAuth2\Client\Provider\GenericProvider([
    		'clientId'                => $id,                     // The client ID assigned to you by the provider
		    'clientSecret'            => $secret,                 // The client password assigned to you by the provider
		    'redirectUri'             => $redirect_uri,           // 'https://my.example.com/your-redirect-url/',
		    'urlAuthorize'            => $urlAuthorize,           // 'https://service.example.com/authorize',
		    'urlAccessToken'          => $urlAccessToken,         // 'https://service.example.com/token',
		    'urlResourceOwnerDetails' => $urlResourceOwnerDetails //'https://service.example.com/resource'
		]);

	}


	/**
	 * Returns Google Mail provider instance
	 */
	public function provider_google_mail( $args=array() ){

        // ============ LOAD ARGS =============
        $default_args = array(

        	'id'                      => '',
        	'secret'                  => ''

        ); foreach ($default_args as $argK => $argV){ $$argK = $argV; if (is_array($args) && isset($args[$argK])) {  if (is_array($args[$argK])){ $newData = $$argK; if (!is_array($newData)) $newData = array(); foreach ($args[$argK] as $subK => $subV){ $newData[$subK] = $subV; }$$argK = $newData;} else { $$argK = $args[$argK]; } } }
        // ============ / LOAD ARGS =============

		// make sure our dependencies are installed
		$this->ensure_packages_installed();

		// args
		$args = array(
		    'clientId'     => $id,
		    'clientSecret' => $secret,
		    'redirectUri'  => $this->get_callback_url( 'google_mail' ),
		    'accessType'   => 'offline', // Required to get a refreshToken
		    'scopes'       => $this->get_provider_detail( 'google_mail', 'scopes' )
		);

		// Hosted Domain, optional; used to restrict access to users on your G Suite/Google Apps for Business accounts
		/* We could later expose this via setting, but leaving unrestricted is more useful for now
		$domain = $zbs->get_domain();
		if ( $domain ){
			$args['hostedDomain'] = $domain;
		}
		*/

		// other params: https://github.com/thephpleague/oauth2-google
		// prompt, scopes, accessType

		// return provider
		return new Google( $args );

	}


	/*
	 * Add our listener action to the stack
	 *
	*/
	public function add_listener_action( $actions = array() ) {

		$actions[] = 'oauth';

		return $actions;

    
	}


	/*
	 * Catch OAuth requests
	 * Fired on load, catches any oauth responses inbound
	*/
	public function catch_oauth_request() {

		// at this point the Endpoint_Listener layer has verified the call via `is_listener_request()`

		// split by oauth providers, retrieve provider
		$provider = sanitize_text_field( $_GET['jpcrm_oauth_provider'] );

		if ( $this->legitimate_provider( $provider ) ){

			// got auth function? (e.g. `catch_google_mail_authorisation`)
			if ( method_exists( $this, 'catch_' . $provider . '_authorisation' ) ){

				// make sure our dependencies are installed
				$this->ensure_packages_installed();

				return call_user_func_array( array( $this, 'catch_' . $provider . '_authorisation' ), array() );

			}

		}

	}


	/*
	* Catch Google Mail Authorisation requests
	* https://github.com/thephpleague/oauth2-google
	*/
	public function catch_google_mail_authorisation(){

		// at this point the Endpoint_Listener layer has verified the call via `is_listener_request()`
		
		global $zbs;

		// require admin
		if ( !zeroBSCRM_isZBSAdminOrAdmin() ){

			return false;

		}

		// retrieve keys
		$config = $this->get_provider_config( 'google_mail' );
		if ( !is_array( $config ) || empty( $config['id'] ) || empty( $config['secret'] ) ){

			// not yet setup, needs id/secret
			$this->display_alert( 
				'info', 
				__( 'Google API Credentials needed', 'zero-bs-crm' ),
				'<p>' . __( 'To authenticate this OAuth connection you\'ll first need to add API credentials.', 'zero-bs-crm' ) . '</p>'
			    . '<p style="text-align:center"><a href="' . jpcrm_esc_link( $zbs->slugs['settings'] . '&tab=oauth&edit-provider=google_mail' ) . '" class="ui button green">' . __( 'Add your credentials', 'zero-bs-crm' ) . '</a></p>',
				'',
				'', 
				true // exit
			);

		} else {

			$google_client_id     = $config['id'];
			$google_client_secret = $config['secret'];

		}

		// quick check
		if ( empty( $google_client_id ) || empty( $google_client_secret ) ){
			return false;
		}

		// start session
		// Remove if session.auto_start=1 in php.ini
		session_start();

		// args
		$args = array(
		    'id'           => $google_client_id,
		    'secret'       => $google_client_secret
		);

		// retrieve provider
		$provider = $this->provider_google_mail( $args );

		// process
		if ( !empty( $_GET['error'] ) ){

		    // Got an error, probably user denied access
			$error_string = htmlspecialchars( $_GET['error'], ENT_QUOTES, 'UTF-8' );
			$this->display_alert( 
				'warning', 
				sprintf( __( 'Google API Connection Error %s', 'zero-bs-crm' ), '#1' ),
				'<p>' . __( 'There was an error connecting to Google:', 'zero-bs-crm' ) . '</p>'
				. '<p><code>' . $error_string . '</code></p>',
				'',
				'', 
				true // exit
			);

		} elseif ( empty( $_GET['code'] ) ) {

		    // If we don't have an authorization code then redirect to get one
		    $authorisation_url = $provider->getAuthorizationUrl(['prompt' => 'consent', 'access_type' => 'offline']);
		    $_SESSION['oauth2state'] = $provider->getState();
		    header( 'Location: ' . $authorisation_url );
		    exit;

		} elseif ( empty( $_GET['state'] ) || ( $_GET['state'] !== $_SESSION['oauth2state'] ) ) {

		    // State is invalid, possible CSRF attack in progress
		    // (though to get here we'd need to be admin, so probably this'll never fire)
		    unset( $_SESSION['oauth2state'] );
		    exit();

		} else {

			try {

			    // Try to get an access token (using the authorization code grant)
			    $token = $provider->getAccessToken('authorization_code', [
			        'code' => $_GET['code']
			    ]);

			    // save token (override any existing)
			    $config['token']         = $token->getToken();
			    $config['refresh_token'] = $token->getRefreshToken();
			    $config['expires']       = $token->getExpires();

			    try {

				    // verify:
				    if ( !$token->hasExpired() ){

		        		$resource_owner = $provider->getResourceOwner( $token );
		        		$resource_owner = $resource_owner->toArray();

		        		// if this is an array, we're definitely connected!
		        		if ( is_array( $resource_owner ) && isset( $resource_owner['email']) ){

		        			// here we can save any meta :)
		        			// (in gmail case, might be useful for mail delivery methods)
		        			if ( !isset( $config['meta'] ) || !is_array( $config['meta'] ) ){
		        				$config['meta'] = array();
		        			}

			        		/* Example:
			        		Array
							(
							    [sub] => 111285058559146358423
							    [name] => Woody Hayday
							    [given_name] => Woody
							    [family_name] => Hayday
							    [picture] => https://lh3.googleusercontent.com/a-/AOh14GgBbDQY4COolmrmvq1G4w_NQdirrJfGSPraw8LzEA=s96-c
							    [email] => woodyhayday@gmail.com
							    [email_verified] => 1
							    [locale] => en-GB
							)
							*/

		        			$fields = array( 'name', 'given_name', 'family_name', 'picture', 'email', 'email_verified', 'locale' );
		        			foreach ( $fields as $field_key ){

			        			if ( isset( $resource_owner[ $field_key ] ) ){
			        				$config['meta'][ $field_key ] = sanitize_text_field( $resource_owner[ $field_key ] );
			        			}

			        		}

		        		}

		        		// Save 
        				$this->update_provider_config( 'google_mail', $config );

        				// visual output
        				$account_details = $resource_owner['email'];
        				if ( isset( $resource_owner['picture'] ) && !empty( $resource_owner['picture'] ) ){

        					$account_details = '<img src="' . $resource_owner['picture'] . '" alt="" style="max-width:60px;margin:1em" />' . $account_details;

        				}

						$this->display_alert(
							'info', 
							__( 'Google API Connection Complete', 'zero-bs-crm' ),
							'<p>' . __( 'You have successfully connected the following Google account to your CRM:', 'zero-bs-crm' ) . '</p>'
							. '<div style="display: flex;align-items: center;justify-content: center;">' . $account_details . '</div>'
							. '<p>' . __( 'You can now close this window and return to your settings page.', 'zero-bs-crm' ) . '</p>',
							'',
							'', 
							true,
							true // refresh on parent close
						);


				    }

				} catch ( Exception $e ){

					$this->display_alert( 
						'warning', 
						sprintf( __( 'Google API Connection Error %s', 'zero-bs-crm' ), '#2' ),
						'<p>' . __( 'There was an error connecting to Google:', 'zero-bs-crm' ) . '</p>'
						. '<p><code>' . $e->getMessage() . '</code></p>',
						'',
						'', 
						true // exit
					);

				}


			} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {

				$this->display_alert( 
					'warning', 
					sprintf( __( 'Google API Connection Error %s', 'zero-bs-crm' ), '#3' ),
					'<p>' . __( 'There was an error connecting to Google:', 'zero-bs-crm' ) . '</p>'
					. '<p><code>' . $e->getMessage() . '</code></p>',
					'',
					'', 
					true // exit
				);

			}

		}
	}


	/*
	 * Retrieve a callback url with provider affixed
	*/
	public function get_callback_url( $provider ){

		global $zbs;

		if ( $this->legitimate_provider( $provider ) ){

			// load listener if it's not already loaded
			$zbs->load_listener();

			return $zbs->listener->get_callback_url( 'oauth' ) . '&jpcrm_oauth_provider=' . urlencode( $provider );

		}

		return false;

	}

	/*
	 * Checks if a provider is on the list
	*/
	public function legitimate_provider( $provider ){

		if ( !empty( $provider ) && in_array( $provider, array_keys( $this->providers ) ) ){

			return true;

		}

		return false;

	}

	/*
	* Retrieves a provider detail, (e.g. google's 'url_authorize')
	*/
	public function get_provider_detail( $provider, $attribute_key ){

		if ( isset( $this->providers[ $provider ] ) && isset( $this->providers[ $provider ][ $attribute_key ] ) ){

			return $this->providers[ $provider ][ $attribute_key ];

		}

		return false;

	}


	/*
	 * Returns provider summary details
	*/
	public function get_provider( $provider ){

		if ( isset( $this->providers[ $provider ] ) ){

			return $this->providers[ $provider ];

		}

	}

	/*
	 * Returns providers array
	*/
	public function get_providers( ){

		return $this->providers;

	}


	/*
	 * get config/token for a provider
	 *
	 * @param string $provider_key - e.g. 'google_mail'
	*/
	public function get_provider_config( $provider_key, $return_token_string_only=false, $refresh_token_if_expired=true ){

		global $zbs;

		// basic check
		if ( !$this->legitimate_provider( $provider_key ) ){
			return false;
		}

		// retrieve token configs as they are
		$existing_token_configs = $zbs->settings->get( 'oauth_tokens', false );

		// retrieve
		if ( is_array( $existing_token_configs ) && isset( $existing_token_configs[ $provider_key ] ) ){

			// get current
			$token_config = $existing_token_configs[ $provider_key ];
			$token_config['expires'] = (int)$token_config['expires'];

			// if expired and $refresh_token_if_expired, regen
			if ( $refresh_token_if_expired && !empty( $token_config['token'] ) && isset( $token_config['expires'] ) && $token_config['expires'] < time() ){

				$this->debug_message( 'Found token which has expired!<br>' );

				// refresh token
				$refreshed_token_config = $this->refresh_token( $provider_key, $token_config );

				if ( is_array( $refreshed_token_config ) ){

					// success
					$token_config = $refreshed_token_config;

				} else {

					// failed refreshing token
					return false;

				}

			}

			// if only the token itself, return that
			if ( $return_token_string_only ){

				return $token_config['token'];

			}

			// else return the whole token info array
			return $token_config;

		}

		// no such token
		return false;

	}


	/*
	 * Update config/token for a provider
	 *
	 * @param string $provider - e.g. 'google_mail'
	 * @param array $args
	*/
	public function update_provider_config( $provider, $args=array() ){

		global $zbs;

        // ============ LOAD ARGS =============
        $default_args = array(

        	'id'            => '',
        	'secret'        => '',
        	'token'         => '',
        	'refresh_token' => '',
        	'expires'       => '',
        	'meta'          => array()

        ); foreach ($default_args as $argK => $argV){ $$argK = $argV; if (is_array($args) && isset($args[$argK])) {  if (is_array($args[$argK])){ $newData = $$argK; if (!is_array($newData)) $newData = array(); foreach ($args[$argK] as $subK => $subV){ $newData[$subK] = $subV; }$$argK = $newData;} else { $$argK = $args[$argK]; } } }
        // ============ / LOAD ARGS =============

		// basic check
		if ( !$this->legitimate_provider( $provider ) ){
			return false;
		}

		// retrieve tokens as they are
		$existing_token_configs = $zbs->settings->get( 'oauth_tokens', false );

		// check is an array
		if ( !is_array( $existing_token_configs ) ) {
			$existing_token_configs = array();
		}

		// hard override
		$existing_token_configs[ $provider ] = array(

			// credentials
			'id'               => $id,
			'secret'           => $secret,

			// token
			'token'            => $token,
			'expires'          => $expires,
			'refresh_token'    => $refresh_token,
			'last_updated_by'  => zeroBSCRM_user(),
			'last_updated'     => time(),

			// optional, per provider extras
			'meta'             => $meta

		);

		// update
		$zbs->settings->update( 'oauth_tokens', $existing_token_configs );

	}


	/*
	 * Delete config/token for a provider
	 *
	 * @param string $provider - e.g. 'google_mail'
	*/
	public function delete_provider_config( $provider ){

		global $zbs;

		// retrieve tokens as they are
		$existing_token_configs = $zbs->settings->get( 'oauth_tokens', false );

		// if exists
		if ( isset( $existing_token_configs[ $provider ] ) ){
			
			// direct unset
			unset( $existing_token_configs[ $provider ] );

			// update
			$zbs->settings->update( 'oauth_tokens', $existing_token_configs );
			
			return true;

		}

		return false;

	}


	/*
	 * Refreshes a user token and returns
	 *
	 * @param string $provider_key - e.g. `google_mail`
	 * @param array|bool $provider_config - either the config (if already available), or false (and this function will load it)
	 *
	 * @return array - updated token config
	*/
	public function refresh_token( $provider_key, $provider_config=false ){

		// if no config, load it
		if ( !is_array( $provider_config ) ){

			// note false for third parameter `refresh_token_if_expired` - this is to avoid an infinite loop
			$provider_config = $this->get_provider_config( $provider_key, false, false );

		}

		// got refresh token?
		if ( !isset( $provider_config['refresh_token'] ) || empty( $provider_config['refresh_token'] ) ){

			return false;

		}

		// make sure our dependencies are installed
		$this->ensure_packages_installed();

		// if we have a specific function for this provider, defer to that:
		if ( method_exists( $this, 'refresh_token_' . $provider_key ) ){

			return call_user_func_array( array( $this, 'refresh_token_' . $provider_key ), array( $provider_config ) );

		}

		// otherwise, generic...
		
		$this->debug_message( 'Refreshing access token...' );

		// build args
		$args = array(


        	'id'                      => $provider_config['id'],
        	'secret'                  => $provider_config['secret'],
        	'redirect_uri'            => $this->get_callback_url( $provider_key ),


        	'urlAuthorize'            => $this->get_provider_detail( $provider_key, 'url_authorize' ),
        	'urlAccessToken'          => $this->get_provider_detail( $provider_key, 'url_access_token' ),
        	'urlResourceOwnerDetails' => $this->get_provider_detail( $provider_key, 'url_resource_owner_details' )


		);

		// retrieve provider
		$provider = $this->provider( $provider_key, $args );

		try {

			// generate new token (generic)
			// https://oauth2-client.thephpleague.com/usage/#refreshing-a-token
			$new_token = $provider->getAccessToken('refresh_token', [
		        'refresh_token' => $provider_config['refresh_token']
			]);

			// success?
			if ( !empty( $new_token->getToken() ) ){

				$this->debug_message( 'Success!' );

				$new_refresh_token = $new_token->getRefreshToken();
				
				// when a new refresh token isn't set, keep the old one
				if ( empty( $new_refresh_token ) ){
					$new_refresh_token = $provider_config['refresh_token'];
				}

				// save it
				$config = $this->get_provider_config( $provider_key, false, false );
				$config['token'] = $new_token->getToken();
			    $config['refresh_token'] = $new_refresh_token;
				$config['expires'] = $new_token->getExpires();
			        		
				$this->update_provider_config( $provider_key, $config );

				// return it
				return $config;

			}


		} catch ( \League\OAuth2\Client\Provider\Exception\IdentityProviderException $e ){

			$this->debug_message( 'Error refreshing token: ' . $e->getMessage() );

			switch ( $e->getMessage() ){

				case 'invalid_request':
				default:

					// Something wrong with the request
					$this->debug_message( '<pre>' . print_r( $e->getResponseBody(), 1 ) . '</pre>' );

					// clear tokens
					$config = $this->get_provider_config( $provider_key, false, false );
					$config['token'] = '';
					$config['expires'] = '';
					$config['refresh_token'] = '';				        		
					$this->update_provider_config( $provider_key, $config );

					break;
			}

		}

		return false;

	}

	/*
	 * Refreshes a Google mail user token and returns refreshed provider config
	 *
	 * @param array|bool $provider_config - either the config (if already available), or false (and this function will load it)
	 *
	 * @return array - updated token config
	*/
	public function refresh_token_google_mail( $provider_config=false ){

		// if no config, load it
		if ( !is_array( $provider_config ) ){

			// note false for third parameter `refresh_token_if_expired` - this is to avoid an infinite loop
			$provider_config = $this->get_provider_config( 'google_mail', false, false );

		}

		// got refresh token?
		if ( !isset( $provider_config['refresh_token'] ) || empty( $provider_config['refresh_token'] ) ){

			return false;

		}

		// make sure our dependencies are installed
		$this->ensure_packages_installed();

		// build arguments
		$args = array(

        	'id'                      => $provider_config['id'],
        	'secret'                  => $provider_config['secret']

        );

		// retrieve provider
		$provider = $this->provider_google_mail( $args );

		// grant type
		$grant = new RefreshToken();

		$this->debug_message( 'Refreshing Google Token...' );

		try {

			$new_token = $provider->getAccessToken( $grant, [ 'refresh_token' => $provider_config['refresh_token'] ] );
			$new_refresh_token = $new_token->getRefreshToken();

			// when a new refresh token isn't set, keep the old one
			if ( empty( $new_refresh_token ) ){
				$new_refresh_token = $provider_config['refresh_token'];
			}


			$this->debug_message( 'Success!' );

			// save it
			$config = $this->get_provider_config( 'google_mail', false, false );
			$config['token']         = $new_token->getToken();
		    $config['refresh_token'] = $new_refresh_token;
			$config['expires']       = $new_token->getExpires();
		        		
			$this->update_provider_config( 'google_mail', $config );

			return $config;

		} catch ( \League\OAuth2\Client\Provider\Exception\IdentityProviderException $e ){

			$this->debug_message( 'Error refreshing Google token: ' . $e->getMessage() );
			$this->debug_message( '<pre>' . print_r( $e->getResponseBody(), 1 ) . '</pre>' );

			// clear expired/faulty token
			$config = $this->get_provider_config( 'google_mail', false, false );
			$config['token'] = '';
			$config['expires'] = '';
			$config['refresh_token'] = '';		        		
			$this->update_provider_config( 'google_mail', $config );

		}		

		return false;

	}


	/*
	 * Returns a connection status (bool) for provider from a provider key or config
	 * Note: Could be hardened by using oauth to re-verify valid token
	 *
	 * @param string $provider - e.g. 'google_mail'
	 * @param array|bool $provider_config - if passed, validity of config is checked
	*/
	public function connection_status( $provider, $provider_config=false ){	   	

		// load if not provided
		if ( !is_array( $provider_config ) ){
   			$provider_config = $this->get_provider_config( $provider );
   		}

   		// got config?
   		if ( is_array( $provider_config ) && !empty( $provider_config['id'] ) && !empty( $provider_config['secret'] ) ){	   		

	        if (
	        	isset( $provider_config['refresh_token'] ) && !empty( $provider_config['refresh_token'] ) 
	        	&&
	        	isset( $provider_config['expires'] ) && !empty( $provider_config['expires'] )
	        ){

	   			return true;

	        }

	    }

	    return false;

	}


	/*
	 * Returns a connection status string for provider
	 *
	 * @param string $provider - e.g. 'google_mail'
	 * @param bool $shorthand - if true will return 'connected' instead of longform html status
	*/
	public function connection_status_string( $provider, $shorthand=false ){
	   	
	   	$status = __( 'Not yet configured', 'zero-bs-crm' );
	   	$short_status = 'no-config';

   		$provider_config = $this->get_provider_config( $provider );

   		// got config?
   		if ( is_array( $provider_config ) && !empty( $provider_config['id'] ) && !empty( $provider_config['secret'] ) ){

	   		$status = __( 'Not yet connected', 'zero-bs-crm' );
	   		$short_status = 'config-no-connect';

	        if (
	        	isset( $provider_config['refresh_token'] ) && !empty( $provider_config['refresh_token'] ) 
	        	&&
	        	isset( $provider_config['expires'] ) && !empty( $provider_config['expires'] )
	        ){

	   			$short_status = 'seems-connected';

            	// some providers can give specific user info to enrich the status string:
            	switch ( $provider ){

            		case 'google_mail':

            			$username = __( 'Generic User', 'zero-bs-crm' );
            			if ( isset( $provider_config['meta'] ) && is_array( $provider_config['meta'] ) ){

            				if ( isset( $provider_config['meta']['name'] ) ){

            					$username = $provider_config['meta']['name'];

            				}

            				if ( isset( $provider_config['meta']['email'] ) ){

            					if ( !empty( $username ) ){
            						$username .= ' ';
            					}

            					$username .= '(' . $provider_config['meta']['email'] . ')';

            				}


            			}


            			$status =  '<span class="ui green label">' . sprintf( __( 'Connected as %s', 'zero-bs-crm' ), $username ) . '</span>';

            			break;

            		default:

                		$status =  '<span class="ui green label">' . __( 'Connected', 'zero-bs-crm' ) . '</span>';

                	break;

                }

	        }

	    }

	    if ( $shorthand ){
	    	
	    	return $short_status;

	    }

        return $status;

	}


	/*
	 * Wrapper for outputting messages, abstracted in case we want to phase into a stack system
	 * There are surely better ways...
	*/
	private function display_alert( $msg_class='', $msg_header='', $msg_html='', $icon_class='', $id='', $singular=false, $refresh_parent = false ){

		// if $singular we're just showing the error as a single page (as used in OAuth flow)
		if ( $singular ){

			?><html>
			<head>
				<link rel="stylesheet" type="text/css" href="<?php echo esc_url( plugins_url('/css/ZeroBSCRM.admin.semantic-ui'.wp_scripts_get_suffix().'.css',ZBS_ROOTFILE) ); ?>">
				<style>
					body {
						vertical-align: middle;
    					display: flex;
					}
					#jpcrm-oauth-error-wrapper {
					    max-width: 600px;
					    margin: auto;
					    vertical-align: middle;
					}
				</style>
				<?php
				if ( $refresh_parent ) {
					?>
					<script>
						// reloads parent window
						window.opener.location.reload();
					</script>
				<?php } ?>
			</head>
			<body>
				<div id="jpcrm-oauth-error-wrapper">
					<?php

		}
		
		// err msg
		echo zeroBSCRM_UI2_messageHTML( $msg_class, $msg_header, $msg_html, $icon_class, $id );

		if ( $singular ){

			?></div></body></html><?php

			exit();

		}

	}

	/*
	* Output debug, if enabled
	*/
	private function debug_message( $message ){

		if ( $this->debug ){

			echo $message;

		}

	}

	/**
	* Returns an authorized Google API client.
	* 
	* @param string $provider_key - key of OAuth provider to use
	* 
	* @return Client the authorized client object
	*/
	// adapted from class-v3-zerobscrm-googlecontact.php
	function get_google_client( $provider_key ){

		// make sure our dependencies are installed
		$this->ensure_packages_installed();

		// retrieve config
		$provider_config = $this->get_provider_config( $provider_key );

   		if ( is_array( $provider_config ) && !empty( $provider_config['token'] ) ){

			global $zbs;

			// Let's make sure we've loaded the Google API library:
			// https://developers.google.com/gmail/api/quickstart/php
			$zbs->autoload_libraries();

			//modified from: https://developers.google.com/people/quickstart/php  since we will always be getting 'offline' access so does not need to re-ask user
			$client = new \Google_Client();

			$client->setAccessType('offline');
			$client->setClientId( $provider_config['id'] );
			$client->setClientSecret( $provider_config['secret'] );
			$client->setAccessToken( $provider_config['token'] );
			$client->setScopes('https://www.googleapis.com/auth/gmail.send');

			return $client;

		}

		return false;
	}
}