Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions inc/admin-pages/class-hosting-integration-wizard-admin-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ public function section_instructions(): void {
*/
public function section_configuration(): void {

$fields = $this->integration->get_fields();
$fields = $this->integration->get_fields();
$configuration_instructions = method_exists($this->integration, 'get_configuration_instructions')
? call_user_func([$this->integration, 'get_configuration_instructions'])
: [];

// Aggregate fields from capability modules if this is a new Integration
if ($this->integration instanceof \WP_Ultimo\Integrations\Integration) {
Expand Down Expand Up @@ -292,10 +295,11 @@ public function section_configuration(): void {
wu_get_template(
'wizards/host-integrations/configuration',
[
'screen' => get_current_screen(),
'page' => $this,
'integration' => $this->integration,
'form' => $form,
'screen' => get_current_screen(),
'page' => $this,
'integration' => $this->integration,
'form' => $form,
'configuration_instructions' => $configuration_instructions,
]
);
}
Expand Down
174 changes: 174 additions & 0 deletions inc/helpers/class-oci-signer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php
/**
* OCI Request Signer.
*
* Utility class for signing Oracle Cloud Infrastructure API requests using
* OCI's RSA-SHA256 HTTP Signature scheme.
*
* @package WP_Ultimo
* @subpackage Helpers
* @since 2.5.0
*/

namespace WP_Ultimo\Helpers;

// Exit if accessed directly
defined('ABSPATH') || exit;

/**
* OCI request signer.
*
* @since 2.5.0
*/
class OCI_Signer {

/**
* Tenancy OCID.
*
* @since 2.5.0
* @var string
*/
private string $tenancy_ocid;

/**
* User OCID.
*
* @since 2.5.0
* @var string
*/
private string $user_ocid;

/**
* API key fingerprint.
*
* @since 2.5.0
* @var string
*/
private string $fingerprint;

/**
* PEM encoded private key.
*
* @since 2.5.0
* @var string
*/
private string $private_key;

/**
* Constructor.
*
* @since 2.5.0
*
* @param string $tenancy_ocid Tenancy OCID.
* @param string $user_ocid User OCID.
* @param string $fingerprint API key fingerprint.
* @param string $private_key PEM encoded private key.
*/
public function __construct(string $tenancy_ocid, string $user_ocid, string $fingerprint, string $private_key) {

$this->tenancy_ocid = $tenancy_ocid;
$this->user_ocid = $user_ocid;
$this->fingerprint = $fingerprint;
$this->private_key = $this->normalize_private_key($private_key);
}

/**
* Sign an OCI HTTP request.
*
* @since 2.5.0
*
* @param string $method HTTP method.
* @param string $url Full request URL.
* @param string $payload Request body.
* @return array<string, string>
*/
public function sign(string $method, string $url, string $payload = ''): array {

$parsed = wp_parse_url($url);
$path = $parsed['path'] ?? '/';
$query = isset($parsed['query']) && '' !== $parsed['query'] ? '?' . $parsed['query'] : '';
$host = $parsed['host'] ?? '';
$date = gmdate('D, d M Y H:i:s \G\M\T');
$method = strtolower($method);

$headers = [
'date' => $date,
'host' => $host,
'(request-target)' => $method . ' ' . $path . $query,
];

$signed_header_names = ['date', '(request-target)', 'host'];

if ('' !== $payload) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- OCI requires a base64-encoded SHA-256 digest header.
$headers['x-content-sha256'] = base64_encode(hash('sha256', $payload, true));
$headers['content-type'] = 'application/json';
$headers['content-length'] = (string) strlen($payload);

$signed_header_names = array_merge($signed_header_names, ['x-content-sha256', 'content-type', 'content-length']);
}

$signing_string = $this->build_signing_string($headers, $signed_header_names);
$signature = '';
$private_key = openssl_pkey_get_private($this->private_key);

if (false === $private_key || ! openssl_sign($signing_string, $signature, $private_key, OPENSSL_ALGO_SHA256)) {
return [];
}

$key_id = $this->tenancy_ocid . '/' . $this->user_ocid . '/' . $this->fingerprint;
$authorization = sprintf(
'Signature version="1",keyId="%s",algorithm="rsa-sha256",headers="%s",signature="%s"',
$key_id,
implode(' ', $signed_header_names),
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- OCI requires the RSA signature to be base64 encoded.
base64_encode($signature)
);

$response_headers = [
'Authorization' => $authorization,
'Date' => $date,
];

if ('' !== $payload) {
$response_headers['x-content-sha256'] = $headers['x-content-sha256'];
$response_headers['Content-Type'] = $headers['content-type'];
$response_headers['Content-Length'] = $headers['content-length'];
}

return $response_headers;
}

/**
* Build the newline-delimited OCI signing string.
*
* @since 2.5.0
*
* @param array<string, string> $headers Header values keyed by lowercase header name.
* @param array<int, string> $signed_header_names Ordered signed header names.
* @return string
*/
private function build_signing_string(array $headers, array $signed_header_names): string {

$lines = [];

foreach ($signed_header_names as $name) {
$lines[] = $name . ': ' . $headers[ $name ];
}

return implode("\n", $lines);
}

/**
* Normalize private keys copied through constants or option forms.
*
* @since 2.5.0
*
* @param string $private_key Private key value.
* @return string
*/
private function normalize_private_key(string $private_key): string {

return str_replace('\\n', "\n", trim($private_key));
}
}
2 changes: 2 additions & 0 deletions inc/integrations/class-integration-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ private function register_core_integrations(): void {
$this->register(new Providers\BunnyNet\BunnyNet_Integration());
$this->register(new Providers\LaravelForge\LaravelForge_Integration());
$this->register(new Providers\Amazon_SES\Amazon_SES_Integration());
$this->register(new Providers\OCI_Email\OCI_Email_Integration());
$this->register(new Providers\CyberPanel\CyberPanel_Integration());
$this->register(new Providers\Hostinger\Hostinger_Integration());
}
Expand Down Expand Up @@ -186,6 +187,7 @@ private function register_core_capabilities(): void {
$this->add_capability('bunnynet', new Providers\BunnyNet\BunnyNet_Domain_Mapping());
$this->add_capability('laravel-forge', new Providers\LaravelForge\LaravelForge_Domain_Mapping());
$this->add_capability('amazon-ses', new Providers\Amazon_SES\Amazon_SES_Transactional_Email());
$this->add_capability('oracle-oci', new Providers\OCI_Email\OCI_Email_Transactional_Email());
$this->add_capability('cyberpanel', new Providers\CyberPanel\CyberPanel_Domain_Mapping());
$this->add_capability('hostinger', new Providers\Hostinger\Hostinger_Domain_Mapping());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,24 @@ public function get_fields(): array {
],
];
}

/**
* Returns provider-specific guidance for the configuration step.
*
* @since 2.5.0
* @return array{title: string, description: string, steps: array<int, string>}
*/
public function get_configuration_instructions(): array {

return [
'title' => __('Amazon SES configuration checklist', 'ultimate-multisite'),
'description' => __('Use IAM credentials that can manage SES identities and send mail in the selected region. After saving, Ultimate Multisite can request domain verification and return the required DNS records.', 'ultimate-multisite'),
'steps' => [
__('Create or choose an IAM access key with SES permissions such as <code>ses:GetAccount</code>, <code>ses:CreateEmailIdentity</code>, <code>ses:GetEmailIdentity</code>, <code>ses:DeleteEmailIdentity</code>, and <code>ses:SendEmail</code>.', 'ultimate-multisite'),
__('Enter the AWS Access Key ID and Secret Access Key. Prefer a restricted IAM user dedicated to Ultimate Multisite instead of a root or broad administrator key.', 'ultimate-multisite'),
__('Set the AWS region where SES is out of sandbox and where you want identities created, for example <code>us-east-1</code>. The region must match your approved SES sending region.', 'ultimate-multisite'),
__('After testing succeeds, add the SPF, DKIM, and DMARC records returned for each domain to DNS. Mail should only be considered fully configured when recipient headers show <code>spf=pass</code>, <code>dkim=pass</code>, and <code>dmarc=pass</code>.', 'ultimate-multisite'),
],
];
}
}
Loading
Loading