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

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