PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
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 5.8.1, at src/traits/plugin.php

556 lines 15.0 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\Component\Quarantine as Quarantine_Component;
12 use WP_Defender\Model\Scan_Item;
13 use WP_Filesystem_Base;
14
15 trait Plugin {
16
17 /**
18 * Version URL.
19 *
20 * @var string Versioned path of the plugin file.
21 */
22 private $url_plugin_vcs = 'https://plugins.svn.wordpress.org/%s/tags/%s/%s';
23
24 /**
25 * Trunk URL.
26 *
27 * @var string Trunk path of the plugin file.
28 */
29 private $url_plugin_vcs_trunk = 'https://plugins.svn.wordpress.org/%s/trunk/%s';
30
31 /**
32 * Key for org slugs transient.
33 *
34 * @var string
35 */
36 public static string $org_slugs = 'wp-defender-org-slugs';
37
38 /**
39 * Key for org responses transient.
40 *
41 * @var string
42 */
43 public static string $org_responses = 'wp-defender-org-responses';
44
45 /**
46 * Get all installed plugins.
47 *
48 * @return array
49 */
50 public function get_plugins(): array {
51 if ( ! function_exists( 'get_plugins' ) ) {
52 require_once ABSPATH . 'wp-admin/includes/plugin.php';
53 }
54
55 // WordPress caches this internally.
56 return get_plugins();
57 }
58
59 /**
60 * Get plugin slug.
61 *
62 * @param string $main_plugin_file The main plugin file name.
63 *
64 * @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'.
65 */
66 public function get_plugin_slug_by( string $main_plugin_file ): string {
67 $dir = dirname( $main_plugin_file );
68 if ( '.' === $dir ) {
69 // If this is a single plugin file in the plugin root, take the file name.
70 $slug = explode( '/', $main_plugin_file );
71 $slug = array_shift( $slug );
72 } else {
73 $slug = $dir;
74 }
75
76 return $slug;
77 }
78
79 /**
80 * Get all slugs.
81 *
82 * @return array
83 */
84 public function get_plugin_slugs(): array {
85 $slugs = array();
86 foreach ( $this->get_plugins() as $main_plugin_file => $plugin ) {
87 $slugs[] = $this->get_plugin_slug_by( $main_plugin_file );
88 }
89
90 return $slugs;
91 }
92
93 /**
94 * Get plugin details by given slug.
95 *
96 * @param string $plugin_slug The plugin slug.
97 *
98 * @return array
99 */
100 public function get_plugin_details_by( string $plugin_slug ): array {
101 foreach ( $this->get_plugins() as $main_plugin_file => $plugin_data ) {
102 if ( $this->get_plugin_slug_by( $main_plugin_file ) === $plugin_slug ) {
103 $plugin_data['slug'] = $plugin_slug;
104
105 return $plugin_data;
106 }
107 }
108
109 return array();
110 }
111
112 /**
113 * Check if a plugin exists on WordPress.org repository by slug.
114 *
115 * @param string $slug The plugin slug.
116 *
117 * @return array|WP_Error
118 */
119 public function ping_wp_org_by_plugin_slug( string $slug ) {
120 $url = 'https://api.wordpress.org/plugins/info/1.0/' . $slug . '.json';
121 $http_args = array(
122 'timeout' => 15,
123 'sslverify' => false, // Many hosts have no updated CA bundle.
124 'user-agent' => defender_get_own_user_agent(),
125 );
126
127 return wp_remote_get( $url, $http_args );
128 }
129
130 /**
131 * Handle a response from wp.org by the plugin slug.
132 *
133 * @param string $slug The plugin slug.
134 *
135 * @return array
136 */
137 public function handle_wp_org_response_by( string $slug ): array {
138 $default_error = esc_html__( 'Plugin unknown error.', 'defender-security' );
139 $response = $this->ping_wp_org_by_plugin_slug( $slug );
140 if ( is_wp_error( $response ) ) {
141 return array(
142 'message' => $default_error,
143 'success' => false,
144 );
145 }
146
147 $results = json_decode( wp_remote_retrieve_body( $response ), true );
148 if ( ! is_array( $results ) ) {
149 return array(
150 'message' => esc_html__( 'Plugin response is not in expected format.', 'defender-security' ),
151 'success' => false,
152 );
153 }
154 // Sometimes wp.org can return 404th response code for an existed plugin but with 'closed' status.
155 if ( 200 !== wp_remote_retrieve_response_code( $response )
156 && isset( $results['error'] ) && 'closed' !== $results['error']
157 ) {
158 return array(
159 'message' => $results['description'] ?? $results['error'],
160 'success' => false,
161 );
162 }
163
164 return array(
165 'success' => true,
166 'message' => '',
167 'body' => $results,
168 );
169 }
170
171 /**
172 * Check if a plugin exists on WordPress.org repository.
173 *
174 * @param string $slug The slug of the plugin.
175 *
176 * @return array Index message: describes what happened.
177 * Index data: Plugin data from wp.org.
178 * Index success: true if plugin in WordPress plugin repository, else false.
179 */
180 public function check_plugin_on_wp_org( string $slug ): array {
181 // Check if data exists in transient.
182 $transient = get_site_transient( self::$org_responses ) ?? array();
183 if ( isset( $transient[ $slug ] ) ) {
184 return array(
185 'message' => __( 'Plugin exists in WordPress repository.', 'defender-security' ),
186 'data' => $transient[ $slug ],
187 'success' => true,
188 );
189 }
190
191 $results = $this->handle_wp_org_response_by( $slug );
192 if ( ! $results['success'] ) {
193 return $results;
194 }
195 // Cache the relevant data in the transient.
196 $filtered_results = array_intersect_key( $results, array_flip( array( 'homepage', 'version', 'author' ) ) );
197 $transient[ $slug ] = $filtered_results;
198 set_site_transient( self::$org_responses, $transient, WEEK_IN_SECONDS );
199
200 return array(
201 'message' => esc_html__( 'Plugin exists in WordPress repository.', 'defender-security' ),
202 'data' => $results,
203 'success' => true,
204 );
205 }
206
207 /**
208 * Check for readme.txt or readme.md files.
209 * Sometimes plugins from wp.org don't have readme.txt file.
210 *
211 * @param string $readme_file_path Path to readme.* file.
212 *
213 * @return bool
214 */
215 public function check_by_readme_file( string $readme_file_path ): bool {
216 if ( file_exists( $readme_file_path ) && is_readable( $readme_file_path ) ) {
217 global $wp_filesystem;
218 // Initialize the WP filesystem, no more using 'file-put-contents' function.
219 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
220 require_once ABSPATH . '/wp-admin/includes/file.php';
221 WP_Filesystem();
222 }
223 $contents = trim( (string) $wp_filesystem->get_contents( $readme_file_path ) );
224
225 if ( false !== strpos( $contents, '===' ) ) {
226 return true;
227 }
228
229 if ( false !== strpos( $contents, '#' ) ) {
230 return true;
231 }
232 }
233
234 return false;
235 }
236
237 /**
238 * Is this plugin likely to be a WordPress.org plugin?
239 *
240 * @param string|null $slug Plugin directory name.
241 *
242 * @return bool Return true if likely, else false.
243 */
244 public function is_likely_wporg_slug( ?string $slug ): bool {
245 // Ensure slug is valid.
246 if ( ! is_string( $slug ) || '' === $slug ) {
247 return false;
248 }
249 // Check if data exists in transient.
250 $transient = get_site_transient( self::$org_slugs );
251 if ( ! is_array( $transient ) ) {
252 $transient = array();
253 }
254 if ( isset( $transient[ $slug ] ) ) {
255 return $transient[ $slug ];
256 }
257 // Criterion #1: Check if plugin name contains 'Pro' mention.
258 $actioned_plugins = get_site_option( \WP_Defender\Component\Scan::PLUGINS_ACTIONED );
259 if ( is_array( $actioned_plugins ) && isset( $actioned_plugins[ $slug ] )
260 && preg_match( '/ Pro$/i', $actioned_plugins[ $slug ]['Name'] )
261 ) {
262 return false;
263 }
264
265 $plugin_path = $this->get_abs_plugin_path_by_slug( $slug ) . DIRECTORY_SEPARATOR;
266 // Some plugins do not follow the readme file naming rule. Let's list the possible names.
267 $readme_files = array(
268 'readme.txt',
269 'README.txt',
270 'readme.md',
271 'README.md',
272 );
273 // Criterion #2: Check if there is Readme file.
274 foreach ( $readme_files as $readme_file ) {
275 if ( $this->check_by_readme_file( $plugin_path . $readme_file ) ) {
276 $transient[ $slug ] = true;
277 // Collect plugin slugs that are on wp.org.
278 set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
279
280 return true;
281 }
282 }
283
284 return false;
285 }
286
287 /**
288 * Check if the plugin is active.
289 *
290 * @param string $file_path Absolute file path to the plugin file.
291 *
292 * @return bool
293 */
294 public function is_active_plugin( $file_path ): bool {
295 $path_data = explode( DIRECTORY_SEPARATOR, plugin_basename( $file_path ) );
296 if ( '' !== $path_data[0] ) {
297 $plugin_slug = $path_data[0];
298 } else {
299 return false;
300 }
301
302 $active = false;
303 foreach ( $this->get_plugins() as $slug => $data ) {
304 if ( $plugin_slug === $slug || 0 === strpos( $slug, $plugin_slug ) ) {
305 $active = is_multisite() ? is_plugin_active_for_network( $slug ) : is_plugin_active( $slug );
306 break;
307 }
308 }
309
310 return $active;
311 }
312
313 /**
314 * Get plugin header from any file of the plugin.
315 *
316 * @param string $file_path Absolute file path to the plugin file.
317 *
318 * @return array Return plugin details as array.
319 */
320 public function get_plugin_headers( $file_path ): array {
321 $plugin_directory = $this->get_plugin_directory_name( $file_path );
322
323 return get_plugins( DIRECTORY_SEPARATOR . $plugin_directory );
324 }
325
326 /**
327 * Get plugin directory name.
328 *
329 * @param string $file_path Absolute file path to the plugin file.
330 *
331 * @return string Return plugin directory name.
332 */
333 public function get_plugin_directory_name( $file_path ): string {
334 return (string) strtok( plugin_basename( $file_path ), '/' );
335 }
336
337 /**
338 * Get plugin relative path.
339 *
340 * @param string $file_path Absolute file path to the plugin file.
341 *
342 * @return string Return plugin relative path.
343 */
344 public function get_plugin_relative_path( $file_path ): string {
345 // Normalize the directory separators for cross-platform compatibility.
346 $normalized_path = str_replace( '\\', '/', plugin_basename( $file_path ) );
347 // Initialize strtok.
348 strtok( $normalized_path, '/' );
349 // Now fetch the next token, if available, otherwise return an empty string.
350 return (string) strtok( '' );
351 }
352
353 /**
354 * Get plugin absolute path.
355 *
356 * @param string $slug Plugin slug.
357 *
358 * @return string
359 */
360 public function get_abs_plugin_path_by_slug( string $slug = '' ): string {
361 if ( defined( 'WP_PLUGIN_DIR' ) ) {
362 return wp_normalize_path( WP_PLUGIN_DIR ) . DIRECTORY_SEPARATOR . $slug;
363 }
364
365 return wp_normalize_path( WP_CONTENT_DIR ) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $slug;
366 }
367
368 /**
369 * Check file exists at wp.org svn.
370 *
371 * @param string $url URL of the file.
372 *
373 * @return boolean Return true for file exists else false.
374 */
375 private function is_origin_file_exists( $url ): bool {
376 $result = wp_remote_head( $url );
377
378 if ( ! is_wp_error( $result ) ) {
379 $http_status_code = wp_remote_retrieve_response_code( $result );
380
381 return 200 === $http_status_code;
382 }
383
384 return false;
385 }
386
387 /**
388 * Generates the URL for a specific version of a plugin file.
389 *
390 * @param string $directory_name The name of the directory.
391 * @param string $version The version of the file.
392 * @param string $file_path The path of the file.
393 *
394 * @return string The URL of the versioned file.
395 */
396 private function get_version_url( string $directory_name, string $version, string $file_path ): string {
397 return sprintf(
398 $this->url_plugin_vcs,
399 $directory_name,
400 $version,
401 $file_path
402 );
403 }
404
405 /**
406 * Generate the URL for the trunk of a plugin based on the directory name and file path.
407 *
408 * @param string $directory_name The name of the directory.
409 * @param string $file_path The path of the file.
410 *
411 * @return string The URL of the trunk of the plugin.
412 */
413 private function get_trunk_url( string $directory_name, string $file_path ): string {
414 return sprintf(
415 $this->url_plugin_vcs_trunk,
416 $directory_name,
417 $file_path
418 );
419 }
420
421 /**
422 * Generate the URL for a file based on the directory name, version, and file path.
423 *
424 * @param string $directory_name The name of the directory.
425 * @param string $version The version of the file.
426 * @param string $file_path The path of the file.
427 *
428 * @return string The URL of the file.
429 */
430 private function get_file_url( string $directory_name, string $version, string $file_path ): string {
431 // First try to use the version number provided.
432 $file_url = $this->get_version_url( $directory_name, $version, $file_path );
433 if ( $this->is_origin_file_exists( $file_url ) ) {
434 return $file_url;
435 }
436 // If that does not exist, try to use the version number with a `.0` suffix.
437 $file_url = $this->get_version_url( $directory_name, $version . '.0', $file_path );
438 if ( $this->is_origin_file_exists( $file_url ) ) {
439 return $file_url;
440 }
441 // If all else fails, use the trunk of the plugin.
442 return $this->get_trunk_url( $directory_name, $file_path );
443 }
444
445 /**
446 * Retrieves the content of a URL by downloading it and reading the contents of the downloaded file.
447 *
448 * @param string $url The URL to download the content from.
449 *
450 * @return string|WP_Error The content of the URL if successful, otherwise a WP_Error object.
451 */
452 private function get_url_content( $url ) {
453 global $wp_filesystem;
454 // Initialize the WP filesystem, no more using 'file-put-contents' function.
455 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
456 require_once ABSPATH . '/wp-admin/includes/file.php';
457 WP_Filesystem();
458 }
459 if ( ! function_exists( 'download_url' ) ) {
460 $ds = DIRECTORY_SEPARATOR;
461 require_once ABSPATH . 'wp-admin' . $ds . 'includes' . $ds . 'file.php';
462 }
463
464 $tmp = download_url( $url );
465 if ( is_wp_error( $tmp ) ) {
466 return $tmp;
467 }
468
469 $content = $wp_filesystem->get_contents( $tmp );
470 wp_delete_file( $tmp );
471
472 return $content;
473 }
474
475 /**
476 * Quarantine a plugin.
477 *
478 * @param string $parent_action Parent action.
479 * @param Scan_Item $scan_item Scan item model object.
480 *
481 * @return array|WP_Error
482 */
483 public function quarantine( string $parent_action, Scan_Item $scan_item ) {
484 if ( ! class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
485 return new WP_Error(
486 'DEFENDER_PRO_ONLY_FEATURE',
487 defender_quarantine_pro_only()
488 );
489 }
490
491 $quarantine_component = wd_di()->get( Quarantine_Component::class );
492
493 return $quarantine_component->quarantine_file( $scan_item, $parent_action );
494 }
495
496 /**
497 * Detect if the plugin file is quarantinable.
498 *
499 * @param string $file_path File path.
500 *
501 * @return bool Return true if file is in wp.org plugin else false.
502 */
503 private function is_quarantinable( $file_path ): bool {
504 return $this->is_likely_wporg_slug(
505 $this->get_plugin_directory_name(
506 $file_path
507 )
508 );
509 }
510
511 /**
512 * Get a list of the active plugins.
513 *
514 * @return array
515 */
516 private function get_active_and_valid_plugin_files(): array {
517 $active_plugins = is_multisite() ? wp_get_active_network_plugins() : array();
518 $active_plugins = array_merge( $active_plugins, wp_get_active_and_valid_plugins() );
519
520 return array_unique( $active_plugins );
521 }
522
523 /**
524 * Get a plugin name.
525 *
526 * @param string $plugin_file Absolute path to the main plugin file.
527 *
528 * @return string
529 */
530 private function get_plugin_name( $plugin_file ): string {
531 $plugin_data = get_plugin_data( $plugin_file );
532
533 return isset( $plugin_data['Name'] ) ? $plugin_data['Name'] : '';
534 }
535
536 /**
537 * Get active plugin names.
538 *
539 * @return array
540 */
541 public function get_active_plugin_names(): array {
542 $active_plugins = $this->get_active_and_valid_plugin_files();
543 $plugin_names = array();
544
545 // Convert paths to names.
546 foreach ( $active_plugins as $plugin_file ) {
547 $plugin_name = $this->get_plugin_name( $plugin_file );
548 if ( '' !== $plugin_name ) {
549 $plugin_names[] = $plugin_name;
550 }
551 }
552
553 return $plugin_names;
554 }
555 }
556