a#', 'title' => 'email-listing-with-message', 'type' => 'object', 'properties' => array( 'message' => array( 'description' => __( 'A message indicating that the action completed successfully.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'post_id' => array( 'description' => __( 'The post ID of the generated email post.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); } /** * Validate the email ID. * * @param string $email_id The email ID to validate. * @return bool|WP_Error True if the email ID is valid, otherwise a WP_Error object. */ private function validate_email_id( string $email_id ) { if ( ! in_array( $email_id, WCTransactionalEmails::get_transactional_emails(), true ) ) { return new \WP_Error( 'woocommerce_rest_not_allowed_email_id', sprintf( 'The provided email ID "%s" is not allowed.', $email_id ), array( 'status' => 400 ), ); } return true; } /** * Permission check for REST API endpoint. * * @param WP_REST_Request $request The request for which the permission is checked. * @return bool|WP_Error True if the current user has the capability, otherwise a WP_Error object. */ private function check_permissions( WP_REST_Request $request ) { $nonce = $request->get_param( 'nonce' ); if ( ! wp_verify_nonce( $nonce, self::NONCE_KEY ) ) { return new WP_Error( 'invalid_nonce', __( 'Invalid nonce.', 'woocommerce' ), array( 'status' => 403 ), ); } return $this->check_permission( $request, 'manage_woocommerce' ); } /** * Handle the POST /settings/email/listing/recreate-email-post. * * Returns the post for the given email type, creating a draft with the * file template content when none exists yet. This follows the WordPress * Site Editor pattern: posts are only created when the user opens the editor, * and only become the rendering source once published. * * @param WP_REST_Request $request The received request. * @return array|WP_Error Request response or an error. */ public function recreate_email_post( WP_REST_Request $request ) { $email_id = $request->get_param( 'email_id' ); $post_manager = WCTransactionalEmailPostsManager::get_instance(); $this->maybe_flush_rewrite_rules(); // A mapped, non-trashed post already exists (the email was saved before). $existing_post = $post_manager->get_email_post( $email_id ); if ( $existing_post && 'trash' !== $existing_post->post_status ) { return array( // translators: %s: WooCommerce transactional email ID. 'message' => sprintf( __( 'Email post already exists for %s.', 'woocommerce' ), $email_id ), 'post_id' => (string) $existing_post->ID, ); } $email = $post_manager->get_email_by_id( (string) $email_id ); if ( ! $email ) { return new WP_Error( 'woocommerce_rest_email_post_generation_failed', // translators: %s: WooCommerce transactional email ID. sprintf( __( 'Error generating email post. Email type "%s" is not registered.', 'woocommerce' ), $email_id ), array( 'status' => 500 ) ); } // Reuse an existing editing scratchpad. When it was never edited, refresh // its content in place so it reflects the current file template (the post // ID stays stable — another admin may have the editor open on it). $scratchpad = $this->find_unpublished_post_for_email_type( $email_id ); if ( $scratchpad ) { $this->scratchpad_refresher->maybe_refresh( $scratchpad, $email ); return array( // translators: %s: WooCommerce transactional email ID. 'message' => sprintf( __( 'Email draft already exists for %s.', 'woocommerce' ), $email_id ), 'post_id' => (string) $scratchpad->ID, ); } try { $post_id = $this->email_template_generator->create_draft( $email ); } catch ( \Exception $e ) { return new WP_Error( 'woocommerce_rest_email_post_generation_failed', // translators: %s: Error message. sprintf( __( 'Error generating email post. Error: %s.', 'woocommerce' ), $e->getMessage() ), array( 'status' => 500 ) ); } return array( // translators: %s: WooCommerce transactional email ID. 'message' => sprintf( __( 'Email draft created for %s.', 'woocommerce' ), $email_id ), 'post_id' => (string) $post_id, ); } /** * Find an existing unpublished post (editing scratchpad) for the given email type. * * Scratchpads are created as drafts. The `auto-draft` status is matched as * well so the lookup keeps working when scratchpad creation switches to * auto-drafts (planned once the Gutenberg auto-draft title blanking is * fixed upstream) — including during the transition, when a site can hold * a mix of both. * * @param string $email_id The email type identifier. * @return \WP_Post|null The post if found, null otherwise. */ private function find_unpublished_post_for_email_type( string $email_id ): ?\WP_Post { $posts = get_posts( array( 'post_type' => Integration::EMAIL_POST_TYPE, 'post_status' => array( 'auto-draft', 'draft' ), 'meta_key' => WCTransactionalEmailPostsManager::EMAIL_TYPE_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Scoped to a handful of unpublished woo_email posts. 'meta_value' => $email_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Scoped to a handful of unpublished woo_email posts. 'posts_per_page' => 1, 'orderby' => 'ID', 'order' => 'DESC', ) ); return $posts[0] ?? null; } /** * Flush rewrite rules once per WC version so `woo_email` permalinks work. * * Replaces the flush that used to happen during bulk post generation. */ private function maybe_flush_rewrite_rules(): void { $wc_version = (string) Constants::get_constant( 'WC_VERSION' ); if ( get_option( self::REWRITE_FLUSH_OPTION ) === $wc_version ) { return; } flush_rewrite_rules(); update_option( self::REWRITE_FLUSH_OPTION, $wc_version, false ); } }