| @@ -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 | } |