PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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
defender-security / src / traits / plugin.php

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

434 lines 11.6 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
13 trait Plugin {
14
15 /**
16 * Version URL.
17 *
18 * @var string Versioned path of the plugin file.
19 */
20 private $url_plugin_vcs = 'https://plugins.svn.wordpress.org/%s/tags/%s/%s';
21
22 /**
23 * Trunk URL.
24 *
25 * @var string Trunk path of the plugin file.
26 */
27 private $url_plugin_vcs_trunk = 'https://plugins.svn.wordpress.org/%s/trunk/%s';
28
29 /**
30 * Key for org slugs transient.
31 *
32 * @var string
33 */
34 public static string $org_slugs = 'wp-defender-org-slugs';
35
36 /**
37 * Key for org responses transient.
38 *
39 * @var string
40 */
41 public static string $org_responses = 'wp-defender-org-responses';
42
43 /**
44 * Get all installed plugins.
45 *
46 * @return array
47 */
48 public function get_plugins(): array {
49 if ( ! function_exists( 'get_plugins' ) ) {
50 require_once ABSPATH . 'wp-admin/includes/plugin.php';
51 }
52
53 // WordPress caches this internally.
54 return get_plugins();
55 }
56
57 /**
58 * Get all slugs.
59 *
60 * @return array
61 */
62 public function get_plugin_slugs(): array {
63 $slugs = array();
64 foreach ( $this->get_plugins() as $slug => $plugin ) {
65 $base_slug = explode( '/', $slug );
66 $slugs[] = array_shift( $base_slug );
67 }
68
69 return $slugs;
70 }
71
72 /**
73 * Retrieve plugin base directory.
74 *
75 * @return string
76 */
77 public function get_plugin_base_dir(): string {
78 if ( defined( 'WP_PLUGIN_DIR' ) ) {
79 return wp_normalize_path( WP_PLUGIN_DIR . '/' );
80 }
81
82 return wp_normalize_path( WP_CONTENT_DIR . '/plugins/' );
83 }
84
85 /**
86 * Check if a plugin exists on WordPress.org repository.
87 *
88 * @param string $slug The slug of the plugin.
89 *
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.
94 */
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
106 $url = 'https://api.wordpress.org/plugins/info/1.0/' . $slug . '.json';
107 $http_args = array(
108 'timeout' => 15,
109 'sslverify' => false, // Many hosts have no updated CA bundle.
110 'user-agent' => 'Defender/' . DEFENDER_VERSION,
111 );
112 $response = wp_remote_get( $url, $http_args );
113
114 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
115 $body_json = json_decode( wp_remote_retrieve_body( $response ), true );
116
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
122 return array(
123 'message' => $message,
124 'success' => false,
125 );
126 }
127
128 $results = json_decode( wp_remote_retrieve_body( $response ), true );
129 if ( ! is_array( $results ) ) {
130 return array(
131 'message' => esc_html__( 'Plugin response is not in expected format.', 'defender-security' ),
132 'success' => false,
133 );
134 }
135 if ( ! empty( $results['closed'] ) ) {
136 return array(
137 'message' => $response['description'],
138 'success' => false,
139 );
140 }
141 // Cache the relevant data in the transient.
142 $filtered_results = array_intersect_key( $results, array_flip( array( 'homepage', 'version', 'author' ) ) );
143 $transient[ $slug ] = $filtered_results;
144 set_site_transient( self::$org_responses, $transient, WEEK_IN_SECONDS );
145
146 return array(
147 'message' => esc_html__( 'Plugin exists in WordPress repository.', 'defender-security' ),
148 'data' => $results,
149 'success' => true,
150 );
151 }
152
153 /**
154 * Check for readme.txt or readme.md files.
155 * Sometimes plugins from wp.org don't have readme.txt file, e.g. 'wp-crontrol'.
156 *
157 * @param string $readme_file Path to readme.* file.
158 *
159 * @return bool
160 */
161 public function check_by_readme_file( $readme_file ): bool {
162 if ( file_exists( $readme_file ) && is_readable( $readme_file ) ) {
163 global $wp_filesystem;
164 // Initialize the WP filesystem, no more using 'file-put-contents' function.
165 if ( empty( $wp_filesystem ) ) {
166 require_once ABSPATH . '/wp-admin/includes/file.php';
167 WP_Filesystem();
168 }
169 $contents = trim( (string) $wp_filesystem->get_contents( $readme_file ) );
170
171 if ( false !== strpos( $contents, '===' ) ) {
172 return true;
173 }
174
175 if ( false !== strpos( $contents, '#' ) ) {
176 return true;
177 }
178 }
179
180 return false;
181 }
182
183 /**
184 * Is this plugin likely to be a WordPress.org plugin?
185 *
186 * @param string|null $slug of the plugin.
187 *
188 * @return bool Return true if likely, else false.
189 */
190 public function is_likely_wporg_slug( ?string $slug ): bool {
191 // Ensure slug is valid.
192 if ( empty( $slug ) || ! is_string( $slug ) ) {
193 return false;
194 }
195 // Check if data exists in transient.
196 $transient = get_site_transient( self::$org_slugs );
197 if ( ! is_array( $transient ) ) {
198 $transient = array();
199 }
200 if ( isset( $transient[ $slug ] ) ) {
201 return $transient[ $slug ];
202 }
203
204 $plugin_path = $this->get_plugin_base_dir() . $slug . '/';
205 $readme_files = array(
206 'readme.txt',
207 'readme.md',
208 'README.md',
209 );
210 // Check each Readme-case.
211 foreach ( $readme_files as $readme_file ) {
212 if ( $this->check_by_readme_file( $plugin_path . $readme_file ) ) {
213 $transient[ $slug ] = true;
214 set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
215
216 return true;
217 }
218 }
219
220 $transient[ $slug ] = false;
221 set_site_transient( self::$org_slugs, $transient, DAY_IN_SECONDS );
222
223 return false;
224 }
225
226 /**
227 * Check if the plugin is active.
228 *
229 * @param string $file_path Absolute file path to the plugin file.
230 *
231 * @return bool
232 */
233 public function is_active_plugin( $file_path ): bool {
234 $path_data = explode( DIRECTORY_SEPARATOR, plugin_basename( $file_path ) );
235 if ( ! empty( $path_data[0] ) ) {
236 $plugin_slug = $path_data[0];
237 } else {
238 return false;
239 }
240
241 $active = false;
242 foreach ( $this->get_plugins() as $slug => $data ) {
243 if ( $plugin_slug === $slug || 0 === strpos( $slug, $plugin_slug ) ) {
244 $active = is_multisite() ? is_plugin_active_for_network( $slug ) : is_plugin_active( $slug );
245 break;
246 }
247 }
248
249 return $active;
250 }
251
252 /**
253 * Get plugin header from any file of the plugin.
254 *
255 * @param string $file_path Absolute file path to the plugin file.
256 *
257 * @return array Return plugin details as array.
258 */
259 public function get_plugin_headers( $file_path ): array {
260 $plugin_directory = $this->get_plugin_directory_name( $file_path );
261
262 return get_plugins( DIRECTORY_SEPARATOR . $plugin_directory );
263 }
264
265 /**
266 * Get plugin directory name.
267 *
268 * @param string $file_path Absolute file path to the plugin file.
269 *
270 * @return string Return plugin directory name.
271 */
272 public function get_plugin_directory_name( $file_path ): string {
273 return (string) strtok( plugin_basename( $file_path ), '/' );
274 }
275
276 /**
277 * Get plugin relative path.
278 *
279 * @param string $file_path Absolute file path to the plugin file.
280 *
281 * @return string Return plugin relative path.
282 */
283 public function get_plugin_relative_path( $file_path ): string {
284 // Normalize the directory separators for cross-platform compatibility.
285 $normalized_path = str_replace( '\\', '/', plugin_basename( $file_path ) );
286 // Initialize strtok.
287 strtok( $normalized_path, '/' );
288 // Now fetch the next token, if available, otherwise return an empty string.
289 return (string) strtok( '' );
290 }
291
292 /**
293 * Check file exists at wp.org svn.
294 *
295 * @param string $url URL of the file.
296 *
297 * @return boolean Return true for file exists else false.
298 */
299 private function is_origin_file_exists( $url ): bool {
300 $result = wp_remote_head( $url );
301
302 if ( ! is_wp_error( $result ) ) {
303 $http_status_code = wp_remote_retrieve_response_code( $result );
304
305 return 200 === $http_status_code;
306 }
307
308 return false;
309 }
310
311 /**
312 * Generates the URL for a specific version of a plugin file.
313 *
314 * @param string $directory_name The name of the directory.
315 * @param string $version The version of the file.
316 * @param string $file_path The path of the file.
317 *
318 * @return string The URL of the versioned file.
319 */
320 private function get_version_url( string $directory_name, string $version, string $file_path ): string {
321 return sprintf(
322 $this->url_plugin_vcs,
323 $directory_name,
324 $version,
325 $file_path
326 );
327 }
328
329 /**
330 * Generate the URL for the trunk of a plugin based on the directory name and file path.
331 *
332 * @param string $directory_name The name of the directory.
333 * @param string $file_path The path of the file.
334 *
335 * @return string The URL of the trunk of the plugin.
336 */
337 private function get_trunk_url( string $directory_name, string $file_path ): string {
338 return sprintf(
339 $this->url_plugin_vcs_trunk,
340 $directory_name,
341 $file_path
342 );
343 }
344
345 /**
346 * Generate the URL for a file based on the directory name, version, and file path.
347 *
348 * @param string $directory_name The name of the directory.
349 * @param string $version The version of the file.
350 * @param string $file_path The path of the file.
351 *
352 * @return string The URL of the file.
353 */
354 private function get_file_url( string $directory_name, string $version, string $file_path ): string {
355 // First try to use the version number provided.
356 $file_url = $this->get_version_url( $directory_name, $version, $file_path );
357 if ( $this->is_origin_file_exists( $file_url ) ) {
358 return $file_url;
359 }
360 // If that does not exist, try to use the version number with a `.0` suffix.
361 $file_url = $this->get_version_url( $directory_name, $version . '.0', $file_path );
362 if ( $this->is_origin_file_exists( $file_url ) ) {
363 return $file_url;
364 }
365 // If all else fails, use the trunk of the plugin.
366 return $this->get_trunk_url( $directory_name, $file_path );
367 }
368
369 /**
370 * Retrieves the content of a URL by downloading it and reading the contents of the downloaded file.
371 *
372 * @param string $url The URL to download the content from.
373 *
374 * @return string|WP_Error The content of the URL if successful, otherwise a WP_Error object.
375 */
376 private function get_url_content( $url ) {
377 global $wp_filesystem;
378 // Initialize the WP filesystem, no more using 'file-put-contents' function.
379 if ( empty( $wp_filesystem ) ) {
380 require_once ABSPATH . '/wp-admin/includes/file.php';
381 WP_Filesystem();
382 }
383 if ( ! function_exists( 'download_url' ) ) {
384 $ds = DIRECTORY_SEPARATOR;
385 require_once ABSPATH . 'wp-admin' . $ds . 'includes' . $ds . 'file.php';
386 }
387
388 $tmp = download_url( $url );
389 if ( is_wp_error( $tmp ) ) {
390 return $tmp;
391 }
392
393 $content = $wp_filesystem->get_contents( $tmp );
394 wp_delete_file( $tmp );
395
396 return $content;
397 }
398
399 /**
400 * Quarantine a plugin.
401 *
402 * @param string $parent_action Parent action.
403 *
404 * @return array|WP_Error
405 */
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 }
413
414 $quarantine_component = wd_di()->get( Quarantine_Component::class );
415
416 return $quarantine_component->quarantine_file( $this->owner, $parent_action );
417 }
418
419 /**
420 * Detect if the plugin file is quarantinable.
421 *
422 * @param string $file_path File path.
423 *
424 * @return bool Return true if file is in wp.org plugin else false.
425 */
426 private function is_quarantinable( $file_path ): bool {
427 return $this->is_likely_wporg_slug(
428 $this->get_plugin_directory_name(
429 $file_path
430 )
431 );
432 }
433 }
434