get_packaged_base_url(); if ( '' === trim( $base ) ) { $base = self::DEFAULT_API_BASE_URL; } $base = (string) apply_filters( 'actionpanel_ai_api_base_url', $base ); $base = untrailingslashit( trim( $base ) ); if ( ! $this->is_valid_base_url( $base ) ) { return self::DEFAULT_API_BASE_URL; } return $base; } /** * Return the API base URL embedded in the plugin package, when present. */ private function get_packaged_base_url(): string { if ( null !== $this->packaged_api_base_url ) { return $this->packaged_api_base_url; } return BuildConfig::get_api_base_url(); } /** * Return the origin portion of the API base URL. */ public function get_origin(): string { $parts = wp_parse_url( $this->get_base_url() ); if ( ! is_array( $parts ) ) { return ''; } $scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : ''; $host = isset( $parts['host'] ) ? (string) $parts['host'] : ''; if ( '' === $scheme || '' === $host ) { return ''; } $origin = $scheme . '://' . $host; if ( isset( $parts['port'] ) ) { $origin .= ':' . (int) $parts['port']; } return $origin; } /** * Validate an API base URL. * * Remote endpoints must use HTTPS. Plain HTTP is allowed only for loopback * hosts or explicitly allowlisted lab hosts. * * @param string $base Candidate base URL. */ private function is_valid_base_url( string $base ): bool { if ( '' === $base ) { return false; } $parts = wp_parse_url( $base ); if ( ! is_array( $parts ) ) { return false; } $scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : ''; $host = isset( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : ''; if ( '' === $host ) { return false; } if ( 'https' === $scheme ) { return true; } return 'http' === $scheme && ( $this->is_loopback_host( $host ) || $this->is_insecure_http_allowed_host( $host ) ); } /** * Return true for hosts that always resolve to the local machine. * * @param string $host Lowercased URL host. */ private function is_loopback_host( string $host ): bool { if ( 'localhost' === $host || '[::1]' === $host || '::1' === $host ) { return true; } return false !== filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && 0 === strpos( $host, '127.' ); } /** * Return true when a test/lab environment explicitly allows HTTP for host. * * Production and staging endpoints still require HTTPS unless they are * loopback. This exists for containerized near-E2E labs where WordPress must * reach a worker-local backend by Docker DNS hostname. * * @param string $host Lowercased URL host. */ private function is_insecure_http_allowed_host( string $host ): bool { $configured = defined( 'ACTIONPANEL_AI_INSECURE_API_HOSTS' ) ? (string) constant( 'ACTIONPANEL_AI_INSECURE_API_HOSTS' ) : ''; $hosts = array_filter( array_map( 'trim', explode( ',', strtolower( $configured ) ) ) ); $hosts = array_merge( $hosts, $this->get_packaged_insecure_api_hosts() ); $hosts = (array) apply_filters( 'actionpanel_ai_insecure_api_hosts', $hosts ); foreach ( $hosts as $allowed_host ) { if ( strtolower( trim( (string) $allowed_host ) ) === $host ) { return true; } } return false; } /** * Return the remote HTTP hosts embedded in the plugin package. * * @return string[] */ private function get_packaged_insecure_api_hosts(): array { if ( null !== $this->packaged_insecure_api_hosts ) { return array_filter( array_map( 'trim', explode( ',', strtolower( $this->packaged_insecure_api_hosts ) ) ) ); } return BuildConfig::get_insecure_api_hosts(); } }