| @@ -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. |