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/component/class-audit.php +303 -10 6.0.1 → 6.3.0 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Simple functionality.
3 + * Responsible for handling audit logs.
4 4 *
5 5 * @package WP_Defender\Component
6 6 */
7 7
@@ -6,20 +6,34 @@
6 6 */
7 7
8 8 namespace WP_Defender\Component;
9 9
10 +use WP_Error;
11 +use DateTime;
12 +use Exception;
13 +use Countable;
14 +use DateInterval;
15 +use WP_Defender\Traits\IO;
10 16 use WP_Defender\Component;
17 +use WP_Defender\Traits\Formats;
18 +use Calotes\Helper\Array_Cache;
19 +use WP_Defender\Model\Audit_Log;
20 +use WP_Defender\Behavior\WPMUDEV;
21 +use WP_Defender\Model\Setting\Audit_Logging;
11 22
12 23 /**
13 - * Simple methods.
24 + * Provides methods for fetching, querying, and managing audit logs.
14 25 */
15 26 class Audit extends Component {
16 27
28 + use IO;
29 + use Formats;
30 +
17 31 public const AUDIT_LOG = 'audit.log';
18 32 public const CACHE_LAST_CHECKPOINT = 'wd_audit_fetch_checkpoint';
19 33
20 34 /**
21 - * Dummy.
35 + * Fetches audit logs, either from local storage or via API if not available locally.
22 36 *
23 37 * @param int $date_from Start date for fetching logs.
24 38 * @param int $date_to End date for fetching logs.
25 39 * @param array $events Specific events to fetch.
@@ -27,34 +41,313 @@
27 41 * @param string $ip IP address to filter logs.
28 42 * @param int $paged Pagination page number.
29 43 * @param int $per_page Number of logs per page.
30 44 *
31 - * @return array
45 + * @return Audit_Log[]|WP_Error Returns an array of Audit_Log objects or WP_Error on failure.
46 + * @throws Exception Throws exception on failure.
32 47 */
33 48 public function fetch( $date_from, $date_to, $events = array(), $user_id = '', $ip = '', $paged = 1, $per_page = 10 ) {
34 - return array();
49 + $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
50 + $this->log( sprintf( 'Found %s from local', count( $internal ) ), self::AUDIT_LOG );
51 + if ( ! wd_di()->get( WPMUDEV::class )->is_pro() ) {
52 + return $internal;
53 + }
54 + $checkpoint = get_site_option( self::CACHE_LAST_CHECKPOINT );
55 + if ( false === $checkpoint ) {
56 + // This case where user install the plugin, have some local data but never reach to Logs page, then check point will be today.
57 + $checkpoint = time();
58 + }
59 + $checkpoint = (int) $checkpoint;
60 + $date_from = ! is_int( $date_from ) ? (int) $date_from : $date_from;
61 + if ( 0 === count( $internal ) && $checkpoint > $date_from ) {
62 + // Have to fetch from API.
63 + $this->log( 'fetch from cloud', self::AUDIT_LOG );
64 + // Todo:need $paged as 'nopaging'-arg?
65 + $cloud = $this->query_from_api( $date_from, $date_to );
66 + if ( is_wp_error( $cloud ) ) {
67 + $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), self::AUDIT_LOG );
68 +
69 + return $cloud;
70 + }
71 + if ( count( $cloud ) ) {
72 + // No data from cloud too.
73 + Audit_Log::mass_insert( $cloud );
74 + // Because this is roughly fetch, so we have to filter out again using the local data.
75 + $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
76 + }
77 + // Cache the last time fetch, this will be useful in case of mixed data.
78 + update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
79 + // This case we have the data, however, maybe it can be out of the cached range, so we have to check.
80 + // Note that, the out of range only happen with date_from, as the local always have the newest data.
81 + } elseif ( $checkpoint > $date_from ) {
82 + // We have some data out of range, fetch and cache.
83 + $this->log(
84 + sprintf(
85 + 'checkpoint %s - date from %s',
86 + wp_date( 'Y-m-d H:i:s', $checkpoint ),
87 + wp_date( 'Y-m-d H:i:s', $date_from )
88 + ),
89 + self::AUDIT_LOG
90 + );
91 + $cloud = $this->query_from_api( $date_from, $checkpoint );
92 + if ( is_wp_error( $cloud ) ) {
93 + $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), self::AUDIT_LOG );
94 +
95 + return $cloud;
96 + }
97 + if ( is_array( $cloud ) ) {
98 + // Silence the error here, as we actually have data.
99 + Audit_Log::mass_insert( $cloud );
100 + $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
101 + // Cache the last time fetch, this will be useful in case of mixed data.
102 + update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
103 + }
104 + }
105 +
106 + return $internal;
35 107 }
36 108
37 109 /**
38 - * Dummy.
110 + * Queries audit logs from the API.
111 + *
112 + * @param int $date_from Start date for the query.
113 + * @param int $date_to End date for the query.
114 + *
115 + * @return array|WP_Error Returns an array of logs or WP_Error on failure.
116 + * @throws Exception Throws exception on failure.
39 117 */
40 - public function flush() {
118 + public function query_from_api( $date_from, $date_to ) {
119 + $date_format = 'Y-m-d H:i:s';
120 + $date_to = wp_date( $date_format, $date_to );
121 + $date_from = wp_date( $date_format, $date_from );
122 + $args = array(
123 + 'site_url' => network_site_url(),
124 + 'order_by' => 'timestamp',
125 + 'order' => 'desc',
126 + 'nopaging' => true,
127 + 'timezone' => get_option( 'gmt_offset' ),
128 + 'date_from' => $date_from,
129 + 'date_to' => $date_to,
130 + );
131 +
132 + $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
133 + $data = $this->make_wpmu_request(
134 + WPMUDEV::API_AUDIT,
135 + $args,
136 + array( 'method' => 'GET' ),
137 + true
138 + );
139 +
140 + if ( is_wp_error( $data ) ) {
141 + $this->log( sprintf( 'Fetch error %s', $data->get_error_message() ), self::AUDIT_LOG );
142 +
143 + return $data;
144 + }
145 +
146 + if ( 'success' !== $data['status'] ) {
147 + return new WP_Error(
148 + Error_Code::API_ERROR,
149 + esc_html__( 'Something wrong happen, please try again!', 'defender-security' )
150 + );
151 + }
152 +
153 + return $data['data'];
41 154 }
42 155
156 +
43 157 /**
44 - * Dummy.
158 + * Delete all local audit log entries from database and reset checkpoint.
159 + *
160 + * @return void
45 161 */
162 + public function reset(): void {
163 + Audit_Log::truncate();
164 + delete_site_option( self::CACHE_LAST_CHECKPOINT );
165 + }
166 +
167 + /**
168 + * Cleans up old logs based on storage settings.
169 + *
170 + * @throws Exception When the $duration cannot be parsed as an interval.
171 + */
46 172 public function audit_clean_up_logs() {
173 + $audit_settings = wd_di()->get( Audit_Logging::class );
174 + $interval = $this->calculate_date_interval( $audit_settings->storage_days );
175 + // Since v5.7.0.
176 + $interval = apply_filters( 'wpdef_audit_logs_store_backward', $interval );
177 + $interval = is_string( $interval ) ? $interval : (string) $interval;
178 +
179 + try {
180 + $interval_obj = new DateInterval( $interval );
181 + } catch ( Exception ) {
182 + // Fallback if the filter supplied an incorrect value.
183 + $interval_obj = new DateInterval( 'P6M' );
184 + }
185 +
186 + $date_from = ( new DateTime() )->setTimezone( wp_timezone() )
187 + ->sub( new DateInterval( 'P1Y' ) )
188 + ->setTime( 0, 0, 0 );
189 + $date_to = ( new DateTime() )->setTimezone( wp_timezone() )
190 + ->sub( $interval_obj );
191 +
192 + if ( $date_from < $date_to ) {
193 + // Count the logs that should be deleted.
194 + $logs_count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
195 + if ( $logs_count > 0 ) {
196 + $this->log( 'Cleaning up old logs from ' . $date_from->format( 'Y-m-d H:i:s' ) . ' to ' . $date_to->format( 'Y-m-d H:i:s' ), self::AUDIT_LOG );
197 + // Since v5.0.0.
198 + $delete_count = apply_filters( 'wpdef_audit_limit_deleted_logs', 50 );
199 + Audit_Log::delete_old_logs(
200 + $date_from->getTimestamp(),
201 + $date_to->getTimestamp(),
202 + is_int( $delete_count ) ? $delete_count : (int) $delete_count,
203 + );
204 + }
205 + }
47 206 }
48 207
49 208 /**
50 - * Dummy.
209 + * Sends data to the API using cURL.
210 + *
211 + * @param array $data Data to be sent to the API.
212 + *
213 + * @return mixed Returns the response from the API.
51 214 */
215 + public function curl_to_api( $data ) {
216 + $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
217 + $ret = $this->make_wpmu_request(
218 + WPMUDEV::API_AUDIT_ADD,
219 + $data,
220 + array(
221 + 'method' => 'POST',
222 + 'timeout' => 3,
223 + 'headers' => array(
224 + 'apikey' => $this->get_apikey(),
225 + ),
226 + )
227 + );
228 +
229 + return $ret;
230 + }
231 +
232 + /**
233 + * Strips the protocol from a URL.
234 + *
235 + * @param string $url URL to process.
236 + *
237 + * @return string Returns the URL without the protocol.
238 + */
239 + private function strip_protocol( $url ) {
240 + $parts = wp_parse_url( $url );
241 + $host = $parts['host'] . ( $parts['path'] ?? null );
242 +
243 + return rtrim( $host, '/' );
244 + }
245 +
246 + /**
247 + * Returns the API endpoint.
248 + *
249 + * @return string Returns the API endpoint URL.
250 + */
251 + private function get_endpoint(): string {
252 + return defined( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
253 + ? constant( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
254 + : 'https://audit.wpmudev.org/';
255 + }
256 +
257 + /**
258 + * Queue all the events listeners, so we can listen and build log base on user behaviors.
259 + * Never catch if it runs from WP CLI or CRON.
260 + */
52 261 public function enqueue_event_listener() {
262 + if ( ! wp_doing_cron() && ! defender_is_wp_cli() ) {
263 + $events_class = array(
264 + new Component\Audit\Comment_Audit(),
265 + new Component\Audit\Core_Audit(),
266 + new Component\Audit\Media_Audit(),
267 + new Component\Audit\Post_Audit(),
268 + new Component\Audit\User_Audit(),
269 + new Component\Audit\Options_Audit(),
270 + new Component\Audit\Menu_Audit(),
271 + new Component\Audit\Theme_Audit(),
272 + new Component\Audit\Feature_Audit(),
273 + new Component\Audit\Password_Audit(),
274 + new Component\Audit\Application_Password_Audit(),
275 + );
276 +
277 + foreach ( $events_class as $class ) {
278 + // since 2.4.7.
279 + $hooks = apply_filters( 'wp_defender_audit_hooks', $class->get_hooks() );
280 + foreach ( $hooks as $key => $hook ) {
281 + $func = function () use ( $key, $hook, $class ) {
282 + global $wp_filter;
283 + if ( isset( $wp_filter['gettext'] ) ) {
284 + $gettext_callbacks = $wp_filter['gettext']->callbacks;
285 +
286 + // Disable all gettext filters.
287 + $wp_filter['gettext']->callbacks = array();
288 + }
289 +
290 + // This is arguments of the hook.
291 + $args = func_get_args();
292 + // This is hook data, defined in each event class.
293 + $class->build_log_data( $key, $args, $hook );
294 +
295 + // Add all filters back for gettext.
296 + if ( isset( $wp_filter['gettext'], $gettext_callbacks ) ) {
297 + $wp_filter['gettext']->callbacks = $gettext_callbacks;
298 + }
299 + };
300 + add_action(
301 + $key,
302 + $func,
303 + 11,
304 + is_array( $hook['args'] ) || $hook['args'] instanceof Countable ? count( $hook['args'] ) : 0
305 + );
306 + }
307 + }
308 + }
53 309 }
54 310
55 311 /**
56 - * Dummy.
312 + * Logs audit events.
57 313 */
58 314 public function log_audit_events() {
315 + $events = Array_Cache::get( 'logs', 'audit', array() );
316 +
317 + if ( ! ( is_array( $events ) || $events instanceof Countable ? count( $events ) : 0 )
318 + || ! class_exists( Audit_Log::class )
319 + ) {
320 + return;
321 + }
322 + $model = new Audit_Log();
323 +
324 + if ( ( is_array( $events ) || $events instanceof Countable ? count( $events ) : 0 ) > 1 ) {
325 + if ( $model->has_method( 'mass_insert' ) ) {
326 + $model->mass_insert( $events, 0 );
327 +
328 + return;
329 + }
330 + }
331 +
332 + foreach ( $events as $event ) {
333 + $model->import( $event );
334 + $model->synced = 0;
335 + $model->save();
336 + }
337 + }
338 +
339 + /**
340 + * Get audit events per week.
341 + */
342 + public static function get_audit_events_per_week(): int {
343 + $timezone = wp_timezone();
344 + $date_to = ( new DateTime( 'now', $timezone ) )
345 + ->setTime( 23, 59, 59 )
346 + ->getTimestamp();
347 + $date_from = ( new DateTime( '-7 days', $timezone ) )
348 + ->setTime( 0, 0, 0 )
349 + ->getTimestamp();
350 +
351 + return Audit_Log::count( $date_from, $date_to );
59 352 }
60 353 }