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
← All changes | src/controller/class-notification.php +337 -213 5.4.0 → 6.3.0 View file →
@@ -13,9 +13,13 @@
13 13 use WP_Defender\Traits\User;
14 14 use Calotes\Component\Request;
15 15 use WP_Defender\Traits\Formats;
16 16 use Calotes\Component\Response;
17 +use WP_Defender\Component\Network_Cron_Manager;
18 +use WP_Defender\Model\Notification\Audit_Report;
17 19 use WP_Defender\Model\Notification\Malware_Report;
20 +use WP_Defender\Model\Notification\Tweak_Reminder;
21 +use WP_Defender\Model\Notification\Firewall_Report;
18 22 use WP_Defender\Component\Config\Config_Hub_Helper;
19 23 use WP_Defender\Model\Notification as Model_Notification;
20 24
21 25 /**
@@ -56,70 +60,45 @@
56 60 /**
57 61 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
58 62 */
59 63 public function __construct() {
60 - $this->register_page(
61 - esc_html__( 'Notifications', 'defender-security' ),
62 - $this->slug,
63 - array( $this, 'main_view' ),
64 - $this->parent_slug
65 - );
66 64 $this->register_routes();
67 65 $this->service = wd_di()->get( \WP_Defender\Component\Notification::class );
68 66 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
69 67 // We use custom ajax endpoint here as the nonce would fail with other user.
70 - add_action( 'wp_ajax_' . self::SLUG_SUBSCRIBE, array( $this, 'verify_subscriber' ) );
71 - add_action( 'wp_ajax_nopriv_' . self::SLUG_SUBSCRIBE, array( $this, 'verify_subscriber' ) );
72 68 add_action( 'wp_ajax_' . self::SLUG_UNSUBSCRIBE, array( $this, 'unsubscribe_and_send_email' ) );
73 69 add_action( 'wp_ajax_nopriv_' . self::SLUG_UNSUBSCRIBE, array( $this, 'unsubscribe_and_send_email' ) );
74 70 add_action( 'defender_notify', array( $this, 'send_notify' ), 10, 2 );
75 - // We will schedule the time to send reports.
76 - if ( ! wp_next_scheduled( 'wdf_maybe_send_report' ) ) {
77 - $timestamp = gmmktime( wp_date( 'H' ), 0, 0 );
78 - wp_schedule_event( $timestamp, 'thirty_minutes', 'wdf_maybe_send_report' );
79 - }
80 - add_action( 'wdf_maybe_send_report', array( $this, 'report_sender' ) );
81 71 add_action( 'admin_notices', array( $this, 'show_actions_with_subscription' ) );
72 + add_action( 'wp_footer', array( $this, 'show_public_subscription_notice' ) );
82 73 }
83 74
84 75 /**
85 76 * For users who have subscribed or unsubscribed confirmation.
86 77 *
87 - * @return null|void
78 + * @return void
88 79 */
89 80 public function show_actions_with_subscription() {
90 81 if ( ! defined( 'IS_PROFILE_PAGE' ) || false === constant( 'IS_PROFILE_PAGE' ) ) {
91 - return null;
82 + return;
92 83 }
93 84 $slug = defender_get_data_from_request( 'slug', 'g' );
94 - if ( empty( $slug ) ) {
95 - return null;
85 + if ( ! is_string( $slug ) || '' === trim( $slug ) ) {
86 + return;
96 87 }
97 88 $m = $this->service->find_module_by_slug( $slug );
98 89 if ( ! is_object( $m ) ) {
99 - return null;
90 + return;
100 91 }
101 92 $context = defender_get_data_from_request( 'context', 'g' );
102 93 if ( 'subscribed' === $context ) {
103 94 $unsubscribe_link = $this->service->create_unsubscribe_url( $m->slug, $this->get_current_user_email() );
104 - $strings = sprintf(
105 - /* translators: 1. Module title. 2. Unsubscribed link. */
106 - esc_html__(
107 - 'You are now subscribed to receive %1$s. Made a mistake? %2$s',
108 - 'defender-security'
109 - ),
110 - '<strong>' . $m->title . '</strong>',
111 - '<a href="' . esc_url_raw( $unsubscribe_link ) . '" style="text-decoration: none">' . esc_html__( 'Unsubscribe', 'defender-security' ) . '</a>'
112 - );
113 - } elseif ( 'unsubscribe' === $context ) {
114 - $strings = sprintf(
115 - /* translators: %s: Module title. */
116 - esc_html__( 'You are now unsubscribed from %s.', 'defender-security' ),
117 - '<strong>' . $m->title . '</strong>'
118 - );
119 - } else {
120 - return null;
121 95 }
96 +
97 + $strings = $this->get_subscription_notice_message( $m, $context, $unsubscribe_link ?? '' );
98 + if ( '' === $strings ) {
99 + return;
100 + }
122 101 ?>
123 102 <div class="notice notice-success" style="position:relative;">
124 103 <p><?php echo wp_kses_post( $strings ); ?></p>
125 104 <a href="<?php echo esc_url_raw( get_edit_profile_url() ); ?>" class="notice-dismiss"
@@ -130,15 +109,96 @@
130 109 <?php
131 110 }
132 111
133 112 /**
134 - * Trigger report check signals.
113 + * Show public confirmation notice for out-of-house recipients.
114 + *
115 + * @return void
135 116 */
136 - public function report_sender() {
137 - $this->service->maybe_dispatch_report();
117 + public function show_public_subscription_notice(): void {
118 + $context = defender_get_data_from_request( 'defender_subscription', 'g' );
119 + if ( 'confirmed' !== $context && 'unsubscribe' !== $context ) {
120 + return;
121 + }
122 +
123 + $slug = defender_get_data_from_request( 'slug', 'g' );
124 + $hash = defender_get_data_from_request( 'hash', 'g' );
125 + if ( ! is_string( $slug ) || '' === trim( $slug ) ) {
126 + return;
127 + }
128 + if ( 'confirmed' === $context && ( ! is_string( $hash ) || '' === trim( $hash ) ) ) {
129 + return;
130 + }
131 +
132 + $m = $this->service->find_module_by_slug( sanitize_key( $slug ) );
133 + if ( ! is_object( $m ) ) {
134 + return;
135 + }
136 +
137 + $unsubscribe_link = '';
138 + if ( 'confirmed' === $context ) {
139 + $unsubscribe_link = add_query_arg(
140 + array(
141 + 'action' => self::SLUG_UNSUBSCRIBE,
142 + 'hash' => sanitize_text_field( $hash ),
143 + 'slug' => $m->slug,
144 + ),
145 + admin_url( 'admin-ajax.php' )
146 + );
147 + }
148 +
149 + $message = $this->get_subscription_notice_message(
150 + $m,
151 + 'confirmed' === $context ? 'subscribed' : 'unsubscribe',
152 + $unsubscribe_link
153 + );
154 + if ( '' === $message ) {
155 + return;
156 + }
157 + ?>
158 + <div class="wpdef-public-subscription-notice" style="position:fixed;right:20px;bottom:20px;z-index:99999;max-width:420px;padding:14px 18px;border-left:4px solid #00a32a;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.18);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#1d2327;">
159 + <?php echo wp_kses_post( $message ); ?>
160 + </div>
161 + <?php
138 162 }
139 163
140 164 /**
165 + * Build subscription notice message.
166 + *
167 + * @param object $module Notification module.
168 + * @param string $context Notice context.
169 + * @param string $unsubscribe_link Unsubscribe URL.
170 + * @return string
171 + */
172 + private function get_subscription_notice_message( object $module, string $context, string $unsubscribe_link = '' ): string {
173 + if ( 'subscribed' === $context ) {
174 + if ( '' === trim( $unsubscribe_link ) ) {
175 + return '';
176 + }
177 +
178 + return sprintf(
179 + /* translators: 1. Module title. 2. Unsubscribe link. */
180 + esc_html__(
181 + 'You are now subscribed to receive %1$s. Made a mistake? %2$s',
182 + 'defender-security'
183 + ),
184 + '<strong>' . esc_html( $module->title ) . '</strong>',
185 + '<a href="' . esc_url( $unsubscribe_link ) . '" style="text-decoration: none">' . esc_html__( 'Unsubscribe', 'defender-security' ) . '</a>'
186 + );
187 + }
188 +
189 + if ( 'unsubscribe' === $context ) {
190 + return sprintf(
191 + /* translators: %s: Module title. */
192 + esc_html__( 'You are now unsubscribed from %s.', 'defender-security' ),
193 + '<strong>' . esc_html( $module->title ) . '</strong>'
194 + );
195 + }
196 +
197 + return '';
198 + }
199 +
200 + /**
141 201 * Renders the main view for this page.
142 202 */
143 203 public function main_view() {
144 204 $this->render( 'main' );
@@ -185,8 +245,77 @@
185 245 }
186 246 }
187 247
188 248 /**
249 + * Updates a WordPress user profile for an in-house recipient.
250 + *
251 + * @param Request $request The request object.
252 + *
253 + * @return Response The response object.
254 + * @defender_route
255 + */
256 + public function update_user_profile( Request $request ): Response {
257 + $data = $request->get_data(
258 + array(
259 + 'id' => array(
260 + 'type' => 'int',
261 + 'sanitize' => 'sanitize_text_field',
262 + ),
263 + 'first_name' => array(
264 + 'type' => 'string',
265 + 'sanitize' => 'sanitize_text_field',
266 + ),
267 + 'last_name' => array(
268 + 'type' => 'string',
269 + 'sanitize' => 'sanitize_text_field',
270 + ),
271 + 'display_name' => array(
272 + 'type' => 'string',
273 + 'sanitize' => 'sanitize_text_field',
274 + ),
275 + )
276 + );
277 +
278 + $user_id = absint( $data['id'] ?? 0 );
279 + $display_name = trim( (string) ( $data['display_name'] ?? '' ) );
280 +
281 + if ( 0 === $user_id || '' === $display_name ) {
282 + return new Response( false, array( 'message' => esc_html__( 'Invalid user data.', 'defender-security' ) ) );
283 + }
284 +
285 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
286 + return new Response( false, array( 'message' => esc_html__( 'You are not allowed to edit this user.', 'defender-security' ) ) );
287 + }
288 +
289 + if ( ! get_user_by( 'id', $user_id ) ) {
290 + return new Response( false, array( 'message' => esc_html__( 'User not found.', 'defender-security' ) ) );
291 + }
292 +
293 + $result = wp_update_user(
294 + array(
295 + 'ID' => $user_id,
296 + 'first_name' => trim( (string) ( $data['first_name'] ?? '' ) ),
297 + 'last_name' => trim( (string) ( $data['last_name'] ?? '' ) ),
298 + 'display_name' => $display_name,
299 + )
300 + );
301 +
302 + if ( is_wp_error( $result ) ) {
303 + return new Response( false, array( 'message' => $result->get_error_message() ) );
304 + }
305 +
306 + return new Response(
307 + true,
308 + array(
309 + 'id' => $user_id,
310 + 'first_name' => trim( (string) ( $data['first_name'] ?? '' ) ),
311 + 'last_name' => trim( (string) ( $data['last_name'] ?? '' ) ),
312 + 'display_name' => $display_name,
313 + )
314 + );
315 + }
316 +
317 + /**
189 318 * Unsubscribe process.
190 319 */
191 320 public function unsubscribe_and_send_email() {
192 321 $slug = HTTP::get( 'slug', '' );
@@ -191,9 +320,9 @@
191 320 public function unsubscribe_and_send_email() {
192 321 $slug = HTTP::get( 'slug', '' );
193 322 $hash = HTTP::get( 'hash', '' );
194 323 $slug = sanitize_text_field( $slug );
195 - if ( empty( $slug ) || empty( $hash ) ) {
324 + if ( ! is_string( $slug ) || '' === trim( $slug ) || ! is_string( $hash ) || '' === trim( $hash ) ) {
196 325 wp_die( esc_html__( 'You shall not pass.', 'defender-security' ) );
197 326 }
198 327 $m = $this->service->find_module_by_slug( $slug );
199 328 if ( ! is_object( $m ) ) {
@@ -198,9 +327,10 @@
198 327 $m = $this->service->find_module_by_slug( $slug );
199 328 if ( ! is_object( $m ) ) {
200 329 wp_die( esc_html__( 'You shall not pass.', 'defender-security' ) );
201 330 }
202 - $inhouse = false;
331 + $inhouse = false;
332 + $processed = false;
203 333 foreach ( $m->in_house_recipients as &$recipient ) {
204 334 $email = $recipient['email'];
205 335 if ( hash_equals( $hash, hash( 'sha256', $email . AUTH_SALT ) ) ) {
206 336 // We skip even an un-logged user, because the admin can change the user's access without notice.
@@ -211,8 +341,9 @@
211 341 $inhouse = true;
212 342 }
213 343 $recipient['status'] = Model_Notification::USER_SUBSCRIBE_CANCELED;
214 344 $m->save();
345 + $processed = true;
215 346 // Send email.
216 347 $this->service->send_unsubscribe_email( $m, $email, $inhouse, $recipient['name'] );
217 348 break;
218 349 }
@@ -224,8 +355,9 @@
224 355 $email = $recipient['email'];
225 356 if ( hash_equals( $hash, hash( 'sha256', $email . AUTH_SALT ) ) ) {
226 357 $recipient['status'] = Model_Notification::USER_SUBSCRIBE_CANCELED;
227 358 $m->save();
359 + $processed = true;
228 360 $this->service->send_unsubscribe_email( $m, $email, $inhouse, $recipient['name'] );
229 361 }
230 362 }
231 363 }
@@ -238,8 +370,18 @@
238 370 ),
239 371 get_edit_profile_url()
240 372 )
241 373 );
374 + } elseif ( $processed ) {
375 + wp_safe_redirect(
376 + add_query_arg(
377 + array(
378 + 'defender_subscription' => 'unsubscribe',
379 + 'slug' => $slug,
380 + ),
381 + get_home_url()
382 + )
383 + );
242 384 } else {
243 385 wp_safe_redirect( get_home_url() );
244 386 }
245 387 exit;
@@ -255,9 +397,9 @@
255 397 * @throws Exception Emits Exception in case of an error.
256 398 */
257 399 public function save( Request $request ): Response {
258 400 $raw_data = $request->get_data();
259 - if ( empty( $raw_data['slug'] ) ) {
401 + if ( ! isset( $raw_data['slug'] ) || ! is_string( $raw_data['slug'] ) || '' === trim( $raw_data['slug'] ) ) {
260 402 return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'defender-security' ) ) );
261 403 }
262 404 $slug = sanitize_textarea_field( $raw_data['slug'] );
263 405 $model = $this->service->find_module_by_slug( $slug );
@@ -269,9 +411,14 @@
269 411 // Check config-values.
270 412 $data['configs'] = $model->type_casting( $data['configs'] );
271 413
272 414 $model->import( $data );
273 - $model->status = Model_Notification::STATUS_ACTIVE;
415 + // Ensure the model is activated. The save route is called when the user explicitly
416 + // saves settings, so the notification must be active after saving. If the imported
417 + // data already carries an active status this is a no-op; otherwise we activate it.
418 + if ( \WP_Defender\Model\Notification::STATUS_ACTIVE !== $model->status ) {
419 + $model->status = \WP_Defender\Model\Notification::STATUS_ACTIVE;
420 + }
274 421 if ( $model->validate() ) {
275 422 if ( 0 === $model->last_sent ) {
276 423 // This means that the notification or report never sent, we will use the moment that it get activate.
277 424 $model->last_sent = time();
@@ -318,20 +465,15 @@
318 465 */
319 466 public function save_bulk( Request $request ): Response {
320 467 $data = $request->get_data(
321 468 array(
322 - 'reports' => array(
469 + 'items' => array(
323 470 'type' => 'array',
324 471 'sanitize' => 'sanitize_textarea_field',
325 472 ),
326 - 'notifications' => array(
327 - 'type' => 'array',
328 - 'sanitize' => 'sanitize_textarea_field',
329 - ),
330 473 )
331 474 );
332 - $this->save_reports( $data['reports'] );
333 - $this->save_notifications( $data['notifications'] );
475 + $this->save_items( $data['items'] );
334 476 Config_Hub_Helper::set_clear_active_flag();
335 477
336 478 return new Response(
337 479 true,
@@ -347,89 +489,102 @@
347 489 );
348 490 }
349 491
350 492 /**
351 - * Process bulk reports saving.
493 + * Sync the shared reports schedule to notification models that persist their own next-run timestamp.
352 494 *
353 - * @param array $data Data to save.
495 + * @param Request $request The request object.
354 496 *
355 - * @throws Exception Emits Exception in case of an error.
497 + * @defender_route
498 + * @return Response
356 499 */
357 - private function save_reports( $data ) {
358 - foreach ( $data['configs'] as $datum ) {
359 - $slug = $datum['slug'];
360 - $model = $this->service->find_module_by_slug( $slug );
361 - if ( ! is_object( $model ) ) {
362 - continue;
363 - }
500 + public function sync_report_schedule( Request $request ): Response {
501 + $data = $request->get_data(
502 + array(
503 + 'frequency' => array(
504 + 'type' => 'string',
505 + 'sanitize' => 'sanitize_text_field',
506 + ),
507 + 'day' => array(
508 + 'type' => 'string',
509 + 'sanitize' => 'sanitize_text_field',
510 + ),
511 + 'day_n' => array(
512 + 'type' => 'int',
513 + 'sanitize' => 'sanitize_text_field',
514 + ),
515 + 'time' => array(
516 + 'type' => 'string',
517 + 'sanitize' => 'sanitize_text_field',
518 + ),
519 + )
520 + );
364 521
365 - $import = array(
366 - // Saving after Bulk-Update must always change the status to Active.
367 - 'status' => Model_Notification::STATUS_ACTIVE,
368 - 'configs' => $model->type_casting( $datum ),
369 - 'in_house_recipients' => $data['in_house_recipients'],
370 - 'out_house_recipients' => $data['out_house_recipients'],
371 - );
372 - // since 2.7.0.
373 - if ( Malware_Report::SLUG !== $slug ) {
374 - $import['frequency'] = $data['frequency'];
375 - $import['day_n'] = $data['day_n'];
376 - $import['day'] = $data['day'];
377 - $import['time'] = $data['time'];
378 - }
379 - foreach ( $import['out_house_recipients'] as $key => $val ) {
380 - if ( ! filter_var( $val['email'], FILTER_VALIDATE_EMAIL ) ) {
381 - unset( $import['out_house_recipients'][ $key ] );
382 - }
383 - }
384 - $model->import( $import );
385 - if ( $model->validate() ) {
386 - if ( 0 === $model->last_sent ) {
387 - $model->last_sent = time();
388 - }
389 - $model->save();
390 - $this->service->send_subscription_confirm_email( $model );
391 - // Track.
392 - if ( $this->is_tracking_active() ) {
393 - $track_data = array(
394 - 'Notification type' => $model->title,
395 - 'Notification schedule' => 'tweak-reminder' === $slug
396 - ? ucfirst( $data['configs']['reminder'] )
397 - : ucfirst( $data['frequency'] ),
398 - );
399 - $this->track_feature( 'def_notification_activated', $track_data );
400 - }
401 - }
522 + $report_classes = array(
523 + Firewall_Report::class,
524 + Audit_Report::class,
525 + Malware_Report::class,
526 + Tweak_Reminder::class,
527 + );
528 +
529 + foreach ( $report_classes as $report_class ) {
530 + $model = wd_di()->get( $report_class );
531 + $model->frequency = $data['frequency'];
532 + $model->day = $data['day'];
533 + $model->day_n = (int) $data['day_n'];
534 + $model->time = $data['time'];
535 +
536 + $model->save();
402 537 }
538 +
539 + Config_Hub_Helper::set_clear_active_flag();
540 +
541 + return new Response( true, $this->data_frontend() );
403 542 }
404 543
405 544 /**
406 - * Saves the notifications based on the provided data.
545 + * Process bulk saving of reports or notifications.
407 546 *
408 - * @param array $data The data containing the configurations for the notifications.
547 + * @param array $data Data to save.
409 548 *
410 - * @return void
411 549 * @throws Exception Emits Exception in case of an error.
412 550 */
413 - private function save_notifications( $data ) {
414 - foreach ( $data['configs'] as $datum ) {
551 + private function save_items( array $data ): void {
552 + $in_house_recipients = $data['in_house_recipients'] ?? array();
553 + $out_house_recipients = $data['out_house_recipients'] ?? array();
554 +
555 + foreach ( $data as $datum ) {
556 + if ( ! is_array( $datum ) || ! isset( $datum['slug'] ) ) {
557 + continue;
558 + }
559 +
415 560 $slug = $datum['slug'];
416 561 $model = $this->service->find_module_by_slug( $slug );
417 562 if ( ! is_object( $model ) ) {
418 563 continue;
419 564 }
565 +
420 566 $import = array(
421 - // Saving after Bulk-Update must always change the status to Active.
422 - 'status' => Model_Notification::STATUS_ACTIVE,
567 + 'status' => $model->status,
423 568 'configs' => $model->type_casting( $datum ),
424 - 'in_house_recipients' => $data['in_house_recipients'],
425 - 'out_house_recipients' => $data['out_house_recipients'],
569 + 'in_house_recipients' => $datum['in_house_recipients'] ?? $in_house_recipients,
570 + 'out_house_recipients' => $datum['out_house_recipients'] ?? $out_house_recipients,
426 571 );
572 + // since 2.7.0.
573 + if ( isset( $datum['is_report'] ) && (bool) $datum['is_report'] && Malware_Report::SLUG !== $slug ) {
574 + $import['frequency'] = $datum['frequency'] ?? $model->frequency;
575 + $import['day_n'] = (int) ( $datum['day_n'] ?? $model->day_n );
576 + $import['day'] = $datum['day'] ?? $model->day;
577 + $import['time'] = $datum['time'] ?? $model->time;
578 + }
427 579 foreach ( $import['out_house_recipients'] as $key => $val ) {
428 580 if ( ! filter_var( $val['email'], FILTER_VALIDATE_EMAIL ) ) {
429 581 unset( $import['out_house_recipients'][ $key ] );
430 582 }
431 583 }
584 + $prev_in_house_emails = array_column( $model->in_house_recipients, null, 'email' );
585 + $prev_out_house_emails = array_column( $model->out_house_recipients, null, 'email' );
586 +
432 587 $model->import( $import );
433 588 if ( $model->validate() ) {
434 589 if ( 0 === $model->last_sent ) {
435 590 $model->last_sent = time();
@@ -434,12 +589,30 @@
434 589 if ( 0 === $model->last_sent ) {
435 590 $model->last_sent = time();
436 591 }
437 592 $model->save();
438 - $this->service->send_subscription_confirm_email( $model );
593 +
594 + $new_in_house_emails = array_column( $import['in_house_recipients'], 'email' );
595 + $new_out_house_emails = array_column( $import['out_house_recipients'], 'email' );
596 + foreach ( $prev_in_house_emails as $email => $recipient ) {
597 + if ( ! in_array( $email, $new_in_house_emails, true ) ) {
598 + $this->service->send_unsubscribe_email( $model, $email, true, $recipient['name'] ?? '' );
599 + }
600 + }
601 + foreach ( $prev_out_house_emails as $email => $recipient ) {
602 + if ( ! in_array( $email, $new_out_house_emails, true ) ) {
603 + $this->service->send_unsubscribe_email( $model, $email, false, $recipient['name'] ?? '' );
604 + }
605 + }
439 606 // Track.
440 607 if ( $this->is_tracking_active() ) {
441 - $this->track_feature( 'def_notification_activated', array( 'Notification type' => $model->title ) );
608 + $track_data = array( 'Notification type' => $model->title );
609 + if ( isset( $datum['is_report'] ) && (bool) $datum['is_report'] ) {
610 + $track_data['Notification schedule'] = 'tweak-reminder' === $slug
611 + ? ucfirst( $datum['reminder'] ?? '' )
612 + : ucfirst( $datum['frequency'] ?? '' );
613 + }
614 + $this->track_feature( 'def_notification_activated', $track_data );
442 615 }
443 616 }
444 617 }
445 618 }
@@ -462,9 +635,9 @@
462 635 ),
463 636 )
464 637 );
465 638 $slugs = $data['slugs'];
466 - if ( empty( $slugs ) ) {
639 + if ( ! is_array( $slugs ) || array() === $slugs ) {
467 640 return new Response( false, array() );
468 641 }
469 642
470 643 foreach ( $slugs as $slug ) {
@@ -507,9 +680,9 @@
507 680 ),
508 681 )
509 682 );
510 683 $slugs = $data['slugs'];
511 - if ( empty( $slugs ) ) {
684 + if ( ! is_array( $slugs ) || array() === $slugs ) {
512 685 return new Response( false, array() );
513 686 }
514 687
515 688 foreach ( $slugs as $slug ) {
@@ -567,77 +740,8 @@
567 740 );
568 741 }
569 742
570 743 /**
571 - * This is a receiver to process subscribe confirmation from email.
572 - */
573 - public function verify_subscriber() {
574 - $hash = HTTP::get( 'hash', '' );
575 - $slug = HTTP::get( 'uid', '' );
576 - $inhouse = HTTP::get( 'inhouse', 0 );
577 - if ( $inhouse && ! is_user_logged_in() ) {
578 - // This is in-house, so we need to redirect.
579 - auth_redirect();
580 - }
581 - if ( empty( $hash ) || empty( $slug ) ) {
582 - wp_die( esc_html__( 'You shall not pass.', 'defender-security' ) );
583 - }
584 - $m = $this->service->find_module_by_slug( $slug );
585 - if ( ! is_object( $m ) ) {
586 - wp_die( esc_html__( 'You shall not pass.', 'defender-security' ) );
587 - }
588 - if ( $inhouse ) {
589 - $processed = false;
590 - foreach ( $m->in_house_recipients as &$recipient ) {
591 - if ( Model_Notification::USER_SUBSCRIBED === $recipient['status'] ) {
592 - continue;
593 - }
594 -
595 - $email = $recipient['email'];
596 - $name = $recipient['name'];
597 - if ( hash_equals( $hash, hash( 'sha256', $email . AUTH_SALT ) )
598 - && $email === $this->get_current_user_email() ) {
599 - $recipient['status'] = Model_Notification::USER_SUBSCRIBED;
600 - $this->service->send_subscribed_email( $email, $m, $name );
601 - $processed = true;
602 - }
603 - }
604 - } else {
605 - foreach ( $m->out_house_recipients as &$recipient ) {
606 - if ( Model_Notification::USER_SUBSCRIBED === $recipient['status'] ) {
607 - continue;
608 - }
609 -
610 - $email = $recipient['email'];
611 - $name = $recipient['name'];
612 - if ( hash_equals( $hash, hash( 'sha256', $email . AUTH_SALT ) ) ) {
613 - $recipient['status'] = Model_Notification::USER_SUBSCRIBED;
614 - $this->service->send_subscribed_email( $email, $m, $name );
615 - }
616 - }
617 - }
618 - $m->save();
619 - if ( $inhouse ) {
620 - if ( $processed ) {
621 - wp_safe_redirect(
622 - add_query_arg(
623 - array(
624 - 'slug' => $m->slug,
625 - 'context' => 'subscribed',
626 - ),
627 - get_edit_profile_url()
628 - )
629 - );
630 - } else {
631 - wp_safe_redirect( home_url() );
632 - }
633 - } else {
634 - wp_safe_redirect( home_url() );
635 - }
636 - exit;
637 - }
638 -
639 - /**
640 744 * Enqueues scripts and styles for this page.
641 745 * Only enqueues assets if the page is active.
642 746 */
643 747 public function enqueue_assets() {
@@ -643,22 +747,8 @@
643 747 public function enqueue_assets() {
644 748 if ( ! $this->is_page_active() ) {
645 749 return;
646 750 }
647 - wp_localize_script(
648 - 'def-notification',
649 - 'notification',
650 - array_merge( $this->data_frontend(), $this->dump_routes_and_nonces() )
651 - );
652 - wp_enqueue_script( 'def-notification' );
653 - $this->enqueue_main_assets();
654 - wp_enqueue_style(
655 - 'def-select2',
656 - defender_asset_url( '/assets/css/select2.min.css' ),
657 - array(),
658 - DEFENDER_VERSION,
659 - true
660 - );
661 751 }
662 752
663 753 /**
664 754 * An endpoint for fetching users pool.
@@ -787,27 +877,17 @@
787 877 *
788 878 * @return array An array of data for the frontend.
789 879 */
790 880 public function data_frontend(): array {
791 - return array(
792 - 'notifications' => $this->service->get_modules(),
793 - 'inactive_notifications' => $this->service->get_inactive_modules(),
794 - 'active_count' => $this->service->count_active(),
795 - 'next_run' => $this->service->get_next_run(),
796 - 'misc' => array(
797 - 'days_of_week' => $this->get_days_of_week(),
798 - 'times_of_day' => $this->get_times(),
799 - 'timezone_text' => sprintf(
800 - /* translators: 1. Timezone. 2. Time. */
801 - esc_html__(
802 - 'Your timezone is set to %1$s, so your current time is %2$s.',
803 - 'defender-security'
804 - ),
805 - '<strong>' . wp_timezone_string() . '</strong>',
806 - '<strong>' . wp_date( 'H:i', time() ) . '</strong>'
807 - ),
808 - 'default_recipient' => $this->get_default_recipient(),
881 + return array_merge(
882 + array(
883 + 'alert_model' => $this->service->get_alert_model(),
884 + 'reports_model' => $this->service->get_reports_model(),
885 + 'active_count' => $this->service->count_active(),
886 + 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
887 + 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
809 888 ),
889 + $this->dump_routes_and_nonces()
810 890 );
811 891 }
812 892
813 893 /**
@@ -893,13 +973,13 @@
893 973 'email' => $data['email'],
894 974 'name' => $data['name'],
895 975 );
896 976
897 - if ( ! empty( $data['id'] ) ) {
977 + if ( isset( $data['id'] ) && is_int( $data['id'] ) && 0 < $data['id'] ) {
898 978 $subscriber['id'] = $data['id'];
899 979 }
900 980 // Resend invite email now.
901 - $sent = $this->service->send_email( $subscriber, $model );
981 + $sent = $this->service->send_email( $subscriber, $model->export() );
902 982
903 983 if ( $sent ) {
904 984 return new Response( true, array( 'message' => esc_html__( 'Invitation sent successfully.', 'defender-security' ) ) );
905 985 }
@@ -920,6 +1000,50 @@
920 1000 public function get_user_roles() {
921 1001 $user_roles = $this->service->get_user_roles();
922 1002
923 1003 wp_send_json_success( $user_roles );
1004 + }
1005 +
1006 + /**
1007 + * Sends a single invite email listing all notification modules the recipient was added to.
1008 + *
1009 + * @param Request $request Request with 'email' and 'name'.
1010 + * @defender_route
1011 + * @return Response
1012 + */
1013 + public function send_invite_all_email( Request $request ): Response {
1014 + $data = $request->get_data(
1015 + array(
1016 + 'email' => array(
1017 + 'type' => 'string',
1018 + 'sanitize' => 'sanitize_email',
1019 + ),
1020 + 'name' => array(
1021 + 'type' => 'string',
1022 + 'sanitize' => 'sanitize_text_field',
1023 + ),
1024 + 'id' => array( 'type' => 'integer' ),
1025 + )
1026 + );
1027 + $email = $data['email'] ?? '';
1028 + $name = $data['name'] ?? '';
1029 + $subscriber = array(
1030 + 'email' => $email,
1031 + 'name' => $name,
1032 + );
1033 + if ( isset( $data['id'] ) && is_int( $data['id'] ) && 0 < $data['id'] ) {
1034 + $subscriber['id'] = $data['id'];
1035 + }
1036 + $active_slugs = array_column( $this->service->get_modules(), 'slug' );
1037 + $modules = array_values(
1038 + array_filter(
1039 + $this->service->get_modules_as_objects(),
1040 + static function ( $module ) use ( $active_slugs ) {
1041 + return in_array( $module->slug, $active_slugs, true );
1042 + }
1043 + )
1044 + );
1045 + $sent = $this->service->send_invite_all_email( $subscriber, $modules );
1046 +
1047 + return new Response( $sent, array() );
924 1048 }
925 1049 }