PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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 / traits / plugin.php

plugin.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/traits/plugin.php

624 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper functions for plugin related tasks.
4 *
5 * @package WP_Defender\Traits
6 */
7
8 namespace WP_Defender\Traits;
9
10 use WP_Error;
11 use WP_Defender\Model\Scan_Item;
12 use WP_Filesystem_Base;
13
14 trait Plugin {
15
16 /**
17 * Version URL.
18 *
19 * @var string Versioned path of the plugin file.
20 */
21 private $url_plugin_vcs = 'https://plugins.svn.wordpress.org/%s/tags/%s/%s';
22
23 /**
24 * Trunk URL.
25 *
26 * @var string Trunk path of the plugin file.
27 */
28 private $url_plugin_vcs_trunk = 'https://plugins.svn.wordpress.org/%s/trunk/%s';
29
30 /**
31 * Key for org slugs transient.
32 *
33 * @var string
34 */
35 public static string $org_slugs = 'wp-defender-org-slugs';
36
37 /**
38 * Key for org responses transient.
39 *
40 * @var string
41 */
42 public static string $org_responses = 'wp-defender-org-responses';
43
44 /**
45 * Get all installed plugins.
46 *
47 * @return array
48 */
49 public function get_plugins(): array {
50 if ( ! function_exists( 'get_plugins' ) ) {
51 require_once ABSPATH . 'wp-admin/includes/plugin.php';
52 }
53
54 // WordPress caches this internally.
55 return get_plugins();
56 }
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 /**
79 * Get all slugs.
80 *
81 * @return array
82 */
83 public function get_plugin_slugs(): array {
84 $slugs = array();
85 foreach ( $this->get_plugins() as $main_plugin_file => $plugin ) {
86 $slugs[] = $this->get_plugin_slug_by( $main_plugin_file );
87 }
88
89 return $slugs;
90 }
91
92 /**
93 * Get plugin details by given slug.
94 *
95 * @param string $plugin_slug The plugin slug.
96 *
97 * @return array
98 */
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 }
106 }
107
108 return array();
109 }
110
111 /**
112 * Check if a plugin exists on WordPress.org repository by slug.
113 *
114 * @param string $slug The plugin slug.
115 *
116 * @return array|WP_Error
117 */
118 public function ping_wp_org_by_plugin_slug( string $slug ) {
119 $url = 'https://api.wordpress.org/plugins/info/1.0/' . $slug . '.json';
120 $http_args = array(
121 'timeout' => 15,
122 'sslverify' => true,
123 'user-agent' => defender_get_own_user_agent(),
124 );
125
126 return wp_remote_get( $url, $http_args );
127 }
128
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 ) ) {
140 return array(
141 'message' => $default_error,
142 'success' => false,
143 );
144 }
145
146 $results = json_decode( wp_remote_retrieve_body( $response ), true );
147 if ( ! is_array( $results ) ) {
148 return array(
149 'message' => esc_html__( 'Plugin response is not in expected format.', 'defender-security' ),
150 'success' => false,
151 );
152 }
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 ) {
157 return array(
158 'message' => $results['description'] ?? $results['error'],
159 'success' => false,
160 );
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 }
194 // Cache the relevant data in the transient.
195 $filtered_results = array_intersect_key( $results, array_flip( array( 'homepage', 'version', 'author' ) ) );
196 $transient[ $slug ] = $filtered_results;
197 set_site_transient( self::$org_responses, $transient, WEEK_IN_SECONDS );
198
199 return array(
200 'message' => esc_html__( 'Plugin exists in WordPress repository.', 'defender-security' ),
201 'data' => $results,
202 'success' => true,
203 );
204 }
205
206 /**
207 * Check for readme.txt or readme.md files.
208 * Sometimes plugins from wp.org don't have readme.txt file.
209 *
210 * @param string $readme_file_path Path to readme.* file.
211 *
212 * @return bool
213 */
214 public function check_by_readme_file( string $readme_file_path ): bool {
215 if ( file_exists( $readme_file_path ) && is_readable( $readme_file_path ) ) {
216 global $wp_filesystem;
217 // Initialize the WP filesystem, no more using 'file-put-contents' function.
218 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
219 require_once ABSPATH . '/wp-admin/includes/file.php';
220 WP_Filesystem();
221 }
222 $contents = trim( (string) $wp_filesystem->get_contents( $readme_file_path ) );
223
224 if ( false !== strpos( $contents, '===' ) ) {
225 return true;
226 }
227
228 if ( false !== strpos( $contents, '#' ) ) {
229 return true;
230 }
231 }
232
233 return false;
234 }
235
236 /**
237 * Is this plugin likely to be a WordPress.org plugin?
238 *
239 * @param string|null $slug Plugin directory name.
240 *
241 * @return bool Return true if likely, else false.
242 */
243 public function is_likely_wporg_slug( ?string $slug ): bool {
244 // Ensure slug is valid.
245 if ( ! is_string( $slug ) || '' === $slug ) {
246 return false;
247 }
248 // Check if data exists in transient.
249 $transient = get_site_transient( self::$org_slugs );
250 if ( ! is_array( $transient ) ) {
251 $transient = array();
252 }
253 if ( isset( $transient[ $slug ] ) ) {
254 return $transient[ $slug ];
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 }
263
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.
266 $readme_files = array(
267 'readme.txt',
268 'README.txt',
269 'readme.md',
270 'README.md',
271 );
272 // Criterion #2: Check if there is Readme file.
273 foreach ( $readme_files as $readme_file ) {
274 if ( $this->check_by_readme_file( $plugin_path . $readme_file ) ) {
275 $transient[ $slug ] = true;
276 // Collect plugin slugs that are on wp.org.
277 set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
278
279 return true;
280 }
281 }
282
283 return false;
284 }
285
286 /**
287 * Check if the plugin is active.
288 *
289 * @param string $file_path Absolute file path to the plugin file.
290 *
291 * @return bool
292 */
293 public function is_active_plugin( $file_path ): bool {
294 $path_data = explode( DIRECTORY_SEPARATOR, plugin_basename( $file_path ) );
295 if ( '' !== $path_data[0] ) {
296 $plugin_slug = $path_data[0];
297 } else {
298 return false;
299 }
300
301 $active = false;
302 foreach ( $this->get_plugins() as $slug => $data ) {
303 if ( $plugin_slug === $slug || 0 === strpos( $slug, $plugin_slug ) ) {
304 $active = is_multisite() ? is_plugin_active_for_network( $slug ) : is_plugin_active( $slug );
305 break;
306 }
307 }
308
309 return $active;
310 }
311
312 /**
313 * Get plugin header from any file of the plugin.
314 *
315 * @param string $file_path Absolute file path to the plugin file.
316 *
317 * @return array Return plugin details as array.
318 */
319 public function get_plugin_headers( $file_path ): array {
320 $plugin_directory = $this->get_plugin_directory_name( $file_path );
321
322 return get_plugins( DIRECTORY_SEPARATOR . $plugin_directory );
323 }
324
325 /**
326 * Get plugin directory name.
327 *
328 * @param string $file_path Absolute file path to the plugin file.
329 *
330 * @return string Return plugin directory name.
331 */
332 public function get_plugin_directory_name( $file_path ): string {
333 return (string) strtok( plugin_basename( $file_path ), '/' );
334 }
335
336 /**
337 * Get plugin relative path.
338 *
339 * @param string $file_path Absolute file path to the plugin file.
340 *
341 * @return string Return plugin relative path.
342 */
343 public function get_plugin_relative_path( $file_path ): string {
344 // Normalize the directory separators for cross-platform compatibility.
345 $normalized_path = str_replace( '\\', '/', plugin_basename( $file_path ) );
346 // Initialize strtok.
347 strtok( $normalized_path, '/' );
348 // Now fetch the next token, if available, otherwise return an empty string.
349 return (string) strtok( '' );
350 }
351
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 /**
368 * Check file exists at wp.org svn.
369 *
370 * @param string $url URL of the file.
371 *
372 * @return boolean Return true for file exists else false.
373 */
374 private function is_origin_file_exists( $url ): bool {
375 $result = wp_remote_head( $url );
376
377 if ( ! is_wp_error( $result ) ) {
378 $http_status_code = wp_remote_retrieve_response_code( $result );
379
380 return 200 === $http_status_code;
381 }
382
383 return false;
384 }
385
386 /**
387 * Generates the URL for a specific version of a plugin file.
388 *
389 * @param string $directory_name The name of the directory.
390 * @param string $version The version of the file.
391 * @param string $file_path The path of the file.
392 *
393 * @return string The URL of the versioned file.
394 */
395 private function get_version_url( string $directory_name, string $version, string $file_path ): string {
396 return sprintf(
397 $this->url_plugin_vcs,
398 $directory_name,
399 $version,
400 $file_path
401 );
402 }
403
404 /**
405 * Generate the URL for the trunk of a plugin based on the directory name and file path.
406 *
407 * @param string $directory_name The name of the directory.
408 * @param string $file_path The path of the file.
409 *
410 * @return string The URL of the trunk of the plugin.
411 */
412 private function get_trunk_url( string $directory_name, string $file_path ): string {
413 return sprintf(
414 $this->url_plugin_vcs_trunk,
415 $directory_name,
416 $file_path
417 );
418 }
419
420 /**
421 * Generate the URL for a file based on the directory name, version, and file path.
422 *
423 * @param string $directory_name The name of the directory.
424 * @param string $version The version of the file.
425 * @param string $file_path The path of the file.
426 *
427 * @return string The URL of the file.
428 */
429 private function get_file_url( string $directory_name, string $version, string $file_path ): string {
430 // First try to use the version number provided.
431 $file_url = $this->get_version_url( $directory_name, $version, $file_path );
432 if ( $this->is_origin_file_exists( $file_url ) ) {
433 return $file_url;
434 }
435 // If that does not exist, try to use the version number with a `.0` suffix.
436 $file_url = $this->get_version_url( $directory_name, $version . '.0', $file_path );
437 if ( $this->is_origin_file_exists( $file_url ) ) {
438 return $file_url;
439 }
440 // If all else fails, use the trunk of the plugin.
441 return $this->get_trunk_url( $directory_name, $file_path );
442 }
443
444 /**
445 * Retrieves the content of a URL by downloading it and reading the contents of the downloaded file.
446 *
447 * @param string $url The URL to download the content from.
448 *
449 * @return string|WP_Error The content of the URL if successful, otherwise a WP_Error object.
450 */
451 private function get_url_content( $url ) {
452 global $wp_filesystem;
453 // Initialize the WP filesystem, no more using 'file-put-contents' function.
454 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
455 require_once ABSPATH . '/wp-admin/includes/file.php';
456 WP_Filesystem();
457 }
458 if ( ! function_exists( 'download_url' ) ) {
459 $ds = DIRECTORY_SEPARATOR;
460 require_once ABSPATH . 'wp-admin' . $ds . 'includes' . $ds . 'file.php';
461 }
462
463 $tmp = download_url( $url );
464 if ( is_wp_error( $tmp ) ) {
465 return $tmp;
466 }
467
468 $content = $wp_filesystem->get_contents( $tmp );
469 wp_delete_file( $tmp );
470
471 return $content;
472 }
473
474 /**
475 * Quarantine a plugin.
476 *
477 * @param string $parent_action Parent action.
478 * @param Scan_Item $scan_item Scan item model object.
479 *
480 * @return array|WP_Error
481 */
482 public function quarantine( string $parent_action, Scan_Item $scan_item ) {
483 $quarantine_component = wd_di()->get( \WP_Defender\Component\Quarantine::class );
484
485 return $quarantine_component->quarantine_file( $scan_item, $parent_action );
486 }
487
488 /**
489 * Detect if the plugin file is quarantinable.
490 *
491 * @param string $file_path File path.
492 *
493 * @return bool Return true if file is in wp.org plugin else false.
494 */
495 private function is_quarantinable( $file_path ): bool {
496 return $this->is_likely_wporg_slug(
497 $this->get_plugin_directory_name(
498 $file_path
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,
621 );
622 }
623 }
624