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-firewall-logs.php +193 -381 5.3.1 → 6.3.0 View file →
@@ -8,9 +8,9 @@
8 8 namespace WP_Defender\Controller;
9 9
10 10 use DateTime;
11 11 use Exception;
12 -use Countable;
12 +use Generator;
13 13 use Valitron\Validator;
14 14 use Calotes\Helper\HTTP;
15 15 use WP_Defender\Controller;
16 16 use Calotes\Component\Request;
@@ -15,14 +15,16 @@
15 15 use WP_Defender\Controller;
16 16 use Calotes\Component\Request;
17 17 use Calotes\Component\Response;
18 18 use WP_Defender\Traits\Formats;
19 +use WP_Defender\Traits\Country;
19 20 use WP_Defender\Behavior\WPMUDEV;
20 21 use WP_Defender\Model\Lockout_Log;
21 22 use WP_Defender\Component\User_Agent;
22 -use WP_Defender\Component\IP\Global_IP;
23 23 use WP_Defender\Component\Table_Lockout;
24 +use WP_Defender\Component\Network_Cron_Manager;
24 25 use WP_Defender\Integrations\Antibot_Global_Firewall_Client;
26 +use WP_Defender\Component\Export\Csv;
25 27 use WP_Defender\Model\Setting\Blacklist_Lockout;
26 28 use WP_Defender\Model\Setting\User_Agent_Lockout;
27 29 use WP_Defender\Component\Firewall_Logs as Firewall_Logs_Component;
28 30
@@ -32,8 +34,9 @@
32 34 */
33 35 class Firewall_Logs extends Controller {
34 36
35 37 use Formats;
38 + use Country;
36 39
37 40 /**
38 41 * The slug identifier for this controller.
39 42 *
@@ -63,8 +66,13 @@
63 66 */
64 67 const AKISMET_BLOCKED_IPS = 'defender_akismet_blocked_ips';
65 68
66 69 /**
70 + * Default number of items per page.
71 + */
72 + public const DEFAULT_PER_PAGE = 10;
73 +
74 + /**
67 75 * Constructor for the class.
68 76 *
69 77 * @param Antibot_Global_Firewall_Client $antibot_client The client for interacting with the Block list API service.
70 78 */
@@ -77,13 +85,18 @@
77 85 $this->antibot_client = $antibot_client;
78 86
79 87 /**
80 88 * Send Firewall logs to AntiBot Global Firewall API.
89 + *
90 + * @var Network_Cron_Manager $network_cron_manager
81 91 */
82 - if ( ! wp_next_scheduled( 'wpdef_firewall_send_compact_logs_to_api' ) ) {
83 - wp_schedule_event( time() + 15, 'twicedaily', 'wpdef_firewall_send_compact_logs_to_api' );
84 - }
85 - add_action( 'wpdef_firewall_send_compact_logs_to_api', array( $this, 'send_compact_logs_to_api' ) );
92 + $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
93 + $network_cron_manager->register_callback(
94 + 'wpdef_firewall_send_compact_logs_to_api',
95 + array( $this, 'send_compact_logs_to_api' ),
96 + 12 * HOUR_IN_SECONDS,
97 + time() + 15
98 + );
86 99 if ( class_exists( 'Akismet' ) ) {
87 100 add_filter( 'http_response', array( $this, 'akismet_http_response' ), 10, 3 );
88 101 }
89 102 }
@@ -95,9 +108,9 @@
95 108 *
96 109 * @return Response The response object with the result of the bulk action.
97 110 * @defender_route
98 111 */
99 - public function bulk( Request $request ) {
112 + public function bulk( Request $request ): Response {
100 113 $data = $request->get_data(
101 114 array(
102 115 'action' => array(
103 116 'type' => 'string',
@@ -102,153 +115,70 @@
102 115 'action' => array(
103 116 'type' => 'string',
104 117 'sanitize' => 'sanitize_text_field',
105 118 ),
106 - 'ids' => array(
119 + 'ips' => array(
107 120 'type' => 'array',
108 121 ),
109 122 )
110 123 );
111 - $ids = $data['ids'];
112 - $ips = array();
113 - $logs = array();
114 - if ( is_array( $ids ) || $ids instanceof Countable ? count( $ids ) : 0 ) {
115 - foreach ( $ids as $id ) {
116 - $model = Lockout_Log::find_by_id( $id );
117 - if ( is_object( $model ) ) {
118 - $bl = wd_di()->get( Blacklist_Lockout::class );
119 - switch ( $data['action'] ) {
120 - case 'ban':
121 - $bl->remove_from_list( $model->ip, 'allowlist' );
122 - $bl->add_to_list( $model->ip, 'blocklist' );
123 - $ips[ $model->ip ] = $model->ip;
124 - $logs[] = $model;
125 - break;
126 - case 'allowlist':
127 - $bl->remove_from_list( $model->ip, 'blocklist' );
128 - $bl->add_to_list( $model->ip, 'allowlist' );
129 - $ips[ $model->ip ] = $model->ip;
130 - $logs[] = $model;
131 - break;
132 - case 'delete':
133 - $ips[ $model->ip ] = $model->ip;
134 - $model->delete();
135 - break;
136 - default:
137 - break;
138 - }
139 - }
140 - }
141 - }
142 124
143 - if ( count( $logs ) > 0 ) {
144 - $logs = Lockout_Log::format_logs( $logs );
145 - }
125 + $ips = implode( PHP_EOL, array_filter( array_map( 'sanitize_text_field', (array) $data['ips'] ), 'boolval' ) );
126 + $bl = wd_di()->get( Blacklist_Lockout::class );
146 127
147 128 switch ( $data['action'] ) {
148 - case 'allowlist':
149 - $messages = sprintf(
150 - /* translators: 1: IP Address(es). 2: URL for Defender > Firewall > IP Banning. */
151 - esc_html__(
152 - 'IP %1$s has been added to your allowlist. You can control your allowlist in %2$s.',
153 - 'defender-security'
154 - ),
155 - implode( ', ', $ips ),
156 - '<a href="' . network_admin_url( 'admin.php?page=wdf-ip-lockout&view=blocklist#tab-ip-allowlist' ) . '">' . esc_html__( 'IP Lockouts', 'defender-security' ) . '</a>'
157 - );
158 - break;
159 129 case 'ban':
160 - $messages = sprintf(
161 - /* translators: 1: IP Address(es). 2: URL for Defender > Firewall > IP Banning. */
162 - esc_html__(
163 - 'IP %1$s has been added to your blocklist. You can control your blocklist in %2$s.',
164 - 'defender-security'
165 - ),
166 - implode( ', ', $ips ),
167 - '<a href="' . network_admin_url( 'admin.php?page=wdf-ip-lockout&view=blocklist' ) . '">' . esc_html__( 'IP Lockouts', 'defender-security' ) . '</a>'
168 - );
130 + $bl->ip_blacklist = $ips;
169 131 break;
170 - case 'delete':
171 - $messages = sprintf(
172 - /* translators: %s: IP Address(es) */
173 - esc_html__( 'IP %s has been deleted', 'defender-security' ),
174 - implode( ', ', $ips )
175 - );
132 + case 'allowlist':
133 + $bl->ip_whitelist = $ips;
176 134 break;
177 135 default:
178 - $messages = '';
179 136 break;
180 137 }
181 138
139 + $bl->save();
140 +
182 141 return new Response(
183 142 true,
184 143 array(
185 - 'message' => $messages,
186 - 'logs' => $logs,
144 + 'banning' => $bl->export(),
187 145 )
188 146 );
189 147 }
190 148
191 149 /**
192 - * Export logs to CSV
150 + * Export all logs matching the current filter to CSV.
193 151 *
194 152 * @return void
195 153 * @defender_route
196 - * @throws Exception On failure.
197 154 */
198 155 public function export_as_csv(): void {
199 - $date_from = HTTP::get( 'date_from', strtotime( '-7 days midnight' ) );
200 - $date_to = HTTP::get( 'date_to', strtotime( 'tomorrow' ) );
201 - // Convert date using timezone.
202 - $timezone = wp_timezone();
203 - $date_from = ( new DateTime( $date_from, $timezone ) )->setTime( 0, 0, 0 )->getTimestamp();
204 - $date_to = ( new DateTime( $date_to, $timezone ) )->setTime( 23, 59, 59 )->getTimestamp();
205 - $filters = array(
156 + $timezone = wp_timezone();
157 + $date_from_str = sanitize_text_field( (string) HTTP::get( 'date_from', '' ) );
158 + $date_to_str = sanitize_text_field( (string) HTTP::get( 'date_to', '' ) );
159 + $date_from = $date_from_str
160 + ? $this->date_string_to_timestamp( $date_from_str )
161 + : ( new DateTime( '-30 days', $timezone ) )->setTime( 0, 0, 0 )->getTimestamp();
162 + $date_to = $date_to_str
163 + ? $this->date_string_to_timestamp( $date_to_str, true )
164 + : ( new DateTime( 'now', $timezone ) )->setTime( 23, 59, 59 )->getTimestamp();
165 + $ip = sanitize_text_field( (string) HTTP::get( 'ip', '' ) );
166 + $user_agent = sanitize_text_field( (string) HTTP::get( 'user_agent', '' ) );
167 + $type = sanitize_text_field( (string) HTTP::get( 'type', '' ) );
168 + $ban_status = sanitize_text_field( (string) HTTP::get( 'ban_status', '' ) );
169 + $sort = sanitize_text_field( (string) HTTP::get( 'sort', Table_Lockout::SORT_DESC ) );
170 + $sort_params = wd_di()->get( Table_Lockout::class )->resolve_sort( $sort );
171 +
172 + $filters = array(
206 173 'from' => $date_from,
207 174 'to' => $date_to,
208 - 'type' => HTTP::get( 'term', '' ),
209 - 'ip' => HTTP::get( 'ip', '' ),
210 - 'ban_status' => HTTP::get( 'ban_status', '' ),
175 + 'ip' => $ip,
176 + 'user_agent' => $user_agent,
177 + 'type' => 'all' === $type ? '' : $type,
178 + 'ban_status' => 'all' === $ban_status ? '' : $ban_status,
211 179 );
212 180
213 - if ( 'all' === $filters['type'] ) {
214 - $filters['type'] = '';
215 - }
216 -
217 - if ( 'all' === $filters['ban_status'] ) {
218 - $filters['ban_status'] = '';
219 - }
220 - // User can export the number of logs that are set.
221 - $per_page = (int) defender_get_data_from_request( 'per_page', 'g' );
222 - if ( 0 === $per_page ) {
223 - $per_page = 20;
224 - }
225 - if ( - 1 === (int) $per_page ) {
226 - $per_page = false;
227 - }
228 -
229 - $paged = (int) defender_get_data_from_request( 'paged', 'g' );
230 - if ( 0 === $paged ) {
231 - $paged = 1;
232 - }
233 - $logs = Lockout_Log::query_logs( $filters, $paged, 'date', 'desc', $per_page );
234 -
235 - $tl_component = new Table_Lockout();
236 -
237 - $ua_component = wd_di()->get( User_Agent::class );
238 -
239 - $filename = 'wdf-lockout-logs-export-' . wp_date( 'ymdHis' ) . '.csv';
240 -
241 - header( 'Expires: 0' );
242 - header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
243 - header( 'Cache-Control: private', false );
244 - header( 'Content-Type: application/octet-stream' );
245 - header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
246 - header( 'Content-Transfer-Encoding: binary' );
247 -
248 - extension_loaded( 'zlib' ) ? ob_start( 'ob_gzhandler' ) : ob_start();
249 -
250 - $fp = fopen( 'php://output', 'w' );
251 181 $headers = array(
252 182 esc_html__( 'Log', 'defender-security' ),
253 183 esc_html__( 'Date / Time', 'defender-security' ),
254 184 esc_html__( 'Type', 'defender-security' ),
@@ -253,32 +183,57 @@
253 183 esc_html__( 'Date / Time', 'defender-security' ),
254 184 esc_html__( 'Type', 'defender-security' ),
255 185 esc_html__( 'IP address', 'defender-security' ),
256 186 esc_html__( 'IP Status', 'defender-security' ),
187 + esc_html__( 'User Agent Name', 'defender-security' ),
257 188 esc_html__( 'User Agent Status', 'defender-security' ),
258 189 );
259 - fputcsv( $fp, $headers, ',', '"', '\\' );
260 190
261 - $flush_limit = Lockout_Log::INFINITE_SCROLL_SIZE;
262 - foreach ( $logs as $key => $log ) {
263 - $item = array(
264 - $log->log,
265 - $this->format_date_time( $log->date ),
266 - $tl_component->get_type( $log->type ),
267 - $log->ip,
268 - $tl_component->get_ip_status_text( $log->ip ),
269 - $ua_component->get_status_text( $log->type, $log->tried ),
191 + try {
192 + Csv::to_browser( 'lockout-logs', $this->get_export_rows( $filters, $sort_params ), $headers );
193 + } catch ( \RuntimeException $e ) {
194 + $this->log( $e->getMessage(), 'firewall-logs' );
195 + }
196 + }
197 +
198 + /**
199 + * Generates firewall log export rows in database-backed pages.
200 + *
201 + * @param array $filters Log filters.
202 + * @param array $sort_params Sort parameters.
203 + *
204 + * @return Generator
205 + */
206 + private function get_export_rows( array $filters, array $sort_params ): Generator {
207 + $page = 1;
208 + $per_page = 1000;
209 + $tl_component = new Table_Lockout();
210 + $ua_component = wd_di()->get( User_Agent::class );
211 +
212 + do {
213 + $logs = Lockout_Log::query_logs(
214 + $filters,
215 + $page,
216 + $sort_params['order_by'],
217 + $sort_params['order'],
218 + $per_page
270 219 );
271 - fputcsv( $fp, $item, ',', '"', '\\' );
272 220
273 - if ( 0 === $key % $flush_limit ) {
274 - ob_flush();
275 - flush();
221 + foreach ( $logs as $log ) {
222 + yield array(
223 + $log->log,
224 + $this->format_date_time( $log->date ),
225 + $tl_component->get_type( $log->type ),
226 + $log->ip,
227 + $tl_component->get_ip_status_text( $log->ip ),
228 + $log->user_agent,
229 + $ua_component->get_status_text( $log->type, $log->tried ),
230 + );
276 231 }
277 - }
278 232
279 - fclose( $fp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
280 - exit();
233 + $has_more = count( $logs ) === $per_page;
234 + ++$page;
235 + } while ( $has_more );
281 236 }
282 237
283 238 /**
284 239 * Toggles an IP address to or from a specified list.
@@ -290,104 +245,34 @@
290 245 */
291 246 public function toggle_ip_to_list( Request $request ): Response {
292 247 $data = $request->get_data(
293 248 array(
294 - 'ip' => array(
249 + 'list' => array(
295 250 'type' => 'string',
296 - 'sanitize' => 'sanitize_text_field',
251 + 'sanitize' => 'sanitize_textarea_field',
297 252 ),
298 - 'list' => array(
253 + 'type' => array(
299 254 'type' => 'string',
300 255 'sanitize' => 'sanitize_text_field',
301 256 ),
302 - 'ban_status' => array(
303 - 'type' => 'string',
304 - 'sanitize' => 'sanitize_text_field',
305 - ),
306 257 )
307 258 );
308 259
309 - $ip = $data['ip'];
310 260 $collection = $data['list'];
261 + $list_type = $data['type'];
311 262
312 263 $model = wd_di()->get( Blacklist_Lockout::class );
313 - if ( $model->is_ip_in_list( $ip, $collection ) ) {
314 - $model->remove_from_list( $ip, $collection );
315 - /* translators: 1: IP address, 2: IP address list, 3: IP address list, 4: URL for Defender > Firewall > IP Banning. */
316 - $message = esc_html__(
317 - 'IP %1$s has been removed from your %2$s. You can control your %3$s in %4$s.',
318 - 'defender-security'
319 - );
320 - } else {
321 - $model->add_to_list( $ip, $collection );
322 -
323 - $global_ip_service = wd_di()->get( Global_IP::class );
324 - if ( $global_ip_service->can_blocklist_autosync() ) {
325 - $global_ip_data = array(
326 - 'block_list' => array( $ip ),
327 - );
328 - $global_ip_service->add_to_global_ip_list( $global_ip_data );
329 - }
330 -
331 - /* translators: 1: IP address. 2: IP address list. 3: IP address list. 4: URL for Defender > Firewall > IP Banning. */
332 - $message = esc_html__(
333 - 'IP %1$s has been added to your %2$s. You can control your %3$s in %4$s.',
334 - 'defender-security'
335 - );
264 + if ( 'blacklist' === $list_type ) {
265 + $model->ip_blacklist = $collection;
266 + } elseif ( 'whitelist' === $list_type ) {
267 + $model->ip_whitelist = $collection;
336 268 }
337 - $filter_data = $request->get_data(
338 - array(
339 - 'date_from' => array(
340 - 'type' => 'string',
341 - 'sanitize' => 'sanitize_text_field',
342 - ),
343 - 'date_to' => array(
344 - 'type' => 'string',
345 - 'sanitize' => 'sanitize_text_field',
346 - ),
347 - 'ip_filter' => array(
348 - 'type' => 'string',
349 - 'sanitize' => 'sanitize_text_field',
350 - ),
351 - 'type' => array(
352 - 'type' => 'string',
353 - 'sanitize' => 'sanitize_text_field',
354 - ),
355 - 'paged' => array(
356 - 'type' => 'int',
357 - 'sanitize' => 'sanitize_text_field',
358 - ),
359 - 'per_page' => array(
360 - 'type' => 'int',
361 - 'sanitize' => 'sanitize_text_field',
362 - ),
363 - )
364 - );
365 - $logs = Lockout_Log::get_logs_and_format(
366 - array(
367 - 'from' => strtotime( $filter_data['date_from'] . ' 00:00:00' ),
368 - 'to' => strtotime( $filter_data['date_to'] . ' 23:59:59' ),
369 - 'ip' => $filter_data['ip_filter'],
370 - // If this is all, then we set to null to exclude it from the filter.
371 - 'type' => 'all' === $filter_data['type'] ? '' : $filter_data['type'],
372 - ),
373 - $filter_data['paged'],
374 - 'id',
375 - 'desc',
376 - $filter_data['per_page']
377 - );
269 + $model->save();
378 270
379 271 return new Response(
380 272 true,
381 273 array(
382 - 'message' => sprintf(
383 - $message,
384 - $data['ip'] ?? '-',
385 - $data['list'],
386 - $data['list'],
387 - '<a href="' . network_admin_url( 'admin.php?page=wdf-ip-lockout&view=blocklist' ) . '">' . esc_html__( 'IP Lockouts', 'defender-security' ) . '</a>'
388 - ),
389 - 'logs' => $logs,
274 + 'banning' => $model->export(),
390 275 )
391 276 );
392 277 }
393 278
@@ -401,113 +286,34 @@
401 286 */
402 287 public function toggle_ua_to_list( Request $request ): Response {
403 288 $data = $request->get_data(
404 289 array(
405 - 'ua' => array(
290 + 'list' => array(
406 291 'type' => 'string',
407 - 'sanitize' => 'sanitize_text_field',
292 + 'sanitize' => 'sanitize_textarea_field',
408 293 ),
409 - 'list' => array(
294 + 'type' => array(
410 295 'type' => 'string',
411 296 'sanitize' => 'sanitize_text_field',
412 297 ),
413 - 'scenario' => array(
414 - 'type' => 'string',
415 - 'sanitize' => 'sanitize_text_field',
416 - ),
417 298 )
418 299 );
419 300
420 - $ua = $data['ua'];
421 301 $collection = $data['list'];
422 - $action = $data['scenario'];
302 + $list_type = $data['type'];
423 303
424 304 $model = wd_di()->get( User_Agent_Lockout::class );
425 -
426 - if ( 'remove' === $action && $model->is_ua_in_list( $ua, $collection ) ) {
427 - $model->remove_from_list( $ua, $collection );
428 - /* translators: 1: User agent. 2: User agent list. 3: User agent list. 4: URL for Defender > Firewall > User Agent Banning. */
429 - $message = esc_html__(
430 - 'User agent %1$s has been removed from your %2$s. You can control your %3$s in %4$s.',
431 - 'defender-security'
432 - );
433 - } elseif ( 'add' === $action ) {
434 -
435 - /**
436 - * Possible scenario on regex blocklist. For e.g. UA term `run` present in allowlist & `r.n` regex in blocklist then remove `run` to block `run` user agent using regex `r.n`.
437 - */
438 - if ( 'blocklist' === $collection && $model->is_ua_in_list( $ua, 'allowlist' ) ) {
439 - $model->remove_from_list( $ua, 'allowlist' );
440 - }
441 -
442 - if ( ! $model->is_ua_in_list( $ua, $collection ) ) {
443 - $model->add_to_list( $ua, $collection );
444 - }
445 - /* translators: 1: User agent. 2: User agent list. 3: User agent list. 4: URL for Defender > Firewall > User Agent Banning. */
446 - $message = esc_html__(
447 - 'User agent %1$s has been added to your %2$s. You can control your %3$s in %4$s.',
448 - 'defender-security'
449 - );
450 - } else {
451 - return new Response(
452 - false,
453 - array( 'message' => esc_html__( 'Wrong result.', 'defender-security' ) )
454 - );
305 + if ( 'blacklist' === $list_type ) {
306 + $model->blacklist = $collection;
307 + } elseif ( 'whitelist' === $list_type ) {
308 + $model->whitelist = $collection;
455 309 }
310 + $model->save();
456 311
457 - $filter_data = $request->get_data(
458 - array(
459 - 'date_from' => array(
460 - 'type' => 'string',
461 - 'sanitize' => 'sanitize_text_field',
462 - ),
463 - 'date_to' => array(
464 - 'type' => 'string',
465 - 'sanitize' => 'sanitize_text_field',
466 - ),
467 - 'ip_filter' => array(
468 - 'type' => 'string',
469 - 'sanitize' => 'sanitize_text_field',
470 - ),
471 - 'type' => array(
472 - 'type' => 'string',
473 - 'sanitize' => 'sanitize_text_field',
474 - ),
475 - 'paged' => array(
476 - 'type' => 'int',
477 - 'sanitize' => 'sanitize_text_field',
478 - ),
479 - 'per_page' => array(
480 - 'type' => 'int',
481 - 'sanitize' => 'sanitize_text_field',
482 - ),
483 - )
484 - );
485 - $logs = Lockout_Log::get_logs_and_format(
486 - array(
487 - 'from' => strtotime( $filter_data['date_from'] . ' 00:00:00' ),
488 - 'to' => strtotime( $filter_data['date_to'] . ' 23:59:59' ),
489 - 'ip' => $filter_data['ip_filter'],
490 - // If this is all, then we set to null to exclude it from the filter.
491 - 'type' => 'all' === $filter_data['type'] ? '' : $filter_data['type'],
492 - ),
493 - $filter_data['paged'],
494 - 'id',
495 - 'desc',
496 - $filter_data['per_page']
497 - );
498 -
499 312 return new Response(
500 313 true,
501 314 array(
502 - 'message' => sprintf(
503 - $message,
504 - '<strong>' . $data['ua'] . '</strong>',
505 - $data['list'],
506 - $data['list'],
507 - '<a href="' . network_admin_url( 'admin.php?page=wdf-ip-lockout&view=ua-lockout' ) . '">' . esc_html__( 'User Agent Banning', 'defender-security' ) . '</a>'
508 - ),
509 - 'logs' => $logs,
315 + 'uaLockout' => $model->export(),
510 316 )
511 317 );
512 318 }
513 319
@@ -534,8 +340,12 @@
534 340 'ip' => array(
535 341 'type' => 'string',
536 342 'sanitize' => 'sanitize_text_field',
537 343 ),
344 + 'user_agent' => array(
345 + 'type' => 'string',
346 + 'sanitize' => 'sanitize_text_field',
347 + ),
538 348 'type' => array(
539 349 'type' => 'string',
540 350 'sanitize' => 'sanitize_text_field',
541 351 ),
@@ -550,57 +360,44 @@
550 360 'ban_status' => array(
551 361 'type' => 'string',
552 362 'sanitize' => 'sanitize_text_field',
553 363 ),
364 + 'per_page' => array(
365 + 'type' => 'int',
366 + 'sanitize' => 'sanitize_text_field',
367 + ),
554 368 )
555 369 );
556 370 // Validate.
557 371 $v = new Validator( $data, array() );
558 372 $v->rule( 'required', array( 'date_from', 'date_to' ) );
559 - $v->rule( 'date', array( 'date_from', 'date_to' ) );
560 373 if ( ! $v->validate() ) {
374 + return new Response( false, array( 'message' => esc_html__( 'Start and end date are required.', 'defender-security' ) ) );
375 + }
376 +
377 + $date_from = $this->date_string_to_timestamp( $data['date_from'] );
378 + $date_to = $this->date_string_to_timestamp( $data['date_to'], true );
379 + if ( $date_from <= 0 || $date_to <= 0 ) {
561 380 return new Response( false, array( 'message' => esc_html__( 'Wrong start and end date.', 'defender-security' ) ) );
562 381 }
563 - $sort = $data['sort'] ?? Table_Lockout::SORT_DESC;
564 - switch ( $sort ) {
565 - case 'ip':
566 - $order = 'desc';
567 - $order_by = 'ip';
568 - break;
569 - case 'oldest':
570 - $order = 'asc';
571 - $order_by = 'id';
572 - break;
573 - case 'user_agent':
574 - $order = 'asc';
575 - $order_by = 'user_agent';
576 - break;
577 - default:
578 - $order = 'desc';
579 - $order_by = 'id';
580 - break;
581 - }
582 - // Convert date using timezone.
583 - $timezone = wp_timezone();
584 - $date_from = ( new DateTime( $data['date_from'], $timezone ) )
585 - ->setTime( 0, 0, 0 )
586 - ->getTimestamp();
587 - $date_to = ( new DateTime( $data['date_to'], $timezone ) )
588 - ->setTime( 23, 59, 59 )
589 - ->getTimestamp();
590 382
383 + $sort = $data['sort'] ?? Table_Lockout::SORT_DESC;
384 + $sort_params = wd_di()->get( Table_Lockout::class )->resolve_sort( $sort );
385 +
591 386 $result = $this->retrieve_logs(
592 387 array(
593 388 'from' => $date_from,
594 389 'to' => $date_to,
595 390 'ip' => $data['ip'],
391 + 'user_agent' => $data['user_agent'] ?? '',
596 392 // If this is all, then we set to null to exclude it from the filter.
597 393 'type' => 'all' === $data['type'] ? '' : $data['type'],
598 394 'ban_status' => 'all' === $data['ban_status'] ? '' : $data['ban_status'],
599 395 ),
600 396 $data['paged'],
601 - $order,
602 - $order_by
397 + $sort_params['order'],
398 + $sort_params['order_by'],
399 + $data['per_page'] ?? 0
603 400 );
604 401
605 402 return new Response( true, $result );
606 403 }
@@ -612,21 +409,8 @@
612 409 public function enqueue_assets() {
613 410 if ( ! $this->is_page_active() ) {
614 411 return;
615 412 }
616 - wp_enqueue_script( 'def-momentjs', defender_asset_url( '/assets/js/vendor/moment/moment.min.js' ), array(), DEFENDER_VERSION, true );
617 - wp_enqueue_script(
618 - 'def-daterangepicker',
619 - defender_asset_url( '/assets/js/vendor/daterangepicker/daterangepicker.js' ),
620 - array(),
621 - DEFENDER_VERSION,
622 - true
623 - );
624 - wp_localize_script(
625 - 'def-iplockout',
626 - 'lockout_logs',
627 - array_merge( $this->data_frontend(), $this->dump_routes_and_nonces() )
628 - );
629 413 }
630 414
631 415 /**
632 416 * Provides data for the frontend.
@@ -633,18 +417,34 @@
633 417 *
634 418 * @return array An array of data for the frontend.
635 419 */
636 420 public function data_frontend(): array {
637 - $def_filters = array( 'misc' => wd_di()->get( Table_Lockout::class )->get_filters() );
638 - $init_filters = array(
639 - 'from' => strtotime( '-30 days' ),
640 - 'to' => time(),
641 - 'type' => '',
642 - 'ip' => '',
421 + $type = defender_get_data_from_request( 'type', 'g' );
422 + $ip = defender_get_data_from_request( 'ip', 'g' );
423 + $user_agent = defender_get_data_from_request( 'user_agent', 'g' );
424 + $timezone = wp_timezone();
425 +
426 + $init_filters = array(
427 + 'from' => ( new DateTime( '-30 days', $timezone ) )->setTime( 0, 0, 0 )->getTimestamp(),
428 + 'to' => ( new DateTime( 'now', $timezone ) )->setTime( 23, 59, 59 )->getTimestamp(),
429 + 'type' => $type,
430 + 'ip' => $ip,
431 + 'user_agent' => $user_agent,
643 432 'ban_status' => '',
644 433 );
434 + $misc = wd_di()->get( Table_Lockout::class )->get_filters();
435 + $misc['countries'] = $this->countries_list();
436 + $def_filters = array(
437 + 'misc' => $misc,
438 + 'default_filter' => $init_filters,
439 + 'per_page' => self::DEFAULT_PER_PAGE,
440 + );
645 441
646 - return array_merge( $this->retrieve_logs( $init_filters, 1 ), $def_filters );
442 + return array_merge(
443 + $this->retrieve_logs( $init_filters ),
444 + $def_filters,
445 + $this->dump_routes_and_nonces()
446 + );
647 447 }
648 448
649 449 /**
650 450 * Retrieves logs based on the given filters, paging, order, and order by.
@@ -657,8 +457,9 @@
657 457 * - 'ban_status': The ban status of the logs.
658 458 * @param int $paged The page number of the logs to retrieve. Default is 1.
659 459 * @param string $order The order of the logs. Default is 'desc'.
660 460 * @param string $order_by The field to order the logs by. Default is 'id'.
461 + * @param int $per_page Number of logs per page. 0 falls back to the default of 10.
661 462 *
662 463 * @return array An array containing the following keys:
663 464 * - 'count': The total count of logs.
664 465 * - 'logs': The retrieved logs.
@@ -664,25 +465,18 @@
664 465 * - 'logs': The retrieved logs.
665 466 * - 'per_page': The number of logs per page.
666 467 * - 'total_pages': The total number of pages.
667 468 */
668 - private function retrieve_logs( $filters, $paged = 1, $order = 'desc', $order_by = 'id' ): array {
669 - // User can set the number of logs to retrieve per page.
670 - $per_page = (int) defender_get_data_from_request( 'per_page', 'p' );
469 + private function retrieve_logs( $filters, $paged = 1, $order = 'desc', $order_by = 'id', $per_page = 0 ): array {
470 + $per_page = (int) $per_page;
671 471 if ( 0 === $per_page ) {
672 - $per_page = 20;
472 + $per_page = self::DEFAULT_PER_PAGE;
673 473 }
674 474 $conditions = array( 'ban_status' => $filters['ban_status'] );
675 475
676 476 $count = Lockout_Log::count( $filters['from'], $filters['to'], $filters['type'], $filters['ip'], $conditions );
677 477 $logs = Lockout_Log::get_logs_and_format( $filters, $paged, $order_by, $order, $per_page );
678 -
679 - if ( - 1 === $per_page ) {
680 - $per_page = Lockout_Log::INFINITE_SCROLL_SIZE;
681 - }
682 -
683 478 return array(
684 - 'count' => $count,
685 479 'logs' => $logs,
686 480 'per_page' => $per_page,
687 481 'total_pages' => ceil( $count / $per_page ),
688 482 );
@@ -714,8 +508,9 @@
714 508 /**
715 509 * Delete all the data & the cache.
716 510 */
717 511 public function remove_data() {
512 + delete_site_transient( self::AKISMET_BLOCKED_IPS );
718 513 }
719 514
720 515 /**
721 516 * Exports strings.
@@ -729,19 +524,24 @@
729 524 /**
730 525 * Exports strings.
731 526 *
732 527 * @param array $logs Prepared logs.
528 + * @param bool $is_staging Send logs to staging.
733 529 */
734 - private function maybe_send_reports( array $logs ): void {
530 + private function maybe_send_reports( array $logs, bool $is_staging = false ): void {
735 531 $offset = 0;
736 532 $length = 1000;
737 533 $logs_chunk = array_slice( $logs, $offset, $length );
738 - while ( ! empty( $logs_chunk ) ) {
534 + while ( array() !== $logs_chunk ) {
739 535 $data = array(
740 536 'logs' => $logs_chunk,
741 537 );
742 538
743 - $response = $this->antibot_client->send_reports( $data );
539 + $antibot_client = $this->antibot_client;
540 + if ( $is_staging ) {
541 + $antibot_client = new Antibot_Global_Firewall_Client( 'https://staging-api.blocklist-service.com' );
542 + }
543 + $response = $antibot_client->send_reports( $data );
744 544
745 545 if ( is_wp_error( $response ) ) {
746 546 $this->log(
747 547 sprintf( 'AntiBot Global Firewall Error: %s', $response->get_error_message() ),
@@ -779,9 +579,10 @@
779 579 * @param bool $status Status for sending logs. Send logs to API if true.
780 580 *
781 581 * @since 4.5.0
782 582 */
783 - $send_logs = (bool) apply_filters( 'wpdef_firewall_send_logs_to_api', true );
583 + $send_logs = apply_filters( 'wpdef_firewall_send_logs_to_api', true );
584 + $send_logs = is_bool( $send_logs ) ? $send_logs : (bool) $send_logs;
784 585
785 586 if (
786 587 ! $send_logs ||
787 588 ! $this->wpmudev->is_dash_activated() ||
@@ -798,10 +599,10 @@
798 599 // Log the site ID where the event is triggered.
799 600 $this->log( "{$event_name} is processing from site {$site_id}", Firewall::FIREWALL_LOG );
800 601 $from = time() - ( 7 * DAY_IN_SECONDS );
801 602
802 - $last_run_time = get_site_option( 'wpdef_ip_blocklist_sync_last_run_time' );
803 - if ( $last_run_time ) {
603 + $last_run_time = get_site_option( 'wpdef_ip_blocklist_sync_last_run_time', 0 );
604 + if ( 0 < $last_run_time ) {
804 605 $time_difference = time() - $last_run_time;
805 606
806 607 if ( $time_difference < 7 * DAY_IN_SECONDS ) { // 7 days in seconds
807 608 $from = $last_run_time;
@@ -809,19 +610,24 @@
809 610 }
810 611 update_site_option( 'wpdef_ip_blocklist_sync_last_run_time', time() );
811 612
812 613 $service = wd_di()->get( Firewall_Logs_Component::class );
813 - $logs = $service->get_compact_logs( $from );
814 614
815 - if ( ! empty( $logs ) ) {
615 + $logs = $service->get_compact_logs( $from );
616 + if ( array() !== $logs ) {
816 617 $this->maybe_send_reports( $logs );
817 618 }
818 619
819 620 $logs = $service->get_akismet_auto_spam_comment_logs();
820 - if ( ! empty( $logs ) ) {
621 + if ( array() !== $logs ) {
821 622 $this->maybe_send_reports( $logs );
822 623 }
823 624
625 + $logs = $service->get_404_intelligence_logs( $from );
626 + if ( array() !== $logs ) {
627 + $this->maybe_send_reports( $logs, true );
628 + }
629 +
824 630 // Release lock after execution.
825 631 $this->release_cron_lock( $event_name );
826 632 }
827 633
@@ -842,25 +648,31 @@
842 648
843 649 // Retrieve response body safely.
844 650 $body = wp_remote_retrieve_body( $response );
845 651 // If the body is empty or does not equal 'true' (indicating spam), return the response as is.
846 - if ( empty( $body ) || trim( $body ) !== 'true' ) {
652 + if ( ! is_string( $body ) || in_array( trim( $body ), array( '', 'true' ), true ) ) {
847 653 return $response;
848 654 }
849 655
850 656 // Ensure the request body contains data; otherwise, return the response.
851 - if ( empty( $parsed_args['body'] ) ) {
657 + $body_arg = $parsed_args['body'] ?? '';
658 + if (
659 + ( is_string( $body_arg ) && '' === trim( $body_arg ) )
660 + || ( is_array( $body_arg ) && array() === $body_arg )
661 + || ( is_object( $body_arg ) && 0 === count( get_object_vars( $body_arg ) ) )
662 + ) {
852 663 return $response;
853 664 }
854 665
855 - $request_data = wp_parse_args( $parsed_args['body'] );
666 + $request_data = wp_parse_args( $body_arg );
856 667 // If the comment author's IP is not present in the request data, return the response.
857 - if ( empty( $request_data['comment_author_IP'] ) ) {
668 + $author_ip = $request_data['comment_author_IP'] ?? '';
669 + if ( '' === $author_ip ) {
858 670 return $response;
859 671 }
860 672
861 673 // Validate the user IP address from the request data.
862 - $user_ip = filter_var( $request_data['comment_author_IP'], FILTER_VALIDATE_IP );
674 + $user_ip = filter_var( $author_ip, FILTER_VALIDATE_IP );
863 675 if ( false === $user_ip ) {
864 676 return $response;
865 677 }
866 678