* @author BoldGrid */ class CdnEngine_Azure_MI_Utility { /** * Entra API version. * * @since 2.7.7 * * @var string */ const ENTRA_API_VERSION = '2019-08-01'; /** * Entra resource URI. * * @since 2.7.7 * * @var string */ const ENTRA_RESOURCE_URI = 'https://storage.azure.com'; /** * Blob API version. * * @since 2.7.7 * * @var string */ const BLOB_API_VERSION = '2020-10-02'; /** * Retrieves an access token from the Managed Identity endpoint. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @return string $access_token * @throws \RuntimeException Runtine Exception. */ public static function get_access_token( string $entra_client_id ): string { // Get environment variables. $identity_header = \getenv( 'IDENTITY_HEADER' ); $identity_endpoint = \getenv( 'IDENTITY_ENDPOINT' ); // Validate variables. if ( empty( $identity_endpoint ) || empty( $identity_header ) || empty( $entra_client_id ) ) { throw new \RuntimeException( 'Error: get_access_token - missing required environment variables.' ); } // Construct URL for cURL request. $url = $identity_endpoint . '?' . http_build_query( array( 'api-version' => self::ENTRA_API_VERSION, 'resource' => self::ENTRA_RESOURCE_URI, 'client_id' => $entra_client_id, ) ); // Initialize and execute cURL request. $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, $url ); \curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'GET' ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'X-IDENTITY-HEADER: ' . $identity_header ) ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: get_access_token - cURL request failed: ' . \esc_html( $error ) ); } $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 200 !== $http_code ) { throw new \RuntimeException( 'Error: get_access_token - HTTP request failed with status code :' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: get_access_token - invalid response data: ' . \esc_html( $response ) ); } // Parse JSON response and extract access_token. $json_response = \json_decode( $response, true ); if ( \json_last_error() !== JSON_ERROR_NONE ) { throw new \RuntimeException( 'Error: get_access_token - failed to parse the JSON response: ' . \esc_html( \json_last_error_msg() ) ); } if ( empty( $json_response['access_token'] ) ) { throw new \RuntimeException( 'Error: get_access_token - no token found in response data: ' . \esc_html( $response ) ); } return $json_response['access_token']; } /** * Get Azure Blob Storage blob properties. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @param string $container_id Container ID. * @param string $blob Blob ID. * @return array * @throws \RuntimeException Runtine Exception. */ public static function get_blob_properties( string $entra_client_id, string $storage_account, string $container_id, $blob ): array { $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', time() ), ) ); \curl_setopt( $ch, CURLOPT_HEADER, true ); \curl_setopt( $ch, CURLOPT_NOBODY, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: get_blob_properties - cURL request failed: ' . \esc_html( $error ) ); } $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 200 !== $http_code ) { throw new \RuntimeException( 'Error: get_blob_properties - HTTP request failed with status code: ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: get_blob_properties - invalid response data: ' . \esc_html( $response ) ); } return self::parse_header( $response ); } /** * Create block blob. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @param string $container_id Container ID. * @param string $blob Blob ID. * @param mixed $contents Contents. * @param string $content_type Content type. * @param string $content_md5 Content MD5 hash. * @param string $cache_control Cache control header value. * @return array * @throws \RuntimeException Runtine Exception. */ public static function create_block_blob( string $entra_client_id, string $storage_account, string $container_id, string $blob, $contents, string $content_type = null, string $content_md5 = null, string $cache_control = null ): array { $headers = array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ), 'x-ms-blob-type: BlockBlob', ); if ( $content_type ) { $headers[] = 'x-ms-blob-content-type: ' . $content_type; } if ( $content_md5 ) { $headers[] = 'x-ms-blob-content-md5: ' . $content_md5; } if ( $cache_control ) { $headers[] = 'x-ms-blob-cache-control: ' . $cache_control; } $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' ); \curl_setopt( $ch, CURLOPT_POSTFIELDS, $contents ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_HEADER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: create_block_blob - cURL request failed: ' . \esc_html( $error ) ); } $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 201 !== $http_code ) { throw new \RuntimeException( 'Error: create_block_blob - HTTP request failed with status code ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: create_block_blob - invalid response data: ' . \esc_html( $response ) ); } return self::parse_header( $response ); } /** * Delete blob. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @param string $container_id Container ID. * @param string $blob Blob ID. * @return array * @throws \RuntimeException Runtine Exception. */ public static function delete_blob( string $entra_client_id, string $storage_account, string $container_id, string $blob ) { $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'DELETE' ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ), ) ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_HEADER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: delete_blob - cURL request failed: ' . \esc_html( $error ) ); } $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( 202 !== $http_code ) { throw new \RuntimeException( 'Error: delete_blob - HTTP request failed with status code ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: delete_blob - invalid response data: ' . \esc_html( $response ) ); } return self::parse_header( $response ); } /** * Create an Azure Blob Storage container/bucket. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @param string $container_id Container ID. * @param string $public_access_type Public access type. * @return array * @throws \RuntimeException Runtine Exception. */ public static function create_container( string $entra_client_id, string $storage_account, string $container_id, string $public_access_type = 'blob' ): array { $headers = array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ), 'Content-Length: 0', ); if ( $public_access_type ) { $headers[] = "x-ms-blob-public-access: $public_access_type"; } $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '?restype=container' ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_HEADER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: create_container - cURL request failed: ' . \esc_html( $error ) ); } $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 201 !== $http_code ) { throw new \RuntimeException( 'Error: create_container - HTTP request failed with status code: ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: create_container - empty response.' ); } return self::parse_header( $response ); } /** * List Azure Blob Storage containers. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @return array * @throws \RuntimeException Runtine Exception. */ public static function list_containers( string $entra_client_id, string $storage_account ): array { $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/?comp=list' ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ), ) ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: list_containers - cURL request failed: ' . \esc_html( $error ) ); } $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 200 !== $http_code ) { throw new \RuntimeException( 'Error: list_containers - HTTP request failed with status code: ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: list_containers - Invalid response data: ' . \esc_html( $response ) ); } // Parse XML response to array. $xml = \simplexml_load_string( $response ); $json = \json_encode( $xml ); $response = \json_decode( $json, true ); $array_response = array(); if ( ! empty( $response['Containers']['Container'] ) ) { $array_response = self::get_array( $response['Containers']['Container'] ); } return $array_response; } /** * Get blob. * * @since 2.7.7 * * @param string $entra_client_id Entra ID. * @param string $storage_account Storage account name. * @param string $container_id Container ID. * @param string $blob Blob ID. * @return array * @throws \RuntimeException Runtine Exception. */ public static function get_blob( string $entra_client_id, string $storage_account, string $container_id, string $blob ): array { $ch = \curl_init(); \curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob ); \curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); \curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Bearer ' . self::get_access_token( $entra_client_id ), 'x-ms-version: ' . self::BLOB_API_VERSION, 'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ), ) ); \curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); \curl_setopt( $ch, CURLOPT_HEADER, true ); $response = \curl_exec( $ch ); if ( \curl_errno( $ch ) ) { $error = \curl_error( $ch ); \curl_close( $ch ); throw new \RuntimeException( 'Error: get_blob - cURL request failed: ' . \esc_html( $error ) ); } $header_size = \curl_getinfo( $ch, CURLINFO_HEADER_SIZE ); $http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE ); \curl_close( $ch ); if ( 200 !== $http_code ) { throw new \RuntimeException( 'Error: get_blob - HTTP request failed with status code: ' . \esc_html( $http_code ) ); } if ( empty( $response ) ) { throw new \RuntimeException( 'Error: get_blob - Invalid response data: ' . \esc_html( $response ) ); } return array( 'headers' => self::parse_header( \substr( $response, 0, $header_size ) ), 'data' => \substr( $response, $header_size ), ); } /** * Get array. * * @since 2.7.7 * @access private * * @param mixed $var Variable used to get array. * @return array */ private static function get_array( $var ): array { if ( empty( $var ) || ! \is_array( $var ) ) { return array(); } foreach ( $var as $value ) { if ( ! \is_array( $value ) ) { return array( $var ); } return $var; } } /** * Parse header from string to array. * * @since 2.7.7 * @access private * * @param string $header Header. * @return array */ private static function parse_header( string $header ): array { $headers = array(); $header_text = \substr( $header, 0, \strpos( $header, "\r\n\r\n" ) ); $header_parts = \explode( "\r\n", $header_text ); foreach ( $header_parts as $header ) { if ( \strpos( $header, ':' ) !== false ) { $header_parts = \explode( ':', $header ); $headers[ $header_parts[0] ] = \trim( $header_parts[1] ); } } return $headers; } }