Issues (942)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/emails/class-wc-email.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Class WC_Email file.
4
 *
5
 * @package WooCommerce\Emails
6
 */
7
8 1
if ( ! defined( 'ABSPATH' ) ) {
9
	exit;
10
}
11
12 1
if ( class_exists( 'WC_Email', false ) ) {
13 1
	return;
14
}
15
16
/**
17
 * Email Class
18
 *
19
 * WooCommerce Email Class which is extended by specific email template classes to add emails to WooCommerce
20
 *
21
 * @class       WC_Email
22
 * @version     2.5.0
23
 * @package     WooCommerce/Classes/Emails
24
 * @extends     WC_Settings_API
25
 */
26
class WC_Email extends WC_Settings_API {
27
28
	/**
29
	 * Email method ID.
30
	 *
31
	 * @var String
32
	 */
33
	public $id;
34
35
	/**
36
	 * Email method title.
37
	 *
38
	 * @var string
39
	 */
40
	public $title;
41
42
	/**
43
	 * 'yes' if the method is enabled.
44
	 *
45
	 * @var string yes, no
46
	 */
47
	public $enabled;
48
49
	/**
50
	 * Description for the email.
51
	 *
52
	 * @var string
53
	 */
54
	public $description;
55
56
	/**
57
	 * Default heading.
58
	 *
59
	 * Supported for backwards compatibility but we recommend overloading the
60
	 * get_default_x methods instead so localization can be done when needed.
61
	 *
62
	 * @var string
63
	 */
64
	public $heading = '';
65
66
	/**
67
	 * Default subject.
68
	 *
69
	 * Supported for backwards compatibility but we recommend overloading the
70
	 * get_default_x methods instead so localization can be done when needed.
71
	 *
72
	 * @var string
73
	 */
74
	public $subject = '';
75
76
	/**
77
	 * Plain text template path.
78
	 *
79
	 * @var string
80
	 */
81
	public $template_plain;
82
83
	/**
84
	 * HTML template path.
85
	 *
86
	 * @var string
87
	 */
88
	public $template_html;
89
90
	/**
91
	 * Template path.
92
	 *
93
	 * @var string
94
	 */
95
	public $template_base;
96
97
	/**
98
	 * Recipients for the email.
99
	 *
100
	 * @var string
101
	 */
102
	public $recipient;
103
104
	/**
105
	 * Object this email is for, for example a customer, product, or email.
106
	 *
107
	 * @var object|bool
108
	 */
109
	public $object;
110
111
	/**
112
	 * Mime boundary (for multipart emails).
113
	 *
114
	 * @var string
115
	 */
116
	public $mime_boundary;
117
118
	/**
119
	 * Mime boundary header (for multipart emails).
120
	 *
121
	 * @var string
122
	 */
123
	public $mime_boundary_header;
124
125
	/**
126
	 * True when email is being sent.
127
	 *
128
	 * @var bool
129
	 */
130
	public $sending;
131
132
	/**
133
	 * True when the email notification is sent manually only.
134
	 *
135
	 * @var bool
136
	 */
137
	protected $manual = false;
138
139
	/**
140
	 * True when the email notification is sent to customers.
141
	 *
142
	 * @var bool
143
	 */
144
	protected $customer_email = false;
145
146
	/**
147
	 *  List of preg* regular expression patterns to search for,
148
	 *  used in conjunction with $plain_replace.
149
	 *  https://raw.github.com/ushahidi/wp-silcc/master/class.html2text.inc
150
	 *
151
	 *  @var array $plain_search
152
	 *  @see $plain_replace
153
	 */
154
	public $plain_search = array(
155
		"/\r/",                                                  // Non-legal carriage return.
156
		'/&(nbsp|#0*160);/i',                                    // Non-breaking space.
157
		'/&(quot|rdquo|ldquo|#0*8220|#0*8221|#0*147|#0*148);/i', // Double quotes.
158
		'/&(apos|rsquo|lsquo|#0*8216|#0*8217);/i',               // Single quotes.
159
		'/&gt;/i',                                               // Greater-than.
160
		'/&lt;/i',                                               // Less-than.
161
		'/&#0*38;/i',                                            // Ampersand.
162
		'/&amp;/i',                                              // Ampersand.
163
		'/&(copy|#0*169);/i',                                    // Copyright.
164
		'/&(trade|#0*8482|#0*153);/i',                           // Trademark.
165
		'/&(reg|#0*174);/i',                                     // Registered.
166
		'/&(mdash|#0*151|#0*8212);/i',                           // mdash.
167
		'/&(ndash|minus|#0*8211|#0*8722);/i',                    // ndash.
168
		'/&(bull|#0*149|#0*8226);/i',                            // Bullet.
169
		'/&(pound|#0*163);/i',                                   // Pound sign.
170
		'/&(euro|#0*8364);/i',                                   // Euro sign.
171
		'/&(dollar|#0*36);/i',                                   // Dollar sign.
172
		'/&[^&\s;]+;/i',                                         // Unknown/unhandled entities.
173
		'/[ ]{2,}/',                                             // Runs of spaces, post-handling.
174
	);
175
176
	/**
177
	 *  List of pattern replacements corresponding to patterns searched.
178
	 *
179
	 *  @var array $plain_replace
180
	 *  @see $plain_search
181
	 */
182
	public $plain_replace = array(
183
		'',                                             // Non-legal carriage return.
184
		' ',                                            // Non-breaking space.
185
		'"',                                            // Double quotes.
186
		"'",                                            // Single quotes.
187
		'>',                                            // Greater-than.
188
		'<',                                            // Less-than.
189
		'&',                                            // Ampersand.
190
		'&',                                            // Ampersand.
191
		'(c)',                                          // Copyright.
192
		'(tm)',                                         // Trademark.
193
		'(R)',                                          // Registered.
194
		'--',                                           // mdash.
195
		'-',                                            // ndash.
196
		'*',                                            // Bullet.
197
		'£',                                            // Pound sign.
198
		'EUR',                                          // Euro sign. € ?.
199
		'$',                                            // Dollar sign.
200
		'',                                             // Unknown/unhandled entities.
201
		' ',                                             // Runs of spaces, post-handling.
202
	);
203
204
	/**
205
	 * Strings to find/replace in subjects/headings.
206
	 *
207
	 * @var array
208
	 */
209
	protected $placeholders = array();
210
211
	/**
212
	 * Strings to find in subjects/headings.
213
	 *
214
	 * @deprecated 3.2.0 in favour of placeholders
215
	 * @var array
216
	 */
217
	public $find = array();
218
219
	/**
220
	 * Strings to replace in subjects/headings.
221
	 *
222
	 * @deprecated 3.2.0 in favour of placeholders
223
	 * @var array
224
	 */
225
	public $replace = array();
226
227
	/**
228
	 * Constructor.
229
	 */
230 1
	public function __construct() {
231
		// Find/replace.
232 1
		$this->placeholders = array_merge(
233
			array(
234 1
				'{site_title}'   => $this->get_blogname(),
235 1
				'{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ),
236
			),
237 1
			$this->placeholders
238
		);
239
240
		// Init settings.
241 1
		$this->init_form_fields();
242 1
		$this->init_settings();
243
244
		// Default template base if not declared in child constructor.
245 1
		if ( is_null( $this->template_base ) ) {
246 1
			$this->template_base = WC()->plugin_path() . '/templates/';
247
		}
248
249 1
		$this->email_type = $this->get_option( 'email_type' );
250 1
		$this->enabled    = $this->get_option( 'enabled' );
251
252 1
		add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) );
