PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.5.0-rc.2
WooCommerce v10.5.0-rc.2
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 / includes / emails / class-wc-email.php
woocommerce / includes / emails Last commit date
class-wc-email-cancelled-order.php 6 months ago class-wc-email-customer-cancelled-order.php 6 months ago class-wc-email-customer-completed-order.php 6 months ago class-wc-email-customer-failed-order.php 6 months ago class-wc-email-customer-fulfillment-created.php 4 months ago class-wc-email-customer-fulfillment-deleted.php 4 months ago class-wc-email-customer-fulfillment-updated.php 4 months ago class-wc-email-customer-invoice.php 6 months ago class-wc-email-customer-new-account.php 6 months ago class-wc-email-customer-note.php 6 months ago class-wc-email-customer-on-hold-order.php 6 months ago class-wc-email-customer-pos-completed-order.php 6 months ago class-wc-email-customer-pos-refunded-order.php 6 months ago class-wc-email-customer-processing-order.php 6 months ago class-wc-email-customer-refunded-order.php 6 months ago class-wc-email-customer-reset-password.php 6 months ago class-wc-email-failed-order.php 6 months ago class-wc-email-new-order.php 6 months ago class-wc-email.php 4 months ago
class-wc-email.php
1732 lines
1 <?php
2 /**
3 * Class WC_Email file.
4 *
5 * @package WooCommerce\Emails
6 */
7
8 use Automattic\WooCommerce\Internal\EmailEditor\BlockEmailRenderer;
9 use Automattic\WooCommerce\Internal\EmailEditor\TransactionalEmailPersonalizer;
10 use Automattic\WooCommerce\Utilities\FeaturesUtil;
11 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\CssInliner;
12 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
13 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( class_exists( 'WC_Email', false ) ) {
20 return;
21 }
22
23 /**
24 * Email Class
25 *
26 * WooCommerce Email Class which is extended by specific email template classes to add emails to WooCommerce
27 *
28 * @class WC_Email
29 * @version 2.5.0
30 * @package WooCommerce\Classes\Emails
31 * @extends WC_Settings_API
32 */
33 class WC_Email extends WC_Settings_API {
34
35 /**
36 * Email method ID.
37 *
38 * @var String
39 */
40 public $id;
41
42 /**
43 * Email method title.
44 *
45 * @var string
46 */
47 public $title;
48
49 /**
50 * 'yes' if the method is enabled.
51 *
52 * @var string yes, no
53 */
54 public $enabled;
55
56 /**
57 * Description for the email.
58 *
59 * @var string
60 */
61 public $description;
62
63 /**
64 * Default heading.
65 *
66 * Supported for backwards compatibility but we recommend overloading the
67 * get_default_x methods instead so localization can be done when needed.
68 *
69 * @var string
70 */
71 public $heading = '';
72
73 /**
74 * Default subject.
75 *
76 * Supported for backwards compatibility but we recommend overloading the
77 * get_default_x methods instead so localization can be done when needed.
78 *
79 * @var string
80 */
81 public $subject = '';
82
83 /**
84 * Plain text template path.
85 *
86 * @var string
87 */
88 public $template_plain;
89
90 /**
91 * HTML template path.
92 *
93 * @var string
94 */
95 public $template_html;
96
97 /**
98 * Initial email block template path.
99 *
100 * @var string
101 */
102 public $template_block;
103
104 /**
105 * Template path.
106 *
107 * @var string
108 */
109 public $template_base;
110
111 /**
112 * Recipients for the email.
113 *
114 * @var string
115 */
116 public $recipient;
117
118 /**
119 * Cc recipients for the email.
120 *
121 * @var string
122 */
123 public $cc;
124
125 /**
126 * Bcc recipients for the email.
127 *
128 * @var string
129 */
130 public $bcc;
131
132 /**
133 * Object this email is for, for example a customer, product, or email.
134 *
135 * @var object|bool
136 */
137 public $object;
138
139 /**
140 * Mime boundary (for multipart emails).
141 *
142 * @var string
143 */
144 public $mime_boundary;
145
146 /**
147 * Mime boundary header (for multipart emails).
148 *
149 * @var string
150 */
151 public $mime_boundary_header;
152
153 /**
154 * True when email is being sent.
155 *
156 * @var bool
157 */
158 public $sending;
159
160 /**
161 * True when the email notification is sent manually only.
162 *
163 * @var bool
164 */
165 protected $manual = false;
166
167 /**
168 * True when the email notification is sent to customers.
169 *
170 * @var bool
171 */
172 protected $customer_email = false;
173
174 /**
175 * Email group slug.
176 *
177 * @var string
178 */
179 public $email_group = '';
180
181 /**
182 * List of preg* regular expression patterns to search for,
183 * used in conjunction with $plain_replace.
184 * https://raw.github.com/ushahidi/wp-silcc/master/class.html2text.inc
185 *
186 * @var array $plain_search
187 * @see $plain_replace
188 */
189 public $plain_search = array(
190 "/\r/", // Non-legal carriage return.
191 '/&(nbsp|#0*160);/i', // Non-breaking space.
192 '/&(quot|rdquo|ldquo|#0*8220|#0*8221|#0*147|#0*148);/i', // Double quotes.
193 '/&(apos|rsquo|lsquo|#0*8216|#0*8217);/i', // Single quotes.
194 '/&gt;/i', // Greater-than.
195 '/&lt;/i', // Less-than.
196 '/&#0*38;/i', // Ampersand.
197 '/&amp;/i', // Ampersand.
198 '/&(copy|#0*169);/i', // Copyright.
199 '/&(trade|#0*8482|#0*153);/i', // Trademark.
200 '/&(reg|#0*174);/i', // Registered.
201 '/&(mdash|#0*151|#0*8212);/i', // mdash.
202 '/&(ndash|minus|#0*8211|#0*8722);/i', // ndash.
203 '/&(bull|#0*149|#0*8226);/i', // Bullet.
204 '/&(pound|#0*163);/i', // Pound sign.
205 '/&(euro|#0*8364);/i', // Euro sign.
206 '/&(dollar|#0*36);/i', // Dollar sign.
207 '/&[^&\s;]+;/i', // Unknown/unhandled entities.
208 '/[ ]{2,}/', // Runs of spaces, post-handling.
209 );
210
211 /**
212 * List of pattern replacements corresponding to patterns searched.
213 *
214 * @var array $plain_replace
215 * @see $plain_search
216 */
217 public $plain_replace = array(
218 '', // Non-legal carriage return.
219 ' ', // Non-breaking space.
220 '"', // Double quotes.
221 "'", // Single quotes.
222 '>', // Greater-than.
223 '<', // Less-than.
224 '&', // Ampersand.
225 '&', // Ampersand.
226 '(c)', // Copyright.
227 '(tm)', // Trademark.
228 '(R)', // Registered.
229 '--', // mdash.
230 '-', // ndash.
231 '*', // Bullet.
232 '£', // Pound sign.
233 'EUR', // Euro sign. € ?.
234 '$', // Dollar sign.
235 '', // Unknown/unhandled entities.
236 ' ', // Runs of spaces, post-handling.
237 );
238
239 /**
240 * Strings to find/replace in subjects/headings.
241 *
242 * @var array
243 */
244 public $placeholders = array();
245
246 /**
247 * Strings to find in subjects/headings.
248 *
249 * @deprecated 3.2.0 in favour of placeholders
250 * @var array
251 */
252 public $find = array();
253
254 /**
255 * Strings to replace in subjects/headings.
256 *
257 * @deprecated 3.2.0 in favour of placeholders
258 * @var array
259 */
260 public $replace = array();
261
262 /**
263 * E-mail type: plain, html or multipart.
264 *
265 * @var string
266 */
267 public $email_type;
268
269 /**
270 * Whether email improvements feature is enabled.
271 *
272 * @var bool
273 */
274 public $email_improvements_enabled;
275
276 /**
277 * Whether email block editor feature is enabled.
278 *
279 * @var bool
280 */
281 public $block_email_editor_enabled;
282
283
284
285 /**
286 * Personalizer instance for converting Personalization tags.
287 *
288 * @var TransactionalEmailPersonalizer
289 */
290 public $personalizer;
291
292 /**
293 * Block content template path.
294 *
295 * @var string
296 */
297 public $template_block_content = 'emails/block/general-block-email.php';
298
299 /**
300 * Constructor.
301 */
302 public function __construct() {
303 $this->email_improvements_enabled = FeaturesUtil::feature_is_enabled( 'email_improvements' );
304 $this->block_email_editor_enabled = FeaturesUtil::feature_is_enabled( 'block_email_editor' );
305
306 // Find/replace.
307 $this->placeholders = array_merge(
308 array(
309 '{site_title}' => $this->get_blogname(),
310 '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ),
311 '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ),
312 '{store_email}' => $this->get_from_address(),
313 ),
314 $this->placeholders
315 );
316
317 // Init settings.
318 $this->init_form_fields();
319 $this->init_settings();
320
321 // Default template base if not declared in child constructor.
322 if ( is_null( $this->template_base ) ) {
323 $this->template_base = WC()->plugin_path() . '/templates/';
324 }
325
326 $this->email_type = $this->get_option( 'email_type' );
327 $this->enabled = $this->get_option( 'enabled' );
328 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
329 $this->cc = $this->get_option( 'cc', '' );
330 $this->bcc = $this->get_option( 'bcc', '' );
331 }
332
333 if ( $this->block_email_editor_enabled ) {
334 $this->personalizer = wc_get_container()->get( TransactionalEmailPersonalizer::class );
335 }
336 add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) );
337 add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) );
338
339 // Use priority 1 to ensure our skip classes are added before lazy loading plugins process the images.
340 add_filter( 'wp_get_attachment_image_attributes', array( $this, 'prevent_lazy_loading_on_attachment' ), 1, 1 );
341 }
342
343 /**
344 * Handle multipart mail.
345 *
346 * @param PHPMailer $mailer PHPMailer object.
347 * @return PHPMailer
348 */
349 public function handle_multipart( $mailer ) {
350 if ( ! $this->sending ) {
351 return $mailer;
352 }
353
354 if ( 'multipart' === $this->get_email_type() ) {
355 $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
356 preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) )
357 );
358 } else {
359 $mailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
360 }
361
362 $this->sending = false;
363 return $mailer;
364 }
365
366 /**
367 * Format email string.
368 *
369 * @param mixed $string Text to replace placeholders in.
370 * @return string
371 */
372 public function format_string( $string ) {
373 $find = array_keys( $this->placeholders );
374 $replace = array_values( $this->placeholders );
375
376 // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0.
377 $find = array_merge( (array) $this->find, $find );
378 $replace = array_merge( (array) $this->replace, $replace );
379
380 // Take care of blogname which is no longer defined as a valid placeholder.
381 $find[] = '{blogname}';
382 $replace[] = $this->get_blogname();
383
384 // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0.
385 if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) {
386 $legacy_find = $this->find;
387 $legacy_replace = $this->replace;
388
389 foreach ( $this->placeholders as $find => $replace ) {
390 $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) );
391 $legacy_find[ $legacy_key ] = $find;
392 $legacy_replace[ $legacy_key ] = $replace;
393 }
394
395 $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string );
396 }
397
398 /**
399 * Filter for main find/replace.
400 *
401 * @since 3.2.0
402 */
403 return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this );
404 }
405
406 /**
407 * Set the locale to the store locale for customer emails to make sure emails are in the store language.
408 */
409 public function setup_locale() {
410
411 /**
412 * Filter the ability to switch email locale.
413 *
414 * @since 6.8.0
415 *
416 * @param bool $default_value The default returned value.
417 * @param WC_Email $email The WC_Email object.
418 */
419 $switch_email_locale = apply_filters( 'woocommerce_allow_switching_email_locale', true, $this );
420
421 if ( $switch_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) {
422 wc_switch_to_site_locale();
423 }
424 }
425
426 /**
427 * Restore the locale to the default locale. Use after finished with setup_locale.
428 */
429 public function restore_locale() {
430
431 /**
432 * Filter the ability to restore email locale.
433 *
434 * @since 6.8.0
435 *
436 * @param bool $default_value The default returned value.
437 * @param WC_Email $email The WC_Email object.
438 */
439 $restore_email_locale = apply_filters( 'woocommerce_allow_restoring_email_locale', true, $this );
440
441 if ( $restore_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) {
442 wc_restore_locale();
443 }
444 }
445
446 /**
447 * Get available email groups with their titles.
448 *
449 * @since 10.3.0
450 * @return array Associative array of email group slugs => titles.
451 */
452 public function get_email_groups() {
453 $email_groups = array(
454 'accounts' => __( 'Accounts', 'woocommerce' ),
455 'orders' => __( 'Orders', 'woocommerce' ),
456 'order-processing' => __( 'Order updates', 'woocommerce' ), // @deprecated Please use 'order-updates' instead. Will be removed in 10.5.0.
457 'order-updates' => __( 'Order updates', 'woocommerce' ),
458 'order-exceptions' => __( 'Order changes', 'woocommerce' ), // @deprecated Please use 'order-changes' instead. Will be removed in 10.5.0.
459 'order-changes' => __( 'Order changes', 'woocommerce' ),
460 'payments' => __( 'Payments', 'woocommerce' ),
461 );
462
463 /**
464 * Filter the available email groups.
465 *
466 * @since 10.3.0
467 * @param array $email_groups Associative array of email group slugs => titles.
468 */
469 return apply_filters( 'woocommerce_email_groups', $email_groups );
470 }
471
472 /**
473 * Get the title for the current email group.
474 *
475 * @since 10.3.0
476 * @return string The email group title. Falls back to the email group slug if not found.
477 */
478 public function get_email_group_title() {
479 $email_groups = $this->get_email_groups();
480 $title = isset( $email_groups[ $this->email_group ] ) ? $email_groups[ $this->email_group ] : $this->email_group;
481
482 /**
483 * Filter the email group title.
484 *
485 * @since 10.3.0
486 * @param string $title The email group title.
487 * @param string $email_group The email group slug.
488 * @param array $email_groups Associative array of email group slugs => titles.
489 */
490 return (string) apply_filters( 'woocommerce_email_group_title', $title, $this->email_group, $email_groups );
491 }
492
493 /**
494 * Get email subject.
495 *
496 * @since 3.1.0
497 * @return string
498 */
499 public function get_default_subject() {
500 return $this->subject;
501 }
502
503 /**
504 * Get email heading.
505 *
506 * @since 3.1.0
507 * @return string
508 */
509 public function get_default_heading() {
510 return $this->heading;
511 }
512
513 /**
514 * Default content to show below main email content.
515 *
516 * @since 3.7.0
517 * @return string
518 */
519 public function get_default_additional_content() {
520 return '';
521 }
522
523 /**
524 * Return content from the additional_content field.
525 *
526 * Displayed above the footer.
527 *
528 * @since 3.7.0
529 * @return string
530 */
531 public function get_additional_content() {
532 /**
533 * Provides an opportunity to inspect and modify additional content for the email.
534 *
535 * @since 3.7.0
536 *
537 * @param string $additional_content Additional content to be added to the email.
538 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
539 * @param WC_Email $email WC_Email instance managing the email.
540 */
541 return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option_or_transient( 'additional_content' ) ), $this->object, $this );
542 }
543
544 /**
545 * Get email subject.
546 *
547 * @return string
548 */
549 public function get_subject() {
550 /**
551 * Provides an opportunity to inspect and modify subject for the email.
552 *
553 * @since 2.0.0
554 *
555 * @param string $subject Subject of the email.
556 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
557 * @param WC_Email $email WC_Email instance managing the email.
558 */
559 $subject = apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option_or_transient( 'subject', $this->get_default_subject() ) ), $this->object, $this );
560 if ( $this->block_email_editor_enabled ) {
561 // Because the new email editor uses rich-text component for subject editing, to be ensure that the subject is always in plain text, we need to strip all tags.
562 $subject = wp_strip_all_tags( $this->personalizer->personalize_transactional_content( $subject, $this ) );
563 }
564 return $subject;
565 }
566
567
568
569 /**
570 * Get email preheader.
571 *
572 * @return string
573 */
574 public function get_preheader() {
575 /**
576 * Provides an opportunity to inspect and modify preheader for the email.
577 *
578 * @since 9.9.0
579 *
580 * @param string $preheader Preheader of the email.
581 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
582 * @param WC_Email $email WC_Email instance managing the email.
583 */
584 $preheader = apply_filters( 'woocommerce_email_preheader' . $this->id, $this->format_string( $this->get_option_or_transient( 'preheader', '' ) ), $this->object, $this );
585 if ( $this->block_email_editor_enabled ) {
586 $preheader = $this->personalizer->personalize_transactional_content( $preheader, $this );
587 }
588 return $preheader;
589 }
590
591 /**
592 * Get email heading.
593 *
594 * @return string
595 */
596 public function get_heading() {
597 /**
598 * Provides an opportunity to inspect and modify heading for the email.
599 *
600 * @since 2.0.0
601 *
602 * @param string $heading Heading to be added to the email.
603 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
604 * @param WC_Email $email WC_Email instance managing the email.
605 */
606 return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option_or_transient( 'heading', $this->get_default_heading() ) ), $this->object, $this );
607 }
608
609 /**
610 * Get valid recipients.
611 *
612 * @return string
613 */
614 public function get_recipient() {
615 /**
616 * Filter the recipient for the email.
617 *
618 * @since 2.0.0
619 * @since 3.7.0 Added $email parameter.
620 * @param string $recipient Recipient.
621 * @param object $object The object (ie, product or order) this email relates to, if any.
622 * @param WC_Email $email WC_Email instance managing the email.
623 */
624 $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this );
625 $recipients = array_map( 'trim', explode( ',', $recipient ?? '' ) );
626 $recipients = array_filter( $recipients, 'is_email' );
627 return implode( ', ', $recipients );
628 }
629
630 /**
631 * Get valid Cc recipients.
632 *
633 * @return string
634 */
635 public function get_cc_recipient() {
636 /**
637 * Filter the Cc recipient for the email.
638 *
639 * @since 9.8.0
640 * @param string $cc Cc recipient.
641 * @param object $object The object (ie, product or order) this email relates to, if any.
642 * @param WC_Email $email WC_Email instance managing the email.
643 */
644 $cc = apply_filters( 'woocommerce_email_cc_recipient_' . $this->id, $this->cc, $this->object, $this );
645 $ccs = array_map( 'trim', explode( ',', $cc ?? '' ) );
646 $ccs = array_filter( $ccs, 'is_email' );
647 $ccs = array_map( 'sanitize_email', $ccs );
648 return implode( ', ', $ccs );
649 }
650
651 /**
652 * Get valid Bcc recipients.
653 *
654 * @return string
655 */
656 public function get_bcc_recipient() {
657 /**
658 * Filter the Bcc recipient for the email.
659 *
660 * @since 9.8.0
661 * @param string $bcc Bcc recipient.
662 * @param object $object The object (ie, product or order) this email relates to, if any.
663 * @param WC_Email $email WC_Email instance managing the email.
664 */
665 $bcc = apply_filters( 'woocommerce_email_bcc_recipient_' . $this->id, $this->bcc, $this->object, $this );
666 $bccs = array_map( 'trim', explode( ',', $bcc ?? '' ) );
667 $bccs = array_filter( $bccs, 'is_email' );
668 $bccs = array_map( 'sanitize_email', $bccs );
669 return implode( ', ', $bccs );
670 }
671
672 /**
673 * Get email headers.
674 *
675 * @return string
676 */
677 public function get_headers() {
678 $header = 'Content-Type: ' . $this->get_content_type() . "\r\n";
679
680 // For order notification emails sent to admin, always use customer's billing email as reply-to.
681 if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) {
682 if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) {
683 $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n";
684 }
685 } else {
686 // Check if custom reply-to is enabled and configured for non-admin notification emails.
687 $reply_to_enabled = $this->get_reply_to_enabled();
688 $reply_to_address = $this->get_reply_to_address();
689 $reply_to_name = $this->get_reply_to_name();
690
691 if ( $reply_to_enabled && ! empty( $reply_to_address ) && is_email( $reply_to_address ) ) {
692 $reply_to_name = ! empty( $reply_to_name ) ? $reply_to_name : $this->get_from_name();
693 $header .= 'Reply-to: ' . $reply_to_name . ' <' . $reply_to_address . ">\r\n";
694 } elseif ( $this->get_from_address() && $this->get_from_name() ) {
695 $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n";
696 }
697 }
698
699 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
700 $cc = $this->get_cc_recipient();
701 if ( ! empty( $cc ) ) {
702 $header .= 'Cc: ' . sanitize_text_field( $cc ) . "\r\n";
703 }
704
705 $bcc = $this->get_bcc_recipient();
706 if ( ! empty( $bcc ) ) {
707 $header .= 'Bcc: ' . sanitize_text_field( $bcc ) . "\r\n";
708 }
709 }
710
711 return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
712 }
713
714 /**
715 * Get email attachments.
716 *
717 * @return array
718 */
719 public function get_attachments() {
720 return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this );
721 }
722
723 /**
724 * Return email type.
725 *
726 * @return string
727 */
728 public function get_email_type() {
729 $email_type = $this->email_type;
730 /**
731 * This filter is documented in templates/emails/email-styles.php
732 *
733 * @since 9.6.0
734 * @param bool $is_email_preview Whether the email is being previewed.
735 */
736 $is_email_preview = apply_filters( 'woocommerce_is_email_preview', false );
737 // Transient is used for live email preview without saving the settings.
738 if ( $is_email_preview ) {
739 $transient = get_transient( "woocommerce_{$this->id}_email_type" );
740 $email_type = $transient ? $transient : $email_type;
741 }
742 return $email_type && class_exists( 'DOMDocument' ) ? $email_type : 'plain';
743 }
744
745 /**
746 * Get block editor email template content.
747 *
748 * @return string
749 */
750 public function get_block_editor_email_template_content() {
751 return wc_get_template_html(
752 $this->template_block_content,
753 array(
754 'order' => $this->object,
755 'sent_to_admin' => false,
756 'plain_text' => false,
757 'email' => $this,
758 )
759 );
760 }
761
762 /**
763 * Get email content type.
764 *
765 * @param string $default_content_type Default wp_mail() content type.
766 * @return string
767 */
768 public function get_content_type( $default_content_type = '' ) {
769 switch ( $this->get_email_type() ) {
770 case 'html':
771 $content_type = 'text/html';
772 break;
773 case 'multipart':
774 $content_type = 'multipart/alternative';
775 break;
776 default:
777 $content_type = 'text/plain';
778 break;
779 }
780
781 return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type );
782 }
783
784 /**
785 * Return the email's title
786 *
787 * @return string
788 */
789 public function get_title() {
790 return apply_filters( 'woocommerce_email_title', $this->title, $this );
791 }
792
793 /**
794 * Return the email's description
795 *
796 * @return string
797 */
798 public function get_description() {
799 return apply_filters( 'woocommerce_email_description', $this->description, $this );
800 }
801
802 /**
803 * Proxy to parent's get_option and attempt to localize the result using gettext.
804 *
805 * @param string $key Option key.
806 * @param mixed $empty_value Value to use when option is empty.
807 * @return string
808 */
809 public function get_option( $key, $empty_value = null ) {
810 $value = parent::get_option( $key, $empty_value );
811 return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value );
812 }
813
814 /**
815 * Checks if this email is enabled and will be sent.
816 *
817 * @return bool
818 */
819 public function is_enabled() {
820 return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this );
821 }
822
823 /**
824 * Checks if this email is manually sent
825 *
826 * @return bool
827 */
828 public function is_manual() {
829 return $this->manual;
830 }
831
832 /**
833 * Checks if this email is customer focussed.
834 *
835 * @return bool
836 */
837 public function is_customer_email() {
838 return $this->customer_email;
839 }
840
841 /**
842 * Get WordPress blog name.
843 *
844 * @return string
845 */
846 public function get_blogname() {
847 return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
848 }
849
850 /**
851 * Get email content.
852 *
853 * @return string
854 */
855 public function get_content() {
856 $this->sending = true;
857
858 $block_email_content = $this->get_block_email_html_content();
859 if ( $block_email_content ) {
860 $this->email_type = 'plain' === $this->email_type ? 'html' : $this->email_type;
861 return $block_email_content;
862 }
863
864 if ( 'plain' === $this->get_email_type() ) {
865 $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 );
866 } else {
867 $email_content = $this->get_content_html();
868 }
869
870 return $email_content;
871 }
872
873 /**
874 * Apply inline styles to dynamic content.
875 *
876 * We only inline CSS for html emails.
877 *
878 * @version 10.2.0
879 * @param string|null $content Content that will receive inline styles.
880 * @return string
881 */
882 public function style_inline( $content ) {
883 if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) {
884 /**
885 * Filter to allow the ability to override the email inline styling method.
886 *
887 * @since 10.2.0
888 *
889 * @param callable $style_inline_callback The default email inline styling callback.
890 * @param string|null $content Content that will receive inline styles.
891 * @param WC_Email $email The WC_Email object.
892 */
893 $style_inline_callback = apply_filters( 'woocommerce_mail_style_inline_callback', array( $this, 'apply_inline_style' ), $content, $this );
894
895 if ( ! is_callable( $style_inline_callback ) ) {
896 $style_inline_callback = array( $this, 'apply_inline_style' );
897 }
898
899 return call_user_func( $style_inline_callback, $content );
900 }
901
902 return $content;
903 }
904
905
906 /**
907 * Apply inline styles to dynamic content using Emogrifier library (if supported).
908 *
909 * @since 10.2.0
910 * @param string|null $content Content that will receive inline styles.
911 * @return string
912 */
913 private function apply_inline_style( $content ) {
914 $css = '';
915 $css .= $this->get_must_use_css_styles();
916 $css .= "\n";
917
918 ob_start();
919 wc_get_template( 'emails/email-styles.php' );
920 $css .= ob_get_clean();
921
922 /**
923 * Provides an opportunity to filter the CSS styles included in e-mails.
924 *
925 * @since 2.3.0
926 *
927 * @param string $css CSS code.
928 * @param \WC_Email $email E-mail instance.
929 */
930 $css = apply_filters( 'woocommerce_email_styles', $css, $this );
931
932 $css_inliner_class = CssInliner::class;
933
934 if ( $this->supports_emogrifier() && class_exists( $css_inliner_class ) ) {
935 try {
936 $css_inliner = CssInliner::fromHtml( $content )->inlineCss( $css );
937
938 /**
939 * Action hook fired when an email content has been processed by Emogrifier CssInliner instance.
940 *
941 * @since 4.1.0
942 *
943 * @param CssInliner $css_inliner CssInliner instance.
944 * @param WC_Email $email WC_Email instance.
945 */
946 do_action( 'woocommerce_emogrifier', $css_inliner, $this );
947
948 $dom_document = $css_inliner->getDomDocument();
949
950 // When the email is rendered in the block editor, we don't want to remove the elements with display: none.
951 // The main reason is using preview text in the email body which is hidden by default.
952 if ( ! $this->block_email_editor_enabled ) {
953 HtmlPruner::fromDomDocument( $dom_document )->removeElementsWithDisplayNone();
954 }
955 $content = CssToAttributeConverter::fromDomDocument( $dom_document )
956 ->convertCssToVisualAttributes()
957 ->render();
958 } catch ( Exception $e ) {
959 $logger = wc_get_logger();
960 $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) );
961 }
962 } else {
963 $content = '<style type="text/css">' . $css . '</style>' . $content;
964 }
965
966 return $content;
967 }
968
969 /**
970 * Returns CSS styles that should be included with all HTML e-mails, regardless of theme specific customizations.
971 *
972 * @since 9.1.0
973 *
974 * @return string
975 */
976 protected function get_must_use_css_styles(): string {
977 $css = <<<'EOF'
978
979 /*
980 * Temporary measure until e-mail clients more properly support the correct styles.
981 * See https://github.com/woocommerce/woocommerce/pull/47738.
982 */
983 .screen-reader-text {
984 display: none;
985 }
986
987 EOF;
988
989 return $css;
990 }
991
992 /**
993 * Return if emogrifier library is supported.
994 *
995 * @version 4.0.0
996 * @since 3.5.0
997 * @return bool
998 */
999 protected function supports_emogrifier() {
1000 return class_exists( 'DOMDocument' );
1001 }
1002
1003 /**
1004 * Get the email content in plain text format.
1005 *
1006 * @return string
1007 */
1008 public function get_content_plain() {
1009 return '';
1010 }
1011
1012 /**
1013 * Get the email content in HTML format.
1014 *
1015 * @return string
1016 */
1017 public function get_content_html() {
1018 return '';
1019 }
1020
1021 /**
1022 * Get the from name for outgoing emails.
1023 *
1024 * @param string $from_name Default wp_mail() name associated with the "from" email address.
1025 * @return string
1026 */
1027 public function get_from_name( $from_name = '' ) {
1028 $default = get_bloginfo( 'name', 'display' );
1029 /**
1030 * Filters the "from" name for outgoing emails.
1031 *
1032 * @since 2.1.0
1033 *
1034 * @param string|mixed $from_name The from name.
1035 * @param WC_Email $email Email object.
1036 * @param string $default_from_name Default from name.
1037 */
1038 $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name', $default ), $this, $from_name );
1039 return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES );
1040 }
1041
1042 /**
1043 * Get the from address for outgoing emails.
1044 *
1045 * @param string $from_email Default wp_mail() email address to send from.
1046 * @return string
1047 */
1048 public function get_from_address( $from_email = '' ) {
1049 $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email );
1050 return sanitize_email( $from_email );
1051 }
1052
1053 /**
1054 * Check if reply-to is enabled for outgoing emails.
1055 *
1056 * @return bool
1057 */
1058 public function get_reply_to_enabled() {
1059 /**
1060 * Filter whether reply-to is enabled for emails.
1061 *
1062 * @since 10.4.0
1063 * @param bool $enabled Whether reply-to is enabled.
1064 * @param WC_Email $email WC_Email instance managing the email.
1065 */
1066 $enabled = apply_filters( 'woocommerce_email_reply_to_enabled', 'yes' === get_option( 'woocommerce_email_reply_to_enabled', 'no' ), $this );
1067 return (bool) $enabled;
1068 }
1069
1070 /**
1071 * Get the reply-to name for outgoing emails.
1072 *
1073 * @param string $reply_to_name Default reply-to name.
1074 * @return string
1075 */
1076 public function get_reply_to_name( $reply_to_name = '' ) {
1077 /**
1078 * Filter the reply-to name for emails.
1079 *
1080 * @since 10.4.0
1081 * @param string $reply_to_name Reply-to name.
1082 * @param WC_Email $email WC_Email instance managing the email.
1083 * @param string $default_name Default reply-to name.
1084 */
1085 $reply_to_name = apply_filters( 'woocommerce_email_reply_to_name', get_option( 'woocommerce_email_reply_to_name', '' ), $this, $reply_to_name );
1086 return wp_specialchars_decode( sanitize_text_field( $reply_to_name ), ENT_QUOTES );
1087 }
1088
1089 /**
1090 * Get the reply-to address for outgoing emails.
1091 *
1092 * @param string $reply_to_email Default reply-to email address.
1093 * @return string
1094 */
1095 public function get_reply_to_address( $reply_to_email = '' ) {
1096 /**
1097 * Filter the reply-to address for emails.
1098 *
1099 * @since 10.4.0
1100 * @param string $reply_to_email Reply-to email address.
1101 * @param WC_Email $email WC_Email instance managing the email.
1102 * @param string $default_email Default reply-to email address.
1103 */
1104 $reply_to_email = apply_filters( 'woocommerce_email_reply_to_address', get_option( 'woocommerce_email_reply_to_address', '' ), $this, $reply_to_email );
1105 return sanitize_email( $reply_to_email );
1106 }
1107
1108 /**
1109 * Set the object for the outgoing email.
1110 *
1111 * @param object $object Object this email is for, e.g. customer, or product.
1112 * @return void
1113 */
1114 public function set_object( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
1115 $this->object = $object;
1116 }
1117
1118 /**
1119 * Send an email.
1120 *
1121 * @param string $to Email to.
1122 * @param string $subject Email subject.
1123 * @param string $message Email message.
1124 * @param string $headers Email headers.
1125 * @param array $attachments Email attachments.
1126 * @return bool success
1127 */
1128 public function send( $to, $subject, $message, $headers, $attachments ) {
1129 add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
1130 add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
1131 add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
1132
1133 $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
1134 $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
1135 $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this );
1136 $return = $mail_callback( ...$mail_callback_params );
1137
1138 remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
1139 remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
1140 remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
1141
1142 // Clear the AltBody (if set) so that it does not leak across to different emails.
1143 $this->clear_alt_body_field();
1144
1145 /**
1146 * Action hook fired when an email is sent.
1147 *
1148 * @since 5.6.0
1149 * @param bool $return Whether the email was sent successfully.
1150 * @param string $id Email ID.
1151 * @param WC_Email $email WC_Email instance.
1152 */
1153 do_action( 'woocommerce_email_sent', $return, (string) $this->id, $this );
1154
1155 return $return;
1156 }
1157
1158 /**
1159 * Initialise Settings Form Fields - these are generic email options most will use.
1160 */
1161 public function init_form_fields() {
1162 /* translators: %s: list of placeholders */
1163 $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
1164 $this->form_fields = array(
1165 'enabled' => array(
1166 'title' => __( 'Enable/Disable', 'woocommerce' ),
1167 'type' => 'checkbox',
1168 'label' => __( 'Enable this email notification', 'woocommerce' ),
1169 'default' => 'yes',
1170 ),
1171 'subject' => array(
1172 'title' => __( 'Subject', 'woocommerce' ),
1173 'type' => 'text',
1174 'desc_tip' => true,
1175 'description' => $placeholder_text,
1176 'placeholder' => $this->get_default_subject(),
1177 'default' => '',
1178 ),
1179 'heading' => array(
1180 'title' => __( 'Email heading', 'woocommerce' ),
1181 'type' => 'text',
1182 'desc_tip' => true,
1183 'description' => $placeholder_text,
1184 'placeholder' => $this->get_default_heading(),
1185 'default' => '',
1186 ),
1187 'additional_content' => array(
1188 'title' => __( 'Additional content', 'woocommerce' ),
1189 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
1190 'css' => 'width:400px; height: 75px;',
1191 'placeholder' => __( 'N/A', 'woocommerce' ),
1192 'type' => 'textarea',
1193 'default' => $this->get_default_additional_content(),
1194 'desc_tip' => true,
1195 ),
1196 'email_type' => array(
1197 'title' => __( 'Email type', 'woocommerce' ),
1198 'type' => 'select',
1199 'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
1200 'default' => 'html',
1201 'class' => 'email_type wc-enhanced-select',
1202 'options' => $this->get_email_type_options(),
1203 'desc_tip' => true,
1204 ),
1205 );
1206 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
1207 $this->form_fields['cc'] = $this->get_cc_field();
1208 $this->form_fields['bcc'] = $this->get_bcc_field();
1209 }
1210 if ( $this->block_email_editor_enabled ) {
1211 $this->form_fields['preheader'] = $this->get_preheader_field();
1212 }
1213 }
1214
1215 /**
1216 * Get the cc field definition.
1217 *
1218 * @return array
1219 */
1220 protected function get_cc_field() {
1221 return array(
1222 'title' => __( 'Cc(s)', 'woocommerce' ),
1223 'type' => 'text',
1224 /* translators: %s: admin email */
1225 'description' => __( 'Enter Cc recipients (comma-separated) for this email.', 'woocommerce' ),
1226 'placeholder' => '',
1227 'default' => '',
1228 'desc_tip' => true,
1229 );
1230 }
1231
1232 /**
1233 * Get the bcc field definition.
1234 *
1235 * @return array
1236 */
1237 protected function get_bcc_field() {
1238 return array(
1239 'title' => __( 'Bcc(s)', 'woocommerce' ),
1240 'type' => 'text',
1241 /* translators: %s: admin email */
1242 'description' => __( 'Enter Bcc recipients (comma-separated) for this email.', 'woocommerce' ),
1243 'placeholder' => '',
1244 'default' => '',
1245 'desc_tip' => true,
1246 );
1247 }
1248
1249 /**
1250 * Get the preheader field definition.
1251 *
1252 * @return array
1253 */
1254 protected function get_preheader_field() {
1255 return array(
1256 'title' => __( 'Preheader', 'woocommerce' ),
1257 'description' => __( 'Shown as a preview in the Inbox, next to the subject line. (Max 150 characters).', 'woocommerce' ),
1258 'placeholder' => '',
1259 'type' => 'text',
1260 'default' => '',
1261 'desc_tip' => true,
1262 );
1263 }
1264
1265 /**
1266 * Email type options.
1267 *
1268 * @return array
1269 */
1270 public function get_email_type_options() {
1271 $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) );
1272
1273 if ( class_exists( 'DOMDocument' ) ) {
1274 $types['html'] = __( 'HTML', 'woocommerce' );
1275 $types['multipart'] = __( 'Multipart', 'woocommerce' );
1276 }
1277
1278 return $types;
1279 }
1280
1281 /**
1282 * Admin Panel Options Processing.
1283 */
1284 public function process_admin_options() {
1285 // Save regular options.
1286 parent::process_admin_options();
1287
1288 $post_data = $this->get_post_data();
1289
1290 // Save templates.
1291 if ( isset( $post_data['template_html_code'] ) ) {
1292 $this->save_template( $post_data['template_html_code'], $this->template_html );
1293 }
1294 if ( isset( $post_data['template_plain_code'] ) ) {
1295 $this->save_template( $post_data['template_plain_code'], $this->template_plain );
1296 }
1297 }
1298
1299 /**
1300 * Get template.
1301 *
1302 * @param string $type Template type. Can be either 'template_html', 'template_plain' or 'template_block'.
1303 * @return string
1304 */
1305 public function get_template( $type ) {
1306 $type = basename( $type );
1307
1308 if ( 'template_html' === $type ) {
1309 return $this->template_html;
1310 } elseif ( 'template_plain' === $type ) {
1311 return $this->template_plain;
1312 } elseif ( 'template_block' === $type ) {
1313 return $this->template_block;
1314 }
1315 return '';
1316 }
1317
1318 /**
1319 * Save the email templates.
1320 *
1321 * @since 2.4.0
1322 * @param string $template_code Template code.
1323 * @param string $template_path Template path.
1324 */
1325 protected function save_template( $template_code, $template_path ) {
1326 if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) {
1327 $saved = false;
1328 $file = $this->get_theme_template_file( $template_path );
1329 $code = wp_unslash( $template_code );
1330
1331 if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable
1332 $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
1333
1334 if ( false !== $f ) {
1335 fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
1336 fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
1337 $saved = true;
1338 }
1339 }
1340
1341 if ( ! $saved ) {
1342 $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) );
1343 wp_safe_redirect( $redirect );
1344 exit;
1345 }
1346 wc_clear_template_cache();
1347 }
1348 }
1349
1350 /**
1351 * Get the template file in the current theme.
1352 *
1353 * @param string $template Template name.
1354 *
1355 * @return string
1356 */
1357 public function get_theme_template_file( $template ) {
1358 return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template;
1359 }
1360
1361 /**
1362 * Move template action.
1363 *
1364 * @param string $template_type Template type.
1365 */
1366 protected function move_template_action( $template_type ) {
1367 $template = $this->get_template( $template_type );
1368 if ( ! empty( $template ) ) {
1369 $theme_file = $this->get_theme_template_file( $template );
1370
1371 if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) {
1372
1373 // Locate template file.
1374 $core_file = $this->template_base . $template;
1375 $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
1376
1377 // Copy template file.
1378 copy( $template_file, $theme_file );
1379
1380 /**
1381 * Action hook fired after copying email template file.
1382 *
1383 * @param string $template_type The copied template type
1384 * @param string $email The email object
1385 */
1386 do_action( 'woocommerce_copy_email_template', $template_type, $this );
1387
1388 wc_clear_template_cache();
1389 ?>
1390 <div class="updated">
1391 <p><?php echo esc_html__( 'Template file copied to theme.', 'woocommerce' ); ?></p>
1392 </div>
1393 <?php
1394 }
1395 }
1396 }
1397
1398 /**
1399 * Delete template action.
1400 *
1401 * @param string $template_type Template type.
1402 */
1403 protected function delete_template_action( $template_type ) {
1404 $template = $this->get_template( $template_type );
1405
1406 if ( $template ) {
1407 if ( ! empty( $template ) ) {
1408 $theme_file = $this->get_theme_template_file( $template );
1409
1410 if ( file_exists( $theme_file ) ) {
1411 unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink
1412
1413 /**
1414 * Action hook fired after deleting template file.
1415 *
1416 * @param string $template The deleted template type
1417 * @param string $email The email object
1418 */
1419 do_action( 'woocommerce_delete_email_template', $template_type, $this );
1420
1421 wc_clear_template_cache();
1422 ?>
1423 <div class="updated">
1424 <p><?php echo esc_html__( 'Template file deleted from theme.', 'woocommerce' ); ?></p>
1425 </div>
1426 <?php
1427 }
1428 }
1429 }
1430 }
1431
1432 /**
1433 * Admin actions.
1434 */
1435 protected function admin_actions() {
1436 // Handle any actions.
1437 if (
1438 ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) )
1439 && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) )
1440 && 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1441 ) {
1442 if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) {
1443 wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
1444 }
1445
1446 if ( ! current_user_can( 'edit_themes' ) ) {
1447 wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'woocommerce' ) );
1448 }
1449
1450 if ( ! empty( $_GET['move_template'] ) ) {
1451 $this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) );
1452 }
1453
1454 if ( ! empty( $_GET['delete_template'] ) ) {
1455 $this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) );
1456 }
1457 }
1458 }
1459
1460 /**
1461 * Admin Options.
1462 *
1463 * Setup the email settings screen.
1464 * Override this in your email.
1465 *
1466 * @since 1.0.0
1467 */
1468 public function admin_options() {
1469 // Do admin actions.
1470 $this->admin_actions();
1471 ?>
1472 <?php wc_back_header( $this->get_title(), __( 'Return to emails', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ); ?>
1473
1474 <?php echo wpautop( wp_kses_post( $this->get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
1475
1476 <?php
1477 /**
1478 * Action hook fired before displaying email settings.
1479 *
1480 * @param string $email The email object
1481 */
1482 do_action( 'woocommerce_email_settings_before', $this );
1483 ?>
1484
1485 <table class="form-table">
1486 <?php $this->generate_settings_html(); ?>
1487 </table>
1488
1489 <?php
1490 /**
1491 * Action hook fired after displaying email settings.
1492 *
1493 * @param string $email The email object
1494 */
1495 do_action( 'woocommerce_email_settings_after', $this );
1496 ?>
1497
1498 <?php
1499
1500 if ( current_user_can( 'edit_themes' ) && ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) ) {
1501 ?>
1502 <div id="template">
1503 <?php
1504 $templates = array(
1505 'template_html' => __( 'HTML template', 'woocommerce' ),
1506 'template_plain' => __( 'Plain text template', 'woocommerce' ),
1507 );
1508
1509 foreach ( $templates as $template_type => $title ) :
1510 $template = $this->get_template( $template_type );
1511
1512 if ( empty( $template ) ) {
1513 continue;
1514 }
1515
1516 $local_file = $this->get_theme_template_file( $template );
1517 $core_file = $this->template_base . $template;
1518 $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
1519 $template_dir = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template );
1520 ?>
1521 <div class="template <?php echo esc_attr( $template_type ); ?>">
1522 <h4><?php echo wp_kses_post( $title ); ?></h4>
1523
1524 <?php if ( file_exists( $local_file ) ) : ?>
1525 <p>
1526 <a href="#" class="button toggle_editor"></a>
1527
1528 <?php if ( is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?>
1529 <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'move_template', 'saved' ), add_query_arg( 'delete_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="delete_template button">
1530 <?php esc_html_e( 'Delete template file', 'woocommerce' ); ?>
1531 </a>
1532 <?php endif; ?>
1533
1534 <?php
1535 /* translators: %s: Path to template file */
1536 printf( esc_html__( 'This template has been overridden by your theme and can be found in: %s.', 'woocommerce' ), '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
1537 ?>
1538 </p>
1539
1540 <div class="editor" style="display:none">
1541 <textarea class="code" cols="25" rows="20"
1542 <?php
1543 if ( ! is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
1544 ?>
1545 readonly="readonly" disabled="disabled"
1546 <?php else : ?>
1547 data-name="<?php echo esc_attr( $template_type ) . '_code'; ?>"<?php endif; ?>><?php echo esc_html( file_get_contents( $local_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
1548 </div>
1549 <?php elseif ( file_exists( $template_file ) ) : ?>
1550 <p>
1551 <a href="#" class="button toggle_editor"></a>
1552
1553 <?php
1554 $emails_dir = get_stylesheet_directory() . '/' . $template_dir . '/emails';
1555 $templates_dir = get_stylesheet_directory() . '/' . $template_dir;
1556 $theme_dir = get_stylesheet_directory();
1557
1558 if ( is_dir( $emails_dir ) ) {
1559 $target_dir = $emails_dir;
1560 } elseif ( is_dir( $templates_dir ) ) {
1561 $target_dir = $templates_dir;
1562 } else {
1563 $target_dir = $theme_dir;
1564 }
1565
1566 if ( is_writable( $target_dir ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
1567 ?>
1568 <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'delete_template', 'saved' ), add_query_arg( 'move_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="button">
1569 <?php esc_html_e( 'Copy file to theme', 'woocommerce' ); ?>
1570 </a>
1571 <?php endif; ?>
1572
1573 <?php
1574 /* translators: 1: Path to template file 2: Path to theme folder */
1575 printf( esc_html__( 'To override and edit this email template copy %1$s to your theme folder: %2$s.', 'woocommerce' ), '<code>' . esc_html( plugin_basename( $template_file ) ) . '</code>', '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
1576 ?>
1577 </p>
1578
1579 <div class="editor" style="display:none">
1580 <textarea class="code" readonly="readonly" disabled="disabled" cols="25" rows="20"><?php echo esc_html( file_get_contents( $template_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
1581 </div>
1582 <?php else : ?>
1583 <p><?php esc_html_e( 'File was not found.', 'woocommerce' ); ?></p>
1584 <?php endif; ?>
1585 </div>
1586 <?php endforeach; ?>
1587 </div>
1588
1589 <?php
1590 $handle = 'wc-admin-settings-email';
1591 wp_register_script( $handle, '', array( 'jquery' ), WC_VERSION, array( 'in_footer' => true ) );
1592 wp_enqueue_script( $handle );
1593 wp_add_inline_script(
1594 $handle,
1595 "jQuery( 'select.email_type' ).on( 'change', function() {
1596
1597 const val = jQuery( this ).val();
1598
1599 jQuery( '.template_plain, .template_html' ).show();
1600
1601 if ( val != 'multipart' && val != 'html' ) {
1602 jQuery('.template_html').hide();
1603 }
1604
1605 if ( val != 'multipart' && val != 'plain' ) {
1606 jQuery('.template_plain').hide();
1607 }
1608
1609 }).trigger( 'change' );
1610
1611 const view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
1612 const hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';
1613
1614 jQuery( 'a.toggle_editor' ).text( view ).on( 'click', function() {
1615 let label = hide;
1616
1617 if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) {
1618 label = view;
1619 }
1620
1621 jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle();
1622 return false;
1623 } );
1624
1625 jQuery( 'a.delete_template' ).on( 'click', function() {
1626 if ( window.confirm('" . esc_js( __( 'Are you sure you want to delete this template file?', 'woocommerce' ) ) . "') ) {
1627 return true;
1628 }
1629
1630 return false;
1631 });
1632
1633 jQuery( '.editor textarea' ).on( 'change', function() {
1634 const name = jQuery( this ).attr( 'data-name' );
1635
1636 if ( name ) {
1637 jQuery( this ).attr( 'name', name );
1638 }
1639 });"
1640 );
1641 }
1642 }
1643
1644 /**
1645 * Clears the PhpMailer AltBody field, to prevent that content from leaking across emails.
1646 */
1647 private function clear_alt_body_field(): void {
1648 global $phpmailer;
1649
1650 if ( $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer ) {
1651 $phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1652 }
1653 }
1654
1655 /**
1656 * Get an option or transient for email preview.
1657 *
1658 * @param string $key Option key.
1659 * @param mixed $empty_value Value to use when option is empty.
1660 */
1661 protected function get_option_or_transient( string $key, $empty_value = null ) {
1662 $option = $this->get_option( $key, $empty_value );
1663
1664 /**
1665 * This filter is documented in templates/emails/email-styles.php
1666 *
1667 * @since 9.6.0
1668 * @param bool $is_email_preview Whether the email is being previewed.
1669 */
1670 $is_email_preview = apply_filters( 'woocommerce_is_email_preview', false );
1671 if ( $is_email_preview ) {
1672 $plugin_id = $this->plugin_id;
1673 $email_id = $this->id;
1674 $transient = get_transient( "{$plugin_id}{$email_id}_{$key}" );
1675 if ( false !== $transient ) {
1676 $option = $transient ? $transient : $empty_value;
1677 }
1678 }
1679
1680 return $option;
1681 }
1682
1683 /**
1684 * Gerenerates the HTML content for the email from a block based email.
1685 * and if so, it renders the block email content.
1686 *
1687 * @return string|null
1688 */
1689 private function get_block_email_html_content(): ?string {
1690 if ( ! $this->block_email_editor_enabled ) {
1691 return null;
1692 }
1693
1694 /** Service for rendering emails from block content @var BlockEmailRenderer $renderer */
1695 $renderer = wc_get_container()->get( BlockEmailRenderer::class );
1696 return $renderer->maybe_render_block_email( $this );
1697 }
1698
1699 /**
1700 * Prevent lazy loading on attachment images in email context by adding skip classes.
1701 * This is hooked into the wp_get_attachment_image_attributes filter.
1702 *
1703 * @param array $attributes The image attributes array.
1704 * @return array The modified image attributes array.
1705 */
1706 public function prevent_lazy_loading_on_attachment( $attributes ) {
1707 // Only process if we're currently sending an email.
1708 if ( ! $this->sending ) {
1709 return $attributes;
1710 }
1711
1712 // Skip classes to prevent lazy loading plugins from applying lazy loading.
1713 // These are the most common skip classes used by popular lazy loading plugins.
1714 $skip_classes = array( 'skip-lazy', 'no-lazyload', 'lazyload-disabled', 'no-lazy', 'skip-lazyload' );
1715
1716 // Add skip classes to prevent lazy loading plugins from applying lazy loading.
1717 if ( isset( $attributes['class'] ) ) {
1718 $classes = array_filter( array_map( 'trim', explode( ' ', $attributes['class'] ) ) );
1719 $classes = array_unique( array_merge( $classes, $skip_classes ) );
1720 $attributes['class'] = implode( ' ', $classes );
1721 } else {
1722 // No class attribute exists, add one with skip classes.
1723 $attributes['class'] = implode( ' ', $skip_classes );
1724 }
1725
1726 // Add data-skip-lazy attribute as an additional safeguard.
1727 $attributes['data-skip-lazy'] = 'true';
1728
1729 return $attributes;
1730 }
1731 }
1732