PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.0
Defender Security – Malware Scanner, Login Security & Firewall v5.7.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 5.7.0, at src/component/class-legacy-versions.php

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