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

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