PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.0.1
Defender Security – Malware Scanner, Login Security & Firewall v3.0.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 / behavior / scan-item / plugin-integrity.php

plugin-integrity.php in Defender Security – Malware Scanner, Login Security & Firewall 3.0.1, at src/behavior/scan-item/plugin-integrity.php

292 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Behavior\Scan_Item;
4
5 use Calotes\Component\Behavior;
6 use WP_Defender\Component\Error_Code;
7 use WP_Defender\Model\Scan;
8 use WP_Defender\Model\Scan_Item;
9 use WP_Defender\Traits\Formats;
10 use WP_Defender\Traits\IO;
11 use WP_Defender\Traits\Plugin;
12 use WP_Error;
13
14 class Plugin_Integrity extends Behavior {
15 use Formats, IO, Plugin;
16
17 public const URL_PLUGIN_VCS = 'https://plugins.svn.wordpress.org/%s/tags/%s/%s';
18
19 /**
20 * Trunk URL.
21 *
22 * @var string Trunk path of the plugin.
23 */
24 public const URL_PLUGIN_VCS_TRUNK = 'https://plugins.svn.wordpress.org/%s/trunk/%s';
25
26 /**
27 * Return general data so we can output on frontend.
28 *
29 * @return array
30 */
31 public function to_array() {
32 $data = $this->owner->raw_data;
33 $file = $data['file'];
34 $file_created_at = @filemtime( $file );
35 if ( $file_created_at ) {
36 $file_created_at = $this->format_date_time( $file_created_at );
37 } else {
38 $file_created_at = 'n/a';
39 }
40 $file_size = @filesize( $file );
41 if ( ! $file_size ) {
42 $file_size = 'n/a';
43 } else {
44 $file_size = $this->format_bytes_into_readable( $file_size );
45 }
46
47 return array(
48 'id' => $this->owner->id,
49 'type' => Scan_Item::TYPE_PLUGIN_CHECK,
50 'file_name' => pathinfo( $file, PATHINFO_BASENAME ),
51 'full_path' => $file,
52 'date_added' => $file_created_at,
53 'size' => $file_size,
54 'scenario' => $data['type'],
55 'short_desc' => $this->get_short_description(),
56 );
57 }
58
59 /**
60 * Get plugin data.
61 *
62 * @return array
63 */
64 private function find_plugin_by_slug( $slug ) {
65 foreach ( $this->get_plugins() as $file => $data ) {
66 // Comparison is faster, but the vast majority of plugins installed are not single file plugins.
67 if ( $file === $slug || 0 === strpos( $file, $slug . '/' ) ) {
68 return $data;
69 }
70 }
71
72 return array();
73 }
74
75 /**
76 * We will get the origin code by looking into svn repo.
77 *
78 * @return false|string|\WP_Error
79 */
80 private function get_origin_code() {
81 $data = $this->owner->raw_data;
82 $file = wp_normalize_path( $data['file'] );
83 $ds = DIRECTORY_SEPARATOR;
84 $relative_path = str_replace( wp_normalize_path( WP_PLUGIN_DIR ) . $ds, '', $file );
85 if ( empty( $relative_path ) ) {
86 return new WP_Error( 'defender_broken_file_path', __( 'Failed relative file path.', 'wpdef' ) );
87 }
88
89 if ( false === strpos( $relative_path, '/' ) ) {
90 // Todo: get correct hashes for single-file plugins. Separate case for 'Hello Dolly'.
91 $slug_and_path = 'hello.php' === $relative_path ? 'hello-dolly' : $relative_path;
92 $path_data = array( $slug_and_path, $slug_and_path );
93 } else {
94 $path_data = explode( $ds, $relative_path, 2 );
95 }
96 if ( ! empty( $path_data ) ) {
97 $plugin_slug = $path_data[0];
98 $file_path = $path_data[1];
99 } else {
100 return new WP_Error( 'defender_broken_file_path', __( 'Broken file path.', 'wpdef' ) );
101 }
102
103 $plugin_data = $this->find_plugin_by_slug( $plugin_slug );
104 if ( empty( $plugin_data ) ) {
105 return new WP_Error( 'defender_broken_file_path', __( 'Empty plugin data.', 'wpdef' ) );
106 }
107
108 // Get original from wp.org e.g. https://plugins.svn.wordpress.org/hello-dolly/tags/1.6/.
109 $source_file_url = sprintf(
110 self::URL_PLUGIN_VCS,
111 $plugin_slug,
112 $plugin_data['Version'],
113 $file_path
114 );
115 // If file doesn't exist then get the latest stable file as a fallback.
116 if ( ! $this->is_origin_file_exists( $source_file_url ) ) {
117 $source_file_url = sprintf(
118 self::URL_PLUGIN_VCS_TRUNK,
119 $plugin_slug,
120 $file_path
121 );
122 }
123
124 if ( ! function_exists( 'download_url' ) ) {
125 require_once ABSPATH . 'wp-admin' . $ds . 'includes' . $ds . 'file.php';
126 }
127 $tmp = download_url( $source_file_url );
128 if ( is_wp_error( $tmp ) ) {
129 return $tmp;
130 }
131 $content = file_get_contents( $tmp );
132 @unlink( $tmp );
133
134 return $content;
135 }
136
137 /**
138 * Restore the file with its origin content.
139 *
140 * @return void|array
141 */
142 public function resolve() {
143 $data = $this->owner->raw_data;
144 if ( 'modified' !== $data['type'] ) {
145 // Should not be here unless case changed.
146 return;
147 }
148
149 $origin = $this->get_origin_code();
150 if ( false === $origin || is_wp_error( $origin ) ) {
151 return;
152 }
153
154 $path = $data['file'];
155 $ret = @file_put_contents( $path, $origin );// phpcs:ignore
156 if ( $ret ) {
157 $scan = Scan::get_last();
158 $scan->remove_issue( $this->owner->id );
159 $this->log( sprintf( '%s is deleted', $path ), 'scan.log' );
160
161 return array(
162 'message' => __( 'This item has been resolved.', 'wpdef' ),
163 );
164 } else {
165 return new WP_Error(
166 'defender_permissions_denied',
167 __( 'Permissions Denied. Defender does not have the needed permissions to edit the file. Please change file permissions to 640 or contact your hosting provider so they could change them for you.', 'wpdef' )
168 );
169 }
170 }
171
172 /**
173 * @return array
174 */
175 public function ignore() {
176 $scan = Scan::get_last();
177 $scan->ignore_issue( $this->owner->id );
178
179 return array(
180 'message' => __( 'The suspicious file has been successfully ignored.', 'wpdef' ),
181 );
182 }
183
184 /**
185 * @return array
186 */
187 public function unignore() {
188 $scan = Scan::get_last();
189 $scan->unignore_issue( $this->owner->id );
190
191 return array(
192 'message' => __( 'The suspicious file has been successfully restored.', 'wpdef' ),
193 );
194 }
195
196 /**
197 * Delete the file or whole folder.
198 * Todo: check it because the option don't have 'unversion' & 'dir' types.
199 *
200 * @return array|\WP_Error
201 */
202 public function delete() {
203 $data = $this->owner->raw_data;
204 $scan = Scan::get_last();
205 if ( 'unversion' === $data['type'] && unlink( $data['file'] ) ) {
206 $scan->remove_issue( $this->owner->id );
207 $this->log( sprintf( '%s is deleted', $data['file'] ), 'scan.log' );
208
209 return array(
210 'message' => __( 'This item has been permanently removed', 'wpdef' ),
211 );
212 } elseif ( 'dir' === $data['type'] && $this->delete_dir( $data['file'] ) ) {
213 $scan->remove_issue( $this->owner->id );
214 $this->log( sprintf( '%s is deleted', $data['file'] ), 'scan.log' );
215
216 return array(
217 'message' => __( 'This item has been permanently removed', 'wpdef' ),
218 );
219 }
220
221 return new \WP_Error( Error_Code::NOT_WRITEABLE, __( 'Defender doesn\'t have enough permission to remove this file', 'wpdef' ) );
222 }
223
224 /**
225 * Return the source code depending on the type of the issue.
226 *
227 * @return array
228 */
229 public function pull_src() {
230 $data = $this->owner->raw_data;
231 if ( ! file_exists( $data['file'] ) && ! is_dir( $data['file'] ) ) {
232 return array(
233 'code' => '',
234 'origin' => '',
235 );
236 }
237 switch ( $data['type'] ) {
238 case 'unversion':
239 return array(
240 'code' => file_get_contents( $data['file'] ),
241 );
242 case 'dir':
243 $dir_tree = new File( $data['file'], true, true, array(), array(), false );
244
245 return array(
246 'code' => implode( PHP_EOL, $dir_tree->get_dir_tree() ),
247 );
248 case 'modified':
249 default:
250 return array(
251 'code' => file_get_contents( $data['file'] ),
252 'origin' => $this->get_origin_code(),
253 );
254 }
255 }
256
257 /**
258 * Todo: check it because the option don't have 'unversion' & 'dir' types.
259 *
260 * @return string
261 */
262 private function get_short_description() {
263 $data = $this->owner->raw_data;
264 if ( 'unversion' === $data['type'] ) {
265 return esc_html__( 'Unknown file in the WordPress plugin', 'wpdef' );
266 } elseif ( 'dir' === $data['type'] ) {
267 return esc_html__( 'This directory does not belong to the WordPress plugin', 'wpdef' );
268 } elseif ( 'modified' === $data['type'] ) {
269 return esc_html__( 'This plugin file appears modified', 'wpdef' );
270 }
271 }
272
273 /**
274 * Check file exists at wp.org svn.
275 *
276 * @param string $url URL of the file.
277 *
278 * @return boolean Return true for file exists else false.
279 */
280 private function is_origin_file_exists( $url ) {
281 $result = wp_remote_head( $url );
282
283 if ( ! is_wp_error( $result ) ) {
284 $http_status_code = wp_remote_retrieve_response_code( $result );
285
286 return 200 === $http_status_code;
287 }
288
289 return false;
290 }
291 }
292