PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0-beta.2
WooCommerce v11.1.0-beta.2
11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / EmailEditor / Integration.php
woocommerce / src / Internal / EmailEditor Last commit date
EmailPatterns 1 year ago EmailTemplates 3 months ago PersonalizationTags 1 month ago WCTransactionalEmails 2 weeks ago BlockEmailRenderer.php 2 weeks ago EmailApiController.php 2 months ago Integration.php 2 weeks ago Logger.php 1 year ago Package.php 1 year ago PageRenderer.php 1 year ago PersonalizationTagManager.php 1 month ago TransactionalEmailPersonalizer.php 2 weeks ago WooContentProcessor.php 1 month ago
Integration.php
762 lines
1 <?php
2
3 declare( strict_types=1 );
4
5 namespace Automattic\WooCommerce\Internal\EmailEditor;
6
7 use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
8 use Automattic\WooCommerce\EmailEditor\Engine\Dependency_Check;
9 use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Renderer;
10 use Automattic\WooCommerce\EmailEditor\Engine\Send_Preview_Email;
11 use Automattic\WooCommerce\Internal\Admin\EmailPreview\EmailPreview;
12 use Automattic\WooCommerce\Internal\EmailEditor\EmailPatterns\PatternsController;
13 use Automattic\WooCommerce\Internal\EmailEditor\EmailTemplates\TemplatesController;
14 use Automattic\WooCommerce\Internal\EmailEditor\EmailTemplates\WooEmailTemplate;
15 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
16 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailScratchpadRefresher;
17 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateAutoApplier;
18 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateDivergenceDetector;
19 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncBackfill;
20 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncTracker;
21 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmails;
22 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
23 use Automattic\WooCommerce\Internal\EmailEditor\EmailTemplates\TemplateApiController;
24 use Automattic\WooCommerce\EmailEditor\Engine\Logger\Email_Editor_Logger;
25 use WP_Post;
26
27 defined( 'ABSPATH' ) || exit;
28
29 /**
30 * Integration class for the Email Editor functionality.
31 */
32 class Integration {
33 const EMAIL_POST_TYPE = 'woo_email';
34
35 /**
36 * The email editor page renderer instance.
37 *
38 * @var PageRenderer
39 */
40 private PageRenderer $editor_page_renderer;
41
42 /**
43 * The dependency check instance.
44 *
45 * @var Dependency_Check
46 */
47 private Dependency_Check $dependency_check;
48
49 /**
50 * The template API controller instance.
51 *
52 * @var TemplateApiController
53 */
54 private TemplateApiController $template_api_controller;
55
56 /**
57 * The email data API controller instance.
58 *
59 * @var EmailApiController
60 */
61 private EmailApiController $email_api_controller;
62
63 /**
64 * Email type of the postless send-preview currently being handled.
65 *
66 * Set while {@see self::send_preview_email_for_email_type()} runs so the
67 * personalizer-context filter can resolve the email without a post.
68 *
69 * @var string
70 */
71 private string $sending_preview_for_email_type = '';
72
73 /**
74 * The WC_Email instance.
75 *
76 * @var \WC_Email
77 */
78 private \WC_Email $wc_email_instance;
79
80 /**
81 * Constructor.
82 */
83 public function __construct() {
84 $editor_container = Email_Editor_Container::container();
85 $this->dependency_check = $editor_container->get( Dependency_Check::class );
86 }
87
88 /**
89 * Initialize the integration.
90 *
91 * @internal
92 */
93 final public function init(): void {
94 if ( ! $this->dependency_check->are_dependencies_met() ) {
95 // If dependencies are not met, do not initialize the email editor integration.
96 return;
97 }
98
99 add_action( 'woocommerce_init', array( $this, 'initialize' ) );
100
101 // Register the post deletion cleanup hook early and unconditionally so it works in
102 // both admin and non-admin contexts (e.g. WP-CLI). This only needs $wpdb and the
103 // posts manager singleton — it must not depend on the full editor init chain.
104 add_action( 'before_delete_post', array( $this, 'delete_email_template_associated_with_email_editor_post' ), 10, 2 );
105 }
106
107 /**
108 * Initialize the integration.
109 */
110 public function initialize() {
111 $this->init_logger();
112 $this->init_hooks();
113 $this->extend_post_api();
114 $this->extend_template_post_api();
115 $this->register_hooks();
116 }
117
118 /**
119 * Initialize the logger.
120 */
121 public function init_logger() {
122 $editor_container = Email_Editor_Container::container();
123 $logger = $editor_container->get( Email_Editor_Logger::class );
124
125 // Register the WooCommerce logger with the email editor package.
126 $logger->set_logger( new Logger( wc_get_logger() ) );
127 }
128
129 /**
130 * Initialize hooks for required classes.
131 */
132 public function init_hooks() {
133 $container = wc_get_container();
134 $container->get( PatternsController::class );
135 $container->get( TemplatesController::class );
136 $container->get( PersonalizationTagManager::class );
137 $container->get( BlockEmailRenderer::class );
138 $container->get( WCTransactionalEmails::class );
139 $this->editor_page_renderer = $container->get( PageRenderer::class );
140 $this->template_api_controller = $container->get( TemplateApiController::class );
141 $this->email_api_controller = $container->get( EmailApiController::class );
142
143 // Using any email class to get the instance.
144 $registered_emails = \WC_Emails::instance()->get_emails();
145 if ( isset( $registered_emails['WC_Email_New_Order'] ) ) {
146 $this->wc_email_instance = $registered_emails['WC_Email_New_Order'];
147 } else {
148 $first_email_key = array_key_first( $registered_emails );
149 $this->wc_email_instance = $registered_emails[ $first_email_key ];
150 }
151 }
152
153 /**
154 * Register hooks for the integration.
155 */
156 public function register_hooks() {
157 add_filter( 'woocommerce_email_editor_post_types', array( $this, 'add_email_post_type' ) );
158 add_filter( 'woocommerce_is_email_editor_page', array( $this, 'is_editor_page' ), 10, 1 );
159 add_filter( 'replace_editor', array( $this, 'replace_editor' ), 10, 2 );
160 add_filter( 'woocommerce_email_editor_send_preview_email_rendered_data', array( $this, 'update_send_preview_email_rendered_data' ), 10, 2 );
161 add_filter( 'woocommerce_email_editor_send_preview_email_personalizer_context', array( $this, 'update_send_preview_email_personalizer_context' ) );
162 add_filter( 'woocommerce_email_editor_preview_post_template_html', array( $this, 'update_preview_post_template_html_data' ), 100, 1 );
163 add_action( 'woocommerce_email_editor_send_preview_email_before_wp_mail', array( $this, 'send_preview_email_before_wp_mail' ), 10 );
164 add_action( 'woocommerce_email_editor_send_preview_email_after_wp_mail', array( $this, 'send_preview_email_after_wp_mail' ), 10 );
165 add_filter( 'woocommerce_email_editor_send_preview_email_subject', array( $this, 'update_email_subject_for_send_preview_email' ), 10, 2 );
166 // Postless send-preview: the listing "Send test email" action sends
167 // `emailType` instead of `postId` for emails without a published post.
168 // Priority 10 runs before the package's post-based handler (11).
169 add_filter( 'woocommerce_email_editor_send_preview_email', array( $this, 'send_preview_email_for_email_type' ), 10, 1 );
170 add_filter( 'woocommerce_email_editor_send_preview_email_without_post_permission', array( $this, 'authorize_postless_send_preview' ), 10, 2 );
171 // Listing Preview page for emails rendering from the file template.
172 add_action( 'admin_init', array( $this, 'render_block_email_preview_page' ) );
173 add_action( 'rest_api_init', array( $this->email_api_controller, 'register_routes' ) );
174 add_action( 'transition_post_status', array( $this, 'save_email_mapping_on_publish' ), 10, 3 );
175 // Priority 11 ensures the email editor's `init` bootstrap (default priority 10)
176 // has registered the `woo_email` post type before we register meta against it.
177 add_action( 'init', array( WCEmailTemplateDivergenceDetector::class, 'register_meta' ), 11 );
178 add_action( 'woocommerce_updated', array( WCEmailTemplateDivergenceDetector::class, 'run_sweep' ), 20 );
179 add_action( WCEmailTemplateSyncBackfill::BACKFILL_COMPLETE_ACTION, array( WCEmailTemplateDivergenceDetector::class, 'run_sweep' ), 10 );
180 // Fresh installs never cross the 10.8 db-update boundary, so the RSM-149
181 // backfill never runs and `BACKFILL_COMPLETE_OPTION` is never written.
182 // Stamp it from the `woocommerce_newly_installed` action so `run_sweep()`
183 // doesn't sit dormant forever on new sites.
184 add_action( 'woocommerce_newly_installed', array( WCEmailTemplateDivergenceDetector::class, 'mark_backfill_complete_on_fresh_install' ), 20 );
185 add_action( WCEmailTemplateAutoApplier::AUTO_APPLY_AS_HOOK, array( WCEmailTemplateAutoApplier::class, 'run' ), 10 );
186 add_action( 'woocommerce_email_template_divergence_sweep_complete', array( WCEmailTemplateAutoApplier::class, 'schedule' ), 10 );
187 // RSM-145 Tracks instrumentation: fire `_backfill_completed` once when the
188 // RSM-149 sync-meta backfill finalises. The backfill class itself is in the
189 // 10.8 feature freeze, so we hook the existing action it already publishes.
190 add_action( WCEmailTemplateSyncBackfill::BACKFILL_COMPLETE_ACTION, array( WCEmailTemplateSyncTracker::class, 'on_backfill_complete' ), 20 );
191 }
192
193 /**
194 * Add WooCommerce email post type to the list of supported post types.
195 *
196 * @param array $post_types List of post types.
197 * @return array Modified list of post types.
198 */
199 public function add_email_post_type( array $post_types ): array {
200 $post_types[] = array(
201 'name' => self::EMAIL_POST_TYPE,
202 'args' => array(
203 'labels' => array(
204 'name' => __( 'Emails', 'woocommerce' ),
205 'singular_name' => __( 'Email', 'woocommerce' ),
206 'add_new_item' => __( 'Add Email', 'woocommerce' ),
207 'edit_item' => __( 'Edit Email', 'woocommerce' ),
208 'new_item' => __( 'New Email', 'woocommerce' ),
209 'view_item' => __( 'View Email', 'woocommerce' ),
210 'search_items' => __( 'Search Emails', 'woocommerce' ),
211 ),
212 'rewrite' => array( 'slug' => self::EMAIL_POST_TYPE ),
213 'supports' => array(
214 'title',
215 'editor' => array(
216 'default-mode' => 'template-locked',
217 ),
218 'excerpt',
219 'custom-fields',
220 ),
221 'capability_type' => self::EMAIL_POST_TYPE,
222 'capabilities' => array(
223 'edit_post' => 'manage_woocommerce',
224 'read_post' => 'manage_woocommerce',
225 'delete_post' => 'manage_woocommerce',
226 'edit_posts' => 'manage_woocommerce',
227 'edit_others_posts' => 'manage_woocommerce',
228 'delete_posts' => 'manage_woocommerce',
229 'publish_posts' => 'manage_woocommerce',
230 'read_private_posts' => 'manage_woocommerce',
231 'create_posts' => 'manage_woocommerce',
232 ),
233 'map_meta_cap' => false,
234 ),
235 );
236 return $post_types;
237 }
238
239 /**
240 * Check if current page is email editor page.
241 *
242 * @param bool $is_editor_page Current editor page status.
243 * @return bool Whether current page is email editor page.
244 */
245 public function is_editor_page( bool $is_editor_page ): bool {
246 if ( $is_editor_page ) {
247 return $is_editor_page;
248 }
249
250 // We need to check early if we are on the email editor page. The check runs early so we can't use current_screen() here.
251 if ( is_admin() && isset( $_GET['post'] ) && isset( $_GET['action'] ) && 'edit' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are not verifying the nonce here because we are not using the nonce in the function and the data is okay in this context (WP-admin errors out gracefully).
252 $post = get_post( (int) $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are not verifying the nonce here because we are not using the nonce in the function and the data is okay in this context (WP-admin errors out gracefully).
253 return $post && self::EMAIL_POST_TYPE === $post->post_type;
254 }
255
256 return false;
257 }
258
259 /**
260 * Replace the default editor with our custom email editor.
261 *
262 * @param bool $replace Whether to replace the editor.
263 * @param WP_Post $post Post object.
264 * @return bool Whether the editor was replaced.
265 */
266 public function replace_editor( $replace, $post ) {
267 $current_screen = get_current_screen();
268 if ( self::EMAIL_POST_TYPE === $post->post_type && $current_screen ) {
269 $this->maybe_refresh_scratchpad( $post );
270 $this->editor_page_renderer->render();
271 return true;
272 }
273 return $replace;
274 }
275
276 /**
277 * Refresh a never-edited scratchpad from the current file template when the
278 * editor opens directly (bookmark, browser refresh) — the listing Edit flow
279 * refreshes through the REST endpoint before redirecting here.
280 *
281 * @param WP_Post $post Post being opened in the editor.
282 */
283 private function maybe_refresh_scratchpad( $post ): void {
284 if ( ! in_array( $post->post_status, array( 'auto-draft', 'draft' ), true ) ) {
285 return;
286 }
287
288 $post_manager = WCTransactionalEmailPostsManager::get_instance();
289 $email_type = $post_manager->get_email_type_from_post_id( $post->ID );
290 if ( ! is_string( $email_type ) || '' === $email_type ) {
291 return;
292 }
293
294 $email = $post_manager->get_email_by_id( $email_type );
295 if ( ! $email ) {
296 return;
297 }
298
299 ( new WCEmailScratchpadRefresher() )->maybe_refresh( $post, $email );
300 }
301
302 /**
303 * Delete the email template associated with the email editor post when the post is permanently deleted.
304 *
305 * @param int $post_id The post ID.
306 * @param WP_Post $post The post object.
307 */
308 public function delete_email_template_associated_with_email_editor_post( $post_id, $post ) {
309 if ( self::EMAIL_POST_TYPE !== $post->post_type ) {
310 return;
311 }
312
313 $post_manager = WCTransactionalEmailPostsManager::get_instance();
314
315 $email_type = $post_manager->get_email_type_from_post_id( $post_id, true );
316
317 if ( empty( $email_type ) ) {
318 return;
319 }
320
321 // Only clear the mapping when it points at the post being deleted —
322 // the email type can also resolve for unpublished scratchpad posts
323 // whose type maps to a different (live) post.
324 $post_manager->delete_email_template( $email_type, (int) $post_id );
325 }
326
327 /**
328 * Save the email type → post ID mapping when a `woo_email` post is published.
329 *
330 * Lazily created posts (drafts) carry only the `_wc_email_type` meta;
331 * the mapping that makes a post the rendering source for its email type is
332 * written here, on the first transition to `publish`. Until then the file
333 * template remains the source of truth.
334 *
335 * @param string $new_status New post status.
336 * @param string $old_status Old post status.
337 * @param \WP_Post $post Post object.
338 *
339 * @since 11.1.0
340 */
341 public function save_email_mapping_on_publish( $new_status, $old_status, $post ): void {
342 if ( ! $post instanceof \WP_Post || self::EMAIL_POST_TYPE !== $post->post_type ) {
343 return;
344 }
345
346 if ( 'publish' !== $new_status || 'publish' === $old_status ) {
347 return;
348 }
349
350 $email_type = get_post_meta( $post->ID, WCTransactionalEmailPostsManager::EMAIL_TYPE_META_KEY, true );
351 if ( empty( $email_type ) || ! is_string( $email_type ) ) {
352 return;
353 }
354
355 $post_manager = WCTransactionalEmailPostsManager::get_instance();
356
357 // Only map registered email types — the meta could carry an arbitrary
358 // string on imported or programmatically created posts.
359 if ( ! $post_manager->get_email_by_id( $email_type ) ) {
360 return;
361 }
362
363 $post_manager->save_email_template_post_id( $email_type, $post->ID );
364 }
365
366 /**
367 * Extend the post API for the wp_template post type to add and save the woocommerce_data field.
368 */
369 public function extend_template_post_api(): void {
370 register_rest_field(
371 'wp_template',
372 'woocommerce_data',
373 array(
374 'get_callback' => array( $this->template_api_controller, 'get_template_data' ),
375 'update_callback' => array( $this->template_api_controller, 'save_template_data' ),
376 'schema' => $this->template_api_controller->get_template_data_schema(),
377 )
378 );
379 }
380
381 /**
382 * Filter email preview data to replace placeholders with actual content.
383 *
384 * This method retrieves the appropriate email type based on the request,
385 * generates the email content using the WooContentProcessor, and replaces
386 * the placeholder in the preview HTML.
387 *
388 * @param string $data The preview data.
389 * @param string $email_type The email type identifier (e.g., 'customer_processing_order').
390 * @param int $post_id The post ID.
391 * @return string The updated preview data with placeholders replaced.
392 */
393 private function update_email_preview_data( $data, string $email_type, $post_id = 0 ) {
394 $type_param = EmailPreview::DEFAULT_EMAIL_TYPE;
395
396 if ( ! empty( $post_id ) ) {
397 $type_param = WCTransactionalEmailPostsManager::get_instance()->get_email_type_class_name_from_post_id( $post_id );
398 } elseif ( ! empty( $email_type ) ) {
399 $type_param = WCTransactionalEmailPostsManager::get_instance()->get_email_type_class_name_from_email_id( $email_type );
400 }
401
402 $email_preview = wc_get_container()->get( EmailPreview::class );
403
404 try {
405 $message = $email_preview->generate_placeholder_content( $type_param );
406 } catch ( \InvalidArgumentException $e ) {
407 // If the provided type was invalid, fall back to the default.
408 try {
409 $message = $email_preview->generate_placeholder_content( EmailPreview::DEFAULT_EMAIL_TYPE );
410 } catch ( \Throwable $e ) {
411 return $data;
412 }
413 } catch ( \Throwable $e ) {
414 return $data;
415 }
416
417 return str_replace( BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER, $message, $data );
418 }
419
420 /**
421 * Filter email preview data used when sending a preview email.
422 *
423 * @param string $data The preview data.
424 * @param WP_Post $post The post object.
425 * @return string The updated preview data with placeholders replaced.
426 */
427 public function update_send_preview_email_rendered_data( $data, $post ) {
428 $email_type = '';
429 $post_body = file_get_contents( 'php://input' );
430
431 if ( $post_body ) {
432 $decoded_body = json_decode( $post_body );
433
434 if ( json_last_error() === JSON_ERROR_NONE && isset( $decoded_body->postId ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
435 $post_id = absint( $decoded_body->postId ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
436
437 $email_type = WCTransactionalEmailPostsManager::get_instance()->get_email_type_from_post_id( $post_id );
438 if ( ! empty( $email_type ) ) {
439 return $this->update_email_preview_data( $data, $email_type );
440 }
441 }
442 } elseif ( ! empty( $post ) && $post instanceof \WP_Post ) {
443 $email_type = WCTransactionalEmailPostsManager::get_instance()->get_email_type_from_post_id( $post->ID );
444 if ( ! empty( $email_type ) ) {
445 return $this->update_email_preview_data( $data, $email_type, $post->ID );
446 }
447 }
448 return $data;
449 }
450
451 /**
452 * Update the personalizer context for the send preview email.
453 *
454 * @param array $context The personalizer context.
455 * @return array The updated personalizer context.
456 */
457 public function update_send_preview_email_personalizer_context( $context ) {
458 $post_manager = WCTransactionalEmailPostsManager::get_instance();
459 $email_id = $post_manager->get_email_type_from_post_id( get_the_ID() );
460 if ( ! $email_id && '' !== $this->sending_preview_for_email_type ) {
461 // Postless send-preview: there is no post to resolve the email
462 // from, the type comes from the request instead.
463 $email_id = $this->sending_preview_for_email_type;
464 }
465 $email_type = $email_id ? $post_manager->get_email_type_class_name_from_email_id( $email_id ) : EmailPreview::DEFAULT_EMAIL_TYPE;
466 $email_preview = wc_get_container()->get( EmailPreview::class );
467
468 try {
469 $email_preview->set_email_type( $email_type );
470 } catch ( \InvalidArgumentException $e ) {
471 // If the email type is invalid, return the context data as is.
472 return $context;
473 }
474
475 $email = $email_preview->get_email();
476 $email->recipient = $context['recipient_email'] ?? '';
477 $personalizer = wc_get_container()->get( TransactionalEmailPersonalizer::class );
478
479 return $personalizer->prepare_context_data( $context, $email );
480 }
481
482 /**
483 * Send a preview email rendered from the file template when the request
484 * carries an email type instead of a post ID.
485 *
486 * Runs before the email editor package's post-based handler; requests with
487 * a post ID pass through untouched. The rendered content is the canonical
488 * file template — exactly what customers receive while the email has no
489 * published post.
490 *
491 * @param array|bool $data Send-preview request data, or a bool when a previous handler already sent the email.
492 * @return array|bool True/false when handled here (email sent / send failed); the unchanged data otherwise.
493 * @throws \InvalidArgumentException When the recipient address or email type is invalid, or the email has no template content.
494 */
495 public function send_preview_email_for_email_type( $data ) {
496 if ( ! is_array( $data ) || ! empty( $data['postId'] ) ) {
497 return $data;
498 }
499
500 $email_type = isset( $data['emailType'] ) ? sanitize_text_field( (string) $data['emailType'] ) : '';
501 if ( '' === $email_type ) {
502 return $data;
503 }
504
505 $recipient = isset( $data['email'] ) ? sanitize_email( (string) $data['email'] ) : '';
506 if ( ! is_email( $recipient ) ) {
507 throw new \InvalidArgumentException( 'Invalid email address' );
508 }
509
510 $preview = $this->render_preview_html_for_email_type( $email_type );
511
512 $send_preview = Email_Editor_Container::container()->get( Send_Preview_Email::class );
513
514 return $send_preview->send_email( $recipient, $preview['subject'], $preview['html'] );
515 }
516
517 /**
518 * Render the file-template preview of an email type — the content customers
519 * receive while the email has no published post — with the preview order
520 * context applied and personalization tags resolved.
521 *
522 * Shared by the postless send-test handler and the listing Preview page.
523 *
524 * @param string $email_type The email type identifier (e.g. `customer_processing_order`).
525 * @return array{subject: string, html: string} The preview subject and full HTML document.
526 * @throws \InvalidArgumentException When the email type is invalid or has no template content.
527 */
528 public function render_preview_html_for_email_type( string $email_type ): array {
529 $email = WCTransactionalEmailPostsManager::get_instance()->get_email_by_id( $email_type );
530 if ( ! $email instanceof \WC_Email || ! in_array( $email_type, WCTransactionalEmails::get_transactional_emails(), true ) ) {
531 throw new \InvalidArgumentException( 'Unknown email type' );
532 }
533
534 $content = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
535 if ( '' === $content ) {
536 throw new \InvalidArgumentException( 'The email has no template content' );
537 }
538
539 $container = Email_Editor_Container::container();
540 $renderer = $container->get( Renderer::class );
541 $send_preview = $container->get( Send_Preview_Email::class );
542
543 $subject = $this->get_preview_subject_for_email_type( $email_type, $email );
544
545 add_filter( 'woocommerce_email_editor_rendering_email_context', array( $send_preview, 'add_preview_context' ) );
546 try {
547 $rendered = $renderer->render_from_content(
548 $content,
549 ( new WooEmailTemplate() )->get_slug(),
550 $subject,
551 __( 'Preview', 'woocommerce' ),
552 get_bloginfo( 'language' )
553 );
554 } finally {
555 remove_filter( 'woocommerce_email_editor_rendering_email_context', array( $send_preview, 'add_preview_context' ) );
556 }
557
558 $html = $this->update_email_preview_data( $rendered['html'], $email_type );
559
560 $this->sending_preview_for_email_type = $email_type;
561 try {
562 $html = $send_preview->set_personalize_content( $html );
563 } finally {
564 $this->sending_preview_for_email_type = '';
565 }
566
567 return array(
568 'subject' => $subject,
569 'html' => $html,
570 );
571 }
572
573 /**
574 * Render the listing Preview page for an email without a published post.
575 *
576 * Mirrors the `preview_woocommerce_mail` admin page pattern: a nonce-gated
577 * query-param handler that echoes the full preview HTML document. Used by
578 * the settings listing's Preview action for emails whose rendering source
579 * is the file template (no post, or an unpublished draft).
580 */
581 public function render_block_email_preview_page(): void {
582 if ( ! isset( $_GET['preview_woo_block_email'] ) ) {
583 return;
584 }
585
586 // Verifies the `_wpnonce` query arg that `wp_nonce_url()` appended to
587 // the listing payload's preview URL; dies when missing or invalid.
588 check_admin_referer( 'preview-woo-block-email' );
589
590 if ( ! current_user_can( 'manage_woocommerce' ) ) {
591 wp_die( esc_html__( 'You do not have permission to preview emails.', 'woocommerce' ) );
592 }
593
594 $email_type = isset( $_GET['email_id'] ) ? sanitize_text_field( wp_unslash( $_GET['email_id'] ) ) : '';
595
596 try {
597 $preview = $this->render_preview_html_for_email_type( $email_type );
598 } catch ( \InvalidArgumentException $e ) {
599 wp_die( esc_html__( 'This email cannot be previewed.', 'woocommerce' ) );
600 }
601
602 echo $preview['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Full HTML document produced by the email renderer; escaping would break it.
603 exit;
604 }
605
606 /**
607 * Authorize postless send-preview requests for registered block emails.
608 *
609 * The package rejects requests without a post ID by default; grant them for
610 * shop managers when the requested email type is registered for the block
611 * editor.
612 *
613 * @param bool $allowed Current authorization.
614 * @param \WP_REST_Request $request The send-preview REST request.
615 * @return bool Whether the request is authorized.
616 *
617 * @phpstan-param \WP_REST_Request<array{emailType?: string}> $request
618 */
619 public function authorize_postless_send_preview( $allowed, $request ) {
620 if ( $allowed ) {
621 return $allowed;
622 }
623
624 if ( ! current_user_can( 'manage_woocommerce' ) ) {
625 return false;
626 }
627
628 $email_type = sanitize_text_field( (string) $request->get_param( 'emailType' ) );
629
630 return '' !== $email_type && in_array( $email_type, WCTransactionalEmails::get_transactional_emails(), true );
631 }
632
633 /**
634 * Build the preview subject for an email type, mirroring the post-based
635 * preview subject (placeholders resolved against the preview order).
636 *
637 * @param string $email_type The email type identifier.
638 * @param \WC_Email $email The email instance, used for fallbacks.
639 * @return string The preview subject.
640 */
641 private function get_preview_subject_for_email_type( string $email_type, \WC_Email $email ): string {
642 $class_name = WCTransactionalEmailPostsManager::get_instance()->get_email_type_class_name_from_email_id( $email_type );
643 if ( ! empty( $class_name ) ) {
644 try {
645 $email_preview = wc_get_container()->get( EmailPreview::class );
646 $email_preview->set_email_type( $class_name );
647 return $email_preview->get_subject();
648 } catch ( \Throwable $e ) {
649 // Fall through to the WC_Email fallbacks below.
650 unset( $e );
651 }
652 }
653
654 // Third-party get_subject() implementations may assume send-time state
655 // (e.g. Bookings dereferences its booking object), so it must only run
656 // guarded; the title is the always-safe last resort.
657 try {
658 return (string) $email->get_subject();
659 } catch ( \Throwable $e ) {
660 return (string) $email->get_title();
661 }
662 }
663
664 /**
665 * Filter email preview data used when previewing the email in new tab.
666 *
667 * @param string $data The preview HTML string.
668 * @return string The updated preview HTML with placeholders replaced.
669 */
670 public function update_preview_post_template_html_data( $data ) {
671 // return early if the data does not contain the placeholder meaning it's already been processed.
672 if ( ! str_contains( (string) $data, BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER ) ) {
673 return $data;
674 }
675
676 // phpcs:disable WordPress.Security.NonceVerification.Recommended
677 // Nonce verification is disabled here because the preview action doesn't modify data,
678 // and the check caused issues with the 'Preview in new tab' feature due to context changes.
679 $type_param = isset( $_GET['woo_email'] ) ? sanitize_text_field( wp_unslash( $_GET['woo_email'] ) ) : '';
680
681 // check for post id (preview id) in the request.
682 $post_id = isset( $_REQUEST['preview_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['preview_id'] ) ) : '';
683
684 // phpcs:enable
685 return $this->update_email_preview_data( $data, $type_param, $post_id );
686 }
687
688 /**
689 * Extend the post API for the woo_email post type to add and save the woocommerce_data field.
690 */
691 public function extend_post_api(): void {
692 register_rest_field(
693 self::EMAIL_POST_TYPE,
694 'woocommerce_data',
695 array(
696 'get_callback' => array( $this->email_api_controller, 'get_email_data' ),
697 'update_callback' => array( $this->email_api_controller, 'save_email_data' ),
698 'schema' => $this->email_api_controller->get_email_data_schema(),
699 )
700 );
701 }
702
703 /**
704 * Action hook callback before sending the preview email via wp_mail
705 *
706 * @since 10.6.0
707 * @return void
708 */
709 public function send_preview_email_before_wp_mail() {
710 add_filter( 'wp_mail_from', array( $this->wc_email_instance, 'get_from_address' ) );
711 add_filter( 'wp_mail_from_name', array( $this->wc_email_instance, 'get_from_name' ) );
712 }
713
714 /**
715 * Action hook callback after sending the preview email via wp_mail.
716 *
717 * @since 10.6.0
718 * @return void
719 */
720 public function send_preview_email_after_wp_mail() {
721 remove_filter( 'wp_mail_from', array( $this->wc_email_instance, 'get_from_address' ) );
722 remove_filter( 'wp_mail_from_name', array( $this->wc_email_instance, 'get_from_name' ) );
723 }
724
725 /**
726 * Update the email subject for the send preview email.
727 *
728 * @param string $subject The email subject.
729 * @param WP_Post $post The post object.
730 * @return string The updated email subject.
731 */
732 public function update_email_subject_for_send_preview_email( $subject, $post ) {
733 if ( ! $post instanceof \WP_Post || self::EMAIL_POST_TYPE !== $post->post_type ) {
734 return $subject;
735 }
736
737 $post_manager = WCTransactionalEmailPostsManager::get_instance();
738
739 $email_type_class_name = $post_manager->get_email_type_class_name_from_post_id( $post->ID );
740
741 if ( empty( $email_type_class_name ) ) {
742 return $subject;
743 }
744
745 /**
746 * Validate the email type class name.
747 *
748 * @var EmailPreview $email_preview
749 */
750 $email_preview = wc_get_container()->get( EmailPreview::class );
751
752 try {
753 $email_preview->set_email_type( $email_type_class_name );
754 return $email_preview->get_subject();
755 } catch ( \InvalidArgumentException $e ) {
756 return $subject;
757 } catch ( \Throwable $e ) {
758 return $subject;
759 }
760 }
761 }
762