PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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
defender-security / src / component / audit.php

audit.php in Defender Security – Malware Scanner, Login Security & Firewall 3.1.0, at src/component/audit.php

360 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Component;
4
5 use Calotes\Helper\Array_Cache;
6 use WP_Defender\Behavior\WPMUDEV;
7 use WP_Defender\Component;
8 use WP_Defender\Model\Audit_Log;
9 use WP_Defender\Traits\IO;
10 use WP_Defender\Traits\Formats;
11
12 class Audit extends Component {
13 use IO, Formats;
14
15 public const CACHE_LAST_CHECKPOINT = 'wd_audit_fetch_checkpoint';
16
17 /**
18 *
19 * All the logs should be fetched through this function, it will automate query the API and fetch local log if the date range not exists.
20 *
21 * @param int $date_from
22 * @param int $date_to
23 * @param array $events
24 * @param string $user_id
25 * @param string $ip
26 * @param int $paged
27 *
28 * return Audit_Log[]
29 */
30 public function fetch( $date_from, $date_to, $events = array(), $user_id = '', $ip = '', $paged = 1 ) {
31 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged );
32 $this->log( sprintf( 'Found %s from local', count( $internal ) ), 'audit.log' );
33 $checkpoint = get_site_option( self::CACHE_LAST_CHECKPOINT );
34 if ( false === $checkpoint ) {
35 // This case where user install the plugin, have some local data but never reach to Logs page, then check point will be today.
36 $checkpoint = time();
37 }
38 $checkpoint = (int) $checkpoint;
39 $date_from = (int) $date_from;
40 if ( 0 === count( $internal ) && $checkpoint > $date_from ) {
41 // Have to fetch from API.
42 $this->log( 'fetch from cloud', 'audit.log' );
43 //Todo:need $paged as 'nopaging'-arg?
44 $cloud = $this->query_from_api( $date_from, $date_to );
45 if ( is_wp_error( $cloud ) ) {
46 $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), 'audit.log' );
47
48 return $cloud;
49 }
50 if ( count( $cloud ) ) {
51 // No data from cloud too.
52 Audit_Log::mass_insert( $cloud );
53 // Because this is roughly fetch, so we have to filter out again using the local data.
54 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged );
55 }
56 // Cache the last time fetch, this will be useful in case of mixed data.
57 update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
58 } else {
59 // This case we have the data, however, maybe it can be out of the cached range, so we have to check.
60 // Note that, the out of range only happen with date_from, as the local always have the newest data.
61 if ( $checkpoint > $date_from ) {
62 // We have some data out of range, fetch and cache.
63 $this->log(
64 sprintf(
65 'checkpoint %s - date from %s',
66 date( 'Y-m-d H:i:s', $checkpoint ),
67 date( 'Y-m-d H:i:s', $date_from )
68 ),
69 'audit.log'
70 );
71 //Todo:need $paged as 'nopaging'-arg?
72 $cloud = $this->query_from_api( $date_from, $checkpoint );
73 if ( is_wp_error( $cloud ) ) {
74 $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), 'audit.log' );
75
76 return $cloud;
77 }
78 if ( is_array( $cloud ) ) {
79 // Silence the error here, as we actually have data.
80 Audit_Log::mass_insert( $cloud );
81 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged );
82 // Cache the last time fetch, this will be useful in case of mixed data.
83 update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
84 }
85 }
86 }
87
88 return $internal;
89 }
90
91 /**
92 * @param int $date_from
93 * @param int $date_to
94 *
95 * @return array|\WP_Error
96 * @throws \Exception
97 */
98 public function query_from_api( $date_from, $date_to ) {
99 $date_format = 'Y-m-d H:i:s';
100 $date_to = date( $date_format, $date_to );
101 $date_from = date( $date_format, $date_from );
102 $args = array(
103 'site_url' => network_site_url(),
104 'order_by' => 'timestamp',
105 'order' => 'desc',
106 'nopaging' => true,
107 'timezone' => get_option( 'gmt_offset' ),
108 'date_from' => $date_from,
109 'date_to' => $date_to,
110 );
111
112 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
113 $data = $this->make_wpmu_request(
114 WPMUDEV::API_AUDIT,
115 $args,
116 array( 'method' => 'GET' ),
117 true
118 );
119
120 if ( is_wp_error( $data ) ) {
121 $this->log( sprintf( 'Fetch error %s', $data->get_error_message() ), 'audit.log' );
122
123 return $data;
124 }
125
126 if ( 'success' !== $data['status'] ) {
127 return new \WP_Error( Error_Code::API_ERROR, __( 'Something wrong happen, please try again!', 'wpdef' ) );
128 }
129
130 return $data['data'];
131 }
132
133 public function flush() {
134 $logs = Audit_Log::get_logs_need_flush();
135 // Build the data.
136 $data = array();
137 foreach ( $logs as $log ) {
138 $item = $log->export();
139 unset( $item['synced'] );
140 unset( $item['safe'] );
141 unset( $item['id'] );
142 $item['msg'] = addslashes( $item['msg'] );
143 $data[] = $item;
144 }
145
146 if ( count( $data ) ) {
147 $ret = $this->curl_to_api( $data );
148 if ( ! is_wp_error( $ret ) ) {
149 foreach ( $logs as $log ) {
150 $log->synced = 1;
151 $log->save();
152 }
153 }
154 }
155 }
156
157 /**
158 * We will clean up the old logs depending on the storage settings.
159 *
160 * @return void
161 */
162 public function audit_clean_up_logs() {
163 $audit_settings = wd_di()->get( \WP_Defender\Model\Setting\Audit_Logging::class );
164 $interval = $this->calculate_date_interval( $audit_settings->storage_days );
165 $date_from = ( new \DateTime() )->setTimezone( wp_timezone() )
166 ->sub( new \DateInterval( 'P1Y' ) )
167 ->setTime( 0, 0, 0 );
168 $date_to = ( new \DateTime() )->setTimezone( wp_timezone() )
169 ->sub( new \DateInterval( $interval ) );
170
171 if ( $date_from < $date_to ) {
172 // Count the logs that should be deleted.
173 $logs_count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
174 if ( $logs_count > 0 ) {
175 Audit_Log::delete_old_logs( $date_from->getTimestamp(), $date_to->getTimestamp(), 50 );
176 }
177 }
178 }
179
180 /**
181 * @param $data
182 */
183 public function curl_to_api( $data ) {
184 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
185 $ret = $this->make_wpmu_request(
186 WPMUDEV::API_AUDIT_ADD,
187 $data,
188 array(
189 'method' => 'POST',
190 'timeout' => 3,
191 'headers' => array(
192 'apikey' => $this->get_apikey(),
193 ),
194 )
195 );
196
197 return $ret;
198 }
199
200 /**
201 * @param $data
202 *
203 * @return bool
204 */
205 public function socket_to_api( $data ) {
206 $sockets = Array_Cache::get( 'sockets', 'audit', array() );
207 // We will need to wait a bit.
208 if ( 0 === (is_array($sockets) || $sockets instanceof \Countable ? count( $sockets ) : 0) ) {
209 // Fall back.
210 return false;
211 }
212 $this->log( sprintf( 'Flush %s to cloud', is_array($data) || $data instanceof \Countable ? count( $data ) : 0 ), 'audit.log' );
213 $start_time = microtime( true );
214 $sks = $sockets;
215 $r = null;
216 $e = null;
217 if ( ( false === @stream_select( $r, $sks, $e, 1 ) ) ) {
218 // This case error happen.
219 return false;
220 }
221
222 $fp = array_shift( $sockets );
223
224 $uri = '/logs/add_multiple';
225 $vars = http_build_query( $data );
226 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
227 fwrite( $fp, 'POST ' . $uri . " HTTP/1.1\r\n" );
228 fwrite( $fp, 'Host: ' . $this->strip_protocol( $this->get_endpoint() ) . "\r\n" );
229 fwrite( $fp, "Content-Type: application/x-www-form-urlencoded\r\n" );
230 fwrite( $fp, 'Content-Length: ' . strlen( $vars ) . "\r\n" );
231 fwrite( $fp, 'apikey:' . $this->get_apikey() . "\r\n" );
232 fwrite( $fp, "Connection: close\r\n" );
233 fwrite( $fp, "\r\n" );
234 fwrite( $fp, $vars );
235 stream_set_timeout( $fp, 5 );
236 $res = '';
237 while ( ! feof( $fp ) ) {
238 $res .= fgets( $fp, 1024 );
239
240 $end_time = microtime( true );
241 if ( $end_time - $start_time > 3 ) {
242 fclose( $fp );
243 break;
244 }
245 }
246
247 return true;
248 }
249
250 /**
251 * Open a socket to API for faster transmit.
252 */
253 public function open_socket() {
254 $sockets = Array_Cache::get( 'sockets', 'audit', array() );
255 $endpoint = $this->strip_protocol( $this->get_endpoint() );
256 if ( empty( $sockets ) ) {
257 $fp = @stream_socket_client( //phpcs:ignore
258 'ssl://' . $endpoint . ':443',
259 $errno,
260 $errstr,
261 5
262 );
263 if ( is_resource( $fp ) ) {
264 Array_Cache::set( 'sockets', array( $fp ), 'audit' );
265 }
266 }
267 }
268
269 /**
270 * @param string $url
271 *
272 * @return string
273 */
274 private function strip_protocol( $url ) {
275 $parts = parse_url( $url );
276
277 $host = $parts['host'] . ( $parts['path'] ?? null );
278 $host = rtrim( $host, '/' );
279
280 return $host;
281 }
282
283 private function get_endpoint() {
284
285 return defined( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
286 ? constant( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
287 : 'https://audit.wpmudev.org/';
288 }
289
290 /**
291 * Queue all the events listeners, so we can listen and build log base on user behaviors.
292 * Never catch if it runs from WP CLI or CRON.
293 */
294 public function enqueue_event_listener() {
295 if ( ! wp_doing_cron() && 'cli' !== php_sapi_name() ) {
296 $events_class = array(
297 new Component\Audit\Comment_Audit(),
298 new Component\Audit\Core_Audit(),
299 new Component\Audit\Media_Audit(),
300 new Component\Audit\Post_Audit(),
301 new Component\Audit\Users_Audit(),
302 new Component\Audit\Options_Audit(),
303 new Component\Audit\Menu_Audit(),
304 );
305
306 foreach ( $events_class as $class ) {
307 // Since 2.4.7.
308 $hooks = apply_filters( 'wp_defender_audit_hooks', $class->get_hooks() );
309 foreach ( $hooks as $key => $hook ) {
310 $func = function () use ( $key, $hook, $class ) {
311 global $wp_filter;
312 if ( isset( $wp_filter['gettext'] ) ) {
313 $gettext_callbacks = $wp_filter['gettext']->callbacks;
314
315 // Disable all gettext filters.
316 $wp_filter['gettext']->callbacks = array();
317 }
318
319 // This is arguments of the hook.
320 $args = func_get_args();
321 // This is hook data, defined in each event class.
322 $class->build_log_data( $key, $args, $hook );
323
324 // Add all filters back for gettext.
325 if ( isset( $wp_filter['gettext'], $gettext_callbacks ) ) {
326 $wp_filter['gettext']->callbacks = $gettext_callbacks;
327 }
328 };
329 add_action( $key, $func, 11, is_array($hook['args']) || $hook['args'] instanceof \Countable ? count( $hook['args'] ) : 0 );
330 }
331 }
332 }
333 }
334
335 /**
336 * @throws \ReflectionException
337 */
338 public function log_audit_events() {
339 $events = Array_Cache::get( 'logs', 'audit', array() );
340
341 if ( ! (is_array($events) || $events instanceof \Countable ? count( $events ) : 0) || ! class_exists( \WP_Defender\Model\Audit_Log::class ) ) {
342 return;
343 }
344 $model = new \WP_Defender\Model\Audit_Log();
345
346 if ( (is_array($events) || $events instanceof \Countable ? count( $events ) : 0) > 1 ) {
347 if ( $model->has_method( 'mass_insert' ) ) {
348 $model->mass_insert( $events );
349 return;
350 }
351 }
352
353 foreach ( $events as $event ) {
354 $model->import( $event );
355 $model->synced = 0;
356 $model->save();
357 }
358 }
359 }
360