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
defender-security / src / component / class-legacy-versions.php

class-legacy-versions.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/class-legacy-versions.php

424 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling legacy data migration and management related to scan issues and ignored items.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Component;
11 use WP_Defender\Model\Scan;
12 use WP_Defender\Model\Scan_Item;
13 use WP_Defender\Controller\Scan as Scan_Controller;
14 use WP_Filesystem_Base;
15
16 /**
17 * Responsible for handling legacy data migration and management related to scan issues and ignored items.
18 *
19 * @since 2.5.6
20 */
21 class Legacy_Versions extends Component {
22
23 public const IGNORE_LIST = 'wdfscanignore';
24
25 /**
26 * Retrieves all scan issue items from the database.
27 *
28 * @return array Array of scan issue items.
29 */
30 private function find_all_scan_issue_items() {
31 global $wpdb;
32
33 $sql = 'SELECT t0.ID AS id,t0.post_parent AS parentId,t1.meta_value AS type,t2.meta_value AS raw';
34 $sql .= ' FROM ' . $wpdb->posts . ' AS t0';
35 $sql .= ' LEFT JOIN ' . $wpdb->postmeta . " as t1 ON t1.post_id=ID AND t1.meta_key='type'";
36 $sql .= ' LEFT JOIN ' . $wpdb->postmeta . " as t2 ON t2.post_id=ID AND t2.meta_key='raw'";
37 $sql .= " WHERE t0.post_type='wdf_scan_item' AND t0.post_status = 'issue'";
38 $sql .= ' GROUP BY ID';
39
40 return $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
41 }
42
43 /**
44 * Retrieves all ignored scan items based on provided IDs.
45 *
46 * @param array $ids Array of IDs to retrieve.
47 *
48 * @return array Array of ignored scan items.
49 */
50 private function find_all_scan_ignored_items( $ids ) {
51 global $wpdb;
52
53 if ( ! is_array( $ids ) ) {
54 return array();
55 }
56
57 $valid_ids = array();
58 foreach ( $ids as $id ) {
59 if ( ! is_int( $id ) && ! ( is_string( $id ) && ctype_digit( $id ) ) ) {
60 continue;
61 }
62
63 $id = filter_var( $id, FILTER_VALIDATE_INT );
64 if ( false !== $id && 0 < $id ) {
65 $valid_ids[ $id ] = $id;
66 }
67 }
68 $ids = array_values( $valid_ids );
69 if ( array() === $ids ) {
70 return array();
71 }
72
73 $placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
74 $sql = 'SELECT t0.ID AS id,t0.post_parent AS parentId,t1.meta_value AS type,t2.meta_value AS raw';
75 $sql .= ' FROM ' . $wpdb->posts . ' AS t0';
76 $sql .= ' LEFT JOIN ' . $wpdb->postmeta . " as t1 ON t1.post_id=ID AND t1.meta_key='type'";
77 $sql .= ' LEFT JOIN ' . $wpdb->postmeta . " as t2 ON t2.post_id=ID AND t2.meta_key='raw'";
78 $sql .= " WHERE t0.post_type='wdf_scan_item' AND t0.post_status = 'ignored'";
79 $sql .= " AND t0.ID IN ({$placeholders})";
80
81 // SQL is prepared above. so we will ignore prepare warning.
82 return $wpdb->get_results( $wpdb->prepare( $sql, $ids ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
83 }
84
85 /**
86 * Adapts old scan data format to the new format.
87 *
88 * @param array $old_data Old scan data to adapt.
89 *
90 * @return array Adapted scan data.
91 */
92 private function adapt_scan_data( $old_data ) {
93 $raw = '';
94 switch ( $old_data['type'] ) {
95 case 'vuln':
96 $new_type = Scan_Item::TYPE_VULNERABILITY;
97 if ( isset( $old_data['raw'] ) && '' !== $old_data['raw'] ) {
98 $raw = maybe_unserialize( $old_data['raw'] );
99 /**
100 * Different $raw['slug'] values depending on the type:
101 * 'type' => 'theme', 'slug' => {THEME_SLUG}, ...
102 * 'type' => 'plugin', 'slug' => {PLUGIN_SLUG}, ...
103 * 'type' => 'WordPress', 'slug' => 'WordPress', ...
104 */
105 $file = is_array( $raw ) && isset( $raw['slug'] ) ? $raw['slug'] : '';
106 } else {
107 $file = '';
108 }
109
110 if ( is_array( $raw ) && 'WordPress' !== $raw['type'] ) {
111 $raw_data = array(
112 'type' => $raw['type'],
113 'slug' => $raw['slug'],
114 'base_slug' => $raw['slug'],
115 'version' => '',
116 'name' => $raw['slug'],
117 'bugs' => $raw['bugs'],
118 );
119 } else {
120 $raw_data = $old_data['raw'];
121 }
122 break;
123 case 'content':
124 $new_type = Scan_Item::TYPE_SUSPICIOUS;
125 if ( isset( $old_data['raw'] ) && '' !== $old_data['raw'] ) {
126 $raw = maybe_unserialize( $old_data['raw'] );
127 $file = is_array( $raw ) && isset( $raw['file'] ) ? $raw['file'] : '';
128 } else {
129 $file = '';
130 }
131
132 $raw_data = is_array( $raw ) && isset( $raw['meta'] ) ? $raw['meta'] : array();
133 $raw_data['file'] = $file;
134 break;
135 case 'core':
136 default:
137 $new_type = Scan_Item::TYPE_INTEGRITY;
138 if ( isset( $old_data['raw'] ) && '' !== $old_data['raw'] ) {
139 $raw = maybe_unserialize( $old_data['raw'] );
140 $file = is_array( $raw ) && isset( $raw['file'] ) ? $raw['file'] : '';
141 } else {
142 $file = '';
143 }
144
145 if ( is_array( $raw ) && isset( $raw['type'] ) ) {
146 $short_desc = 'unknown' === $raw['type'] ? 'unversion' : $raw['type'];
147 } else {
148 $short_desc = '';
149 }
150 $raw_data = array(
151 'file' => $file,
152 'type' => $short_desc,
153 );
154 break;
155 }
156
157 return array(
158 'type' => $new_type,
159 'file' => $file,
160 'raw' => $raw_data,
161 );
162 }
163
164 /**
165 * Saves scan issue data to the database.
166 *
167 * @param array $issue_items Array of issue items to save.
168 * @param Scan $model Scan model instance to use for saving.
169 */
170 private function save_scan_issue_data( $issue_items, $model ) {
171 if ( ! is_array( $issue_items ) || array() === $issue_items ) {
172 return;
173 }
174 // Get existed issues.
175 $existed_issues = $model->get_issues( null, Scan_Item::STATUS_ACTIVE );
176 $unique_arr = array();
177 foreach ( $issue_items as $item ) {
178 $data = $this->adapt_scan_data( $item );
179 if ( '' === $data['file'] ) {
180 continue;
181 }
182 if ( is_array( $existed_issues ) && array() !== $existed_issues ) {
183 // Scan was started in version > 2.3.2.
184 foreach ( $existed_issues as $issue ) {
185 if ( Scan_Item::TYPE_VULNERABILITY === $data['type'] ) {
186 $file_name = $issue->raw_data['base_slug'] ?? '';
187 } else {
188 $file_name = $issue->raw_data['file'] ?? '';
189 }
190 // Check for uniqueness of elements.
191 if ( $data['file'] !== $file_name && ! in_array( $file_name, $unique_arr, true ) ) {
192 $unique_arr[] = $file_name;
193 $model->add_item( $data['type'], $data['raw'], Scan_Item::STATUS_ACTIVE );
194 }
195 }
196 } else {
197 $file_name = $data['file'] ?? $data['raw']['file'] ?? false;
198 // Scan wasn't start.
199 if ( $file_name && ! in_array( $file_name, $unique_arr, true ) ) {
200 $unique_arr[] = $file_name;
201 $model->add_item( $data['type'], $data['raw'], Scan_Item::STATUS_ACTIVE );
202 }
203 }
204 }
205 }
206
207 /**
208 * Saves ignored scan data to the database.
209 *
210 * @param array $ignored_items Array of ignored items to save.
211 * @param Scan $model Scan model instance to use for saving.
212 */
213 private function save_scan_ignored_data( $ignored_items, $model ) {
214 if ( ! is_array( $ignored_items ) || array() === $ignored_items ) {
215 return;
216 }
217 // Get INDEXER of ignored issues.
218 $index_lists = get_site_option( Scan::IGNORE_INDEXER, array() );
219 // Get existed ignored issues.
220 $existed_issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE );
221 foreach ( $ignored_items as $item ) {
222 $data = $this->adapt_scan_data( $item );
223 if ( '' === $data['file'] ) {
224 continue;
225 }
226 if ( is_array( $existed_issues ) && array() !== $existed_issues ) {
227 // Scan was started in version > 2.3.2 .
228 foreach ( $existed_issues as $issue ) {
229 if ( Scan_Item::TYPE_VULNERABILITY === $data['type'] ) {
230 $file_name = $issue->raw_data['base_slug'] ?? '';
231 } else {
232 $file_name = $issue->raw_data['file'] ?? '';
233 }
234 // Check for uniqueness of elements.
235 if ( $data['file'] !== $file_name && ! in_array( $file_name, $index_lists, true ) ) {
236 if ( isset( $data['file'] ) ) {
237 $index_lists[] = $data['file'];
238 } elseif ( isset( $data['raw']['slug'] ) ) {
239 $index_lists[] = $data['raw']['slug'];
240 }
241 $model->add_item( $data['type'], $data['raw'], Scan_Item::STATUS_IGNORE );
242 }
243 }
244 } else {
245 $file_name = $data['file'] ?? $data['raw']['slug'] ?? false;
246 // Scan wasn't start.
247 if ( $file_name && ! in_array( $file_name, $index_lists, true ) ) {
248 $index_lists[] = $file_name;
249 $model->add_item( $data['type'], $data['raw'], Scan_Item::STATUS_IGNORE );
250 }
251 }
252 }
253 // Update IGNORE_INDEXER.
254 $model->update_ignore_list( $index_lists );
255 }
256
257 /**
258 * Retrieves all scan issue data.
259 *
260 * @return array Array of all scan issue data.
261 */
262 public function get_scan_issue_data() {
263 return $this->find_all_scan_issue_items();
264 }
265
266 /**
267 * Retrieves all scan ignored data.
268 *
269 * @return array Array of all scan ignored data.
270 */
271 public function get_scan_ignored_data() {
272 $ids = get_option( self::IGNORE_LIST, false );
273 if ( ! is_array( $ids ) ) {
274 $ids = get_site_option( self::IGNORE_LIST, array() );
275 if ( ! is_array( $ids ) ) {
276 return array();
277 }
278 }
279
280 return $this->find_all_scan_ignored_items( $ids );
281 }
282
283 /**
284 * Runs a simple scan.
285 *
286 * @return int ID of the scan.
287 */
288 private function run_simlpe_scan() {
289 $scan_component = wd_di()->get( \WP_Defender\Component\Scan::class );
290 $scan_model = wd_di()->get( Scan::class );
291 // The simplest data.
292 $scan_model->percent = 100;
293 $scan_model->total_tasks = 1;
294 $scan_model->task_checkpoint = '';
295 $scan_model->status = Scan::STATUS_FINISH;
296 $scan_model->date_start = gmdate( 'Y-m-d H:i:s' );
297 $scan_model->date_end = gmdate( 'Y-m-d H:i:s' );
298 $scan_model->is_automation = false;
299 $last_id = $scan_model->save();
300 $this->log( 'Scan ID during data migration: ' . $last_id, Scan_Controller::SCAN_LOG );
301 $scan_component->advanced_scan_actions( $scan_model );
302
303 return $last_id;
304 }
305
306 /**
307 * Migrates scan data from old format to new format.
308 *
309 * @param array $issue_list List of issue items to migrate.
310 * @param array $ignored_list List of ignored items to migrate.
311 */
312 public function migrate_scan_data( $issue_list, $ignored_list ) {
313 $scan = Scan::get_active();
314 if ( is_object( $scan ) ) {
315 $this->log( 'Scan is still running', Scan_Controller::SCAN_LOG );
316
317 return;
318 }
319 $model = Scan::get_last();
320 // Scan was started and finished before.
321 if ( is_object( $model ) && ! is_wp_error( $model ) ) {
322 $this->save_scan_ignored_data( $ignored_list, $model );
323 $this->save_scan_issue_data( $issue_list, $model );
324 } else {
325 /**
326 * Scan hasn't started before. Next steps:
327 * create Scan record,
328 * no send notification,
329 * create Scan items.
330 */
331 $this->run_simlpe_scan();
332 $model = Scan::get_last();
333 if ( is_object( $model ) && ! is_wp_error( $model ) ) {
334 $this->save_scan_ignored_data( $ignored_list, $model );
335 $this->save_scan_issue_data( $issue_list, $model );
336 }
337 }
338 }
339
340 /**
341 * Removes old scan data from the database.
342 *
343 * @param array $issue_list List of issue items to remove.
344 * @param array $ignored_list List of ignored items to remove.
345 */
346 public function remove_old_scan_data( $issue_list, $ignored_list ) {
347 // Delete the list of scan issues.
348 if ( is_array( $issue_list ) && array() !== $issue_list ) {
349 $parent_post_id = (int) $issue_list[0]['parentId'];
350 foreach ( $issue_list as $key => $issue ) {
351 // Delete scan items.
352 wp_delete_post( (int) $issue['id'] );
353 }
354 // Delete scan.
355 wp_delete_post( $parent_post_id );
356 }
357 // Delete the ignored list.
358 if ( is_array( $ignored_list ) && array() !== $ignored_list ) {
359 $parent_post_id = (int) $ignored_list[0]['parentId'];
360 foreach ( $ignored_list as $key => $item ) {
361 // Delete scan items.
362 wp_delete_post( (int) $item['id'] );
363 }
364 // Delete scan.
365 wp_delete_post( $parent_post_id );
366
367 delete_site_option( self::IGNORE_LIST );
368 delete_option( self::IGNORE_LIST );
369 }
370 }
371
372 /**
373 * Changes the onboarding status in the database.
374 */
375 public function change_onboarding_status() {
376 update_site_option( 'wp_defender_shown_activator', true );
377 }
378
379 /**
380 * Decrypts data using a public key.
381 *
382 * @param string $encrypt_data Data to decrypt.
383 * @param string $key Public key to use for decryption.
384 *
385 * @return bool|string Decrypted data or false on failure.
386 */
387 private static function decrypt_data_with_pub_key( $encrypt_data, $key ) {
388 // This is not obfuscation. Just decode a base64-encoded string.
389 $str = base64_decode( $encrypt_data, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
390 if ( ! $str ) {
391 return false;
392 }
393 $cipher = 'aes-256-cbc';
394 $iv_len = openssl_cipher_iv_length( $cipher );
395 $iv = substr( $str, 0, $iv_len );
396 $ciphertext_raw = substr( $str, $iv_len + 32 );
397
398 return openssl_decrypt( $ciphertext_raw, $cipher, $key, OPENSSL_RAW_DATA, $iv );
399 }
400
401 /**
402 * Decrypts data using a stored public key.
403 *
404 * @param string $data Data to decrypt.
405 *
406 * @return bool|string Decrypted data or false on failure.
407 */
408 public static function get_decrypted_data_with_pub_key( $data ) {
409 global $wp_filesystem;
410 // Initialize the WP filesystem, no more using 'file-put-contents' function.
411 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
412 require_once ABSPATH . '/wp-admin/includes/file.php';
413 WP_Filesystem();
414 }
415 $path_to_pub_key = __DIR__ . '/def.key';
416 if ( ! file_exists( $path_to_pub_key ) ) {
417 return false;
418 }
419 $key = $wp_filesystem->get_contents( $path_to_pub_key );
420
421 return false !== $key ? self::decrypt_data_with_pub_key( $data, $key ) : '';
422 }
423 }
424