PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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 / class-abandoned-plugin.php

class-abandoned-plugin.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at src/behavior/scan/class-abandoned-plugin.php

278 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 * Handle abandoned plugin scan.
4 *
5 * @package WP_Defender\Behavior\Scan
6 */
7
8 namespace WP_Defender\Behavior\Scan;
9
10 use Calotes\Component\Behavior;
11 use WP_Defender\Component\Scan as Scan_Component;
12 use WP_Defender\Controller\Scan as Scan_Controller;
13 use WP_Defender\Model\Scan;
14 use WP_Defender\Model\Scan_Item;
15 use WP_Defender\Traits\IO;
16 use WP_Defender\Traits\Plugin;
17
18 /**
19 * Handle abandoned plugin scan.
20 */
21 class Abandoned_Plugin extends Behavior {
22
23 use IO;
24 use Plugin;
25
26 /**
27 * Hold the Scan model or null if not set.
28 *
29 * @var Scan|null
30 */
31 private ?Scan $scan;
32
33 /**
34 * Outdated plugin period.
35 *
36 * @var string
37 */
38 private const OUTDATED_PERIOD = '2 years';
39
40 /**
41 * Initialize Scan instance.
42 *
43 * @param Scan $scan The Scan model.
44 */
45 public function __construct( Scan $scan ) {
46 $this->scan = $scan;
47 }
48
49 /**
50 * Get the outdated period of the plugin.
51 *
52 * @return string
53 */
54 public static function get_outdated_period(): string {
55 // Filter to override outdated threshold period.
56 $period = apply_filters( 'wpdef_scan_outdated_period', self::OUTDATED_PERIOD );
57 $period = is_string( $period ) ? $period : (string) $period;
58 // If time period is invalid, use the default one.
59 return strtotime( '- ' . $period ) ? $period : self::OUTDATED_PERIOD;
60 }
61
62 /**
63 * Check if there are abandoned plugins.
64 *
65 * @return bool
66 */
67 public function abandoned_plugin_check(): bool {
68 $ignored_issues = $this->get_ignored_issues();
69
70 $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
71 $plugin_list = Scan_Component::are_actioned_plugins( $actioned_plugins )
72 ? $actioned_plugins
73 : $this->get_plugin_slugs();
74
75 foreach ( $plugin_list as $slug => $plugin_data ) {
76 $this->check_abandoned_plugin_by( $slug );
77 }
78
79 $this->scan->calculate_percent( 100, 4 );
80 $this->add_ignored_issues( $ignored_issues );
81
82 return true;
83 }
84
85 /**
86 * Retrieve the list of ignored issues.
87 *
88 * @return array The list of ignored issues.
89 */
90 private function get_ignored_issues(): array {
91 $last = Scan::get_last();
92
93 return is_object( $last )
94 ? $last->get_issues(
95 Scan::get_abandoned_types(),
96 Scan_Item::STATUS_IGNORE
97 )
98 : array();
99 }
100
101 /**
102 * Add ignored issues to the scan.
103 *
104 * @param array $ignored_issues The array of ignored issues to add.
105 */
106 private function add_ignored_issues( array $ignored_issues ) {
107 $this->scan->carry_forward_ignored_issues( Scan::get_abandoned_types(), $ignored_issues, 'slug' );
108 }
109
110 /**
111 * Handle the error by different actions, e.g. logging.
112 *
113 * @param string $error_message The error message.
114 */
115 private function handle_error( string $error_message ) {
116 $this->log( $error_message, Scan_Controller::SCAN_LOG );
117 }
118
119 /**
120 * Prepare data before saving.
121 *
122 * @param array $body_json Data from WP API.
123 * @param array $plugin_details Data from local plugin headers.
124 *
125 * @return array {
126 * Plugin data. Values will be empty if not supplied by the plugin or API sides.
127 *
128 * @type string $name Plugin name.
129 * @type string $slug Plugin slug.
130 * @type string $url Plugin URL.
131 * @type string $version Plugin version.
132 * @type string $tested_wp Latest tested WordPress version.
133 * @type string $last_updated Last updated date.
134 * @type string $reason_text Reason for saving or update.
135 * }
136 */
137 private function prepare_item_data( array $body_json, array $plugin_details ): array {
138 $data = array();
139 // Check plugin name.
140 if ( isset( $body_json['name'] ) ) {
141 $data['name'] = $body_json['name'];
142 } elseif ( isset( $plugin_details['Name'] ) ) {
143 $data['name'] = $plugin_details['Name'];
144 } else {
145 $data['name'] = '';
146 }
147 // Check plugin slug.
148 if ( isset( $body_json['slug'] ) ) {
149 $data['slug'] = $body_json['slug'];
150 } elseif ( isset( $plugin_details['slug'] ) ) {
151 $data['slug'] = $plugin_details['slug'];
152 } else {
153 $data['slug'] = '';
154 }
155 // Check plugin URL. Sometimes URL values are empty.
156 if ( isset( $body_json['homepage'] ) && '' !== $body_json['homepage'] ) {
157 $data['url'] = $body_json['homepage'];
158 } elseif ( isset( $plugin_details['PluginURI'] ) && '' !== $plugin_details['PluginURI'] ) {
159 $data['url'] = $plugin_details['PluginURI'];
160 } elseif ( isset( $plugin_details['AuthorURI'] ) && '' !== $plugin_details['AuthorURI'] ) {
161 $data['url'] = $plugin_details['AuthorURI'];
162 } else {
163 $data['url'] = '';
164 }
165 // Check plugin version.
166 if ( isset( $body_json['version'] ) ) {
167 $data['version'] = $body_json['version'];
168 } elseif ( isset( $plugin_details['Version'] ) ) {
169 $data['version'] = $plugin_details['Version'];
170 } else {
171 $data['version'] = '';
172 }
173 // Check a tested WP version.
174 if ( isset( $body_json['tested'] ) ) {
175 $data['tested_wp'] = $body_json['tested'];
176 } else {
177 $data['tested_wp'] = '';
178 }
179 // Check plugin updated time.
180 if ( isset( $body_json['closed_date'] ) ) {
181 $data['last_updated'] = $body_json['closed_date'];
182 } elseif ( isset( $body_json['last_updated'] ) ) {
183 $data['last_updated'] = $body_json['last_updated'];
184 } else {
185 $data['last_updated'] = '';
186 }
187 // Specific key for closed plugin.
188 if ( isset( $body_json['reason_text'] ) ) {
189 $data['reason_text'] = $body_json['reason_text'];
190 } else {
191 $data['reason_text'] = '';
192 }
193
194 return $data;
195 }
196
197 /**
198 * Get list of plugin slugs whose names do not match the specified names on wp.org.
199 */
200 private function get_plugin_slugs_with_mismatched_names(): array {
201 return array(
202 'miniorange-malware-protection',
203 'admin-bar-user-switching',
204 'custom-post-template',
205 );
206 }
207
208 /**
209 * Is abandoned plugin?
210 *
211 * @param string $slug The slug to check.
212 *
213 * @return bool
214 */
215 public function check_abandoned_plugin_by( string $slug ): bool {
216 // Check if the plugin is not from wp.org.
217 $premium_plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, array() );
218 if ( ( is_array( $premium_plugin_slugs ) && in_array( $slug, $premium_plugin_slugs, true ) )
219 || ! $this->is_likely_wporg_slug( $slug )
220 ) {
221 return true;
222 }
223 if ( $this->scan->is_issue_ignored( $slug ) ) {
224 $this->log( sprintf( 'skip %s because it is already ignored.', $slug ), Scan_Controller::SCAN_LOG );
225 return true;
226 }
227
228 $results = $this->handle_wp_org_response_by( $slug );
229 if ( ! $results['success'] ) {
230 $this->handle_error( $results['message'] );
231
232 return false;
233 } else {
234 $body_json = $results['body'];
235 $plugin_details = $this->get_plugin_details_by( $slug );
236 if (
237 isset( $body_json['name'], $plugin_details['Name'] )
238 && $plugin_details['Name'] !== $body_json['name']
239 && ! in_array( $slug, $this->get_plugin_slugs_with_mismatched_names(), true )
240 ) {
241 // This is a premium plugin with the same slug as on wp.org but with another name OR the plugin developer named the plugin differently on wp.org and inside the main file.
242 return false;
243 }
244
245 if ( isset( $body_json['error'] ) && 'closed' === $body_json['error'] ) {
246 $this->log( sprintf( 'Plugin slug closed: %s', $slug ), Scan_Controller::SCAN_LOG );
247
248 $this->scan->add_item(
249 Scan_Item::TYPE_PLUGIN_CLOSED,
250 $this->prepare_item_data( $body_json, $plugin_details )
251 );
252
253 return true;
254 }
255
256 if ( '' !== $body_json['last_updated'] ) {
257 $last_updated_time = $body_json['last_updated'];
258 $ttl = strtotime( $last_updated_time );
259 if ( $ttl < strtotime( '- ' . self::get_outdated_period() ) ) {
260 $this->log( sprintf( 'Plugin slug outdated: %s', $slug ), Scan_Controller::SCAN_LOG );
261 $plugin_details = $this->prepare_item_data( $body_json, $plugin_details );
262 // Improve the date view.
263 $plugin_details['last_updated'] = gmdate( 'F j, Y', $ttl );
264
265 $this->scan->add_item(
266 Scan_Item::TYPE_PLUGIN_OUTDATED,
267 $plugin_details
268 );
269
270 return true;
271 }
272 }
273
274 return false;
275 }
276 }
277 }
278