253 1
		add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) );
254
	}
255
256
	/**
257
	 * Handle multipart mail.
258
	 *
259
	 * @param  PHPMailer $mailer PHPMailer object.
260
	 * @return PHPMailer
261
	 */
262 1
	public function handle_multipart( $mailer ) {
263 1
		if ( $this->sending && 'multipart' === $this->get_email_type() ) {
264
			$mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
265
				preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) )
266
			);
267
			$this->sending   = false;
268
		}
269 1
		return $mailer;
270
	}
271
272
	/**
273
	 * Format email string.
274
	 *
275
	 * @param mixed $string Text to replace placeholders in.
276
	 * @return string
277
	 */
278 1
	public function format_string( $string ) {
279 1
		$find    = array_keys( $this->placeholders );
280 1
		$replace = array_values( $this->placeholders );
281
282
		// If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0.
283 1
		$find    = array_merge( (array) $this->find, $find );
284 1
		$replace = array_merge( (array) $this->replace, $replace );
285
286
		// Take care of blogname which is no longer defined as a valid placeholder.
287 1
		$find[]    = '{blogname}';
288 1
		$replace[] = $this->get_blogname();
289
290
		// 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.
291 1
		if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) {
292
			$legacy_find    = $this->find;
0 ignored issues
show
Deprecated Code introduced by
The property WC_Email::$find has been deprecated with message: 3.2.0 in favour of placeholders

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
293
			$legacy_replace = $this->replace;
0 ignored issues
show
Deprecated Code introduced by
The property WC_Email::$replace has been deprecated with message: 3.2.0 in favour of placeholders

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
294
295
			foreach ( $this->placeholders as $find => $replace ) {
296
				$legacy_key                    = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) );
