PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.3
Defender Security – Malware Scanner, Login Security & Firewall v6.2.3
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 3.1.1 All 140 releases
defender-security / src / model / class-notification.php

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

521 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles interactions with the database table for notifications.
4 *
5 * @package WP_Defender\Model
6 */
7
8 namespace WP_Defender\Model;
9
10 use WP_User;
11 use DateTime;
12 use Exception;
13 use DateInterval;
14 use Calotes\Model\Setting;
15 use WP_Defender\Traits\User;
16 use WP_Defender\Traits\Formats;
17 use WP_Defender\Model\Notification\Malware_Report;
18 use function wp_timezone;
19
20 /**
21 * Model for the notifications table.
22 */
23 abstract class Notification extends Setting {
24
25 use User;
26 use Formats;
27
28 public const STATUS_DISABLED = 'disabled', STATUS_ACTIVE = 'enabled';
29 public const USER_SUBSCRIBED = 'subscribed', USER_SUBSCRIBE_WAITING = 'waiting', USER_SUBSCRIBE_CANCELED = 'cancelled', USER_SUBSCRIBE_NA = 'na';
30
31 /**
32 * Notification title.
33 *
34 * @var string
35 * @defender_property
36 * @sanitize_text_field
37 */
38 public $title;
39
40 /**
41 * Unique ID for this notification.
42 *
43 * @var string
44 * @defender_property
45 * @sanitize_text_field
46 */
47 public $slug;
48
49 /**
50 * Table column for description.
51 *
52 * @var string
53 * @defender_property
54 */
55 public $description;
56
57 /**
58 * This is the status of the current notification, can be active or disabled.
59 *
60 * @var string
61 * @defender_property
62 * @sanitize_text_field
63 */
64 public $status;
65
66 /**
67 * This is notification or report.
68 *
69 * @var string
70 * @defender_property
71 * @sanitize_text_field
72 */
73 public $type;
74
75 /**
76 * Report sending frequency. Only when $type is report.
77 *
78 * @var string
79 * @defender_property
80 * @sanitize_text_field
81 */
82 public $frequency;
83
84 /**
85 * Report sending day. Only when $type is report.
86 *
87 * @var string
88 * @defender_property
89 * @sanitize_text_field
90 */
91 public $day;
92
93 /**
94 * This is for when user select report as monthly, we will have the day number, instead of text.
95 *
96 * @var int
97 * @defender_property
98 */
99 public int $day_n = 1;
100
101 /**
102 * Same as $day.
103 *
104 * @var string
105 * @defender_property
106 * @sanitize_text_field
107 */
108 public $time;
109
110 /**
111 * Holding a list of site user ids, so when sending, we send though this list.
112 *
113 * @var array
114 * @defender_property
115 * @sanitize_text_field
116 */
117 public $in_house_recipients = array();
118
119 /**
120 * For additional users, this should contain a list of email and name.
121 *
122 * @var array
123 * @defender_property
124 * @sanitize_text_field
125 */
126 public $out_house_recipients = array();
127
128 /**
129 * This when we want to run the report/notification without any email sending.
130 *
131 * @var bool
132 * @defender_property
133 */
134 public $dry_run = false;
135
136 /**
137 * This is contains the meta settings of this notification.
138 *
139 * @var array
140 * @defender_property
141 */
142 public $configs = array();
143
144 /**
145 * Tracking.
146 *
147 * @var int
148 * @defender_property
149 */
150 public $last_sent = 0;
151
152 /**
153 * Table column for estimated timestamp.
154 *
155 * @var int
156 * @defender_property
157 */
158 public $est_timestamp;
159
160 /**
161 * Return the default user, we will use this if there is no user in the notification.
162 *
163 * @return array
164 */
165 protected function get_default_user(): array {
166 if ( ! is_user_logged_in() ) {
167 return array();
168 }
169
170 $user_id = get_current_user_id();
171
172 $email = $this->get_current_user_email( $user_id );
173
174 return array(
175 $email => array(
176 'name' => $this->get_user_display( $user_id ),
177 'id' => $user_id,
178 'email' => $email,
179 'role' => $this->get_current_user_role( $user_id ),
180 'avatar' => get_avatar_url( $email ),
181 'status' => self::USER_SUBSCRIBED,
182 ),
183 );
184 }
185
186 /**
187 * Check if the current moment is right for sending.
188 *
189 * @return bool
190 */
191 public function maybe_send() {
192 // @since 2.7.0 We can remove 'dry_run'-condition in the next version.
193 if ( true === $this->dry_run ) {
194 // No send, but need to track as sent, so we can requeue it.
195 if ( 'report' === $this->type ) {
196 $this->last_sent = $this->est_timestamp;
197
198 if ( $this->get_next_run() instanceof DateTime ) {
199 $this->est_timestamp = $this->get_next_run()->getTimestamp();
200 }
201
202 $this->save();
203 }
204
205 return false;
206 }
207
208 if ( ! $this->check_active_status() ) {
209 return false;
210 }
211
212 if ( 'notification' === $this->type ) {
213 return true;
214 }
215
216 if ( 0 === $this->last_sent ) {
217 return false;
218 }
219
220 $now = new DateTime( 'now', wp_timezone() );
221 $time = apply_filters( 'defender_current_time_for_report', $now );
222 // Testing.
223 if ( defined( 'WP_DEFENDER_TESTING' ) && true === constant( 'WP_DEFENDER_TESTING' ) ) {
224 return true;
225 }
226
227 return $time->getTimestamp() >= $this->est_timestamp;
228 }
229
230 /**
231 * Calculates the next run date based on the current date and the frequency of the notification.
232 *
233 * @return DateTime|false The next run date or false if the notification type is 'notification' or the status is
234 * not active.
235 * @throws Exception If an error occurs while creating the DateTime objects or modifying the date.
236 */
237 public function get_next_run() {
238 if ( 'notification' === $this->type ) {
239 return false;
240 }
241 if ( ! $this->check_active_status() ) {
242 return false;
243 }
244
245 // Create estimate object.
246 $est = new DateTime( 'now', wp_timezone() );
247 if ( $this->last_sent > 0 ) {
248 // set the timestamp of previous.
249 $est->setTimestamp( $this->last_sent );
250 }
251
252 // Est should be set as the last send. Create now timestamp.
253 $now = new DateTime( 'now', wp_timezone() );
254 $interval = DateInterval::createFromDateString( (string) $est->getOffset() . 'seconds' );
255 $time_parts = array_pad( explode( ':', (string) $this->time ), 2, 0 );
256 [ $hour, $min ] = $time_parts;
257 $hour = (int) $hour;
258 $min = (int) $min;
259 switch ( $this->frequency ) {
260 case 'daily':
261 // Set the time.
262 $est->add( $interval );
263 $est->setTime( $hour, $min, 0 );
264 // Convert to current timezone.
265 while ( $est->getTimestamp() < $now->getTimestamp() ) {
266 $est->add( new DateInterval( 'P1D' ) );
267 $est->setTime( $hour, $min, 0 );
268 }
269 break;
270 case 'weekly':
271 if ( '' === $this->day || null === $this->day ) {
272 break;
273 }
274 $est->modify( 'this ' . $this->day );
275 $est->add( $interval );
276 $est->setTime( $hour, $min, 0 );
277 while ( $est->getTimestamp() < $now->getTimestamp() ) {
278 $est->modify( 'next ' . $this->day );
279 $est->setTime( $hour, $min, 0 );
280 }
281 break;
282 case 'monthly':
283 // We will need to check if the date is passed today, if not, use this, if yes, then queue for next month.
284 $est->setDate( (int) $est->format( 'Y' ), (int) $est->format( 'm' ), 1 );
285 // Clamp day_n to a valid day in the current month.
286 $day_n = max( 1, min( $this->day_n, (int) $est->format( 't' ) ) );
287 $est->add( new DateInterval( 'P' . ( $day_n - 1 ) . 'D' ) );
288 $est->setTime( $hour, $min, 0 );
289 while ( $est->getTimestamp() < $now->getTimestamp() ) {
290 // Already over, move to first day of next month.
291 $est->modify( 'first day of next month' );
292 // Re-clamp for the new month's length.
293 $day_n = max( 1, min( $this->day_n, (int) $est->format( 't' ) ) );
294 $est->add( new DateInterval( 'P' . ( $day_n - 1 ) . 'D' ) );
295 $est->setTime( $hour, $min, 0 );
296 }
297 break;
298 }
299
300 return $est;
301 }
302
303 /**
304 * We have multiple issues where the email keep sending for no reason, this for debugging later.
305 *
306 * @param string $email Email address.
307 */
308 public function save_log( $email ): void {
309 $track = new Email_Track();
310 $track->timestamp = time();
311 $track->source = $this->slug;
312 $track->to = $email;
313 $track->save();
314 }
315
316 /**
317 * Checks the active status of the notification.
318 *
319 * @return bool Returns true if the notification is active, false otherwise.
320 */
321 public function check_active_status(): bool {
322 // Exception after migrating Scheduled scanning to Scan settings.
323 if ( Malware_Report::SLUG === $this->slug
324 && ( new \WP_Defender\Model\Setting\Scan() )->is_enabled_scheduled_scanning()
325 ) {
326 return true;
327 }
328
329 return self::STATUS_ACTIVE === $this->status;
330 }
331
332 /**
333 * This will return the interval at string.
334 *
335 * @return string
336 * @throws Exception Emits Exception in case of an error.
337 */
338 public function to_string(): string {
339 if ( ! $this->check_active_status() ) {
340 return '-';
341 }
342 $date = new DateTime( 'now', wp_timezone() );
343 $date->setTimestamp( $this->est_timestamp );
344 switch ( $this->frequency ) {
345 case 'daily':
346 return sprintf(
347 /* translators: 1: Notification sending frequency, 2: Time of a day. */
348 esc_html__( '%1$s at %2$s', 'defender-security' ),
349 ucfirst( $this->frequency ),
350 $date->format( 'h:i A' )
351 );
352 case 'weekly':
353 return sprintf(
354 /* translators: 1: Notification sending frequency, 2: Day of the week, 3: Time of a day. */
355 esc_html__( '%1$s on %2$s at %3$s', 'defender-security' ),
356 ucfirst( $this->frequency ),
357 ucfirst( $this->day ),
358 $date->format( 'h:i A' )
359 );
360 case 'monthly':
361 default:
362 return sprintf(
363 /* translators: 1: Notification sending frequency, 2: Day of the month, 3: Time of a day. */
364 esc_html__( '%1$s/%2$d, %3$s', 'defender-security' ),
365 ucfirst( $this->frequency ),
366 $this->day_n,
367 $date->format( 'h:i A' )
368 );
369 }
370 }
371
372 /**
373 * Returns the next run date as a string, based on the type of notification and the active status.
374 *
375 * @param bool $for_hub Whether the next run date is for the hub. Default is false.
376 *
377 * @return bool|string Returns false if the notification type is 'notification' or the active status is false.
378 * Returns the next run date as a string in the format specified by the date_format and
379 * time_format options. Returns 'Never' if the active status is false.
380 * @throws Exception If an error occurs while creating the DateTime objects or modifying the date.
381 */
382 public function get_next_run_as_string( bool $for_hub = false ) {
383 if ( 'notification' === $this->type ) {
384 return $for_hub ? false : esc_html__( 'Never', 'defender-security' );
385 }
386
387 $est_timestamp = ! is_int( $this->est_timestamp ) ? (int) $this->est_timestamp : $this->est_timestamp;
388 if ( $for_hub ) {
389 return $this->check_active_status()
390 ? $this->persistent_hub_datetime_format( $est_timestamp )
391 : false;
392 } elseif ( $this->check_active_status() ) {
393 $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
394 $date = new DateTime( 'now', wp_timezone() );
395 $date->setTimestamp( $est_timestamp );
396
397 return $date->format( $format );
398 } else {
399 return esc_html__( 'Never', 'defender-security' );
400 }
401 }
402
403 /**
404 * We still need to validate the out house recipients email.
405 */
406 protected function after_validate(): void {
407 foreach ( $this->out_house_recipients as $recipient ) {
408 $recipient['email'] = trim( $recipient['email'] );
409 if ( '' === $recipient['email'] ) {
410 continue;
411 }
412 if ( ! filter_var( $recipient['email'], FILTER_VALIDATE_EMAIL ) ) {
413 /* translators: %s: Email address of a recipient. */
414 $this->errors[] = sprintf( esc_html__( 'Email %s is invalid format', 'defender-security' ), $recipient['email'] );
415 }
416 }
417 }
418
419 /**
420 * Saves the current state of the object.
421 *
422 * @return void
423 */
424 public function save(): void {
425 if ( $this->last_sent <= 0 ) {
426 $this->last_sent = time();
427 }
428 $next_run = $this->get_next_run();
429 if ( is_object( $next_run ) ) {
430 $this->est_timestamp = $next_run->getTimestamp();
431 }
432 parent::save();
433 }
434
435 /**
436 * Inject next run to parent function.
437 *
438 * @return array
439 */
440 public function export(): array {
441 $data = parent::export();
442
443 $data['next_run'] = $this->get_next_run_as_string();
444 $data['all_subscribers'] = array_merge( $this->in_house_recipients, $this->out_house_recipients );
445
446 return $data;
447 }
448
449 /**
450 * Prepare data for persistence.
451 *
452 * Recipients are indexed by email in memory (see after_load()), so re-index them back to
453 * sequential arrays before storing. Otherwise they are JSON-encoded as objects, which on
454 * downgrade breaks Defender 5 with "all_subscribers.slice is not a function".
455 *
456 * @param array $data The data array to import values from.
457 *
458 * @return array
459 */
460 protected function prepare_data( $data = array() ): array {
461 $data = parent::prepare_data( $data );
462
463 foreach ( array( 'in_house_recipients', 'out_house_recipients', 'all_subscribers' ) as $key ) {
464 if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) ) {
465 $data[ $key ] = array_values( $data[ $key ] );
466 }
467 }
468
469 return $data;
470 }
471
472 /**
473 * Overrided method to manipulate user details dynamically.
474 */
475 protected function after_load(): void {
476 $in_house_recipients = array();
477
478 foreach ( $this->in_house_recipients as $recipient ) {
479 $id = $recipient['id'];
480 $user_data = get_userdata( $id );
481
482 if ( $user_data instanceof WP_User ) {
483 $in_house_recipients[ $user_data->user_email ] = array(
484 'name' => $user_data->display_name,
485 'id' => $user_data->ID,
486 'email' => $user_data->user_email,
487 'role' => $this->get_first_user_role( $user_data ),
488 'avatar' => get_avatar_url( $id ),
489 'status' => $recipient['status'] ?? '',
490 );
491 }
492 }
493
494 $this->in_house_recipients = $in_house_recipients;
495 $this->out_house_recipients = $this->get_email_indexed_recipients( $this->out_house_recipients );
496 }
497
498 /**
499 * Indexes recipients by email.
500 *
501 * @param array $recipients Recipients list.
502 *
503 * @return array
504 */
505 private function get_email_indexed_recipients( array $recipients ): array {
506 $email_indexed_recipients = array();
507
508 foreach ( $recipients as $recipient ) {
509 if ( ! isset( $recipient['email'] ) ) {
510 continue;
511 }
512
513 $email = trim( $recipient['email'] );
514 $recipient['email'] = $email;
515 $email_indexed_recipients[ $email ] = $recipient;
516 }
517
518 return $email_indexed_recipients;
519 }
520 }
521