Preview: EmailApiController.php
Size: 17.08 KB
/home/nshryvcy/radiantskinclinics.org/wp-content/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\EmailEditor;
use Automattic\WooCommerce\EmailEditor\Validator\Builder;
use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateDivergenceDetector;
use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncRegistry;
use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
use WC_Email;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* API Controller for managing WooCommerce email templates via extending the post type API.
*
* @internal
*/
class EmailApiController {
/**
* The WooCommerce transactional email post manager.
*
* @var WCTransactionalEmailPostsManager|null
*/
private ?WCTransactionalEmailPostsManager $post_manager = null;
/**
* The WooCommerce transactional email posts generator.
*
* @var WCTransactionalEmailPostsGenerator|null
*/
private ?WCTransactionalEmailPostsGenerator $posts_generator = null;
/**
* Initialize the controller.
*
* @internal
*/
final public function init(): void {
$this->post_manager = WCTransactionalEmailPostsManager::get_instance();
$this->posts_generator = new WCTransactionalEmailPostsGenerator();
}
/**
* Returns the data from wp_options table for the given post.
*
* @param array $post_data - Post data.
* @return array - The email data.
*/
public function get_email_data( $post_data ): array {
$email_type = $this->post_manager->get_email_type_from_post_id( $post_data['id'] );
$email = $this->get_email_by_type( $email_type ?? '' );
// When the email type is not found, it means that the email type is not supported.
if ( ! $email ) {
return array(
'subject' => null,
'subject_full' => null,
'subject_partial' => null,
'preheader' => null,
'default_subject' => null,
'email_type' => null,
'recipient' => null,
'cc' => null,
'bcc' => null,
);
}
$form_fields = $email->get_form_fields();
$enabled = $email->get_option( 'enabled' );
return array(
'enabled' => is_null( $enabled ) ? $email->is_enabled() : 'yes' === $enabled,
'is_manual' => $email->is_manual(),
'subject' => $email->get_option( 'subject' ),
'subject_full' => $email->get_option( 'subject_full' ), // For customer_refunded_order email type because it has two different subjects.
'subject_partial' => $email->get_option( 'subject_partial' ),
'preheader' => $email->get_option( 'preheader' ),
'default_subject' => $email->get_default_subject(),
'email_type' => $email_type,
// Recipient is possible to set only for the specific type of emails. When the field `recipient` is set in the form fields, it means that the email type has a recipient field.
'recipient' => array_key_exists( 'recipient', $form_fields ) ? $email->get_option( 'recipient', get_option( 'admin_email' ) ) : null,
'cc' => $email->get_option( 'cc' ),
'bcc' => $email->get_option( 'bcc' ),
);
}
/**
* Update WooCommerce specific option data by post name.
*
* @param array $data - Data that are stored in the wp_options table.
* @param \WP_Post $post - WP_Post object.
* @return \WP_Error|null Returns WP_Error if email validation fails, null otherwise.
*/
public function save_email_data( array $data, \WP_Post $post ): ?\WP_Error {
$error = $this->validate_email_data( $data );
if ( is_wp_error( $error ) ) {
return new \WP_Error( 'invalid_email_data', implode( ' ', $error->get_error_messages() ), array( 'status' => 400 ) );
}
if ( ! array_key_exists( 'subject', $data ) && ! array_key_exists( 'preheader', $data ) ) {
return null;
}
$email_type = $this->post_manager->get_email_type_from_post_id( $post->ID );
$email = $this->get_email_by_type( $email_type ?? '' );
if ( ! $email ) {
return null; // not saving of type wc_email. Allow process to continue.
}
// Handle customer_refunded_order email type because it has two different subjects.
if ( 'customer_refunded_order' === $email_type ) {
if ( array_key_exists( 'subject_full', $data ) ) {
$email->update_option( 'subject_full', $data['subject_full'] );
}
if ( array_key_exists( 'subject_partial', $data ) ) {
$email->update_option( 'subject_partial', $data['subject_partial'] );
}
} elseif ( array_key_exists( 'subject', $data ) ) {
$email->update_option( 'subject', $data['subject'] );
}
if ( array_key_exists( 'preheader', $data ) ) {
$email->update_option( 'preheader', $data['preheader'] );
}
if ( array_key_exists( 'enabled', $data ) ) {
$email->update_option( 'enabled', $data['enabled'] ? 'yes' : 'no' );
}
if ( array_key_exists( 'recipient', $data ) ) {
$email->update_option( 'recipient', $data['recipient'] );
}
if ( array_key_exists( 'cc', $data ) ) {
$email->update_option( 'cc', $data['cc'] );
}
if ( array_key_exists( 'bcc', $data ) ) {
$email->update_option( 'bcc', $data['bcc'] );
}
return null;
}
/**
* Validate the email data.
*
* @param array $data - The email data.
* @return \WP_Error|null Returns WP_Error if email validation fails, null otherwise.
*/
private function validate_email_data( array $data ) {
$error = new \WP_Error();
// Validate 'recipient' email(s) field.
$invalid_recipients = $this->filter_invalid_email_addresses( $data['recipient'] ?? '' );
if ( ! empty( $invalid_recipients ) ) {
$error_message = sprintf(
// translators: %s will be replaced by comma-separated email addresses. For example, "invalidemail1@example.com,invalidemail2@example.com".
__( 'One or more Recipient email addresses are invalid: “%s”. Please enter valid email addresses separated by commas.', 'woocommerce' ),
implode( ',', $invalid_recipients )
);
$error->add( 'invalid_recipient_email_address', $error_message );
}
// Validate 'cc' email(s) field.
$invalid_cc = $this->filter_invalid_email_addresses( $data['cc'] ?? '' );
if ( ! empty( $invalid_cc ) ) {
$error_message = sprintf(
// translators: %s will be replaced by comma-separated email addresses. For example, "invalidemail1@example.com,invalidemail2@example.com".
__( 'One or more CC email addresses are invalid: “%s”. Please enter valid email addresses separated by commas.', 'woocommerce' ),
implode( ',', $invalid_cc )
);
$error->add( 'invalid_cc_email_address', $error_message );
}
// Validate 'bcc' email(s) field.
$invalid_bcc = $this->filter_invalid_email_addresses( $data['bcc'] ?? '' );
if ( ! empty( $invalid_bcc ) ) {
$error_message = sprintf(
// translators: %s will be replaced by comma-separated email addresses. For example, "invalidemail1@example.com,invalidemail2@example.com".
__( 'One or more BCC email addresses are invalid: “%s”. Please enter valid email addresses separated by commas.', 'woocommerce' ),
implode( ',', $invalid_bcc )
);
$error->add( 'invalid_bcc_email_address', $error_message );
}
if ( $error->has_errors() ) {
return $error;
}
return null;
}
/**
* Filter in invalid email addresses from a comma-separated string.
*
* @param string $comma_separated_email_addresses - A comma-separated string of email addresses.
* @return array - An array of invalid email addresses.
*/
private function filter_invalid_email_addresses( $comma_separated_email_addresses ) {
$invalid_email_addresses = array();
if ( empty( trim( $comma_separated_email_addresses ) ) ) {
return $invalid_email_addresses;
}
foreach ( explode( ',', $comma_separated_email_addresses ) as $email_address ) {
if ( ! filter_var( trim( $email_address ), FILTER_VALIDATE_EMAIL ) ) {
$invalid_email_addresses[] = trim( $email_address );
}
}
return $invalid_email_addresses;
}
/**
* Get the schema for the WooCommerce email post data.
*
* @return array
*/
public function get_email_data_schema(): array {
return Builder::object(
array(
'subject' => Builder::string()->nullable(),
'subject_full' => Builder::string()->nullable(), // For customer_refunded_order email type because it has two different subjects.
'subject_partial' => Builder::string()->nullable(),
'preheader' => Builder::string()->nullable(),
'default_subject' => Builder::string()->nullable(),
'email_type' => Builder::string()->nullable(),
'recipient' => Builder::string()->nullable(),
'cc' => Builder::string()->nullable(),
'bcc' => Builder::string()->nullable(),
)
)->to_array();
}
/**
* Get all WooCommerce emails.
*
* @return \WC_Email[]
*/
protected function get_emails(): array {
return WC()->mailer()->get_emails();
}
/**
* Get the email object by ID.
*
* @param string $id - The email ID.
* @return \WC_Email|null - The email object or null if not found.
*/
private function get_email_by_type( ?string $id ): ?WC_Email {
foreach ( $this->get_emails() as $email ) {
if ( $email->id === $id ) {
return $email;
}
}
return null;
}
/**
* Register REST API routes for the email API controller.
*/
public function register_routes(): void {
register_rest_route(
'woocommerce-email-editor/v1',
'/emails/(?P<id>\d+)/default-content',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_default_content_response' ),
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'args' => array(
'id' => array(
'description' => __( 'The ID of the woo_email post.', 'woocommerce' ),
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
),
),
'schema' => array( $this, 'get_default_content_schema' ),
)
);
register_rest_route(
'woocommerce-email-editor/v1',
'/emails/(?P<id>\d+)/reset',
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'reset_response' ),
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'args' => array(
'id' => array(
'description' => __( 'The ID of the woo_email post.', 'woocommerce' ),
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
),
),
'schema' => array( $this, 'get_reset_schema' ),
)
);
}
/**
* Get the schema for the default content endpoint response.
*
* @return array
*/
public function get_default_content_schema(): array {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'woo_email_default_content',
'type' => 'object',
'properties' => array(
'content' => array(
'description' => __( 'The default block content for the email.', 'woocommerce' ),
'type' => 'string',
'readonly' => true,
),
),
);
}
/**
* Return the default (plugin-distributed) block content for a woo_email post.
*
* @param WP_REST_Request $request The REST request.
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
* @return WP_REST_Response|WP_Error
*/
public function get_default_content_response( WP_REST_Request $request ) {
if ( ! ( $this->post_manager && $this->posts_generator ) ) {
return new WP_Error(
'woocommerce_email_editor_not_initialized',
__( 'Email editor is not initialized.', 'woocommerce' ),
array( 'status' => 500 )
);
}
$post_id = (int) $request->get_param( 'id' );
$email_type = $this->post_manager->get_email_type_from_post_id( $post_id );
$email = $this->get_email_by_type( $email_type ?? '' );
if ( ! $email ) {
return new WP_Error(
'woocommerce_email_not_found',
__( 'No email found for the given post ID.', 'woocommerce' ),
array( 'status' => 404 )
);
}
return new WP_REST_Response(
array( 'content' => $this->posts_generator->get_email_template( $email ) ),
200
);
}
/**
* Get the schema for the reset endpoint response.
*
* @return array
*/
public function get_reset_schema(): array {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'woo_email_reset',
'type' => 'object',
'properties' => array(
'content' => array(
'description' => __( 'The canonical block content written to the post.', 'woocommerce' ),
'type' => 'string',
'readonly' => true,
),
'version' => array(
'description' => __( 'The core block template @version stamped on the post, or null when the email is not sync-enabled.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'readonly' => true,
),
'source_hash' => array(
'description' => __( 'sha1 of the canonical block content stamped on the post, or null when the email is not sync-enabled.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'readonly' => true,
),
'synced_at' => array(
'description' => __( 'UTC timestamp when the post was stamped (Y-m-d H:i:s), or null when the email is not sync-enabled.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'The post-reset sync status (in_sync on success for sync-enabled emails, null otherwise).', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'readonly' => true,
),
),
);
}
/**
* Reset a `woo_email` post to its current core template render and (when sync-enabled) stamp sync meta.
*
* Writes the canonical post content (byte-identical to what
* {@see WCTransactionalEmailPostsGenerator} would produce on a fresh recreate). For emails
* that are opted in to template sync (registered in {@see WCEmailTemplateSyncRegistry}),
* also stamps `_wc_email_template_version`, `_wc_email_template_source_hash`,
* `_wc_email_last_synced_at`, and `_wc_email_template_status = in_sync`. Meta writes are
* conditional on the post update succeeding, so a `wp_update_post` failure leaves the
* post — and any pre-existing meta — untouched.
*
* Non-sync-enabled emails (e.g. third-party templates without an `@version` header)
* still receive a successful content reset, just without the meta stamp. This mirrors
* the pre-RSM-148 behaviour where the standalone REST PUT performed the content reset
* and stamping was a separate side effect, preserving backward compatibility.
*
* @param WP_REST_Request $request The REST request.
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
* @return WP_REST_Response|WP_Error
*
* @since 10.8.0
*/
public function reset_response( WP_REST_Request $request ) {
if ( ! ( $this->post_manager && $this->posts_generator ) ) {
return new WP_Error(
'woocommerce_email_editor_not_initialized',
__( 'Email editor is not initialized.', 'woocommerce' ),
array( 'status' => 500 )
);
}
$post_id = (int) $request->get_param( 'id' );
$email_type = $this->post_manager->get_email_type_from_post_id( $post_id );
$email = $this->get_email_by_type( $email_type ?? '' );
if ( ! $email ) {
return new WP_Error(
'woocommerce_email_not_found',
__( 'No email found for the given post ID.', 'woocommerce' ),
array( 'status' => 404 )
);
}
$canonical = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
$sync_config = WCEmailTemplateSyncRegistry::get_email_sync_config( (string) $email_type );
$update_result = wp_update_post(
array(
'ID' => $post_id,
'post_content' => $canonical,
),
true
);
if ( is_wp_error( $update_result ) ) {
return new WP_Error(
'woocommerce_email_reset_failed',
sprintf(
/* translators: %s: underlying error message */
__( 'Failed to reset email content: %s', 'woocommerce' ),
$update_result->get_error_message()
),
array( 'status' => 500 )
);
}
$source_hash = null;
$synced_at = null;
if ( null !== $sync_config ) {
$source_hash = sha1( $canonical );
$synced_at = gmdate( 'Y-m-d H:i:s' );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, (string) $sync_config['version'] );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, $source_hash );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, $synced_at );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::STATUS_META_KEY, WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC );
}
return new WP_REST_Response(
array(
'content' => $canonical,
'version' => null !== $sync_config ? (string) $sync_config['version'] : null,
'source_hash' => $source_hash,
'synced_at' => $synced_at,
'status' => null !== $sync_config ? WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC : null,
),
200
);
}
}
Directory Contents
Dirs: 4 × Files: 9