297
				$legacy_find[ $legacy_key ]    = $find;
298
				$legacy_replace[ $legacy_key ] = $replace;
299
			}
300
301
			$string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string );
302
		}
303
304
		/**
305
		 * Filter for main find/replace.
306
		 *
307
		 * @since 3.2.0
308
		 */
309 1
		return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this );
310
	}
311
312
	/**
313
	 * Set the locale to the store locale for customer emails to make sure emails are in the store language.
314
	 */
315 1
	public function setup_locale() {
316 1
		if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) {
317 1
			wc_switch_to_site_locale();
318
		}
319
	}
320
321
	/**
322
	 * Restore the locale to the default locale. Use after finished with setup_locale.
323
	 */
324 1
	public function restore_locale() {
325 1
		if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) {
326 1
			wc_restore_locale();
327
		}
328
	}
329
330
	/**
331
	 * Get email subject.
332
	 *
333
	 * @since  3.1.0
334
	 * @return string
335
	 */
336
	public function get_default_subject() {
337
		return $this->subject;
338
	}
339
340
	/**
341
	 * Get email heading.
342
	 *
343
	 * @since  3.1.0
344
	 * @return string
345
	 */
346
	public function get_default_heading() {
347
		return $this->heading;
348
	}
349
350
	/**
351
	 * Default content to show below main email content.
352
	 *
353
	 * @since 3.7.0
354
	 * @return string
355
	 */
356
	public function get_default_additional_content() {
357
		return '';
358
	}
359
360
	/**
361
	 * Return content from the additional_content field.
362
	 *
363
	 * Displayed above the footer.
364
	 *
365
	 * @since 3.7.0
366
	 * @return string
367
	 */
368 1
	public function get_additional_content() {
369 1
		$content = $this->get_option( 'additional_content', '' );
370
371 1
		return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this );
372
	}
373
374
	/**
375
	 * Get email subject.
376
	 *
377
	 * @return string
378
	 */
379 1
	public function get_subject() {
380 1
		return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this );
381
	}
382
383
	/**
384
	 * Get email heading.
385
	 *
386
	 * @return string
387
	 */
388 1
	public function get_heading() {
389 1
		return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this );
390
	}
391
392
	/**
393
	 * Get valid recipients.
394
	 *
395
	 * @return string
396
	 */
397 1
	public function get_recipient() {
398 1
		$recipient  = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this );
399 1
		$recipients = array_map( 'trim', explode( ',', $recipient ) );
