# zip-ai/0.0.4/classes/core/helper.php

ZIP AI – AI Website Builder &amp; AI Agent, version 0.0.4. 714 lines.

- Page: https://pluginprobe.com/plugins/zip-ai/0.0.4/code/classes/core/helper.php
- Raw: https://pluginprobe.com/plugins/zip-ai/0.0.4/raw/classes/core/helper.php
- Modified: 2026-07-02T12:36:30+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/zip-ai/0.0.4/code/classes/core/helper.php#L10-L20`.

```php
<?php
/**
 * Zip AI - Helper.
 *
 * This file contains the helper functions of Zip AI.
 * Helpers are functions that are used throughout the library.
 *
 * @package zip-ai
 */

namespace ZipAI\Classes\Core;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Classes to be used, in alphabetical order.
use ZipAI\Classes\Core\Utils;

/**
 * The Helper Class.
 */
class Helper {

	/**
	 * Check if SSL verification should be enabled for remote requests.
	 *
	 * SSL verification is enabled by default for security. It can be disabled
	 * for local development environments using the ZIP_AI_DISABLE_SSL_VERIFY
	 * constant or the 'zip_ai_sslverify' filter.
	 *
	 * @since 0.0.1
	 * @return bool True if SSL should be verified, false otherwise.
	 */
	public static function should_verify_ssl() {
		// Default to true (SSL verification enabled) for security.
		$verify_ssl = true;

		// Allow disabling via constant for local development.
		if ( defined( 'ZIP_AI_DISABLE_SSL_VERIFY' ) && ZIP_AI_DISABLE_SSL_VERIFY ) {
			$verify_ssl = false;
		}

		/**
		 * Filter whether SSL verification should be enabled for remote requests.
		 *
		 * @since 0.0.1
		 * @param bool $verify_ssl Whether to verify SSL. Default true.
		 */
		return apply_filters( 'zip_ai_sslverify', $verify_ssl );
	}

	/**
	 * Get an option from the database.
	 *
	 * @param string  $key              The option key.
	 * @param mixed   $default          The option default value if option is not available.
	 * @param boolean $network_override Whether to allow the network admin setting to be overridden on subsites.
	 * @since 0.0.1
	 * @return mixed  The option value.
	 */
	public static function get_admin_settings_option( $key, $default = false, $network_override = false ) {
		// Get the site-wide option if we're in the network admin.
		return $network_override && is_multisite() ? get_site_option( $key, $default ) : get_option( $key, $default );
	}

	/**
	 * Update an option from the database.
	 *
	 * @param string $key              The option key.
	 * @param mixed  $value            The value to update.
	 * @param bool   $network_override Whether to allow the network_override admin setting to be overridden on subsites.
	 * @since 0.0.1
	 * @return bool True if the option was updated, false otherwise.
	 */
	public static function update_admin_settings_option( $key, $value, $network_override = false ) {
		// Update the site-wide option if we're in the network admin, and return the updated status.
		return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value );
	}

	/**
	 * Delete an option from the database for.
	 *
	 * @param string  $key              The option key.
	 * @param boolean $network_override Whether to allow the network admin setting to be overridden on subsites.
	 * @since 0.0.1
	 * @return void
	 */
	public static function delete_admin_settings_option( $key, $network_override = false ) {
		// Delete the site-wide option if we're in the network admin.
		if ( $network_override && is_multisite() ) {
			delete_site_option( $key );
		} else {
			delete_option( $key );
		}
	}

	/**
	 * Check if Zip AI is authorized.
	 *
	 * @since 0.0.1
	 * @return boolean True if Zip AI is authorized, false otherwise.
	 */
	public static function is_authorized() {
		// Check zip_ai_settings for auth_token.
		$auth_token = self::get_decrypted_auth_token();

		return ! empty( $auth_token ) && is_string( $auth_token ) && ! empty( trim( $auth_token ) );
	}

	/**
	 * Get the Zip AI Settings.
	 *
	 * If used with a key, it will return that specific setting.
	 * If used without a key, it will return the entire settings array.
	 *
	 * @param string $key The setting key.
	 * @param mixed  $default The default value to return if the setting is not found.
	 * @since 0.0.1
	 * @return mixed|array The setting value, or the default.
	 */
	public static function get_setting( $key = '', $default = array() ) {

		// Get the Zip AI settings.
		$existing_settings = self::get_admin_settings_option( 'zip_ai_settings' );

		// If the Zip AI settings are empty, return the fallback.
		if ( empty( $existing_settings ) || ! is_array( $existing_settings ) ) {
			return $default;
		}

		// If the key is empty, return the entire settings array - otherwise return the specific setting or the fallback.
		if ( empty( $key ) ) {
			return $existing_settings;
		} else {
			return isset( $existing_settings[ $key ] ) ? $existing_settings[ $key ] : $default;
		}
	}

	/**
	 * Get the decrypted auth token from zip_ai_settings.
	 *
	 * @since 0.0.1
	 * @return string The decrypted auth token.
	 */
	public static function get_decrypted_auth_token() {
		static $resolved_token = null;
		static $is_resolved    = false;

		if ( $is_resolved ) {
			return $resolved_token;
		}

		$is_resolved  = true;
		$mcp_settings = get_option( 'zip_ai_settings', array() );

		if ( ! is_array( $mcp_settings ) ) {
			$resolved_token = '';
			return $resolved_token;
		}

		$auth_token    = ! empty( $mcp_settings['auth_token'] ) && is_string( $mcp_settings['auth_token'] ) ? Utils::decrypt( $mcp_settings['auth_token'] ) : '';
		$zip_token     = ! empty( $mcp_settings['zip_token'] ) && is_string( $mcp_settings['zip_token'] ) ? Utils::decrypt( $mcp_settings['zip_token'] ) : '';
		$email         = ! empty( $mcp_settings['user_email'] ) ? sanitize_email( $mcp_settings['user_email'] ) : '';
		$name          = ! empty( $mcp_settings['user_name'] ) ? sanitize_text_field( $mcp_settings['user_name'] ) : '';
		$current_api   = self::get_credit_server_identifier();
		$stored_api    = ! empty( $mcp_settings['auth_token_server'] ) ? untrailingslashit( (string) $mcp_settings['auth_token_server'] ) : '';
		$is_valid_auth = null;

		if ( '' !== $auth_token && $stored_api === $current_api ) {
			$resolved_token = $auth_token;
			return $resolved_token;
		}

		if ( '' !== $auth_token && '' === $stored_api ) {
			$is_valid_auth = self::validate_credit_server_auth_token( $auth_token );

			if ( true === $is_valid_auth ) {
				$mcp_settings['auth_token_server'] = $current_api;
				update_option( 'zip_ai_settings', $mcp_settings, false );
				$resolved_token = $auth_token;
				return $resolved_token;
			}
		}

		if ( '' !== $zip_token && '' !== $email ) {
			$exchange_result = self::exchange_zip_ai_token_for_local_auth_token( $zip_token, $email, $name );

			if ( ! empty( $exchange_result['token'] ) ) {
				$mcp_settings['auth_token']        = Utils::encrypt( $exchange_result['token'] );
				$mcp_settings['auth_token_server'] = $current_api;

				if ( ! empty( $exchange_result['email'] ) ) {
					$mcp_settings['user_email'] = sanitize_email( $exchange_result['email'] );
				}

				if ( ! empty( $exchange_result['name'] ) ) {
					$mcp_settings['user_name'] = sanitize_text_field( $exchange_result['name'] );
				}

				update_option( 'zip_ai_settings', $mcp_settings, false );
				$resolved_token = $exchange_result['token'];
				return $resolved_token;
			}
		}

		if ( '' !== $auth_token && null === $is_valid_auth && '' === $stored_api ) {
			$resolved_token = $auth_token;
			return $resolved_token;
		}

		$resolved_token = '';
		return $resolved_token;
	}

	/**
	 * Validate the auth token against the configured credit server.
	 *
	 * @param string $auth_token The auth token to validate.
	 * @since 0.0.1
	 * @return bool|null True when valid, false when rejected, null when validation could not be completed.
	 */
	public static function validate_credit_server_auth_token( $auth_token ) {
		if ( empty( $auth_token ) || ! is_string( $auth_token ) ) {
			return false;
		}

		$response = wp_remote_post(
			ZIP_AI_CREDIT_SERVER_API . 'auth/validate',
			array(
				'headers'   => array(
					'Content-Type'  => 'application/json',
					'Accept'        => 'application/json',
					'Authorization' => 'Bearer ' . $auth_token,
				),
				'body'      => wp_json_encode( array() ),
				'timeout'   => 15,
				'sslverify' => self::should_verify_ssl(),
			)
		);

		if ( is_wp_error( $response ) ) {
			return null;
		}

		$status_code   = wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		$response_data = json_decode( $response_body, true );

		if ( 200 === $status_code ) {
			return ! empty( $response_data['valid'] );
		}

		if ( 401 === $status_code || 403 === $status_code ) {
			return false;
		}

		return null;
	}

	/**
	 * Exchange a ZipWP app token for a local credit-server auth token.
	 *
	 * @param string $zip_token The ZipWP app token.
	 * @param string $email The user email.
	 * @param string $name The user name.
	 * @since 0.0.1
	 * @return array The exchange result containing token details, or an empty array on failure.
	 */
	public static function exchange_zip_ai_token_for_local_auth_token( $zip_token, $email, $name = '' ) {
		if ( empty( $zip_token ) || ! is_string( $zip_token ) || empty( $email ) || ! is_string( $email ) ) {
			return array();
		}

		$request_body = array(
			'token'   => $zip_token,
			'email'   => $email,
			'site_id' => self::get_site_id(),
		);

		if ( '' !== $name ) {
			$request_body['name'] = $name;
		}

		if ( get_current_user_id() > 0 ) {
			$request_body['user_id'] = get_current_user_id();
		}

		$response = wp_remote_post(
			ZIP_AI_CREDIT_SERVER_API . 'token/exchange',
			array(
				'headers'   => array(
					'Content-Type' => 'application/json',
					'Accept'       => 'application/json',
				),
				'body'      => wp_json_encode( $request_body ),
				'timeout'   => 15,
				'sslverify' => self::should_verify_ssl(),
			)
		);

		if ( is_wp_error( $response ) ) {
			return array();
		}

		$status_code   = wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		$response_data = json_decode( $response_body, true );

		if ( ! in_array( $status_code, array( 200, 201 ), true ) || empty( $response_data['success'] ) || empty( $response_data['token'] ) ) {
			return array();
		}

		return array(
			'token' => sanitize_text_field( $response_data['token'] ),
			'email' => ! empty( $response_data['email'] ) ? sanitize_email( $response_data['email'] ) : $email,
			'name'  => ! empty( $response_data['name'] ) ? sanitize_text_field( $response_data['name'] ) : $name,
		);
	}

	/**
	 * Get the configured credit server identifier for token compatibility checks.
	 *
	 * @since 0.0.1
	 * @return string The normalized credit server API URL.
	 */
	private static function get_credit_server_identifier() {
		return untrailingslashit( ZIP_AI_CREDIT_SERVER_API );
	}

	/**
	 * Generate a shared secret for HMAC authentication.
	 *
	 * @since 0.0.1
	 * @return string The generated shared secret.
	 */
	public static function generate_shared_secret() {
		return bin2hex( random_bytes( 32 ) );
	}
	/**
	 * Get or generate the shared secret for HMAC authentication.
	 *
	 * @since 0.0.1
	 * @return string The shared secret.
	 */
	public static function get_shared_secret() {
		// Get the encrypted shared secret from settings.
		$encrypted_shared_secret = self::get_setting( 'shared_secret', '' );

		if ( empty( $encrypted_shared_secret ) ) {
			// Generate new shared secret.
			$shared_secret = self::generate_shared_secret();
			self::update_setting( 'shared_secret', $shared_secret );
			return $shared_secret;
		}

		// Decrypt and return the existing shared secret.
		return Utils::decrypt( $encrypted_shared_secret );
	}

	/**
	 * Update a specific setting in the Zip AI settings.
	 *
	 * @param string $key The setting key.
	 * @param mixed  $value The setting value.
	 * @since 0.0.1
	 * @return bool True if the setting was updated, false otherwise.
	 */
	public static function update_setting( $key, $value ) {
		$existing_settings = self::get_admin_settings_option( 'zip_ai_settings', array() );

		if ( ! is_array( $existing_settings ) ) {
			$existing_settings = array();
		}

		$existing_settings[ $key ] = Utils::encrypt( $value );

		return self::update_admin_settings_option( 'zip_ai_settings', $existing_settings );
	}

	/**
	 * Generate HMAC signature for request.
	 *
	 * @param string $request_body The request body.
	 * @param string $timestamp The timestamp.
	 * @since 0.0.1
	 * @return string The HMAC signature.
	 */
	public static function generate_hmac_signature( $request_body, $timestamp ) {
		$shared_secret = self::get_shared_secret();
		$data          = $request_body . $timestamp;
		return hash_hmac( 'sha256', $data, $shared_secret );
	}

	/**
	 * Get the site ID for HMAC authentication.
	 *
	 * @since 0.0.1
	 * @return string The site ID.
	 */
	public static function get_site_id() {
		return get_site_url();
	}

	/**
	 * Register the shared secret with Laravel server during plugin activation.
	 *
	 * @since 0.0.1
	 * @return array The registration response.
	 */
	public static function register_shared_secret_with_laravel() {
		$shared_secret = self::get_shared_secret();
		$site_id       = self::get_site_id();

		// Register endpoint - this should point to your Laravel server.
		$register_endpoint = ZIP_AI_CREDIT_SERVER_API . 'auth/register-secret';

		$registration_data = array(
			'site_id'     => $site_id,
			'secret'      => $shared_secret,
			'site_name'   => get_bloginfo( 'name' ),
			'admin_email' => get_option( 'admin_email' ),
		);

		$response = wp_remote_post(
			$register_endpoint,
			array(
				'headers' => array(
					'Content-Type' => 'application/json',
				),
				'body'    => wp_json_encode( $registration_data ),
				'timeout' => 30,
			)
		);

		if ( is_wp_error( $response ) ) {
			return array(
				'error' => $response->get_error_message(),
				'code'  => 'registration_failed',
			);
		}

		$response_body = wp_remote_retrieve_body( $response );
		$status_code   = wp_remote_retrieve_response_code( $response );

		if ( 200 !== $status_code ) {
			return array(
				'error' => __( 'Failed to register with Laravel server.', 'zip-ai' ),
				'code'  => 'registration_failed',
			);
		}

		$response_data = json_decode( $response_body, true );

		// Store the registration status.
		self::update_setting( 'hmac_registered', 'true' );

		return $response_data;
	}

	/**
	 * Check if HMAC is registered with Laravel.
	 *
	 * @since 0.0.1
	 * @return bool True if registered, false otherwise.
	 */
	public static function is_hmac_registered() {
		return 'true' === self::get_setting( 'hmac_registered', 'false' );
	}

	/**
	 * Get a response from the ZipWP API server with HMAC authentication.
	 *
	 * @param string $endpoint The endpoint to get the response from.
	 * @param array  $body The data to be passed as the request body, if any.
	 * @since 0.0.1
	 * @return array The ZipWP API Response.
	 */
	public static function get_zip_ai_api_response_with_hmac( $endpoint, $body = array() ) {
		// If the endpoint is not a string, then abandon ship.
		if ( ! is_string( $endpoint ) ) {
			return array(
				'error' => __( 'The ZipWP Endpoint was not declared', 'zip-ai' ),
			);
		}

		// Set the API URL.
		$api_url = ZIP_AI_CREDIT_SERVER_API . $endpoint;

		// Prepare request body for HMAC signing.
		$request_body = '';
		if ( ! empty( $body ) && is_array( $body ) ) {
			$request_body = wp_json_encode( $body );
		}

		// Generate timestamp in ISO8601 format.
		$timestamp = gmdate( 'c' );

		// Generate HMAC signature.
		$signature = self::generate_hmac_signature( $request_body, $timestamp );

		// Prepare request arguments with HMAC headers.
		$request_args = array(
			'headers'   => array(
				'Content-Type'     => 'application/json',
				'Accept'           => 'application/json',
				'X-Site-Id'        => self::get_site_id(),
				'X-Timestamp'      => $timestamp,
				'X-Signature'      => $signature,
				'X-Plugin-Version' => ZIP_AI_VERSION,
			),
			'timeout'   => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- 30 seconds is required sometime for the ZipWP API response
			'sslverify' => self::should_verify_ssl(),
		);

		// Add request body if provided.
		if ( ! empty( $request_body ) ) {
			$request_args['body'] = $request_body;
			$response             = wp_remote_post( $api_url, $request_args );
		} else {
			$response = wp_remote_get( $api_url, $request_args );
		}

		// If the response was an error, or not a 200 status code, then abandon ship.
		if ( is_wp_error( $response ) || empty( $response['response'] ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return array(
				'error' => __( 'The ZipWP API server is not responding.', 'zip-ai' ),
			);
		}

		// Get the response body.
		$response_body = wp_remote_retrieve_body( $response );

		// If the response body is not a JSON, then abandon ship.
		if ( empty( $response_body ) || ! json_decode( $response_body ) ) {
			return array(
				'error' => __( 'The ZipWP API server encountered an error.', 'zip-ai' ),
			);
		}

		// Return the response body.
		return json_decode( $response_body, true );
	}

	/**
	 * Check if the current user is allowed to use ZipWP MCP.
	 *
	 * @since 0.0.1
	 * @return bool True if user is allowed, false otherwise.
	 */
	public static function is_user_allowed() {
		return current_user_can( 'manage_zip_ai_assistant' );
	}

	/**
	 * Get all ZipWP MCP settings.
	 *
	 * @since 0.0.1
	 * @return array The settings array.
	 */
	public static function get_settings() {
		return self::get_setting();
	}

	/**
	 * Get the auth middleware URL with redirect.
	 * Matches UAG implementation exactly.
	 *
	 * @param array $params Optional parameters.
	 * @since 0.0.1
	 * @return string The auth middleware URL.
	 */
	public static function get_auth_middleware_url( $params = array() ) {

		// Create the Redirect URL.
		$redirect_url = add_query_arg(
			array(
				'nonce'         => wp_create_nonce( 'zip_ai_auth_nonce' ),
				'scs-authorize' => 'true',
			),
			admin_url()
		);

		// Create the Authentication URL.
		$auth_url = add_query_arg(
			array(
				'type'         => 'token',
				'redirect_url' => rawurlencode( $redirect_url ),
			),
			ZIP_AI_MIDDLEWARE
		);

		// Add the plugin param if passed.
		if ( ! empty( $params['plugin'] ) && is_string( $params['plugin'] ) ) {
			$auth_url = add_query_arg(
				'plugin',
				sanitize_text_field( $params['plugin'] ),
				$auth_url
			);
		}

		// Add the source param if passed.
		if ( ! empty( $params['source'] ) && is_string( $params['source'] ) ) {
			$auth_url = add_query_arg(
				'source',
				sanitize_text_field( $params['source'] ),
				$auth_url
			);
		}

		return $auth_url;
	}

	/**
	 * Prepare a block array for WordPress serialize_blocks().
	 *
	 * LLM-generated blocks only have blockName/attrs/innerBlocks.
	 * WordPress serialize_block() requires innerHTML and innerContent
	 * to know how to render the block tree. This adds the missing keys.
	 *
	 * - Blocks with innerBlocks: innerContent = [null, null, ...] (one per child)
	 * - Blocks without innerBlocks: innerContent = [] (self-closing)
	 * - Already-prepared blocks (from parse_blocks): left untouched
	 *
	 * @since 0.0.1
	 * @param array $blocks Array of block objects.
	 * @return array Blocks ready for serialize_blocks().
	 */
	public static function prepare_blocks_for_serialization( $blocks ) {
		if ( ! is_array( $blocks ) ) {
			return array();
		}

		return array_map(
			function ( $block ) {
				if ( ! is_array( $block ) ) {
						return $block;
				}

				// Already prepared (e.g. from parse_blocks) — skip
				if ( isset( $block['innerContent'] ) ) {
					// Still recurse into innerBlocks in case they need preparation
					if ( ! empty( $block['innerBlocks'] ) ) {
						$block['innerBlocks'] = Helper::prepare_blocks_for_serialization( $block['innerBlocks'] );
					}
					return $block;
				}

				// Ensure attrs is an array
				if ( ! isset( $block['attrs'] ) || ! is_array( $block['attrs'] ) ) {
					$block['attrs'] = array();
				}

				// Recursively prepare innerBlocks first
				if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
					$block['innerBlocks'] = Helper::prepare_blocks_for_serialization( $block['innerBlocks'] );
					// One null per inner block — tells serialize_block() where to place each child
					$block['innerContent'] = array_fill( 0, count( $block['innerBlocks'] ), null );
				} else {
					$block['innerBlocks']  = array();
					$block['innerContent'] = array();
				}

				// innerHTML is empty for Spectra blocks (content is in attrs + innerBlocks)
				if ( ! isset( $block['innerHTML'] ) ) {
					$block['innerHTML'] = '';
				}

				return $block;
			},
			$blocks 
		);
	}

	/**
	 * Get installed plugins with their versions.
	 *
	 * Returns a map of plugin_slug => version for all installed plugins (active and inactive).
	 *
	 * @since 0.0.1
	 * @return array Map of plugin_slug => version.
	 */
	public static function get_installed_plugin_versions() {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$all_plugins = get_plugins();
		$result      = array();

		// Map of plugin directories to their canonical slugs (for known aliases).
		$slug_aliases = array(
			'ultimate-addons-for-gutenberg' => 'spectra',
		);

		foreach ( $all_plugins as $plugin_file => $plugin_data ) {
			// Get the plugin directory (first part of plugin_file).
			$plugin_dir = dirname( $plugin_file );
			if ( '.' === $plugin_dir ) {
				// Single file plugin, use filename without .php.
				$plugin_dir = str_replace( '.php', '', $plugin_file );
			}

			// Use canonical slug if there's an alias, otherwise use directory name.
			$slug = $slug_aliases[ $plugin_dir ] ?? $plugin_dir;

			$result[ $slug ] = $plugin_data['Version'] ?? '0.0.0';
		}

		return $result;
	}
}

```
