PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / model / notification / class-malware-notification.php

class-malware-notification.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/model/notification/class-malware-notification.php

359 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the scheduling and sending of malware-related notifications.
4 *
5 * @package WP_Defender\Model\Notification
6 */
7
8 namespace WP_Defender\Model\Notification;
9
10 use Countable;
11 use WP_Defender\Traits\IO;
12 use WP_Defender\Model\Scan;
13 use WP_Defender\Component\Mail;
14 use WP_Defender\Component\Notification;
15 use WP_Defender\Model\Scan_Item;
16 use WP_Defender\Traits\Scan_Email_Template;
17 use WP_Defender\Traits\Scan_Upsell;
18
19 /**
20 * Handles the scheduling and sending of malware-related notifications.
21 */
22 class Malware_Notification extends \WP_Defender\Model\Notification {
23
24 use IO;
25 use Scan_Email_Template;
26 use Scan_Upsell;
27
28 /**
29 * Table name.
30 *
31 * @var string
32 */
33 protected $table = 'wd_malware_scanning_notification';
34 /**
35 * Slug identifier for the malware notification.
36 *
37 * @var string
38 */
39 public const SLUG = 'malware-notification';
40
41 /**
42 * Retrieves the default values for the malware notification.
43 *
44 * @return array The default values for the malware notification.
45 */
46 public function get_default_values(): array {
47 $template = $this->get_email_template();
48
49 return array(
50 'subject_issue_found' => $template['found']['subject'],
51 'subject_issue_not_found' => $template['not_found']['subject'],
52 'subject_error' => $template['error']['subject'],
53 'content_issue_found' => $template['found']['body'],
54 'content_issue_not_found' => $template['not_found']['body'],
55 'content_error' => $template['error']['body'],
56 );
57 }
58
59 /**
60 * Sets default values for the malware notification.
61 *
62 * @return void
63 */
64 protected function before_load(): void {
65 $default = array(
66 'slug' => self::SLUG,
67 'title' => esc_html__( 'Issue - Alert', 'defender-security' ),
68 'status' => self::STATUS_DISABLED,
69 'description' => esc_html__(
70 'Get email alerts when Defender has finished manual scans.',
71 'defender-security'
72 ),
73 // @since 3.0.0 Fix 'Guest'-line.
74 'in_house_recipients' => array(),
75 'out_house_recipients' => array(),
76 'type' => 'notification',
77 'dry_run' => false,
78 'configs' => array(
79 'always_send' => false,
80 'error_send' => false,
81 'template' => $this->get_email_template(),
82 ),
83 );
84 $this->import( $default );
85 }
86
87 /**
88 * Sends the malware notification email to all subscribed users.
89 *
90 * @param Scan $scan The scan object.
91 * @param object $recipient_list The object containing the recipient lists.
92 * @param string $subject The subject of the email.
93 * @param string $content The content of the email.
94 * @param array $issues The list of issues to include in the email.
95 */
96 protected function email_engine( Scan $scan, $recipient_list, $subject, $content, $issues ): void {
97 $service = wd_di()->get( Notification::class );
98 foreach ( $recipient_list->in_house_recipients as $user ) {
99 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
100 continue;
101 }
102 $this->send_to_user( $user['email'], $user['name'], $subject, $content, $issues, $service );
103 }
104 foreach ( $recipient_list->out_house_recipients as $user ) {
105 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
106 continue;
107 }
108 $this->send_to_user( $user['email'], $user['name'], $subject, $content, $issues, $service );
109 }
110 }
111
112 /**
113 * Sends a manual scan notification to the user who triggered the scan.
114 *
115 * @param Scan $scan The scan object.
116 *
117 * @return bool Whether the manual scan notification was handled.
118 */
119 private function send_manual_scan( Scan $scan ): bool {
120 $user_id = (int) get_transient( 'defender_scan_triggered_by_' . $scan->id );
121 delete_transient( 'defender_scan_triggered_by_' . $scan->id );
122 if ( $user_id <= 0 ) {
123 return false;
124 }
125
126 $user = get_userdata( $user_id );
127 if ( ! $user ) {
128 return true;
129 }
130
131 $issues = $scan->get_issues( null, Scan_Item::STATUS_ACTIVE );
132 if ( in_array( $scan->status, array( Scan::STATUS_ERROR, Scan::STATUS_IDLE ), true ) ) {
133 $templates = $this->configs['template']['error'];
134 } elseif ( 0 === count( $issues ) ) {
135 return true;
136 } else {
137 $templates = $this->configs['template']['found'];
138 }
139
140 $subject = $templates['subject'];
141 $content = nl2br( $templates['body'] );
142 $service = wd_di()->get( Notification::class );
143 $this->send_to_user( $user->user_email, $user->display_name, $subject, $content, $issues, $service );
144
145 return true;
146 }
147
148 /**
149 * Sends the malware notification email.
150 * Constructs and sends the malware notification email to the recipients.
151 *
152 * @param Scan $scan The scan object.
153 *
154 * @return void
155 */
156 public function send( Scan $scan ): void {
157 // Manual scan: notify only the triggering user when issues are found or the scan fails.
158 if ( ! $scan->is_automation && $this->send_manual_scan( $scan ) ) {
159 return;
160 }
161
162 $object = $this;
163 if ( $scan->is_automation ) {
164 $object = wd_di()->get( Malware_Report::class );
165 // If Malware Reporting is unchecked, then return.
166 if ( self::STATUS_ACTIVE !== $object->status ) {
167 return;
168 }
169 // @since 2.7.0 Remove 'dry_run'-condition.
170 }
171 $issues = $scan->get_issues( null, Scan_Item::STATUS_ACTIVE );
172 /**
173 * When the Defender is unable to scan files for some reason.
174 * The 'error_send' option was removed in v6. Its stored value is preserved for backward
175 * compatibility (downgrade safety), but error notifications are now always sent regardless
176 * of what the legacy value is.
177 * Possible reasons:
178 * - no connection to Scan API,
179 * - no checksums received from wp.org,
180 * - scan process stuck.
181 */
182 if ( in_array( $scan->status, array( Scan::STATUS_ERROR, Scan::STATUS_IDLE ), true ) ) {
183 $subject = $object->configs['template']['error']['subject'];
184 $content = nl2br( $object->configs['template']['error']['body'] );
185
186 $this->email_engine( $scan, $object, $subject, $content, $issues );
187
188 return;
189 }
190
191 // The 'always_send' option was removed in v6. Its stored value is preserved for backward
192 // compatibility (downgrade safety), but it must have no effect here — never send when
193 // there are no issues, regardless of what the legacy value is.
194 if ( 0 === count( $issues ) ) {
195 return;
196 }
197 // For another case.
198 $templates = count( $issues ) ? $object->configs['template']['found'] : $object->configs['template']['not_found'];
199 $subject = $templates['subject'];
200 $content = nl2br( $templates['body'] );
201
202 $this->email_engine( $scan, $object, $subject, $content, $issues );
203 }
204
205 /**
206 * Sends the malware notification email to a specific user.
207 * Constructs and sends the malware notification email to the specified recipient.
208 *
209 * @param string $email The recipient's email address.
210 * @param string $name The recipient's name.
211 * @param string $subject The email subject.
212 * @param string $content The email content.
213 * @param array $issues The malware issues detected.
214 * @param object $service The notification service object.
215 *
216 * @return void
217 */
218 private function send_to_user( $email, $name, $subject, $content, $issues, $service ) {
219 $controller_scan = wd_di()->get( \WP_Defender\Controller\Scan::class );
220 $replacers = array(
221 'issues_count' => is_array( $issues ) || $issues instanceof \Countable ? (string) count( $issues ) : '0',
222 'issues_list' => $controller_scan->render_partial(
223 'email/scan-issue-list',
224 array(
225 'issues' => $issues,
226 'email' => $email,
227 ),
228 false
229 ),
230 'site_url' => network_site_url(),
231 'user_name' => $name,
232 );
233
234 /**
235 * Filters the replaced data. It's without replacement.
236 *
237 * @param array $replacers Email params.
238 *
239 * @deprecated 3.2.0
240 */
241 $replacers = apply_filters_deprecated( 'wd_notification_email_params', array( $replacers ), '3.2.0' );
242
243 $filter_name = ( is_array( $issues ) || $issues instanceof Countable ? count( $issues ) : 0 ) > 0
244 ? 'wd_notification_email_subject_issue'
245 : 'wd_notification_email_subject';
246 $subject = apply_filters( $filter_name, $subject );
247
248 /**
249 * It's without replacement.
250 *
251 * @deprecated 3.2.0
252 */
253 $content = apply_filters_deprecated(
254 'wd_notification_email_content_before',
255 array( $content ),
256 '3.2.0'
257 );
258 foreach ( $replacers as $key => $replacer ) {
259
260 if ( is_scalar( $replacer ) && ! is_string( $replacer ) ) {
261 $replacer = (string) $replacer;
262 }
263
264 $content = str_replace( "{{$key}}", $replacer, $content );
265 $content = str_replace( '{' . strtoupper( $key ) . '}', $replacer, $content );
266 $subject = str_replace( "{{$key}}", $replacer, $subject );
267 $subject = str_replace( '{' . strtoupper( $key ) . '}', $replacer, $subject );
268 }
269 /**
270 * It's without replacement.
271 *
272 * @deprecated 3.2.0
273 */
274 $content = apply_filters_deprecated(
275 'wd_notification_email_content_after',
276 array( $content ),
277 '3.2.0'
278 );
279 $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email );
280 $template = $controller_scan->render_partial(
281 'email/index',
282 array(
283 'title' => esc_html__( 'Issues report', 'defender-security' ),
284 'content_body' => sprintf( '<div style="font-size: 16px">%s</div>', $content ),
285 'unsubscribe_link' => $unsubscribe_link,
286 ),
287 false
288 );
289
290 $headers = wd_di()->get( Mail::class )->get_headers(
291 defender_noreply_email( 'wd_scan_noreply_email' ),
292 self::SLUG
293 );
294
295 $ret = wp_mail( $email, $subject, $template, $headers );
296 if ( $ret ) {
297 $this->save_log( $email );
298 }
299 }
300
301 /**
302 * Defines labels for the malware notification settings.
303 *
304 * @return array The array of malware notification settings labels.
305 */
306 public function labels(): array {
307 return array(
308 'notification' => esc_html__( 'Issue - Alert', 'defender-security' ),
309 'always_send_notification' => esc_html__( 'Send alerts when no issues are detected', 'defender-security' ),
310 'error_send' => esc_html__(
311 "Send alerts when Defender couldn't scan the files",
312 'defender-security'
313 ),
314 'email_subject_issue_found' => esc_html__( 'Email subject when an issue is found', 'defender-security' ),
315 'email_subject_issue_not_found' => esc_html__( 'Email subject when no issues are found', 'defender-security' ),
316 'email_subject_error' => esc_html__( 'Email subject when failed to scan', 'defender-security' ),
317 'email_content_issue_found' => esc_html__( 'Email body when an issue is found', 'defender-security' ),
318 'email_content_issue_not_found' => esc_html__( 'Email body when no issues are found', 'defender-security' ),
319 'email_content_error' => esc_html__( 'When failed to scan', 'defender-security' ),
320 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ),
321 );
322 }
323
324 /**
325 * Checks if the malware scanning notification options are enabled.
326 * Checks whether the malware scanning notification is enabled or if the malware reporting is enabled or the scan
327 * is automated.
328 *
329 * @return bool True if the malware scanning notification options are enabled, false otherwise.
330 */
331 public function check_options(): bool {
332 return self::STATUS_ACTIVE === $this->status
333 || self::STATUS_ACTIVE === wd_di()->get( Malware_Report::class )->status;
334 }
335
336 /**
337 * Additional converting rules.
338 *
339 * @param array $configs The configuration data.
340 *
341 * @return array The type-casted configuration data.
342 * @since 3.1.0
343 */
344 public function type_casting( array $configs ): array {
345 $configs['always_send'] = (bool) $configs['always_send'];
346 $configs['error_send'] = (bool) $configs['error_send'];
347 // Additional security layer because parent 'configs'-property doesn't have sanitized stuff for nested properties.
348 $template = $configs['template'];
349 $configs['template']['found']['subject'] = sanitize_text_field( $template['found']['subject'] );
350 $configs['template']['found']['body'] = sanitize_textarea_field( $template['found']['body'] );
351 $configs['template']['not_found']['subject'] = sanitize_text_field( $template['not_found']['subject'] );
352 $configs['template']['not_found']['body'] = sanitize_textarea_field( $template['not_found']['body'] );
353 $configs['template']['error']['subject'] = sanitize_text_field( $template['error']['subject'] );
354 $configs['template']['error']['body'] = sanitize_textarea_field( $template['error']['body'] );
355
356 return $configs;
357 }
358 }
359