400 1
		$recipients = array_filter( $recipients, 'is_email' );
401 1
		return implode( ', ', $recipients );
402
	}
403
404
	/**
405
	 * Get email headers.
406
	 *
407
	 * @return string
408
	 */
409 1
	public function get_headers() {
410 1
		$header = 'Content-Type: ' . $this->get_content_type() . "\r\n";
411
412 1
		if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) {
413
			if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) {
414
				$header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n";
415
			}
416 1
		} elseif ( $this->get_from_address() && $this->get_from_name() ) {
417 1
			$header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n";
418
		}
419
420 1
		return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
421
	}
422
423
	/**
424
	 * Get email attachments.
425
	 *
426
	 * @return array
427
	 */
428 1
	public function get_attachments() {
429 1
		return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this );
430
	}
431
432
	/**
433
	 * Return email type.
434
	 *
435
	 * @return string
436
	 */
437 1
	public function get_email_type() {
438 1
		return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain';
439
	}
440
441
	/**
442
	 * Get email content type.
443
	 *
444
	 * @return string
445
	 */
446 1
	public function get_content_type() {
447 1
		switch ( $this->get_email_type() ) {
448
			case 'html':
449 1
				return 'text/html';
450
			case 'multipart':
451
				return 'multipart/alternative';
452
			default:
453
				return 'text/plain';
454
		}
455
	}
456
457
	/**
458
	 * Return the email's title
459
	 *
460
	 * @return string
461
	 */
462
	public function get_title() {
463
		return apply_filters( 'woocommerce_email_title', $this->title, $this );
464
	}
465
466
	/**
467
	 * Return the email's description
468
	 *
469
	 * @return string
470
	 */
471
	public function get_description() {
472
		return apply_filters( 'woocommerce_email_description', $this->description, $this );
473
	}
474
475
	/**
476
	 * Proxy to parent's get_option and attempt to localize the result using gettext.
477
	 *
478
	 * @param string $key Option key.
479
	 * @param mixed  $empty_value Value to use when option is empty.
480
	 * @return string
481
	 */
482 1
	public function get_option( $key, $empty_value = null ) {
483 1
		$value = parent::get_option( $key, $empty_value );
484 1
		return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value );
485
	}
486
487
	/**
488
	 * Checks if this email is enabled and will be sent.
489
	 *
490
	 * @return bool
491
	 */
492 1
	public function is_enabled() {
493 1
		return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this );
494
	}
495
496
	/**
497
	 * Checks if this email is manually sent
498
	 *
499
	 * @return bool
500
	 */
501
	public function is_manual() {
502
		return $this->manual;
503
	}
504
505
	/**
506
	 * Checks if this email is customer focussed.
507
	 *
508
	 * @return bool
509
	 */
510 1
	public function is_customer_email() {
511 1
		return $this->customer_email;
512
	}
513
514
	/**
515
	 * Get WordPress blog name.
516
	 *
517
	 * @return string
518
	 */
519 1
	public function get_blogname() {
520 1
		return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
521
	}
522
523
	/**
524
	 * Get email content.
525
	 *
526
	 * @return string
527
	 */
528 1
	public function get_content() {
529 1
		$this->sending = true;
530
531 1
		if ( 'plain' === $this->get_email_type() ) {
532
			$email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 );
533
		} else {
534 1
			$email_content = $this->get_content_html();
535
		}
536
537 1
		return $email_content;
538
	}
539
540
	/**
541
	 * Apply inline styles to dynamic content.
542
	 *
543
	 * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported).
544
	 *
545
	 * @param string|null $content Content that will receive inline styles.
546
	 * @return string
547
	 */
548 1
	public function style_inline( $content ) {
549 1
		if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) {
550 1
			ob_start();
551 1
			wc_get_template( 'emails/email-styles.php' );
552 1
			$css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this );
