HEX
Server: Apache/2.4.65 (Debian)
System: Linux web6 5.10.0-36-amd64 #1 SMP Debian 5.10.244-1 (2025-09-29) x86_64
User: innocamp (1028)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /home/remoteac/public_html/wp-content/plugins/fusion-builder/inc/class-fusion-hubspot.php
<?php
/**
 * Fusion Hubspot.
 *
 * @package Fusion-Builder
 * @since 3.1
 */

// Do not allow directly accessing this file.
if ( ! defined( 'ABSPATH' ) ) {
	exit( 'Direct script access denied.' );
}

/**
 * Fusion Hubspot class.
 *
 * @since 3.1
 */
class Fusion_Hubspot {
	/**
	 * The one, true instance of this object.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var object
	 */
	private static $instance;

	/**
	 * API key.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $key = null;

	/**
	 * Token data.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $token = null;

	/**
	 * Refresh token.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $refresh_token = null;

	/**
	 * Properties.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $properties = null;

	/**
	 * Properties.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $localize_status = null;

	/**
	 * Type of connection.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $type;

	/**
	 * Markup for notices.
	 *
	 * @static
	 * @access private
	 * @since 3.1
	 * @var mixed
	 */
	private $notices = '';

	/**
	 * Class constructor.
	 *
	 * @since 3.1
	 * @access private
	 */
	private function __construct() {
		$fusion_settings = awb_get_fusion_settings();
		$this->type      = $fusion_settings->get( 'hubspot_api' );

		// Enqueue the OAuth script where required.
		$this->oauth_enqueue();

		// Add the PO options to the form CPT.
		add_filter( 'avada_form_submission_sections', [ $this, 'maybe_add_option' ] );

		// This is a redirect from our site with token.
		if ( is_admin() && current_user_can( 'manage_options' ) ) {

			// Trying to save a token.
			if ( isset( $_GET['hubspot'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				$this->authenticate();
			}

			// Trying to revoke a token.
			if ( isset( $_GET['revoke_hubspot'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				$this->revoke_access();
			}
		}

		// Render notices if we have any.
		add_action( 'avada_dashboard_notices', [ $this, 'render_notices' ] );

		// Reset Caches.
		add_action( 'wp_ajax_fusion_reset_hubspot_caches', [ $this, 'reset_caches_handler' ] );

		// If not enabled, no need to load anything.
		if ( ! apply_filters( 'fusion_load_hubspot', ( 'off' !== $this->type ) ) ) {
			return;
		}

		// Enqueue the JS script for the PO mapping option.
		add_action( 'avada_page_option_scripts', [ $this, 'option_script' ], 10, 2 );

		// Add property list to live editor.
		add_action( 'fusion_app_preview_data', [ $this, 'add_preview_data' ], 10, 3 );

		// Add HubSpot form.
		add_action( 'save_post', [ $this, 'add_form' ], 10, 3 );
	}

	/**
	 * If set, render admin notices.
	 *
	 * @access public
	 * @since 3.1
	 * @return void
	 */
	public function render_notices() {
		if ( '' !== $this->notices ) {
			echo $this->notices; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Adds form to HubSpot server.
	 *
	 * @access public
	 * @since 3.3.1
	 * @param string $post_id The post ID.
	 * @param object $post    The post object.
	 * @param bool   $update  Is update or publish.
	 * @return void
	 */
	public function add_form( $post_id, $post, $update ) {

		if ( 'fusion_form' !== $post->post_type || ! strlen( $post->post_content ) || 'contact' !== fusion_data()->post_meta( $post_id )->get( 'hubspot_action' ) ) {
			return;
		}

		$elements   = [
			'fusion_form_checkbox'     => [ 'text', 'string' ],
			'fusion_form_date'         => [ 'date', 'date' ],
			'fusion_form_email'        => [ 'text', 'string' ],
			'fusion_form_hidden'       => [ 'text', 'string' ],
			'fusion_form_image_select' => [ 'text', 'string' ],
			'fusion_form_number'       => [ 'number', 'number' ],
			'fusion_form_password'     => [ 'text', 'string' ],
			'fusion_form_phone_number' => [ 'text', 'string' ],
			'fusion_form_radio'        => [ 'text', 'string' ],
			'fusion_form_range'        => [ 'text', 'string' ],
			'fusion_form_rating'       => [ 'text', 'string' ],
			'fusion_form_select'       => [ 'text', 'string' ],
			'fusion_form_text'         => [ 'text', 'string' ],
			'fusion_form_textarea'     => [ 'textarea', 'string' ],
			'fusion_form_time'         => [ 'text', 'string' ],
			'fusion_form_upload'       => [ 'file', 'string' ],
		];
		$properties = [];
		$mapping    = fusion_data()->post_meta( $post_id )->get( 'hubspot_map' );
		$response   = '';

		$pattern = get_shortcode_regex( array_keys( $elements ) );

		if ( is_string( $mapping ) ) {
			$mapping = json_decode( $mapping, true );
		}

		preg_match_all( "/$pattern/s", $post->post_content, $fields );

		foreach ( $fields[0] as $key => $field ) {

			// Get field name.
			preg_match_all( '/\[fusion_form_[^\]]*\sname=\"([^\"]*)\"/', $field, $matches );
			$field_name = isset( $matches[1][0] ) ? $matches[1][0] : false;

			// Get field label.
			preg_match_all( '/\[fusion_form_[^\]]*\slabel=\"([^\"]*)\"/', $field, $matches );
			$field_label = isset( $matches[1][0] ) ? $matches[1][0] : false;

			// Use mapping name if exists.
			if ( isset( $mapping[ $field_name ] ) && '' !== $mapping[ $field_name ] && 'fusion-none' !== $mapping[ $field_name ] ) {
				$field_name = $mapping[ $field_name ];
			}

			$item = [
				'fields' => [
					'label'     => $field_label ? $field_label : $field_name,
					'name'      => $field_name,
					'fieldType' => $elements[ $fields[2][ $key ] ][0],
					'type'      => $elements[ $fields[2][ $key ] ][1],
					'hidden'    => 'fusion_form_hidden' === $fields[2][ $key ] ? true : false,
				],
			];

			array_push( $properties, $item );
		}

		$data = [
			'name'            => get_the_title( $post_id ),
			'formFieldGroups' => $properties,
		];

		$action = $update && '' !== get_post_meta( $post_id, 'form_hubspot_map', true ) ? 'update_form' : 'create_form';

		if ( 'update_form' === $action ) {
			$meta = get_post_meta( $post_id, 'form_hubspot_map', true );

			$data['guid']     = $meta['guid'];
			$data['portalId'] = $meta['portalId'];

			$response = $this->api_request( 'update_form', $data );

		} else {
			$response = $this->api_request( 'create_form', $data );
		}

		if ( is_array( $response ) && isset( $response['portalId'] ) && isset( $response['guid'] ) ) {
			$meta = [
				'guid'     => $response['guid'],
				'portalId' => $response['portalId'],
			];

			update_post_meta( $post_id, 'form_hubspot_map', $meta );
		}
	}

	/**
	 * Add note for rendering.
	 *
	 * @access public
	 * @since 3.1
	 * @param string $message The message to display.
	 * @param string $type The type of message.
	 * @return void
	 */
	public function add_notice( $message = '', $type = 'success' ) {
		$this->notices .= '<div id="fusion-hubspot-notice" class="notice notice-' . esc_attr( $type ) . ' avada-db-card avada-db-' . esc_attr( $type ) . '" style="display:block !important;"><h2>' . esc_html( $message ) . '</h2></div>';
	}

	/**
	 * Add property options to live editor.
	 *
	 * @access public
	 * @since 3.1
	 * @return void
	 */
	public function oauth_enqueue() {

		// Back-end TO page, enqueue so markup is updated.
		if ( is_admin() && current_user_can( 'manage_options' ) && ( ( isset( $_GET['page'] ) && 'avada_options' === $_GET['page'] ) || isset( $_GET['hubspot'] ) || isset( $_GET['revoke_hubspot'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_api_script' ] );
		}

		// Live editor JS script always, in case they change value.
		add_action( 'fusion_builder_enqueue_live_scripts', [ $this, 'enqueue_api_script' ] );
	}

	/**
	 * Add property options to live editor.
	 *
	 * @access public
	 * @since  3.1
	 * @param  array  $data The data already added.
	 * @param  int    $page_id The post ID being edited.
	 * @param  string $post_type The post type being edited.
	 * @return array $data The data with panel data added.
	 */
	public function add_preview_data( $data, $page_id = 0, $post_type = 'page' ) {
		if ( 'fusion_form' === $post_type ) {
			$data['hubspot'] = [
				'properties' => $this->get_properties(),
				'automatic'  => __( 'Automatic Property', 'fusion-builder' ),
				'none'       => __( 'No Property', 'fusion-builder' ),
				'common'     => __( 'Common Properties', 'fusion-builder' ),
				'other'      => __( 'Other Properties', 'fusion-builder' ),
			];

		}
		return $data;
	}

	/**
	 * Enqueue script for handling OAuth.
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function enqueue_api_script() {
		wp_enqueue_script( 'fusion_hubspot_api', FUSION_BUILDER_PLUGIN_URL . 'assets/admin/js/fusion-hubspot-oauth.js', [], FUSION_BUILDER_VERSION, true );

		wp_localize_script(
			'fusion_hubspot_api',
			'fusionHubspotOAuth',
			[
				'status' => $this->localize_status,
			]
		);
	}

	/**
	 * If we have details to try and connect.
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function can_connect() {
		if ( 'auth' === $this->type ) {
			return $this->get_token();
		} elseif ( 'key' === $this->type ) {
			return $this->get_api_key();
		}
		return false;
	}

	/**
	 * Get the API key
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function get_api_key() {

		// We already have retrieved key.
		if ( null !== $this->key ) {
			return $this->key;
		}

		// No transient, attempt to refresh token.
		$fusion_settings = awb_get_fusion_settings();
		$this->key       = $fusion_settings->get( 'hubspot_key' );

		if ( empty( $this->key ) ) {
			$this->key = false;
		}

		return $this->key;
	}

	/**
	 * Get the token data.
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function get_token() {

		// We already have retrieved a token, continue to use it.
		if ( null !== $this->token ) {
			return $this->token;
		}

		$this->token = get_transient( 'fusion_hubspot_token' );

		// No transient, attempt to refresh token.
		if ( ! $this->token ) {
			$this->refresh_token();
		}

		// Return what we have.
		return $this->token;
	}

	/**
	 * Render info about connection status.
	 *
	 * @since 3.1
	 * @access public
	 * @return string
	 */
	public function maybe_render_button() {
		$auth_url = 'https://app.hubspot.com/oauth/authorize?client_id=999cc7c3-e358-4a3b-9984-a37dfbd319fa&redirect_uri=' . FUSION_UPDATES_URL . '/hubspot-api&scope=actions%20timeline%20oauth%20forms%20crm.objects.contacts.write%20crm.schemas.contacts.read%20crm.schemas.companies.read%20crm.schemas.deals.read&state=' . rawurlencode( admin_url( 'admin.php?page=avada' ) );

		$type = 'connected';
		if ( isset( $_GET['error'] ) && ! empty( $_GET['error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
			$type = 'error';
		} elseif ( ! $this->get_token() ) {
			$type = 'no_token';
		}

		$output  = '<div id="fusion-hubspot-content">';
		$output .= '<div data-id="error" style="display:' . ( 'error' === $type ? 'flex' : 'none' ) . '">';
		$output .= '<span><strong>' . esc_html__( 'There was a problem when trying to connect. ', 'fusion-builder' ) . '</strong>';
		$output .= '<a target="_blank" href="https://theme-fusion.com/documentation/avada/forms/how-to-integrate-hubspot-with-avada-forms/">' . esc_html__( 'HubSpot integration with Avada Forms documentation.', 'fusion-builder' ) . '</a></span>';
		$output .= '<a class="button-primary" target="_blank" href="' . $auth_url . '">' . esc_html__( 'Try again.', 'fusion-builder' ) . '</a>';
		$output .= '</div>';
		$output .= '<div data-id="no_token"  style="display:' . ( 'no_token' === $type ? 'flex' : 'none' ) . '">';
		$output .= '<span><strong>' . esc_html__( 'Currently not connected. ', 'fusion-builder' ) . '</strong>';
		$output .= '<a target="_blank" href="https://theme-fusion.com/documentation/avada/forms/how-to-integrate-hubspot-with-avada-forms/">' . esc_html__( 'HubSpot integration with Avada Forms documentation.', 'fusion-builder' ) . '</a></span>';
		$output .= '<a class="button-primary" target="_blank" href="' . $auth_url . '">' . esc_html__( 'Connect with HubSpot', 'fusion-builder' ) . '</a>';
		$output .= '</div>';
		$output .= '<div data-id="connected"  style="display:' . ( 'connected' === $type ? 'flex' : 'none' ) . '">';
		$output .= '<strong>' . esc_html__( 'Connected with HubSpot', 'fusion-builder' ) . '</strong>';
		$output .= '<a class="button-primary" target="_blank" href="' . esc_url( admin_url( 'admin.php?page=avada&revoke_hubspot=1' ) ) . '">' . __( 'Revoke Access', 'fusion-builder' ) . '</a>';
		$output .= '</div>';

		return $output;
	}

	/**
	 * Revoke account access.
	 *
	 * @since 3.1
	 * @access public
	 * @return void
	 */
	public function revoke_access() {
		$this->reset_token();
		$this->reset_refresh_token();
		$this->localize_response( 'revoke' );
		$this->add_notice( __( 'Your HubSpot account has been disconnected.', 'fusion-builder' ), 'success' );
		$this->update_global( 'off' );
	}

	/**
	 * Update global hubspot option.
	 *
	 * @since 3.1
	 * @access public
	 * @param string $type Type of Hubspot api.
	 * @return void
	 */
	private function update_global( $type = 'auth' ) {
		$fusion_settings = awb_get_fusion_settings();
		$fusion_settings->set( 'hubspot_api', $type );
		$this->type = $type;

		delete_transient( 'fusion_tos' );
		delete_transient( 'fusion_fb_tos' );
	}

	/**
	 * Localize the API scripts.
	 *
	 * @since 3.1
	 * @access public
	 * @param string $status Status type for localization.
	 * @return void
	 */
	private function localize_response( $status = 'error' ) {
		$this->localize_status = $status;
	}

	/**
	 * Reset stored access token.
	 *
	 * @since 3.1
	 * @access public
	 * @return void
	 */
	public function reset_token() {
		delete_transient( 'fusion_hubspot_token' );
		$this->token = null;
	}

	/**
	 * Reset stored refresh token.
	 *
	 * @since 3.1
	 * @access public
	 * @return void
	 */
	public function reset_refresh_token() {
		delete_option( 'fusion_hubspot_refresh' );
		$this->refresh_token = null;
	}

	/**
	 * Retrieve API response.
	 *
	 * @since 3.1
	 * @access public
	 * @param string $action Action/endpoint for API.
	 * @param array  $options Options for request.
	 * @return mixed
	 */
	public function api_request( $action = '', $options = [] ) {
		if ( '' === $action || ! $this->can_connect() ) {
			return false;
		}

		$method              = 'GET';
		$url                 = false;
		$submission_response = [];
		$args                = [
			'headers'      => [
				'User-Agent' => 'Fusion Hubspot',
			],
			'timeout'      => 60,
			'headers_data' => false,
		];

		// Shared args.
		if ( 'auth' === $this->type ) {
			$args['headers']['Authorization'] = 'Bearer ' . $this->token;
		}

		// Switch for action, vary url and args.
		switch ( $action ) {
			case 'get_properties':
				$url = 'https://api.hubapi.com/properties/v1/contacts/properties';
				break;
			case 'update_contact':
				$url                             = 'https://api.hubapi.com/contacts/v1/contact/createOrUpdate/email/' . $options['email'];
				$args['body']                    = wp_json_encode( $options['body'] );
				$args['headers']['content-type'] = 'application/json';
				$method                          = 'POST';
				break;
			case 'submit_form':
				$url                             = 'https://forms.hubspot.com/submissions/v3/integration/submit/' . $options['portalId'] . '/' . $options['guid'];
				$args['body']                    = wp_json_encode( $options['body'] );
				$args['headers']['content-type'] = 'application/json';
				$method                          = 'POST';
				break;
			case 'create_form':
				$url                             = 'https://api.hubapi.com/forms/v2/forms';
				$args['body']                    = wp_json_encode( $options );
				$args['headers']['content-type'] = 'application/json';
				$method                          = 'POST';
				break;
			case 'update_form':
				$url                             = 'https://api.hubapi.com/forms/v2/forms/' . $options['guid'];
				$args['body']                    = wp_json_encode( $options );
				$args['headers']['content-type'] = 'application/json';
				$method                          = 'POST';
				break;
		}

		// Check for no URL.
		if ( ! $url ) {
			return;
		}

		// If we are connecting via key, add to URL.
		if ( 'key' === $this->type ) {
			$url .= '?hapikey=' . $this->get_api_key();
		}

		// We have URL, token, action and args.  Send the API request.
		if ( 'GET' === $method ) {
			$response = wp_remote_get( $url, $args );
		} else {
			$response = wp_remote_post( $url, $args );
		}

		// Token invalid, reset token.  Next request will then trigger refresh if possible.
		if ( 401 === (int) wp_remote_retrieve_response_code( $response ) ) {
			$this->reset_token();
			$this->api_request( $action, $options );
		}

		// Check for error.
		if ( ! is_wp_error( $response ) && isset( $response['body'] ) ) {

			$submission_response = [
				'status'  => isset( $response['response']['code'] ) ? $response['response']['code'] : '',
				'message' => isset( $response['response']['message'] ) ? $response['response']['message'] : '',
			];

			$response = json_decode( $response['body'], true );

			// Do more error checking here.
			if ( is_array( $response ) && isset( $response['status'] ) && 'error' === $response['status'] ) {

				$submission_response = [
					'status'  => $response['status'],
					'message' => isset( $response['message'] ) ? $response['message'] : '',
					'errors'  => isset( $response['errors'] ) ? $response['errors'] : [],
				];

				return 'update_contact' === $action || 'submit_form' === $action ? wp_json_encode( $submission_response ) : false;
			}

			return 'update_contact' === $action || 'submit_form' === $action ? wp_json_encode( $submission_response ) : $response;
		}

		return false;
	}

	/**
	 * Add property data.
	 *
	 * @since 3.1
	 * @access public
	 * @param string $post_type Post type being added to.
	 * @return void
	 */
	public function option_script( $post_type ) {
		// Not editing a form then we don't need it.
		if ( 'fusion_form' !== $post_type ) {
			return;
		}

		// No connection to API then it can't work.
		if ( ! $this->can_connect() ) {
			return;
		}

		wp_enqueue_script( 'fusion_hubspot_option', FUSION_BUILDER_PLUGIN_URL . 'assets/admin/js/fusion-hubspot-option.js', [], FUSION_BUILDER_VERSION, true );

		$properties = $this->get_properties();
		if ( $properties ) {

			// Add property data.
			wp_localize_script(
				'fusion_hubspot_option',
				'fusionHubspot',
				[
					'properties' => $properties,
					'automatic'  => __( 'Automatic Property', 'fusion-builder' ),
					'none'       => __( 'No Property', 'fusion-builder' ),
					'common'     => __( 'Common Properties', 'fusion-builder' ),
					'other'      => __( 'Other Properties', 'fusion-builder' ),
				]
			);
		}
	}

	/**
	 * Get full array of properties;
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function get_properties() {

		// Have already retrieved, return.
		if ( null !== $this->properties ) {
			return $this->properties;
		}

		// Retrieve from transient if available.
		$properties = get_transient( 'fusion_hubspot_properties' );
		if ( $properties ) {
			$this->properties = $properties;
			return $this->properties;
		}

		// Not in transient, need to request it.
		$this->properties = $this->api_request( 'get_properties' );
		if ( $this->properties ) {
			set_transient( 'fusion_hubspot_properties', $this->properties, DAY_IN_SECONDS );
		}
		return $this->properties;
	}

	/**
	 * Get the token data and store it.
	 *
	 * @since 3.1
	 * @access public
	 * @return void
	 */
	public function authenticate() {

		// Some kind of error reporting here.
		if ( ! isset( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$this->localize_response( 'error' );
			$this->add_notice( __( 'There was an error authenticating your HubSpot token.', 'fusion-builder' ), 'notice' );
			return;
		}

		// Transient with expiry to match.
		$token      = sanitize_text_field( wp_unslash( $_GET['token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		$expiration = isset( $_GET['expires'] ) ? (int) sanitize_text_field( wp_unslash( $_GET['expires'] ) ) : 1000; // phpcs:ignore WordPress.Security.NonceVerification
		set_transient( 'fusion_hubspot_token', $token, $expiration );
		$this->token = $token;

		// Update refresh token, which does not expire.
		$refresh_token = isset( $_GET['refresh'] ) ? sanitize_text_field( wp_unslash( $_GET['refresh'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification
		update_option( 'fusion_hubspot_refresh', $refresh_token );
		$this->refresh_token = $refresh_token;

		$this->localize_response( 'success' );
		$this->add_notice( __( 'Your HubSpot account has been successfully connected.', 'fusion-builder' ), 'success' );

		$this->update_global( 'auth' );
	}

	/**
	 * Return stored refresh token.
	 *
	 * @since 3.1
	 * @access public
	 * @return mixed
	 */
	public function get_refresh_token() {
		if ( null !== $this->refresh_token ) {
			return $this->refresh_token;
		}

		$this->refresh_token = get_option( 'fusion_hubspot_refresh', false );

		return $this->refresh_token;
	}

	/**
	 * Refresh the token via our website.
	 *
	 * @since 3.1
	 * @access public
	 * @return void
	 */
	public function refresh_token() {
		if ( ! $this->get_refresh_token() ) {
			return;
		}

		$args = [
			'timeout'    => 60,
			'user-agent' => 'fusion-hubspot-refresh',
		];

		$response = wp_remote_get( 'https://updates.theme-fusion.com/wp-json/hubspot/' . $this->refresh_token, $args );

		if ( ! is_wp_error( $response ) && isset( $response['body'] ) ) {

			$response = json_decode( $response['body'], true );
			if ( $response ) {
				// Transient with expiry to match.
				set_transient( 'fusion_hubspot_token', esc_attr( $response['access_token'] ), (int) $response['expires_in'] );

				// Update refresh token, which does not expire.
				update_option( 'fusion_hubspot_refresh', esc_attr( $response['refresh_token'] ) );

				$this->token         = esc_attr( $response['access_token'] );
				$this->refresh_token = esc_attr( $response['refresh_token'] );
				return;
			}

			// Invalid refresh token most likely, wipe out option to prevent looping.
			$this->reset_refresh_token();
			return;
		}

		// Response failed entirely.
	}

	/**
	 * Create a HubSpot contact.
	 *
	 * @since 3.1
	 * @access public
	 * @param array $form_data Data from form which needs to be stored.
	 * @param array $mapping Information to map from the form to HubSpot properties.
	 * @param array $labels Array of label and field names.
	 * @return void
	 */
	public function create_contact( $form_data, $mapping = [], $labels = [] ) {
		if ( is_string( $mapping ) ) {
			$mapping = json_decode( $mapping, true );
		}

		// Property list, try to auto match.
		$properties = (array) $this->get_properties();

		// Empty starting data.
		$mapped_data = [
			'properties' => [],
		];

		// Request options.
		$options = [];

		// Array of assigned properties.
		$used_properties = [];

		// Loop each form field to check for mapping match.
		foreach ( $form_data['data'] as $field => $value ) {
			$field_value = ( is_array( $value ) ) ? implode( ' | ', $value ) : $value;

			// Update to correct format.
			$form_data['data'][ $field ] = $field_value;

			// Check if we have a desired property set in map.
			if ( isset( $mapping[ $field ] ) && '' !== $mapping[ $field ] ) {

				// If its set to have no property match, entirely exclude from matching.
				if ( 'fusion-none' === $mapping[ $field ] ) {
					unset( $form_data['data'][ $field ] );
					unset( $labels[ $field ] );
					continue;
				}

				// If we are matching to email, set as target contact.
				if ( 'email' === $mapping[ $field ] ) {
					$options['email'] = $field_value;
				}

				// Add to mapped data we will send.
				$mapped_data['properties'][] = [
					'property' => $mapping[ $field ],
					'value'    => $field_value,
				];

				$used_properties[ $mapping[ $field ] ] = true;
			}
		}

		// Auto matching if not all are set already.
		if ( count( $form_data['data'] ) !== count( $mapped_data['properties'] ) ) {
			foreach ( $properties as $property ) {

				$value = false;

				// Property is already assigned, do not assign again.
				if ( isset( $used_properties[ $property['name'] ] ) ) {
					continue;
				}

				// Property name matches input name.
				if ( isset( $form_data['data'][ $property['name'] ] ) ) {
					$value                       = $form_data['data'][ $property['name'] ];
					$mapped_data['properties'][] = [
						'property' => $property['name'],
						'value'    => $value,
					];

					// Property label matches input label.
				} elseif ( false !== $field_id = array_search( $property['label'], $labels, true ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments, WordPress.CodeAnalysis.AssignmentInCondition
					$value                       = $form_data['data'][ $field_id ];
					$mapped_data['properties'][] = [
						'property' => $property['name'],
						'value'    => $value,
					];

					// Property name matches input label.
				} elseif ( false !== $field_id = array_search( $property['name'], $labels, true ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments, WordPress.CodeAnalysis.AssignmentInCondition
					$value                       = $form_data['data'][ $field_id ];
					$mapped_data['properties'][] = [
						'property' => $property['name'],
						'value'    => $value,
					];

					// Property label matches input name.
				} elseif ( isset( $form_data['data'][ str_replace( ' ', '', strtolower( $property['label'] ) ) ] ) ) {
					$field_id                    = str_replace( ' ', '', strtolower( $property['label'] ) );
					$value                       = $form_data['data'][ $field_id ];
					$mapped_data['properties'][] = [
						'property' => $property['name'],
						'value'    => $value,
					];
				}

				// If email is one, add to options.
				if ( $value && 'email' === $property['name'] ) {
					$options['email'] = $value;
				}
			}
		}

		// No valid email, contact cannot be created or updated.
		if ( ! isset( $options['email'] ) || empty( $options['email'] ) ) {
			return;
		}

		// We made it this far, add data and set request.
		$options['body'] = $mapped_data;

		// Do some error logging here.
		return $this->api_request( 'update_contact', $options );
	}

	/**
	 * Submits form.
	 *
	 * @since 3.1
	 * @access public
	 * @param array $form_data Data from form which needs to be stored.
	 * @param array $labels    Array of label and field names.
	 * @param int   $form_id   The form ID.
	 * @return mixed
	 */
	public function submit_form( $form_data, $labels = [], $form_id = '' ) {
		$mapping = fusion_data()->post_meta( $form_id )->get( 'hubspot_map' );
		$meta    = get_post_meta( $form_id, 'form_hubspot_map', true );

		if ( ! empty( $mapping ) && is_string( $mapping ) ) {
			$mapping = json_decode( $mapping, true );
		}

		// Property list, try to auto match.
		$properties = (array) $this->get_properties();

		// Empty starting data.
		$mapped_data = [
			'fields' => [],
		];

		// Request options.
		$options = [];

		// Array of assigned fields.
		$used_properties = [];

		// Loop each form field to check for mapping match.
		foreach ( $form_data['data'] as $field => $value ) {
			$field_value = ( is_array( $value ) ) ? implode( ' | ', $value ) : $value;

			// Update to correct format.
			$form_data['data'][ $field ] = $field_value;

			// Check if we have a desired property set in map.
			if ( isset( $mapping[ $field ] ) && '' !== $mapping[ $field ] ) {

				// If its set to have no property match, entirely exclude from matching.
				if ( 'fusion-none' === $mapping[ $field ] ) {
					unset( $form_data['data'][ $field ] );
					unset( $labels[ $field ] );
					continue;
				}

				// If we are matching to email, set as target contact.
				if ( 'email' === $mapping[ $field ] ) {
					$options['email'] = $field_value;
				}

				// Add to mapped data we will send.
				$mapped_data['fields'][] = [
					'name'  => $mapping[ $field ],
					'value' => $field_value,
				];

				$used_properties[ $mapping[ $field ] ] = true;
			}
		}

		// Auto matching if not all are set already.
		if ( count( $form_data['data'] ) !== count( $mapped_data['fields'] ) ) {
			foreach ( $properties as $property ) {

				$value = false;

				// Property is already assigned, do not assign again.
				if ( isset( $used_properties[ $property['name'] ] ) ) {
					continue;
				}

				// Property name matches input name.
				if ( isset( $form_data['data'][ $property['name'] ] ) ) {
					$value = $form_data['data'][ $property['name'] ];

					$mapped_data['fields'][] = [
						'name'  => $property['name'],
						'value' => $value,
					];

					// Property label matches input label.
				} elseif ( false !== $field_id = array_search( $property['label'], $labels, true ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments, WordPress.CodeAnalysis.AssignmentInCondition
					$value = $form_data['data'][ $field_id ];

					$mapped_data['fields'][] = [
						'name'  => $property['name'],
						'value' => $value,
					];

					// Property name matches input label.
				} elseif ( false !== $field_id = array_search( $property['name'], $labels, true ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments, WordPress.CodeAnalysis.AssignmentInCondition
					$value = $form_data['data'][ $field_id ];

					$mapped_data['fields'][] = [
						'name'  => $property['name'],
						'value' => $value,
					];

					// Property label matches input name.
				} elseif ( isset( $form_data['data'][ str_replace( ' ', '', strtolower( $property['label'] ) ) ] ) ) {
					$field_id = str_replace( ' ', '', strtolower( $property['label'] ) );
					$value    = $form_data['data'][ $field_id ];

					$mapped_data['fields'][] = [
						'name'  => $property['name'],
						'value' => $value,
					];
				}

				// If email is one, add to options.
				if ( $value && 'email' === $property['name'] ) {
					$options['email'] = $value;
				}
			}
		}

		// No valid email, contact cannot be created or updated.
		if ( ! isset( $options['email'] ) || empty( $options['email'] ) ) {
			return;
		}

		// We made it this far, add data and set request.
		$options['body']     = $mapped_data;
		$options['guid']     = $meta['guid'];
		$options['portalId'] = $meta['portalId'];

		return $this->api_request( 'submit_form', $options );

	}

	/**
	 * Creates or returns an instance of this class.
	 *
	 * @static
	 * @access public
	 * @param array $sections Page options.
	 * @since 3.1
	 */
	public function maybe_add_option( $sections ) {
		if ( 'off' === $this->type ) {
			$hubspot_link  = '<a target="_blank" rel="noopener noreferrer" href="https://hubs.to/39wVbJ">HubSpot account</a>';
			$document_link = '<a target="_blank" rel="noopener noreferrer" href="https://theme-fusion.com/documentation/avada/forms/how-to-integrate-hubspot-with-avada-forms/">HubSpot integration guide</a>';
			$sections['form_submission']['fields']['hubspot_info'] = [
				'type'        => 'custom',
				'label'       => '',

				/* translators: 1: HubSpot link. 2: Documentation link. */
				'description' => '<div class="fusion-redux-important-notice">' . sprintf( __( 'Sign up for a %1$s and manage your contacts in their free CRM.  For more information check out our %2$s. ', 'fusion-builder' ), $hubspot_link, $document_link ) . '</div>',
				'id'          => 'hubspot_info',
				'dependency'  => [
					[
						'field'      => 'form_type',
						'value'      => 'ajax',
						'comparison' => '==',
					],
					[
						'field'      => 'form_actions',
						'value'      => 'hubspot',
						'comparison' => 'contains',
					],
				],
			];

			return $sections;
		}

		$properties = $this->get_properties();
		if ( $this->can_connect() && $properties ) {
			$sections['form_submission']['fields']['hubspot_options'] = [
				'type'       => 'toggle',
				'row_title'  => esc_html__( 'Hubspot', 'fusion-builder' ),
				'id'         => 'hubspot_options',
				'dependency' => [
					[
						'field'      => 'form_type',
						'value'      => 'ajax',
						'comparison' => '==',
					],
					[
						'field'      => 'form_actions',
						'value'      => 'hubspot',
						'comparison' => 'contains',
					],
				],
				'fields'     => [
					'hubspot_info'   => [
						'type'        => 'custom',
						'label'       => '',
						'description' => '<div class="fusion-redux-important-notice">' . __( 'You are currently connected to the HubSpot API.', 'fusion-builder' ) . '</div>',
						'id'          => 'hubspot_info',
					],
					'hubspot_action' => [
						'type'        => 'radio-buttonset',
						'label'       => esc_html__( 'HubSpot Action', 'fusion-builder' ),
						'description' => esc_html__( 'Select if you want to perform a HubSpot action after form submission.', 'fusion-builder' ),
						'id'          => 'hubspot_action',
						'default'     => 'no',
						'transport'   => 'postMessage',
						'choices'     => [
							'no'      => esc_html__( 'None', 'fusion-builder' ),
							'contact' => esc_html__( 'Create/Update Contact', 'fusion-builder' ),
						],
					],
					'hubspot_map'    => [
						'type'        => 'hubspot_map',
						'label'       => esc_html__( 'HubSpot Mapping', 'fusion-builder' ),
						'description' => __( 'Map fields from the form to HubSpot contact properties.  <strong>Note</strong>, the email property is required for creating or updating a contact. When mapping is set to "Automatic", Avada will try to map based on field label, name and tags.', 'fusion-builder' ),
						'id'          => 'hubspot_map',
						'transport'   => 'postMessage',
						'dependency'  => [
							[
								'field'      => 'hubspot_action',
								'value'      => 'contact',
								'comparison' => '==',
							],
						],
					],
				],
			];
			return $sections;
		}

		$hubspot_link = '<a target="_blank" rel="noopener noreferrer" href="' . esc_url( admin_url( 'themes.php?page=avada_options#hubspot_api' ) ) . '">HubSpot</a>';
		$sections['form_submission']['fields']['hubspot_info'] = [
			'type'        => 'custom',
			'label'       => '',
			/* translators: Global link. */
			'description' => '<div class="fusion-redux-important-notice">' . sprintf( __( 'Connect to your %s account to create contacts from your form.', 'fusion-builder' ), $hubspot_link ) . '</div>',
			'id'          => 'hubspot_info',
		];

		return $sections;
	}

	/**
	 * Creates or returns an instance of this class.
	 *
	 * @static
	 * @access public
	 * @since 3.1
	 */
	public static function get_instance() {

		// If an instance hasn't been created and set to $instance create an instance and set it to $instance.
		if ( null === self::$instance ) {
			self::$instance = new Fusion_Hubspot();
		}
		return self::$instance;
	}

	/**
	 * Handles resetting caches.
	 *
	 * @access public
	 * @since 3.5
	 * @return void
	 */
	public function reset_caches_handler() {
		if ( is_multisite() && is_main_site() ) {
			$sites = get_sites();
			foreach ( $sites as $site ) {
				switch_to_blog( $site->blog_id );
				delete_transient( 'fusion_hubspot_properties' );
				restore_current_blog();
			}
			return;
		}
		delete_transient( 'fusion_hubspot_properties' );
	}
}

/**
 * Instantiates the Fusion_Hubspot class.
 * Make sure the class is properly set-up.
 *
 * @since object 3.1
 * @return object Fusion_App
 */
function Fusion_Hubspot() { // phpcs:ignore WordPress.NamingConventions
	return Fusion_Hubspot::get_instance();
}
Fusion_Hubspot();