Passed
Push — master ( 59650c...0b7aa3 )
by Chris
11:04 queued 06:15
created

MonsterInsights_WP_Emails::get_html()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 4
rs 10
1
<?php
2
3
/**
4
 * Emails.
5
 *
6
 * This class handles all (notification) emails sent by MonsterInsights.
7
 *
8
 * Heavily influenced by the AffiliateWP plugin by Pippin Williamson.
9
 * https://github.com/AffiliateWP/AffiliateWP/blob/master/includes/emails/class-affwp-emails.php
10
 *
11
 * @since 7.10.5
12
 */
13
class MonsterInsights_WP_Emails {
14
15
	/**
16
	 * Holds the from address.
17
	 *
18
	 * @since 7.10.5
19
	 *
20
	 * @var string
21
	 */
22
	private $from_address;
23
24
	/**
25
	 * Holds the from name.
26
	 *
27
	 * @since 7.10.5
28
	 *
29
	 * @var string
30
	 */
31
	private $from_name;
32
33
	/**
34
	 * Holds the reply-to address.
35
	 *
36
	 * @since 7.10.5
37
	 *
38
	 * @var string
39
	 */
40
	private $reply_to = false;
41
42
	/**
43
	 * Holds the carbon copy addresses.
44
	 *
45
	 * @since 7.10.5
46
	 *
47
	 * @var string
48
	 */
49
	private $cc = false;
50
51
	/**
52
	 * Holds the email content type.
53
	 *
54
	 * @since 7.10.5
55
	 *
56
	 * @var string
57
	 */
58
	private $content_type;
59
60
	/**
61
	 * Holds the email headers.
62
	 *
63
	 * @since 7.10.5
64
	 *
65
	 * @var string
66
	 */
67
	private $headers;
68
69
	/**
70
	 * Whether to send email in HTML.
71
	 *
72
	 * @since 7.10.5
73
	 *
74
	 * @var bool
75
	 */
76
	private $html = true;
77
78
	/**
79
	 * The email template to use.
80
	 *
81
	 * @since 7.10.5
82
	 *
83
	 * @var string
84
	 */
85
	private $template;
86
87
	/**
88
	 * Header/footer/body arguments.
89
	 *
90
	 * @since 7.10.5
91
	 *
92
	 * @var array
93
	 */
94
	protected $args;
95
96
	/**
97
	 * Get things going.
98
	 *
99
	 * @since 7.10.5
100
	 */
101
	public function __construct( $template ) {
102
		$this->template = $template;
103
104
		$this->set_initial_args();
105
106
		add_action( 'monsterinsights_email_send_before', array( $this, 'send_before' ) );
107
		add_action( 'monsterinsights_email_send_after', array( $this, 'send_after' ) );
108
	}
109
110
	/**
111
	 * Set a property.
112
	 *
113
	 * @since 7.10.5
114
	 *
115
	 * @param string $key   Object property key.
116
	 * @param mixed  $value Object property value.
117
	 */
118
	public function __set( $key, $value ) {
119
		$this->$key = $value;
120
	}
121
122
	/**
123
	 * Get the email from name.
124
	 *
125
	 * @since 7.10.5
126
	 *
127
	 * @return string The email from name
128
	 */
129
	public function get_from_name() {
130
131
		if ( ! empty( $this->from_name ) ) {
132
			$this->from_name = $this->process_tag( $this->from_name );
133
		} else {
134
			$this->from_name = get_bloginfo( 'name' );
135
		}
136
137
		return apply_filters( 'monsterinsights_email_from_name', monsterinsights_decode_string( $this->from_name ), $this );
138
	}
139
140
	/**
141
	 * Get the email from address.
142
	 *
143
	 * @since 7.10.5
144
	 *
145
	 * @return string The email from address.
146
	 */
147
	public function get_from_address() {
148
149
		if ( ! empty( $this->from_address ) ) {
150
			$this->from_address = $this->process_tag( $this->from_address );
151
		} else {
152
			$this->from_address = get_option( 'admin_email' );
0 ignored issues
show
Documentation Bug introduced by
It seems like get_option('admin_email') can also be of type false. However, the property $from_address is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
153
		}
154
155
		return apply_filters( 'monsterinsights_email_from_address', $this->from_address, $this );
156
	}
157
158
	/**
159
	 * Get the email reply-to.
160
	 *
161
	 * @since 7.10.5
162
	 *
163
	 * @return string The email reply-to address.
164
	 */
165
	public function get_reply_to() {
166
167
		if ( ! empty( $this->reply_to ) ) {
168
169
			$this->reply_to = $this->process_tag( $this->reply_to );
170
171
			if ( ! is_email( $this->reply_to ) ) {
172
				$this->reply_to = false;
0 ignored issues
show
Documentation Bug introduced by
The property $reply_to was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
173
			}
174
		}
175
176
		return apply_filters( 'monsterinsights_email_reply_to', $this->reply_to, $this );
177
	}
178
179
	/**
180
	 * Get the email carbon copy addresses.
181
	 *
182
	 * @since 7.10.5
183
	 *
184
	 * @return string The email reply-to address.
185
	 */
186
	public function get_cc() {
187
188
		if ( ! empty( $this->cc ) ) {
189
190
			$this->cc = $this->process_tag( $this->cc );
191
192
			$addresses = array_map( 'trim', explode( ',', $this->cc ) );
193
194
			foreach ( $addresses as $key => $address ) {
195
				if ( ! is_email( $address ) ) {
196
					unset( $addresses[ $key ] );
197
				}
198
			}
199
200
			$this->cc = implode( ',', $addresses );
201
		}
202
203
		return apply_filters( 'monsterinsights_email_cc', $this->cc, $this );
204
	}
205
206
	/**
207
	 * Get the email content type.
208
	 *
209
	 * @since 7.10.5
210
	 *
211
	 * @return string The email content type.
212
	 */
213
	public function get_content_type() {
214
215
		if ( ! $this->content_type && $this->html ) {
216
			$this->content_type = apply_filters( 'monsterinsights_email_default_content_type', 'text/html', $this );
217
		} elseif ( ! $this->html ) {
218
			$this->content_type = 'text/plain';
219
		}
220
221
		return apply_filters( 'monsterinsights_email_content_type', $this->content_type, $this );
222
	}
223
224
	/**
225
	 * Get the email headers.
226
	 *
227
	 * @since 7.10.5
228
	 *
229
	 * @return string The email headers.
230
	 */
231
	public function get_headers() {
232
233
		if ( ! $this->headers ) {
234
			$this->headers = "From: {$this->get_from_name()} <{$this->get_from_address()}>\r\n";
235
			if ( $this->get_reply_to() ) {
236
				$this->headers .= "Reply-To: {$this->get_reply_to()}\r\n";
237
			}
238
			if ( $this->get_cc() ) {
239
				$this->headers .= "Cc: {$this->get_cc()}\r\n";
240
			}
241
			$this->headers .= "Content-Type: {$this->get_content_type()}; charset=utf-8\r\n";
242
		}
243
244
		return apply_filters( 'monsterinsights_email_headers', $this->headers, $this );
245
	}
246
247
	/**
248
	 * Set initial arguments to use in a template.
249
	 *
250
	 * @since 7.10.5
251
	 */
252
	public function set_initial_args() {
253
		$header_args = array(
0 ignored issues
show
Unused Code introduced by
The assignment to $header_args is dead and can be removed.
Loading history...
254
			'title' => esc_html__( 'MonsterInsights', 'google-analytics-for-wordpress' ),
255
		);
256
257
		$args = array(
258
			'header' => array(),
259
			'body'   => array(),
260
			'footer' => array(),
261
		);
262
263
		$from_address = $this->get_from_address();
264
		if ( ! empty( $from_address ) ) {
265
			$args['footer']['from_address'] = $from_address;
266
		}
267
268
		$args = apply_filters( 'monsterinsights_emails_templates_set_initial_args', $args, $this );
269
270
		$this->set_args( $args );
271
	}
272
273
	/**
274
	 * Set header/footer/body/style arguments to use in a template.
275
	 *
276
	 * @since 7.10.5
277
	 *
278
	 * @param array $args  Arguments to set.
279
	 * @param bool  $merge Merge the arguments with existing once or replace.
280
	 *
281
	 * @return MI_WP_Emails
0 ignored issues
show
Bug introduced by
The type MI_WP_Emails was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
282
	 */
283
	public function set_args( $args, $merge = true ) {
284
285
		$args = apply_filters( 'monsterinsights_emails_templates_set_args', $args, $this );
286
287
		if ( empty( $args ) || ! is_array( $args ) ) {
288
			return $this;
289
		}
290
291
		foreach ( $args as $type => $value ) {
292
293
			if ( ! is_array( $value ) ) {
294
				continue;
295
			}
296
297
			if ( ! isset( $this->args[ $type ] ) || ! is_array( $this->args[ $type ] ) ) {
298
				$this->args[ $type ] = array();
299
			}
300
301
			$this->args[ $type ] = $merge ? array_merge( $this->args[ $type ], $value ) : $value;
302
		}
303
304
		return $this;
305
	}
306
307
	/**
308
	 * Get header/footer/body arguments
309
	 *
310
	 * @since 7.10.5
311
	 *
312
	 * @param string $type Header/footer/body.
313
	 *
314
	 * @return array
315
	 */
316
	public function get_args( $type ) {
317
		if ( ! empty( $type ) ) {
318
			return isset( $this->args[ $type ] ) ? apply_filters( 'monsterinsights_emails_templates_get_args_' . $type, $this->args[ $type ], $this ) : array();
319
		}
320
321
		return apply_filters( 'monsterinsights_emails_templates_get_args', $this->args, $this );
322
	}
323
324
	/**
325
	 * Build the email.
326
	 *
327
	 * @since 7.10.5
328
	 *
329
	 * @param string $message The email message.
330
	 *
331
	 * @return string
332
	 */
333
	public function build_email( $message=null ) {
334
		// process plain text email
335
		if ( false === $this->html ) {
336
			$body 		= $this->get_template_part( 'body', $this->get_template(), true );
337
			$body 	 	= wp_strip_all_tags( $body );
338
			$message 	= str_replace( '{email}', $message, $body );
339
340
			return apply_filters( 'monsterinsights_email_message', $message, $this );
341
		}
342
343
		// process html email template
344
		$email_parts = array();
345
		$email_parts['header'] = $this->get_template_part( 'header', $this->get_template(), true );
346
347
		// Hooks into the email header.
348
		do_action( 'monsterinsights_email_header', $email_parts['header'] );
349
350
		$email_parts['body'] = $this->get_template_part( 'body', $this->get_template(), true );
351
352
		// Hooks into the email body.
353
		do_action( 'monsterinsights_email_body', $email_parts['body'] );
354
355
		$email_parts['footer'] = $this->get_template_part( 'footer', $this->get_template(), true );
356
357
		// Hooks into the email footer.
358
		do_action( 'monsterinsights_email_footer', $email_parts['footer'] );
359
360
361
		$body 	 = implode( $email_parts );
362
		$message = $this->process_tag( $message, false );
363
		$message = nl2br( $message );
364
		$message = str_replace( '{email}', $message, $body );
365
		//$message = make_clickable( $message );
366
367
		return apply_filters( 'monsterinsights_email_message', $message, $this );
368
	}
369
370
	/**
371
	 * Send the email.
372
	 *
373
	 * @since 7.10.5
374
	 *
375
	 * @param string $to The To address.
376
	 * @param string $subject The subject line of the email.
377
	 * @param string $message The body of the email.
378
	 * @param array  $attachments Attachments to the email.
379
	 *
380
	 * @return bool
381
	 */
382
	public function send( $to, $subject, $message=null, $attachments = array() ) {
383
384
		if ( ! did_action( 'init' ) && ! did_action( 'admin_init' ) ) {
385
			_doing_it_wrong( __FUNCTION__, esc_html__( 'You cannot send emails with MI_WP_Emails() until init/admin_init has been reached.', 'google-analytics-for-wordpress' ), null );
386
387
			return false;
388
		}
389
390
		// Don't send anything if emails have been disabled.
391
		if ( $this->is_email_disabled() ) {
392
			return false;
393
		}
394
395
		// Don't send if email address is invalid.
396
		if ( ! is_email( $to ) ) {
397
			return false;
398
		}
399
400
		// Hooks before email is sent.
401
		do_action( 'monsterinsights_email_send_before', $this );
402
403
		/*
404
		 * Allow to filter data on per-email basis,
405
		 * useful for localizations based on recipient email address, form settings,
406
		 * or for specific notifications - whatever available in MI_WP_Emails class.
407
		 */
408
		$data = apply_filters(
409
			'monsterinsights_emails_send_email_data',
410
			array(
411
				'to'          => $to,
412
				'subject'     => $subject,
413
				'message'     => $message,
414
				'headers'     => $this->get_headers(),
415
				'attachments' => $attachments,
416
			),
417
			$this
418
		);
419
420
		// Let's do this.
421
		$sent = wp_mail(
422
			$data['to'],
423
			monsterinsights_decode_string( $this->process_tag( $data['subject'] ) ),
424
			$this->build_email( $data['message'] ),
425
			$data['headers'],
426
			$data['attachments']
427
		);
428
429
		// Hooks after the email is sent.
430
		do_action( 'monsterinsights_email_send_after', $this );
431
432
		return $sent;
433
	}
434
435
	/**
436
	 * Add filters/actions before the email is sent.
437
	 *
438
	 * @since 7.10.5
439
	 */
440
	public function send_before() {
441
442
		add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
443
		add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
444
		add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
445
	}
446
447
	/**
448
	 * Remove filters/actions after the email is sent.
449
	 *
450
	 * @since 7.10.5
451
	 */
452
	public function send_after() {
453
454
		remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
455
		remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
456
		remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
457
	}
458
459
	/**
460
	 * Process a smart tag.
461
	 *
462
	 * @since 7.10.5
463
	 *
464
	 * @param string $string     String that may contain tags.
465
	 * @param bool   $sanitize   Toggle to maybe sanitize.
466
	 * @param bool   $linebreaks Toggle to process linebreaks.
467
	 *
468
	 * @return string
469
	 */
470
	public function process_tag( $string = '', $sanitize = true, $linebreaks = false ) {
471
472
		$tag = apply_filters( 'monsterinsights_process_smart_tags', $string );
473
474
		$tag = monsterinsights_decode_string( $tag );
475
476
		if ( $sanitize ) {
477
			if ( $linebreaks ) {
478
				$tag = monsterinsights_sanitize_textarea_field( $tag );
479
			} else {
480
				$tag = sanitize_text_field( $tag );
481
			}
482
		}
483
484
		return $tag;
485
	}
486
487
	/**
488
	 * Email kill switch if needed.
489
	 *
490
	 * @since 7.10.5
491
	 *
492
	 * @return bool
493
	 */
494
	public function is_email_disabled() {
495
		return (bool) apply_filters( 'monsterinsights_disable_all_emails', false, $this );
496
	}
497
498
	/**
499
	 * Get the enabled email template.
500
	 *
501
	 * @since 7.10.5
502
	 *
503
	 * @return string When filtering return 'default' to switch to text/plain email.
504
	 */
505
	public function get_template() {
506
507
		if ( ! empty( $this->template ) ) {
508
			$this->template = $this->process_tag( $this->template );
509
		} else {
510
			$this->template = 'default';
511
		}
512
513
		return apply_filters( 'monsterinsights_email_template', $this->template);
514
	}
515
516
	/**
517
	 * Retrieves a template content.
518
	 *
519
	 * @since 7.10.5
520
	 *
521
	 * @param string $slug Template file slug.
522
	 * @param string $name Optional. Default null.
523
	 * @param bool   $load Maybe load.
524
	 *
525
	 * @return string
526
	 */
527
	public function get_template_part( $slug, $name = null, $load = true ) {
0 ignored issues
show
Unused Code introduced by
The parameter $load is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

527
	public function get_template_part( $slug, $name = null, /** @scrutinizer ignore-unused */ $load = true ) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
528
529
		if ( false === $this->html ) {
530
			$name .= '-plain';
531
		}
532
533
		if ( isset( $name ) ) {
534
			$template = $slug . '-' . $name;
535
536
			$html = $this->get_html(
537
				$template,
538
				$this->get_args( $slug ),
539
				true
540
			);
541
542
			return apply_filters( 'monsterinsights_emails_templates_get_content_part', $html, $template, $this );
543
		}
544
545
	}
546
547
	/**
548
	 * Like $this->include_html, but returns the HTML instead of including.
549
	 *
550
	 * @since 7.10.5
551
	 *
552
	 * @param string $template_name Template name.
553
	 * @param array  $args          Arguments.
554
	 * @param bool   $extract       Extract arguments.
555
	 *
556
	 * @return string
557
	 */
558
	public static function get_html( $template_name, $args = array(), $extract = false ) {
559
		ob_start();
560
		self::include_html( $template_name, $args, $extract );
561
		return ob_get_clean();
562
	}
563
564
	/**
565
	 * Include a template.
566
	 * Uses 'require' if $args are passed or 'load_template' if not.
567
	 *
568
	 * @since 7.10.5
569
	 *
570
	 * @param string $template_name Template name.
571
	 * @param array  $args          Arguments.
572
	 * @param bool   $extract       Extract arguments.
573
	 *
574
	 * @throws \RuntimeException If extract() tries to modify the scope.
575
	 */
576
	public static function include_html( $template_name, $args = array(), $extract = false ) {
577
578
		$template_name .= '.php';
579
580
		// Allow 3rd party plugins to filter template file from their plugin.
581
		$located = apply_filters( 'monsterinsights_helpers_templates_include_html_located', self::locate_template( $template_name ), $template_name, $args, $extract );
582
		$args    = apply_filters( 'monsterinsights_helpers_templates_include_html_args', $args, $template_name, $extract );
583
584
		if ( empty( $located ) || ! is_readable( $located ) ) {
585
			return;
586
		}
587
588
		// Load template WP way if no arguments were passed.
589
		if ( empty( $args ) ) {
590
			load_template( $located, false );
591
			return;
592
		}
593
594
		$extract = apply_filters( 'monsterinsights_helpers_templates_include_html_extract_args', $extract, $template_name, $args );
595
596
		if ( $extract && is_array( $args ) ) {
597
598
			$created_vars_count = extract( $args, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract
599
600
			// Protecting existing scope from modification.
601
			if ( count( $args ) !== $created_vars_count ) {
602
				throw new RuntimeException( 'Extraction failed: variable names are clashing with the existing ones.' );
603
			}
604
		}
605
606
		require $located;
607
	}
608
609
	/**
610
	 * Locate a template and return the path for inclusion.
611
	 *
612
	 * @since 7.10.5
613
	 *
614
	 * @param string $template_name Template name.
615
	 *
616
	 * @return string
617
	 */
618
	public static function locate_template( $template_name ) {
619
620
		// Trim off any slashes from the template name.
621
		$template_name = ltrim( $template_name, '/' );
622
623
		if ( empty( $template_name ) ) {
624
			return apply_filters( 'monsterinsights_helpers_templates_locate', '', $template_name );
625
		}
626
627
		$located = '';
628
629
		// Try locating this template file by looping through the template paths.
630
		foreach ( self::get_theme_template_paths() as $template_path ) {
631
			if ( file_exists( $template_path . $template_name ) ) {
632
				$located = $template_path . $template_name;
633
				break;
634
			}
635
		}
636
637
		return apply_filters( 'monsterinsights_helpers_templates_locate', $located, $template_name );
638
	}
639
640
	/**
641
	 * Return a list of paths to check for template locations
642
	 *
643
	 * @since 7.10.5
644
	 *
645
	 * @return array
646
	 */
647
	public static function get_theme_template_paths() {
648
649
		$template_dir = 'monsterinsights-email';
650
651
		$file_paths = array(
652
			1   => trailingslashit( get_stylesheet_directory() ) . $template_dir,
653
			10  => trailingslashit( get_template_directory() ) . $template_dir,
654
			100 => trailingslashit( MONSTERINSIGHTS_PLUGIN_DIR ) . 'includes/emails/templates',
655
		);
656
657
		$file_paths = apply_filters( 'monsterinsights_email_template_paths', $file_paths );
658
659
		// Sort the file paths based on priority.
660
		ksort( $file_paths, SORT_NUMERIC );
661
662
		return array_map( 'trailingslashit', $file_paths );
663
	}
664
}
665