553
554 1
			if ( $this->supports_emogrifier() ) {
555 1
				$emogrifier_class = '\\Pelago\\Emogrifier';
556 1
				if ( ! class_exists( $emogrifier_class ) ) {
557 1
					include_once dirname( dirname( __FILE__ ) ) . '/libraries/class-emogrifier.php';
558
				}
559
				try {
560 1
					$emogrifier = new $emogrifier_class( $content, $css );
561 1
					$content    = $emogrifier->emogrify();
562
				} catch ( Exception $e ) {
563
					$logger = wc_get_logger();
564 1
					$logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) );
565
				}
566
			} else {
567
				$content = '<style type="text/css">' . $css . '</style>' . $content;
568
			}
569
		}
570 1
		return $content;
571
	}
572
573
	/**
574
	 * Return if emogrifier library is supported.
575
	 *
576
	 * @since 3.5.0
577
	 * @return bool
578
	 */
579 1
	protected function supports_emogrifier() {
580 1
		return class_exists( 'DOMDocument' ) && version_compare( PHP_VERSION, '5.5', '>=' );
581
	}
582
583
	/**
584
	 * Get the email content in plain text format.
585
	 *
586
	 * @return string
587
	 */
588
	public function get_content_plain() {
589
		return ''; }
590
591
	/**
592
	 * Get the email content in HTML format.
593
	 *
594
	 * @return string
595
	 */
596
	public function get_content_html() {
597
		return ''; }
598
599
	/**
600
	 * Get the from name for outgoing emails.
601
	 *
602
	 * @return string
603
	 */
604 1
	public function get_from_name() {
605 1
		$from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this );
606 1
		return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES );
607
	}
608
609
	/**
610
	 * Get the from address for outgoing emails.
611
	 *
612
	 * @return string
613
	 */
614 1
	public function get_from_address() {
615 1
		$from_address = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this );
616 1
		return sanitize_email( $from_address );
617
	}
618
619
	/**
620
	 * Send an email.
621
	 *
622
	 * @param string $to Email to.
623
	 * @param string $subject Email subject.
624
	 * @param string $message Email message.
625
	 * @param string $headers Email headers.
626
	 * @param array  $attachments Email attachments.
627
	 * @return bool success
628
	 */
629 1
	public function send( $to, $subject, $message, $headers, $attachments ) {
630 1
		add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
631 1
		add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
632 1
		add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
633
634 1
		$message              = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
635 1
		$mail_callback        = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
636 1
		$mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this );
637 1
		$return               = call_user_func_array( $mail_callback, $mail_callback_params );
638
639 1
		remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
640 1
		remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
641 1
		remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
642
643 1
		return $return;
644
	}
645
646
	/**
647
	 * Initialise Settings Form Fields - these are generic email options most will use.
648
	 */
649 1
	public function init_form_fields() {
650
		/* translators: %s: list of placeholders */
651 1
		$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
652 1
		$this->form_fields = array(
653
			'enabled'            => array(
654 1
				'title'   => __( 'Enable/Disable', 'woocommerce' ),
655 1
				'type'    => 'checkbox',
656 1
				'label'   => __( 'Enable this email notification', 'woocommerce' ),
657 1
				'default' => 'yes',
658
			),
659
			'subject'            => array(
660 1
				'title'       => __( 'Subject', 'woocommerce' ),
661 1
				'type'        => 'text',
662
				'desc_tip'    => true,
663 1
				'description' => $placeholder_text,
664 1
				'placeholder' => $this->get_default_subject(),
665 1
				'default'     => '',
666
			),
667
			'heading'            => array(
668 1
				'title'       => __( 'Email heading', 'woocommerce' ),
669 1
				'type'        => 'text',
670
				'desc_tip'    => true,
671 1
				'description' => $placeholder_text,
672 1
				'placeholder' => $this->get_default_heading(),
673 1
				'default'     => '',
674
			),
675
			'additional_content' => array(
676 1
				'title'       => __( 'Additional content', 'woocommerce' ),
677 1
				'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
678 1
				'css'         => 'width:400px; height: 75px;',
679 1
				'placeholder' => __( 'N/A', 'woocommerce' ),
680 1
				'type'        => 'textarea',
681 1
				'default'     => $this->get_default_additional_content(),
682
				'desc_tip'    => true,
683
			),
684
			'email_type'         => array(
685 1
				'title'       => __( 'Email type', 'woocommerce' ),
686 1
				'type'        => 'select',
687 1
				'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
688 1
				'default'     => 'html',
689 1
				'class'       => 'email_type wc-enhanced-select',
690 1
				'options'     => $this->get_email_type_options(),
691
				'desc_tip'    => true,
692
			),
693
		);
694
	}
