PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.0.1
Defender Security – Malware Scanner, Login Security & Firewall v6.0.1
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 / class-notification.php

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

499 lines 12.8 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 * @sanitize_text_field
98 * @defender_property
99 */
100 public int $day_n = 1;
101
102 /**
103 * Same as $day.
104 *
105 * @var string
106 * @defender_property
107 * @sanitize_text_field
108 */
109 public $time;
110
111 /**
112 * Holding a list of site user ids, so when sending, we send though this list.
113 *
114 * @var array
115 * @defender_property
116 * @sanitize_text_field
117 */
118 public $in_house_recipients = array();
119
120 /**
121 * For additional users, this should contain a list of email and name.
122 *
123 * @var array
124 * @defender_property
125 * @sanitize_text_field
126 */
127 public $out_house_recipients = array();
128
129 /**
130 * This when we want to run the report/notification without any email sending.
131 *
132 * @var bool
133 * @defender_property
134 */
135 public $dry_run = false;
136
137 /**
138 * This is contains the meta settings of this notification.
139 *
140 * @var array
141 * @defender_property
142 */
143 public $configs = array();
144
145 /**
146 * Tracking.
147 *
148 * @var int
149 * @defender_property
150 */
151 public $last_sent = 0;
152
153 /**
154 * Table column for estimated timestamp.
155 *
156 * @var int
157 * @defender_property
158 */
159 public $est_timestamp;
160
161 /**
162 * Return the default user, we will use this if there is no user in the notification.
163 *
164 * @return array
165 */
166 protected function get_default_user(): array {
167 if ( ! is_user_logged_in() ) {
168 return array();
169 }
170
171 $user_id = get_current_user_id();
172
173 $email = $this->get_current_user_email( $user_id );
174
175 return array(
176 $email => array(
177 'name' => $this->get_user_display( $user_id ),
178 'id' => $user_id,
179 'email' => $email,
180 'role' => $this->get_current_user_role( $user_id ),
181 'avatar' => get_avatar_url( $email ),
182 'status' => self::USER_SUBSCRIBED,
183 ),
184 );
185 }
186
187 /**
188 * Check if the current moment is right for sending.
189 *
190 * @return bool
191 */
192 public function maybe_send() {
193 // @since 2.7.0 We can remove 'dry_run'-condition in the next version.
194 if ( true === $this->dry_run ) {
195 // No send, but need to track as sent, so we can requeue it.
196 if ( 'report' === $this->type ) {
197 $this->last_sent = $this->est_timestamp;
198
199 if ( $this->get_next_run() instanceof DateTime ) {
200 $this->est_timestamp = $this->get_next_run()->getTimestamp();
201 }
202
203 $this->save();
204 }
205
206 return false;
207 }
208
209 if ( ! $this->check_active_status() ) {
210 return false;
211 }
212
213 if ( 'notification' === $this->type ) {
214 return true;
215 }
216
217 if ( 0 === $this->last_sent ) {
218 return false;
219 }
220
221 $now = new DateTime( 'now', wp_timezone() );
222 $time = apply_filters( 'defender_current_time_for_report', $now );
223 // Testing.
224 if ( defined( 'WP_DEFENDER_TESTING' ) && true === constant( 'WP_DEFENDER_TESTING' ) ) {
225 return true;
226 }
227
228 return $time->getTimestamp() >= $this->est_timestamp;
229 }
230
231 /**
232 * Calculates the next run date based on the current date and the frequency of the notification.
233 *
234 * @return DateTime|false The next run date or false if the notification type is 'notification' or the status is
235 * not active.
236 * @throws Exception If an error occurs while creating the DateTime objects or modifying the date.
237 */
238 public function get_next_run() {
239 if ( 'notification' === $this->type ) {
240 return false;
241 }
242 if ( ! $this->check_active_status() ) {
243 return false;
244 }
245
246 // Create estimate object.
247 $est = new DateTime( 'now', wp_timezone() );
248 if ( $this->last_sent > 0 ) {
249 // set the timestamp of previous.
250 $est->setTimestamp( $this->last_sent );
251 }
252
253 // Est should be set as the last send. Create now timestamp.
254 $now = new DateTime( 'now', wp_timezone() );
255 $interval = DateInterval::createFromDateString( (string) $est->getOffset() . 'seconds' );
256 $time_parts = array_pad( explode( ':', (string) $this->time ), 2, 0 );
257 [ $hour, $min ] = $time_parts;
258 $hour = (int) $hour;
259 $min = (int) $min;
260 switch ( $this->frequency ) {
261 case 'daily':
262 // Set the time.
263 $est->add( $interval );
264 $est->setTime( $hour, $min, 0 );
265 // Convert to current timezone.
266 while ( $est->getTimestamp() < $now->getTimestamp() ) {
267 $est->add( new DateInterval( 'P1D' ) );
268 $est->setTime( $hour, $min, 0 );
269 }
270 break;
271 case 'weekly':
272 if ( empty( $this->day ) ) {
273 break;
274 }
275 $est->modify( 'this ' . $this->day );
276 $est->add( $interval );
277 $est->setTime( $hour, $min, 0 );
278 while ( $est->getTimestamp() < $now->getTimestamp() ) {
279 $est->modify( 'next ' . $this->day );
280 $est->setTime( $hour, $min, 0 );
281 }
282 break;
283 case 'monthly':
284 // We will need to check if the date is passed today, if not, use this, if yes, then queue for next month.
285 $est->setDate( (int) $est->format( 'Y' ), (int) $est->format( 'm' ), 1 );
286 // Clamp day_n to a valid day in the current month.
287 $day_n = max( 1, min( $this->day_n, (int) $est->format( 't' ) ) );
288 $est->add( new DateInterval( 'P' . ( $day_n - 1 ) . 'D' ) );
289 $est->setTime( $hour, $min, 0 );
290 while ( $est->getTimestamp() < $now->getTimestamp() ) {
291 // Already over, move to first day of next month.
292 $est->modify( 'first day of next month' );
293 // Re-clamp for the new month's length.
294 $day_n = max( 1, min( $this->day_n, (int) $est->format( 't' ) ) );
295 $est->add( new DateInterval( 'P' . ( $day_n - 1 ) . 'D' ) );
296 $est->setTime( $hour, $min, 0 );
297 }
298 break;
299 }
300
301 return $est;
302 }
303
304 /**
305 * We have multiple issues where the email keep sending for no reason, this for debugging later.
306 *
307 * @param string $email Email address.
308 */
309 public function save_log( $email ): void {
310 $track = new Email_Track();
311 $track->timestamp = time();
312 $track->source = $this->slug;
313 $track->to = $email;
314 $track->save();
315 }
316
317 /**
318 * Checks the active status of the notification.
319 *
320 * @return bool Returns true if the notification is active, false otherwise.
321 */
322 public function check_active_status(): bool {
323 // Exception after migrating Scheduled scanning to Scan settings.
324 if ( Malware_Report::SLUG === $this->slug
325 && ( new \WP_Defender\Model\Setting\Scan() )->is_enabled_scheduled_scanning()
326 ) {
327 return true;
328 }
329
330 return self::STATUS_ACTIVE === $this->status;
331 }
332
333 /**
334 * This will return the interval at string.
335 *
336 * @return string
337 * @throws Exception Emits Exception in case of an error.
338 */
339 public function to_string(): string {
340 if ( ! $this->check_active_status() ) {
341 return '-';
342 }
343 $date = new DateTime( 'now', wp_timezone() );
344 $date->setTimestamp( $this->est_timestamp );
345 switch ( $this->frequency ) {
346 case 'daily':
347 return sprintf(
348 /* translators: 1: Notification sending frequency, 2: Time of a day. */
349 esc_html__( '%1$s at %2$s', 'defender-security' ),
350 ucfirst( $this->frequency ),
351 $date->format( 'h:i A' )
352 );
353 case 'weekly':
354 return sprintf(
355 /* translators: 1: Notification sending frequency, 2: Day of the week, 3: Time of a day. */
356 esc_html__( '%1$s on %2$s at %3$s', 'defender-security' ),
357 ucfirst( $this->frequency ),
358 ucfirst( $this->day ),
359 $date->format( 'h:i A' )
360 );
361 case 'monthly':
362 default:
363 return sprintf(
364 /* translators: 1: Notification sending frequency, 2: Day of the month, 3: Time of a day. */
365 esc_html__( '%1$s/%2$d, %3$s', 'defender-security' ),
366 ucfirst( $this->frequency ),
367 $this->day_n,
368 $date->format( 'h:i A' )
369 );
370 }
371 }
372
373 /**
374 * Returns the next run date as a string, based on the type of notification and the active status.
375 *
376 * @param bool $for_hub Whether the next run date is for the hub. Default is false.
377 *
378 * @return bool|string Returns false if the notification type is 'notification' or the active status is false.
379 * Returns the next run date as a string in the format specified by the date_format and
380 * time_format options. Returns 'Never' if the active status is false.
381 * @throws Exception If an error occurs while creating the DateTime objects or modifying the date.
382 */
383 public function get_next_run_as_string( bool $for_hub = false ) {
384 if ( 'notification' === $this->type ) {
385 return $for_hub ? false : esc_html__( 'Never', 'defender-security' );
386 }
387
388 $est_timestamp = ! is_int( $this->est_timestamp ) ? (int) $this->est_timestamp : $this->est_timestamp;
389 if ( $for_hub ) {
390 return $this->check_active_status()
391 ? $this->persistent_hub_datetime_format( $est_timestamp )
392 : false;
393 } elseif ( $this->check_active_status() ) {
394 $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
395 $date = new DateTime( 'now', wp_timezone() );
396 $date->setTimestamp( $est_timestamp );
397
398 return $date->format( $format );
399 } else {
400 return esc_html__( 'Never', 'defender-security' );
401 }
402 }
403
404 /**
405 * We still need to validate the out house recipients email.
406 */
407 protected function after_validate(): void {
408 foreach ( $this->out_house_recipients as $recipient ) {
409 $recipient['email'] = trim( $recipient['email'] );
410 if ( '' === $recipient['email'] ) {
411 continue;
412 }
413 if ( ! filter_var( $recipient['email'], FILTER_VALIDATE_EMAIL ) ) {
414 /* translators: %s: Email address of a recipient. */
415 $this->errors[] = sprintf( esc_html__( 'Email %s is invalid format', 'defender-security' ), $recipient['email'] );
416 }
417 }
418 }
419
420 /**
421 * Saves the current state of the object.
422 *
423 * @return void
424 */
425 public function save(): void {
426 if ( $this->last_sent <= 0 ) {
427 $this->last_sent = time();
428 }
429 $next_run = $this->get_next_run();
430 if ( is_object( $next_run ) ) {
431 $this->est_timestamp = $next_run->getTimestamp();
432 }
433 parent::save();
434 }
435
436 /**
437 * Inject next run to parent function.
438 *
439 * @return array
440 */
441 public function export(): array {
442 $data = parent::export();
443
444 $data['next_run'] = $this->get_next_run_as_string();
445 $data['all_subscribers'] = array_merge( $this->in_house_recipients, $this->out_house_recipients );
446
447 return $data;
448 }
449
450 /**
451 * Overrided method to manipulate user details dynamically.
452 */
453 protected function after_load(): void {
454 $in_house_recipients = array();
455
456 foreach ( $this->in_house_recipients as $recipient ) {
457 $id = $recipient['id'];
458 $user_data = get_userdata( $id );
459
460 if ( $user_data instanceof WP_User ) {
461 $in_house_recipients[ $user_data->user_email ] = array(
462 'name' => $user_data->display_name,
463 'id' => $user_data->ID,
464 'email' => $user_data->user_email,
465 'role' => $this->get_first_user_role( $user_data ),
466 'avatar' => get_avatar_url( $id ),
467 'status' => $recipient['status'] ?? '',
468 );
469 }
470 }
471
472 $this->in_house_recipients = $in_house_recipients;
473 $this->out_house_recipients = $this->get_email_indexed_recipients( $this->out_house_recipients );
474 }
475
476 /**
477 * Indexes recipients by email.
478 *
479 * @param array $recipients Recipients list.
480 *
481 * @return array
482 */
483 private function get_email_indexed_recipients( array $recipients ): array {
484 $email_indexed_recipients = array();
485
486 foreach ( $recipients as $recipient ) {
487 if ( empty( $recipient['email'] ) ) {
488 continue;
489 }
490
491 $email = trim( $recipient['email'] );
492 $recipient['email'] = $email;
493 $email_indexed_recipients[ $email ] = $recipient;
494 }
495
496 return $email_indexed_recipients;
497 }
498 }
499