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/behavior/scan/class-plugin-integrity.php +86 -47 5.4.0 → 6.3.0 View file →
@@ -18,8 +18,9 @@
18 18 use WP_Defender\Integrations\Smush;
19 19 use WP_Defender\Model\Setting\Scan as Scan_Settings;
20 20 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
21 21 use WP_Defender\Controller\Scan as Scan_Controller;
22 +use WP_Defender\Component\Scan as Scan_Component;
22 23
23 24 /**
24 25 * It is responsible for performing integrity checks on plugins.
25 26 */
@@ -27,11 +28,12 @@
27 28
28 29 use IO;
29 30 use Plugin;
30 31
31 - public const URL_PLUGIN_VCS = 'https://downloads.wordpress.org/plugin-checksums/';
32 - public const PLUGIN_SLUGS = 'wd_plugin_slugs_changes';
33 - public const PLUGIN_PREMIUM_SLUGS = 'wd_plugin_premium_slugs';
32 + public const URL_PLUGIN_VCS = 'https://downloads.wordpress.org/plugin-checksums/';
33 + public const PLUGIN_SLUGS = 'wd_plugin_slugs_changes';
34 + public const PLUGIN_SLUGS_CHECKPOINT = 'wd_plugin_slugs_changes_checkpoint';
35 + public const PLUGIN_PREMIUM_SLUGS = 'wd_plugin_premium_slugs';
34 36 /**
35 37 * List of premium plugin slugs.
36 38 *
37 39 * @var array
@@ -44,10 +46,10 @@
44 46 * @param string $slug The slug to check.
45 47 *
46 48 * @return bool Returns true if the slug is valid, false otherwise.
47 49 */
48 - private function is_valid_wporg_slug( $slug ): bool {
49 - return ! empty( $slug ) && '.' !== $slug;
50 + private function is_valid_wporg_slug( string $slug ): bool {
51 + return '' !== $slug && '.' !== $slug;
50 52 }
51 53
52 54 /**
53 55 * Plucks a specific field from an array of objects or arrays and returns a new array with the plucked values.
@@ -52,23 +54,21 @@
52 54 /**
53 55 * Plucks a specific field from an array of objects or arrays and returns a new array with the plucked values.
54 56 *
55 57 * @param array $collection The array to pluck values from.
56 - * @param string $field The field to pluck from each element in the array.
57 58 * @param string $prefix Optional. A prefix to add to each key in the new array. Default is an empty string.
58 59 *
59 60 * @return array The new array with the plucked values.
60 61 */
61 - private function pluck( $collection, $field, $prefix = '' ): array {
62 + private function pluck( array $collection, $prefix = '' ): array {
62 63 $new_list = array();
63 64
64 65 foreach ( $collection as $key => $value ) {
65 66 $prefix_key = defender_replace_line( $prefix . $key );
66 -
67 - if ( is_object( $value ) ) {
68 - $new_list[ $prefix_key ] = $value->$field;
69 - } else {
70 - $new_list[ $prefix_key ] = $value[ $field ];
67 + if ( is_object( $value ) && property_exists( $value, 'md5' ) ) {
68 + $new_list[ $prefix_key ] = $value->md5;
69 + } elseif ( is_array( $value ) && isset( $value['md5'] ) ) {
70 + $new_list[ $prefix_key ] = $value['md5'];
71 71 }
72 72 }
73 73
74 74 return $new_list;
@@ -74,21 +74,43 @@
74 74 return $new_list;
75 75 }
76 76
77 77 /**
78 - * Retrieve hash for a given plugin from wordpress.org.
78 + * Check if a plugin is a WPMUDEV pro plugin by checking for WDP ID header.
79 79 *
80 + * @param string $plugin_file Plugin main file path.
81 + *
82 + * @return bool True if it's a WPMUDEV pro plugin, false otherwise.
83 + */
84 + private function is_wpmudev_pro_plugin( $plugin_file ): bool {
85 + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file );
86 + return array_key_exists( 'WDP ID', $plugin_data ) && '' !== $plugin_data['WDP ID'];
87 + }
88 +
89 + /**
90 + * Retrieve hash for a given plugin from wp.org.
91 + * Excludes premium plugins and WPMUDEV pro plugins from hash retrieval.
92 + *
80 93 * @param string $slug Plugin folder.
81 94 * @param string $version Plugin version.
95 + * @param string $plugin_file Plugin main file path.
82 96 *
83 97 * @return array
84 98 */
85 - private function get_plugin_hash( $slug, $version ): array {
99 + private function get_plugin_hash( string $slug, $version, $plugin_file = '' ): array {
86 100 if ( ! $this->is_valid_wporg_slug( $slug ) ) {
87 101 $this->premium_slugs[] = $slug;
88 102
89 103 return array();
90 104 }
105 +
106 + // Check if this is a WPMUDEV pro plugin that upgraded from free version.
107 + if ( '' !== $plugin_file && $this->is_wpmudev_pro_plugin( $plugin_file ) ) {
108 + $this->premium_slugs[] = $slug;
109 +
110 + return array();
111 + }
112 +
91 113 // Get original from wp.org e.g. https://downloads.wordpress.org/plugin-checksums/hello-dolly/1.6.json.
92 114 $response = wp_remote_get( self::URL_PLUGIN_VCS . $slug . '/' . $version . '.json' );
93 115
94 116 if ( is_wp_error( $response ) ) {
@@ -112,9 +134,9 @@
112 134 }
113 135
114 136 $data = json_decode( $body, true );
115 137
116 - if ( ! $data || empty( $data['files'] ) ) {
138 + if ( ! isset( $data['files'] ) || ! is_array( $data['files'] ) || array() === $data['files'] ) {
117 139 return array();
118 140 }
119 141
120 142 if ( ! $this->is_likely_wporg_slug( $slug ) ) {
@@ -122,9 +144,9 @@
122 144
123 145 return array();
124 146 }
125 147
126 - return $this->pluck( $data['files'], 'md5', $slug . DIRECTORY_SEPARATOR );
148 + return $this->pluck( $data['files'], $slug . DIRECTORY_SEPARATOR );
127 149 }
128 150
129 151 /**
130 152 * Fetch the checksums.
@@ -132,35 +154,27 @@
132 154 * @return array
133 155 */
134 156 protected function plugin_checksum(): array {
135 157 $all_plugin_hashes = array();
136 - /**
137 - * Exclude plugin slugs.
138 - *
139 - * @param array $slugs Slugs of excluded plugins.
140 - *
141 - * @since 3.1.0
142 - */
143 - $excluded_slugs = (array) apply_filters( 'wd_scan_excluded_plugin_slugs', array() );
158 + $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
144 159
145 - foreach ( $this->get_plugins() as $slug => $plugin ) {
146 - if ( false === strpos( $slug, '/' ) ) {
147 - // Todo: get correct hashes for single-file plugins.
148 - // Separate case for 'Hello Dolly'.
149 - $base_slug = 'hello.php' === $slug ? 'hello-dolly' : $slug;
150 - } else {
151 - $base_slug = explode( '/', $slug );
152 - $base_slug = array_shift( $base_slug );
153 - }
160 + if ( Scan_Component::are_actioned_plugins( $actioned_plugins ) ) {
161 + foreach ( $actioned_plugins as $slug => $plugin_data ) {
162 + $plugin_hashes = $this->get_plugin_hash( $slug, $plugin_data['Version'], $plugin_data['Slug'] );
154 163
155 - if ( in_array( $base_slug, $excluded_slugs, true ) ) {
156 - continue;
164 + if ( array() !== $plugin_hashes ) {
165 + $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
166 + }
157 167 }
168 + } else {
169 + // An emergency option if for some reason the plugin's quick data is not saved in the first step.
170 + foreach ( $this->get_plugins() as $plugin_file => $plugin ) {
171 + $slug = $this->get_plugin_slug_by( $plugin_file );
172 + $plugin_hashes = $this->get_plugin_hash( $slug, $plugin['Version'], $plugin_file );
158 173
159 - $plugin_hashes = $this->get_plugin_hash( $base_slug, $plugin['Version'] );
160 -
161 - if ( ! empty( $plugin_hashes ) ) {
162 - $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
174 + if ( array() !== $plugin_hashes ) {
175 + $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
176 + }
163 177 }
164 178 }
165 179
166 180 return $all_plugin_hashes;
@@ -185,9 +199,14 @@
185 199 $settings->filesize
186 200 );
187 201
188 202 $plugin_files = $plugins->get_dir_tree();
189 - $plugin_files = array_filter( $plugin_files );
203 + $plugin_files = array_filter(
204 + $plugin_files,
205 + function ( $value ) {
206 + return is_string( $value ) && '' !== trim( $value );
207 + }
208 + );
190 209
191 210 $plugin_files = new ArrayIterator( $plugin_files );
192 211 $checksums = $this->plugin_checksum();
193 212 $timer = new Timer();
@@ -193,8 +212,11 @@
193 212 $timer = new Timer();
194 213 $model = $this->owner->scan;
195 214 $pos = (int) $model->task_checkpoint;
196 215 $plugin_files->seek( $pos );
216 + if ( 0 === $pos ) {
217 + delete_site_option( self::PLUGIN_SLUGS_CHECKPOINT );
218 + }
197 219 $slugs_of_edited_plugins = array();
198 220 $integration_smush = wd_di()->get( Smush::class );
199 221 $exist_smush_images = $integration_smush->exist_image_table();
200 222 while ( $plugin_files->valid() ) {
@@ -285,16 +307,15 @@
285 307 // We will check if we have any ignore issue from last scan, so we can bring it here.
286 308 $last = Scan::get_last();
287 309 if ( is_object( $last ) ) {
288 310 $ignored_issues = $last->get_issues( Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::STATUS_IGNORE );
289 - foreach ( $ignored_issues as $issue ) {
290 - $model->add_item( Scan_Item::TYPE_PLUGIN_CHECK, $issue->raw_data, Scan_Item::STATUS_IGNORE );
291 - }
311 + $model->carry_forward_ignored_issues( Scan_Item::TYPE_PLUGIN_CHECK, $ignored_issues, 'file' );
292 312 }
293 313 // Done, reset this, so we can use later.
294 314 $model->task_checkpoint = '';
295 315 }
296 316 $model->save();
317 +
297 318 /**
298 319 * Reduce false positive reports. Check it only if enabled 'Suspicious code' option.
299 320 *
300 321 * @since 2.4.10
@@ -299,17 +320,35 @@
299 320 *
300 321 * @since 2.4.10
301 322 */
302 323 if ( ( new Scan_Settings() )->scan_malware ) {
303 - if ( ! empty( $slugs_of_edited_plugins ) ) {
304 - update_site_option( self::PLUGIN_SLUGS, array_unique( $slugs_of_edited_plugins ) );
305 - }
306 - if ( ! empty( $this->premium_slugs ) ) {
324 + $this->persist_changed_plugin_slugs( $slugs_of_edited_plugins, ! $plugin_files->valid() );
325 + if ( array() !== $this->premium_slugs ) {
307 326 update_site_option( self::PLUGIN_PREMIUM_SLUGS, $this->premium_slugs );
308 327 }
309 328 }
310 329
311 330 return ! $plugin_files->valid();
331 + }
332 +
333 + /**
334 + * Persist changed plugin slugs across checkpoint scan batches.
335 + *
336 + * @param array $batch_slugs Slugs found in the current batch.
337 + * @param bool $is_complete Whether the plugin integrity task has completed.
338 + *
339 + * @return void
340 + */
341 + private function persist_changed_plugin_slugs( array $batch_slugs, bool $is_complete ): void {
342 + $checkpoint_slugs = (array) get_site_option( self::PLUGIN_SLUGS_CHECKPOINT, array() );
343 + $changed_slugs = array_values( array_unique( array_merge( $checkpoint_slugs, $batch_slugs ) ) );
344 +
345 + if ( $is_complete ) {
346 + update_site_option( self::PLUGIN_SLUGS, $changed_slugs );
347 + delete_site_option( self::PLUGIN_SLUGS_CHECKPOINT );
348 + } else {
349 + update_site_option( self::PLUGIN_SLUGS_CHECKPOINT, $changed_slugs );
350 + }
312 351 }
313 352
314 353 /**
315 354 * Checks if given file is a valid image.