695
696
	/**
697
	 * Email type options.
698
	 *
699
	 * @return array
700
	 */
701 1
	public function get_email_type_options() {
702 1
		$types = array( 'plain' => __( 'Plain text', 'woocommerce' ) );
703
704 1
		if ( class_exists( 'DOMDocument' ) ) {
705 1
			$types['html']      = __( 'HTML', 'woocommerce' );
706 1
			$types['multipart'] = __( 'Multipart', 'woocommerce' );
707
		}
708
709 1
		return $types;
710
	}
711
712
	/**
713
	 * Admin Panel Options Processing.
714
	 */
715
	public function process_admin_options() {
716
		// Save regular options.
717
		parent::process_admin_options();
718
719
		$post_data = $this->get_post_data();
720
721
		// Save templates.
722
		if ( isset( $post_data['template_html_code'] ) ) {
723
			$this->save_template( $post_data['template_html_code'], $this->template_html );
724
		}
725
		if ( isset( $post_data['template_plain_code'] ) ) {
726
			$this->save_template( $post_data['template_plain_code'], $this->template_plain );
727
		}
728
	}
729
730
	/**
731
	 * Get template.
732
	 *
733
	 * @param  string $type Template type. Can be either 'template_html' or 'template_plain'.
734
	 * @return string
735
	 */
736
	public function get_template( $type ) {
737
		$type = basename( $type );
738
739
		if ( 'template_html' === $type ) {
740
			return $this->template_html;
741
		} elseif ( 'template_plain' === $type ) {
742
			return $this->template_plain;
743
		}
744
		return '';
745
	}
746
747
	/**
748
	 * Save the email templates.
749
	 *
750
	 * @since 2.4.0
751
	 * @param string $template_code Template code.
752
	 * @param string $template_path Template path.
753
	 */
754
	protected function save_template( $template_code, $template_path ) {
755
		if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) {
756
			$saved = false;
757
			$file  = get_stylesheet_directory() . '/' . WC()->template_path() . $template_path;
758
			$code  = wp_unslash( $template_code );
759
760
			if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable
761
				$f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
762
763
				if ( false !== $f ) {
764
					fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
765
					fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
766
					$saved = true;
767
				}
768
			}
769
770
			if ( ! $saved ) {
771
				$redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) );
772
				wp_safe_redirect( $redirect );
773
				exit;
774
			}
775
		}
776
	}
777
778
	/**
779
	 * Get the template file in the current theme.
780
	 *
781
	 * @param  string $template Template name.
782
	 *
783
	 * @return string
784
	 */
785
	public function get_theme_template_file( $template ) {
786
		return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template;
787
	}
788
789
	/**
790
	 * Move template action.
791
	 *
792
	 * @param string $template_type Template type.
793
	 */
794
	protected function move_template_action( $template_type ) {
795
		$template = $this->get_template( $template_type );
796
		if ( ! empty( $template ) ) {
797
			$theme_file = $this->get_theme_template_file( $template );
798
799
			if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) {
800
801
				// Locate template file.
802
				$core_file     = $this->template_base . $template;
803
				$template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
804
805
				// Copy template file.
806
				copy( $template_file, $theme_file );
807
808
				/**
809
				 * Action hook fired after copying email template file.
810
				 *
811
				 * @param string $template_type The copied template type
812
				 * @param string $email The email object
813
				 */
814
				do_action( 'woocommerce_copy_email_template', $template_type, $this );
815
816
				?>
817
				<div class="updated">
818
					<p><?php echo esc_html__( 'Template file copied to theme.', 'woocommerce' ); ?></p>
819
				</div>
820
				<?php
821
			}
822
		}
