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/traits/plugin.php +256 -66 5.4.1 → 6.3.0 View file →
@@ -7,9 +7,10 @@
7 7
8 8 namespace WP_Defender\Traits;
9 9
10 10 use WP_Error;
11 -use WP_Defender\Component\Quarantine as Quarantine_Component;
11 +use WP_Defender\Model\Scan_Item;
12 +use WP_Filesystem_Base;
12 13
13 14 trait Plugin {
14 15
15 16 /**
@@ -54,8 +55,28 @@
54 55 return get_plugins();
55 56 }
56 57
57 58 /**
59 + * Get plugin slug.
60 + *
61 + * @param string $main_plugin_file The main plugin file name.
62 + *
63 + * @return string Plugin dir name, e.g. 'plugin-test', or a file name with the extension if this is a single plugin file, e.g. 'plugin-test.php'.
64 + */
65 + public function get_plugin_slug_by( string $main_plugin_file ): string {
66 + $dir = dirname( $main_plugin_file );
67 + if ( '.' === $dir ) {
68 + // If this is a single plugin file in the plugin root, take the file name.
69 + $slug = explode( '/', $main_plugin_file );
70 + $slug = array_shift( $slug );
71 + } else {
72 + $slug = $dir;
73 + }
74 +
75 + return $slug;
76 + }
77 +
78 + /**
58 79 * Get all slugs.
59 80 *
60 81 * @return array
61 82 */
@@ -60,11 +81,10 @@
60 81 * @return array
61 82 */
62 83 public function get_plugin_slugs(): array {
63 84 $slugs = array();
64 - foreach ( $this->get_plugins() as $slug => $plugin ) {
65 - $base_slug = explode( '/', $slug );
66 - $slugs[] = array_shift( $base_slug );
85 + foreach ( $this->get_plugins() as $main_plugin_file => $plugin ) {
86 + $slugs[] = $this->get_plugin_slug_by( $main_plugin_file );
67 87 }
68 88
69 89 return $slugs;
70 90 }
@@ -69,59 +89,57 @@
69 89 return $slugs;
70 90 }
71 91
72 92 /**
73 - * Retrieve plugin base directory.
93 + * Get plugin details by given slug.
74 94 *
75 - * @return string
95 + * @param string $plugin_slug The plugin slug.
96 + *
97 + * @return array
76 98 */
77 - public function get_plugin_base_dir(): string {
78 - if ( defined( 'WP_PLUGIN_DIR' ) ) {
79 - return wp_normalize_path( WP_PLUGIN_DIR . '/' );
99 + public function get_plugin_details_by( string $plugin_slug ): array {
100 + foreach ( $this->get_plugins() as $main_plugin_file => $plugin_data ) {
101 + if ( $this->get_plugin_slug_by( $main_plugin_file ) === $plugin_slug ) {
102 + $plugin_data['slug'] = $plugin_slug;
103 +
104 + return $plugin_data;
105 + }
80 106 }
81 107
82 - return wp_normalize_path( WP_CONTENT_DIR . '/plugins/' );
108 + return array();
83 109 }
84 110
85 111 /**
86 - * Check if a plugin exists on WordPress.org repository.
112 + * Check if a plugin exists on WordPress.org repository by slug.
87 113 *
88 - * @param string $slug The slug of the plugin.
114 + * @param string $slug The plugin slug.
89 115 *
90 - * @return array Index message: describes what happened.
91 - * Index data: Plugin data from wp.org.
92 - * Index success: true if plugin in WordPress plugin repository
93 - * else false.
116 + * @return array|WP_Error
94 117 */
95 - public function check_plugin_on_wp_org( string $slug ): array {
96 - // Check if data exists in transient.
97 - $transient = get_site_transient( self::$org_responses ) ?? array();
98 - if ( isset( $transient[ $slug ] ) ) {
99 - return array(
100 - 'message' => __( 'Plugin exists in WordPress repository.', 'defender-security' ),
101 - 'data' => $transient[ $slug ],
102 - 'success' => true,
103 - );
104 - }
105 -
118 + public function ping_wp_org_by_plugin_slug( string $slug ) {
106 119 $url = 'https://api.wordpress.org/plugins/info/1.0/' . $slug . '.json';
107 120 $http_args = array(
108 121 'timeout' => 15,
109 - 'sslverify' => false, // Many hosts have no updated CA bundle.
110 - 'user-agent' => 'Defender/' . DEFENDER_VERSION,
122 + 'sslverify' => true,
123 + 'user-agent' => defender_get_own_user_agent(),
111 124 );
112 - $response = wp_remote_get( $url, $http_args );
113 125
114 - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
115 - $body_json = json_decode( wp_remote_retrieve_body( $response ), true );
126 + return wp_remote_get( $url, $http_args );
127 + }
116 128
117 - $message = esc_html__( 'Plugin unknown error.', 'defender-security' );
118 - if ( is_array( $body_json ) && isset( $body_json['error'] ) ) {
119 - $message = $body_json['error'];
120 - }
121 -
129 + /**
130 + * Handle a response from wp.org by the plugin slug.
131 + *
132 + * @param string $slug The plugin slug.
133 + *
134 + * @return array
135 + */
136 + public function handle_wp_org_response_by( string $slug ): array {
137 + $default_error = esc_html__( 'Plugin unknown error.', 'defender-security' );
138 + $response = $this->ping_wp_org_by_plugin_slug( $slug );
139 + if ( is_wp_error( $response ) ) {
122 140 return array(
123 - 'message' => $message,
141 + 'message' => $default_error,
124 142 'success' => false,
125 143 );
126 144 }
127 145
@@ -131,14 +149,49 @@
131 149 'message' => esc_html__( 'Plugin response is not in expected format.', 'defender-security' ),
132 150 'success' => false,
133 151 );
134 152 }
135 - if ( ! empty( $results['closed'] ) ) {
153 + // Sometimes wp.org can return 404th response code for an existed plugin but with 'closed' status.
154 + if ( 200 !== wp_remote_retrieve_response_code( $response )
155 + && isset( $results['error'] ) && 'closed' !== $results['error']
156 + ) {
136 157 return array(
137 - 'message' => $response['description'],
158 + 'message' => $results['description'] ?? $results['error'],
138 159 'success' => false,
139 160 );
140 161 }
162 +
163 + return array(
164 + 'success' => true,
165 + 'message' => '',
166 + 'body' => $results,
167 + );
168 + }
169 +
170 + /**
171 + * Check if a plugin exists on WordPress.org repository.
172 + *
173 + * @param string $slug The slug of the plugin.
174 + *
175 + * @return array Index message: describes what happened.
176 + * Index data: Plugin data from wp.org.
177 + * Index success: true if plugin in WordPress plugin repository, else false.
178 + */
179 + public function check_plugin_on_wp_org( string $slug ): array {
180 + // Check if data exists in transient.
181 + $transient = get_site_transient( self::$org_responses ) ?? array();
182 + if ( isset( $transient[ $slug ] ) ) {
183 + return array(
184 + 'message' => __( 'Plugin exists in WordPress repository.', 'defender-security' ),
185 + 'data' => $transient[ $slug ],
186 + 'success' => true,
187 + );
188 + }
189 +
190 + $results = $this->handle_wp_org_response_by( $slug );
191 + if ( ! $results['success'] ) {
192 + return $results;
193 + }
141 194 // Cache the relevant data in the transient.
142 195 $filtered_results = array_intersect_key( $results, array_flip( array( 'homepage', 'version', 'author' ) ) );
143 196 $transient[ $slug ] = $filtered_results;
144 197 set_site_transient( self::$org_responses, $transient, WEEK_IN_SECONDS );
@@ -151,23 +204,23 @@
151 204 }
152 205
153 206 /**
154 207 * Check for readme.txt or readme.md files.
155 - * Sometimes plugins from wp.org don't have readme.txt file, e.g. 'wp-crontrol'.
208 + * Sometimes plugins from wp.org don't have readme.txt file.
156 209 *
157 - * @param string $readme_file Path to readme.* file.
210 + * @param string $readme_file_path Path to readme.* file.
158 211 *
159 212 * @return bool
160 213 */
161 - public function check_by_readme_file( $readme_file ): bool {
162 - if ( file_exists( $readme_file ) && is_readable( $readme_file ) ) {
214 + public function check_by_readme_file( string $readme_file_path ): bool {
215 + if ( file_exists( $readme_file_path ) && is_readable( $readme_file_path ) ) {
163 216 global $wp_filesystem;
164 217 // Initialize the WP filesystem, no more using 'file-put-contents' function.
165 - if ( empty( $wp_filesystem ) ) {
218 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
166 219 require_once ABSPATH . '/wp-admin/includes/file.php';
167 220 WP_Filesystem();
168 221 }
169 - $contents = trim( (string) $wp_filesystem->get_contents( $readme_file ) );
222 + $contents = trim( (string) $wp_filesystem->get_contents( $readme_file_path ) );
170 223
171 224 if ( false !== strpos( $contents, '===' ) ) {
172 225 return true;
173 226 }
@@ -182,15 +235,15 @@
182 235
183 236 /**
184 237 * Is this plugin likely to be a WordPress.org plugin?
185 238 *
186 - * @param string|null $slug of the plugin.
239 + * @param string|null $slug Plugin directory name.
187 240 *
188 241 * @return bool Return true if likely, else false.
189 242 */
190 243 public function is_likely_wporg_slug( ?string $slug ): bool {
191 244 // Ensure slug is valid.
192 - if ( empty( $slug ) || ! is_string( $slug ) ) {
245 + if ( ! is_string( $slug ) || '' === $slug ) {
193 246 return false;
194 247 }
195 248 // Check if data exists in transient.
196 249 $transient = get_site_transient( self::$org_slugs );
@@ -199,19 +252,29 @@
199 252 }
200 253 if ( isset( $transient[ $slug ] ) ) {
201 254 return $transient[ $slug ];
202 255 }
256 + // Criterion #1: Check if plugin name contains 'Pro' mention.
257 + $actioned_plugins = get_site_option( \WP_Defender\Component\Scan::PLUGINS_ACTIONED );
258 + if ( is_array( $actioned_plugins ) && isset( $actioned_plugins[ $slug ] )
259 + && preg_match( '/ Pro$/i', $actioned_plugins[ $slug ]['Name'] )
260 + ) {
261 + return false;
262 + }
203 263
204 - $plugin_path = $this->get_plugin_base_dir() . $slug . '/';
264 + $plugin_path = $this->get_abs_plugin_path_by_slug( $slug ) . DIRECTORY_SEPARATOR;
265 + // Some plugins do not follow the readme file naming rule. Let's list the possible names.
205 266 $readme_files = array(
206 267 'readme.txt',
268 + 'README.txt',
207 269 'readme.md',
208 270 'README.md',
209 271 );
210 - // Check each Readme-case.
272 + // Criterion #2: Check if there is Readme file.
211 273 foreach ( $readme_files as $readme_file ) {
212 274 if ( $this->check_by_readme_file( $plugin_path . $readme_file ) ) {
213 275 $transient[ $slug ] = true;
276 + // Collect plugin slugs that are on wp.org.
214 277 set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
215 278
216 279 return true;
217 280 }
@@ -216,11 +279,8 @@
216 279 return true;
217 280 }
218 281 }
219 282
220 - $transient[ $slug ] = false;
221 - set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
222 -
223 283 return false;
224 284 }
225 285
226 286 /**
@@ -231,9 +291,9 @@
231 291 * @return bool
232 292 */
233 293 public function is_active_plugin( $file_path ): bool {
234 294 $path_data = explode( DIRECTORY_SEPARATOR, plugin_basename( $file_path ) );
235 - if ( ! empty( $path_data[0] ) ) {
295 + if ( '' !== $path_data[0] ) {
236 296 $plugin_slug = $path_data[0];
237 297 } else {
238 298 return false;
239 299 }
@@ -289,8 +349,23 @@
289 349 return (string) strtok( '' );
290 350 }
291 351
292 352 /**
353 + * Get plugin absolute path.
354 + *
355 + * @param string $slug Plugin slug.
356 + *
357 + * @return string
358 + */
359 + public function get_abs_plugin_path_by_slug( string $slug = '' ): string {
360 + if ( defined( 'WP_PLUGIN_DIR' ) ) {
361 + return wp_normalize_path( WP_PLUGIN_DIR ) . DIRECTORY_SEPARATOR . $slug;
362 + }
363 +
364 + return wp_normalize_path( WP_CONTENT_DIR ) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $slug;
365 + }
366 +
367 + /**
293 368 * Check file exists at wp.org svn.
294 369 *
295 370 * @param string $url URL of the file.
296 371 *
@@ -375,9 +450,9 @@
375 450 */
376 451 private function get_url_content( $url ) {
377 452 global $wp_filesystem;
378 453 // Initialize the WP filesystem, no more using 'file-put-contents' function.
379 - if ( empty( $wp_filesystem ) ) {
454 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
380 455 require_once ABSPATH . '/wp-admin/includes/file.php';
381 456 WP_Filesystem();
382 457 }
383 458 if ( ! function_exists( 'download_url' ) ) {
@@ -398,23 +473,17 @@
398 473
399 474 /**
400 475 * Quarantine a plugin.
401 476 *
402 - * @param string $parent_action Parent action.
477 + * @param string $parent_action Parent action.
478 + * @param Scan_Item $scan_item Scan item model object.
403 479 *
404 480 * @return array|WP_Error
405 481 */
406 - public function quarantine( string $parent_action ) {
407 - if ( ! class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
408 - return new WP_Error(
409 - 'DEFENDER_PRO_ONLY_FEATURE',
410 - defender_quarantine_pro_only()
411 - );
412 - }
482 + public function quarantine( string $parent_action, Scan_Item $scan_item ) {
483 + $quarantine_component = wd_di()->get( \WP_Defender\Component\Quarantine::class );
413 484
414 - $quarantine_component = wd_di()->get( Quarantine_Component::class );
415 -
416 - return $quarantine_component->quarantine_file( $this->owner, $parent_action );
485 + return $quarantine_component->quarantine_file( $scan_item, $parent_action );
417 486 }
418 487
419 488 /**
420 489 * Detect if the plugin file is quarantinable.
@@ -427,7 +496,128 @@
427 496 return $this->is_likely_wporg_slug(
428 497 $this->get_plugin_directory_name(
429 498 $file_path
430 499 )
500 + );
501 + }
502 +
503 + /**
504 + * Get a list of the active plugins.
505 + *
506 + * @return array
507 + */
508 + private function get_active_and_valid_plugin_files(): array {
509 + $active_plugins = is_multisite() ? wp_get_active_network_plugins() : array();
510 + $active_plugins = array_merge( $active_plugins, wp_get_active_and_valid_plugins() );
511 +
512 + return array_unique( $active_plugins );
513 + }
514 +
515 + /**
516 + * Get a plugin name.
517 + *
518 + * @param string $plugin_file Absolute path to the main plugin file.
519 + *
520 + * @return string
521 + */
522 + private function get_plugin_name( $plugin_file ): string {
523 + $plugin_data = get_plugin_data( $plugin_file );
524 +
525 + return isset( $plugin_data['Name'] ) ? $plugin_data['Name'] : '';
526 + }
527 +
528 + /**
529 + * Get active plugin names.
530 + *
531 + * @return array
532 + */
533 + public function get_active_plugin_names(): array {
534 + $active_plugins = $this->get_active_and_valid_plugin_files();
535 + $plugin_names = array();
536 +
537 + // Convert paths to names.
538 + foreach ( $active_plugins as $plugin_file ) {
539 + $plugin_name = $this->get_plugin_name( $plugin_file );
540 + if ( '' !== $plugin_name ) {
541 + $plugin_names[] = $plugin_name;
542 + }
543 + }
544 +
545 + return $plugin_names;
546 + }
547 +
548 + /**
549 + * Data for the “update Defender” notification bubble (React).
550 + *
551 + * Uses the update_plugins site transient (WordPress.org for Free; WPMU DEV Dashboard merges
552 + * Pro updates into the same transient when that plugin is active).
553 + *
554 + * @return array{
555 + * available:bool,
556 + * newVersion:string,
557 + * learnMoreUrl:string,
558 + * updateUrl:string,
559 + * canUpdate:bool,
560 + * currentVersion:string,
561 + * releaseDate:string
562 + * }
563 + */
564 + public function get_plugin_update_notice_data(): array {
565 + $basename = DEFENDER_PLUGIN_BASENAME;
566 +
567 + $default_learn_more = defender_is_wp_org_version()
568 + ? 'https://wordpress.org/plugins/defender-security/'
569 + : 'https://wpmudev.com/project/wp-defender/#changelog_all';
570 +
571 + $learn_more_url = $default_learn_more;
572 + $new_version = '';
573 + $has_update = false;
574 +
575 + $updates = get_site_transient( 'update_plugins' );
576 + if ( is_object( $updates ) && isset( $updates->response[ $basename ] ) ) {
577 + $pkg = $updates->response[ $basename ];
578 + if ( is_object( $pkg ) ) {
579 + $has_update = true;
580 + $new_version = isset( $pkg->new_version ) ? (string) $pkg->new_version : '';
581 + if ( isset( $pkg->url ) && is_string( $pkg->url ) && '' !== $pkg->url ) {
582 + $learn_more_url = $pkg->url;
583 + }
584 + } elseif ( is_array( $pkg ) ) {
585 + $has_update = true;
586 + $new_version = isset( $pkg['new_version'] ) ? (string) $pkg['new_version'] : '';
587 + if ( isset( $pkg['url'] ) && is_string( $pkg['url'] ) && '' !== $pkg['url'] ) {
588 + $learn_more_url = $pkg['url'];
589 + }
590 + }
591 + }
592 +
593 + $can_update = $has_update && current_user_can( 'update_plugins' );
594 + $update_url = '';
595 + if ( $can_update ) {
596 + // Build a plain URL for wp_localize_script (wp_nonce_url() HTML-escapes and breaks JSON).
597 + $update_url = add_query_arg(
598 + array(
599 + '_wpnonce' => wp_create_nonce( 'upgrade-plugin_' . $basename ),
600 + 'action' => 'upgrade-plugin',
601 + 'plugin' => $basename,
602 + ),
603 + self_admin_url( 'update.php' )
604 + );
605 + }
606 +
607 + $release_date_raw = defined( 'DEFENDER_RELEASE_DATE' ) ? DEFENDER_RELEASE_DATE : '';
608 + $timestamp = strtotime( $release_date_raw );
609 + $release_date = false !== $timestamp
610 + ? date_i18n( get_option( 'date_format' ), $timestamp )
611 + : '';
612 +
613 + return array(
614 + 'available' => $has_update,
615 + 'newVersion' => $new_version,
616 + 'learnMoreUrl' => esc_url_raw( $learn_more_url ),
617 + 'updateUrl' => esc_url_raw( $update_url ),
618 + 'canUpdate' => $can_update && '' !== $update_url,
619 + 'currentVersion' => DEFENDER_VERSION,
620 + 'releaseDate' => $release_date,
431 621 );
432 622 }
433 623 }