823
	}
824
825
	/**
826
	 * Delete template action.
827
	 *
828
	 * @param string $template_type Template type.
829
	 */
830
	protected function delete_template_action( $template_type ) {
831
		$template = $this->get_template( $template_type );
832
833
		if ( $template ) {
834
			if ( ! empty( $template ) ) {
835
				$theme_file = $this->get_theme_template_file( $template );
836
837
				if ( file_exists( $theme_file ) ) {
838
					unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink
839
840
					/**
841
					 * Action hook fired after deleting template file.
842
					 *
843
					 * @param string $template The deleted template type
844
					 * @param string $email The email object
845
					 */
846
					do_action( 'woocommerce_delete_email_template', $template_type, $this );
847
					?>
848
					<div class="updated">
849
						<p><?php echo esc_html__( 'Template file deleted from theme.', 'woocommerce' ); ?></p>
850
					</div>
851
					<?php
852
				}
853
			}
854
		}
855
	}
856
857
	/**
858
	 * Admin actions.
859
	 */
860
	protected function admin_actions() {
861
		// Handle any actions.
862
		if (
863
			( ! empty( $this->template_html ) || ! empty( $this->template_plain ) )
864
			&& ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) )
865
			&& 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
866
		) {
867 View Code Duplication
			if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) {
868
				wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
869
			}
870
871
			if ( ! current_user_can( 'edit_themes' ) ) {
872
				wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'woocommerce' ) );
873
			}
874
875
			if ( ! empty( $_GET['move_template'] ) ) {
876
				$this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) );
877
			}
878
879
			if ( ! empty( $_GET['delete_template'] ) ) {
880
				$this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) );
881
			}
882
		}
883
	}
884
885
	/**
886
	 * Admin Options.
887
	 *
888
	 * Setup the email settings screen.
889
	 * Override this in your email.
890
	 *
891
	 * @since 1.0.0
892
	 */
893
	public function admin_options() {
894
		// Do admin actions.
895
		$this->admin_actions();
896
		?>
897
		<h2><?php echo esc_html( $this->get_title() ); ?> <?php wc_back_link( __( 'Return to emails', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ); ?></h2>
898
899
		<?php echo wpautop( wp_kses_post( $this->get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
900
901
		<?php
902
		/**
903
		 * Action hook fired before displaying email settings.
904
		 *
905
		 * @param string $email The email object
906
		 */
907
		do_action( 'woocommerce_email_settings_before', $this );
908
		?>
909
910
		<table class="form-table">
911
			<?php $this->generate_settings_html(); ?>
912
		</table>
913
914
		<?php
915
		/**
916
		 * Action hook fired after displaying email settings.
917
		 *
918
		 * @param string $email The email object
919
		 */
920
		do_action( 'woocommerce_email_settings_after', $this );
921
		?>
922
923
		<?php
924
925
		if ( current_user_can( 'edit_themes' ) && ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) ) {
926
			?>
927
			<div id="template">
928
				<?php
929
				$templates = array(
930
					'template_html'  => __( 'HTML template', 'woocommerce' ),
931
					'template_plain' => __( 'Plain text template', 'woocommerce' ),
932
				);
933
934
				foreach ( $templates as $template_type => $title ) :
935
					$template = $this->get_template( $template_type );
936
937
					if ( empty( $template ) ) {
938
						continue;
939
					}
940
941
					$local_file    = $this->get_theme_template_file( $template );
942
					$core_file     = $this->template_base . $template;
943
					$template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
944
					$template_dir  = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template );
945
					?>
946
					<div class="template <?php echo esc_attr( $template_type ); ?>">
947
						<h4><?php echo wp_kses_post( $title ); ?></h4>
948
949
						<?php if ( file_exists( $local_file ) ) : ?>
950
							<p>
951
								<a href="#" class="button toggle_editor"></a>
952
953 View Code Duplication
								<?php if ( is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?>
954
									<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">
955
										<?php esc_html_e( 'Delete template file', 'woocommerce' ); ?>
956
									</a>
957
								<?php endif; ?>
958
959
								<?php
960
								/* translators: %s: Path to template file */
961
								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>' );
962
								?>
963
							</p>
964
965
							<div class="editor" style="display:none">
966
								<textarea class="code" cols="25" rows="20"
967
								<?php
968
								if ( ! is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
969
									?>
970
									readonly="readonly" disabled="disabled"
971
								<?php else : ?>
972
									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>
973
							</div>
974
						<?php elseif ( file_exists( $template_file ) ) : ?>
975
							<p>
976
								<a href="#" class="button toggle_editor"></a>
977
978
								<?php
979
								$emails_dir    = get_stylesheet_directory() . '/' . $template_dir . '/emails';
980
								$templates_dir = get_stylesheet_directory() . '/' . $template_dir;
981
								$theme_dir     = get_stylesheet_directory();
982
983
								if ( is_dir( $emails_dir ) ) {
984
									$target_dir = $emails_dir;
985
								} elseif ( is_dir( $templates_dir ) ) {
986
									$target_dir = $templates_dir;
987
								} else {
988
									$target_dir = $theme_dir;
989
								}
990
991 View Code Duplication
								if ( is_writable( $target_dir ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
992
									?>
993
									<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">
994
										<?php esc_html_e( 'Copy file to theme', 'woocommerce' ); ?>
995
									</a>
996
								<?php endif; ?>
997
998
								<?php
999
								/* translators: 1: Path to template file 2: Path to theme folder */
1000
								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>' );
1001
								?>
1002
							</p>
1003
1004
							<div class="editor" style="display:none">
1005
								<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>
1006
							</div>
1007
						<?php else : ?>
1008
							<p><?php esc_html_e( 'File was not found.', 'woocommerce' ); ?></p>
1009
						<?php endif; ?>
1010
					</div>
1011
				<?php endforeach; ?>
1012
			</div>
1013
1014
			<?php
1015
			wc_enqueue_js(
1016
				"jQuery( 'select.email_type' ).change( function() {
1017
1018
					var val = jQuery( this ).val();
1019
1020
					jQuery( '.template_plain, .template_html' ).show();
1021
1022
					if ( val != 'multipart' && val != 'html' ) {
1023
						jQuery('.template_html').hide();
1024
					}
1025
1026
					if ( val != 'multipart' && val != 'plain' ) {
1027
						jQuery('.template_plain').hide();
1028
					}
1029
1030
				}).change();
1031
1032
				var view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
1033
				var hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';
1034
1035
				jQuery( 'a.toggle_editor' ).text( view ).toggle( function() {
1036
					jQuery( this ).text( hide ).closest(' .template' ).find( '.editor' ).slideToggle();
1037
					return false;
1038
				}, function() {
1039
					jQuery( this ).text( view ).closest( '.template' ).find( '.editor' ).slideToggle();
1040
					return false;
1041
				} );
1042
1043
				jQuery( 'a.delete_template' ).click( function() {
1044
					if ( window.confirm('" . esc_js( __( 'Are you sure you want to delete this template file?', 'woocommerce' ) ) . "') ) {
1045
						return true;
1046
					}
1047
1048
					return false;
1049
				});
1050
1051
				jQuery( '.editor textarea' ).change( function() {
1052
					var name = jQuery( this ).attr( 'data-name' );
1053
1054
					if ( name ) {
1055
						jQuery( this ).attr( 'name', name );
1056
					}
1057
				});"
1058
			);
1059
		}
1060
	}
1061
}
1062