PluginProbe
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking / trunk
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking vtrunk
1.5.0 1.4.0 1.3.0 1.3.1 trunk 0.0.0-alpha.1 0.0.0-alpha.2 0.0.0-alpha.3 0.0.1-beta.1 0.0.1-beta.2 0.0.1-beta.3 0.0.1-beta.4 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4
surecookie / inc / api / plugin.php

plugin.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking trunk, at inc/api/plugin.php

482 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin class
4 *
5 * Handles installed products related REST API endpoints for the SureCookie plugin.
6 *
7 * @package SureCookie\Inc\API
8 */
9
10 namespace SureCookie\Inc\API;
11
12 use SureCookie\Admin\Product_Promotion;
13 use SureCookie\Inc\Functions\Get;
14 use SureCookie\Inc\Functions\SendJson;
15 use SureCookie\Inc\Traits\GetInstance;
16 use WP_REST_Server;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 /**
23 * Class Plugin
24 *
25 * Handles this related REST API endpoints.
26 */
27 class Plugin extends Base {
28 use GetInstance;
29
30 /**
31 * Get conflicting plugins.
32 */
33 protected const CONFLICTING_PLUGINS = '/plugin/get-conflicts';
34
35 /**
36 * Route activate plugin.
37 */
38 protected const ACTIVATE_PLUGIN = '/plugin/activate';
39
40 /**
41 * Route deactivate plugin.
42 */
43 protected const DEACTIVATE_PLUGIN = '/plugin/deactivate';
44
45 /**
46 * Route get promotional plugins data.
47 */
48 protected const GET_PROMOTIONS = '/plugin/get-promotions';
49
50 /**
51 * Register API routes.
52 *
53 * @since 0.0.1
54 * @return void
55 */
56 public function register_routes(): void {
57 register_rest_route(
58 $this->get_api_namespace(),
59 self::CONFLICTING_PLUGINS,
60 [
61 'methods' => WP_REST_Server::READABLE,
62 'callback' => [ $this, 'get_conflicting_plugins' ],
63 'permission_callback' => [ $this, 'validate_permission' ],
64 ]
65 );
66
67 register_rest_route(
68 $this->get_api_namespace(),
69 self::DEACTIVATE_PLUGIN,
70 [
71 'methods' => WP_REST_Server::CREATABLE,
72 'callback' => [ $this, 'deactivate_plugin' ],
73 'permission_callback' => [ $this, 'validate_permission' ],
74 'args' => [
75 'plugin_path' => [
76 'type' => 'string',
77 'required' => true,
78 ],
79 ],
80 ]
81 );
82
83 register_rest_route(
84 $this->get_api_namespace(),
85 self::GET_PROMOTIONS,
86 [
87 'methods' => WP_REST_Server::READABLE,
88 'callback' => [ $this, 'get_promotional_plugins_data' ],
89 'permission_callback' => [ $this, 'validate_permission' ],
90 ]
91 );
92
93 register_rest_route(
94 $this->get_api_namespace(),
95 self::ACTIVATE_PLUGIN,
96 [
97 'methods' => WP_REST_Server::CREATABLE,
98 'callback' => [ $this, 'get_plugin_activate' ],
99 'permission_callback' => [ $this, 'validate_permission' ],
100 'args' => [
101 'plugin_path' => [
102 'type' => 'string',
103 'required' => true,
104 ],
105 ],
106 ]
107 );
108 }
109
110 /**
111 * Get promotional plugins data.
112 *
113 * @param \WP_REST_Request<array<string, mixed>> $request Full data about the request.
114 * @since 0.0.1
115 */
116 public function get_promotional_plugins_data( $request ): void {
117 $promoting_plugins = Product_Promotion::get_instance()->get_promotional_plugins();
118
119 SendJson::success(
120 [
121 'message' => __( 'Promotional plugins data retrieved.', 'surecookie' ),
122 'plugins' => $promoting_plugins,
123 ]
124 );
125 }
126
127 /**
128 * Activate plugin helper.
129 *
130 * @param string $plugin_path Plugin path.
131 * @return \WP_Error|null
132 * @since 0.0.1
133 */
134 public function activate_plugin( $plugin_path ) {
135 if ( ! function_exists( 'activate_plugin' ) ) {
136 require_once ABSPATH . 'wp-admin/includes/plugin.php';
137 }
138
139 // Disable redirection to plugin page after activation.
140 add_filter( 'wp_redirect', '__return_false' );
141
142 return activate_plugin( $plugin_path );
143 }
144
145 /**
146 * Activate plugin via REST.
147 *
148 * @param \WP_REST_Request<array<string, mixed>> $request Full data about the request.
149 * @since 0.0.1
150 */
151 public function get_plugin_activate( $request ): void {
152 $plugin_path = $request->get_param( 'plugin_path' );
153 if ( ! current_user_can( 'activate_plugins' ) ) {
154 SendJson::error( [ 'message' => __( 'You don\'t have permission to activate plugins.', 'surecookie' ) ] );
155 }
156
157 if ( ! $plugin_path ) {
158 SendJson::error( [ 'message' => __( 'Plugin not found.', 'surecookie' ) ] );
159 }
160
161 $activate_result = $this->activate_plugin( $plugin_path );
162 if ( is_wp_error( $activate_result ) ) {
163 SendJson::error(
164 [
165 'message' => $activate_result->get_error_message(),
166 ]
167 );
168 }
169
170 SendJson::success(
171 [
172 'message' => __( 'Plugin activated successfully.', 'surecookie' ),
173 'activated_plugin' => $plugin_path,
174 ]
175 );
176 }
177
178 /**
179 * Deactivate plugin.
180 *
181 * @param \WP_REST_Request<array<string, mixed>> $request Full data about the request.
182 */
183 public function deactivate_plugin( $request ): void {
184 $plugin_path = $request->get_param( 'plugin_path' );
185
186 if ( ! current_user_can( 'activate_plugins' ) ) {
187 SendJson::error( [ 'message' => __( 'You don\'t have permission to deactivate plugins.', 'surecookie' ) ] );
188 }
189
190 if ( ! $plugin_path ) {
191 SendJson::error( [ 'message' => __( 'Plugin not found.', 'surecookie' ) ] );
192 }
193
194 // Load plugin.php if not already loaded.
195 if ( ! function_exists( 'is_plugin_active' ) ) {
196 require_once ABSPATH . 'wp-admin/includes/plugin.php';
197 }
198
199 $plugin_path = (string) $plugin_path;
200
201 // Check if plugin is already inactive.
202 if ( ! is_plugin_active( $plugin_path ) ) {
203 SendJson::success( [ 'message' => __( 'Plugin is already inactive.', 'surecookie' ) ] );
204 }
205
206 $is_network_active = $this->is_network_activated_plugin( $plugin_path );
207
208 // Network-activated plugins can only be deactivated network-wide by a
209 // Super Admin. A regular site admin is pointed to Network Admin instead
210 // of silently failing.
211 if ( $is_network_active && ! current_user_can( 'manage_network_plugins' ) ) {
212 SendJson::error(
213 [
214 'message' => __( 'This plugin is network-activated and must be deactivated by a Network Administrator from Network Admin > Plugins.', 'surecookie' ),
215 'requiresNetwork' => true,
216 'networkPluginsUrl' => $this->get_plugin_management_url( $plugin_path, true ),
217 ]
218 );
219 }
220
221 deactivate_plugins( [ $plugin_path ], false, $is_network_active );
222
223 // Never report success while the plugin is still active.
224 if ( is_plugin_active( $plugin_path ) ) {
225 SendJson::error( [ 'message' => __( 'Plugin could not be deactivated.', 'surecookie' ) ] );
226 }
227
228 SendJson::success( [ 'message' => __( 'Plugin deactivated successfully.', 'surecookie' ) ] );
229 }
230
231 /**
232 * Get plugins that can be conflicting with the main plugin.
233 *
234 * @param \WP_REST_Request<array<string, mixed>> $request Full data about the request.
235 * @since 0.0.1
236 */
237 public function get_conflicting_plugins( $request ): void {
238 SendJson::success(
239 [
240 'message' => __( 'Conflicting plugins retrieved.', 'surecookie' ),
241 'plugins' => $this->detect_conflicting_plugins(),
242 ]
243 );
244 }
245
246 /**
247 * Active plugins that look like competing cookie-consent plugins.
248 *
249 * Split out of the REST handler so the site-health ability can run the same
250 * detection: that handler terminates the request through SendJson.
251 *
252 * @since 1.4.0
253 * @return array<int, array<string, mixed>>
254 */
255 public function detect_conflicting_plugins(): array {
256 if ( ! function_exists( 'get_plugins' ) ) {
257 require_once ABSPATH . 'wp-admin/includes/plugin.php';
258 }
259
260 $all_plugins = get_plugins();
261 $active_plugins = Get::option( 'active_plugins', [], 'array' );
262 // On multisite, network-activated plugins are active everywhere but live
263 // in a separate network option, so treat them as active too.
264 $network_active = $this->get_network_active_plugins();
265
266 $conflicting_plugins = [];
267
268 foreach ( $all_plugins as $plugin_path => $plugin_data ) {
269 $is_network_active = in_array( $plugin_path, $network_active, true );
270
271 if ( ! in_array( $plugin_path, $active_plugins, true ) && ! $is_network_active ) {
272 continue;
273 }
274
275 if ( strpos( $plugin_path, 'surecookie' ) !== false ) {
276 continue;
277 }
278
279 // Check if plugin matches conflicting keywords.
280 $plugin_name = strtolower( $plugin_data['Name'] );
281 $plugin_description = strtolower( $plugin_data['Description'] );
282 $text_domain = isset( $plugin_data['TextDomain'] ) ? strtolower( $plugin_data['TextDomain'] ) : '';
283
284 if ( ! $this->is_plugin_conflicting( $plugin_name, $plugin_description, $text_domain ) ) {
285 continue;
286 }
287
288 // Extract plugin slug from path for icon URL.
289 $slug = strpos( $plugin_path, '/' ) !== false
290 ? dirname( $plugin_path )
291 : str_replace( '.php', '', $plugin_path );
292
293 $conflicting_plugins[ $plugin_path ] = [
294 'name' => $plugin_data['Name'],
295 'slug' => $slug,
296 'path' => $plugin_path,
297 'version' => $plugin_data['Version'],
298 'description' => $plugin_data['Description'],
299 'managementUrl' => $this->get_plugin_management_url( $plugin_path, $is_network_active ),
300 'icon' => "https://ps.w.org/{$slug}/assets/icon-256x256.png",
301 'isNetworkActivated' => $is_network_active,
302 'canDeactivate' => $this->can_deactivate( $is_network_active ),
303 ];
304 }
305
306 return array_values( $conflicting_plugins );
307 }
308
309 /**
310 * Get the plugins screen URL for a plugin. Network-activated plugins resolve
311 * to the Network Admin plugins screen (the only place they can be managed).
312 *
313 * @param string $plugin_path Plugin path.
314 * @param bool $is_network_activated Whether the plugin is network-activated.
315 * @since 0.0.1
316 * @return string
317 */
318 private function get_plugin_management_url( string $plugin_path, bool $is_network_activated = false ): string {
319 $plugin_slug = strpos( $plugin_path, '/' ) !== false
320 ? dirname( $plugin_path )
321 : str_replace( '.php', '', $plugin_path );
322
323 $base_url = $is_network_activated
324 ? network_admin_url( 'plugins.php' )
325 : self_admin_url( 'plugins.php' );
326
327 return (string) add_query_arg(
328 [
329 'plugin_status' => 'all',
330 's' => $plugin_slug,
331 ],
332 $base_url
333 );
334 }
335
336 /**
337 * Plugin paths that are network-activated on this multisite network.
338 * Empty on single-site installs.
339 *
340 * @since 1.2.0
341 * @return array<int, string>
342 */
343 private function get_network_active_plugins(): array {
344 if ( ! is_multisite() ) {
345 return [];
346 }
347
348 return array_keys( (array) get_site_option( 'active_sitewide_plugins', [] ) );
349 }
350
351 /**
352 * Whether a plugin is network-activated.
353 *
354 * @since 1.2.0
355 * @param string $plugin_path Plugin file path.
356 * @return bool
357 */
358 private function is_network_activated_plugin( string $plugin_path ): bool {
359 return in_array( $plugin_path, $this->get_network_active_plugins(), true );
360 }
361
362 /**
363 * Whether the current user can deactivate the plugin from the SureCookie UI.
364 * Mirrors the deactivate endpoint's gate: every plugin requires
365 * activate_plugins, and network-activated plugins additionally require
366 * manage_network_plugins.
367 *
368 * @since 1.2.0
369 * @param bool $is_network_activated Whether the plugin is network-activated.
370 * @return bool
371 */
372 private function can_deactivate( bool $is_network_activated ): bool {
373 if ( ! current_user_can( 'activate_plugins' ) ) {
374 return false;
375 }
376
377 return ! $is_network_activated || current_user_can( 'manage_network_plugins' );
378 }
379
380 /**
381 * Get brand-name keywords that always indicate a conflicting consent plugin.
382 *
383 * @return array<string> List of known consent tool brand keywords.
384 * @since 0.0.1
385 */
386 private function get_brand_keywords(): array {
387 return [
388 'trustarc',
389 'iubenda',
390 'onetrust',
391 'cookiebot',
392 'cookieyes',
393 'cookiehub',
394 'webtoffee',
395 'wpgdpr',
396 'moove-gdpr',
397 'borlabs',
398 'real-cookie-banner',
399 'complianz',
400 'optanon',
401 ];
402 }
403
404 /**
405 * Get generic keywords that indicate a conflict only when combined with a cookie/consent signal.
406 *
407 * @return array<string> List of generic consent-related keywords.
408 * @since 0.0.0-alpha.2
409 */
410 private function get_generic_keywords(): array {
411 return [
412 'cookie consent',
413 'cookie-consent',
414 'cookie banner',
415 'cookie-banner',
416 'cookie notice',
417 'cookie-notice',
418 'cookie popup',
419 'cookie-popup',
420 'cookie widget',
421 'cookie-widget',
422 'cookie preference',
423 'cookie-preference',
424 'cookie manager',
425 'cookie-manager',
426 'cookie control',
427 'cookie-control',
428 'cookie solution',
429 'cookie-solution',
430 'cookie tracking',
431 'cookie-tracking',
432 'cookie regulation',
433 'cookie-regulation',
434 'cookie law',
435 'cookie-law',
436 'eu cookie law',
437 'consent management',
438 'consent-management',
439 'user consent',
440 'user-consent',
441 'gdpr cookie',
442 'ccpa cookie',
443 'eprivacy',
444 'privacy compliance',
445 'privacy-compliance',
446 ];
447 }
448
449 /**
450 * Check if a plugin matches conflicting keywords.
451 *
452 * Brand keywords match immediately. Generic keywords require the combined
453 * plugin name + description to contain a cookie/consent signal, preventing
454 * false positives from plugins that merely mention GDPR or privacy.
455 *
456 * @param string $plugin_name Lowercased plugin name.
457 * @param string $plugin_description Lowercased plugin description.
458 * @param string $text_domain Lowercased text domain.
459 * @return bool
460 * @since 0.0.0-alpha.2
461 */
462 private function is_plugin_conflicting( string $plugin_name, string $plugin_description, string $text_domain ): bool {
463 $searchable = $plugin_name . ' ' . $plugin_description . ' ' . $text_domain;
464
465 // Brand keywords - always a conflict.
466 foreach ( $this->get_brand_keywords() as $keyword ) {
467 if ( strpos( $searchable, $keyword ) !== false ) {
468 return true;
469 }
470 }
471
472 // Generic keywords - match only if present.
473 foreach ( $this->get_generic_keywords() as $keyword ) {
474 if ( strpos( $searchable, $keyword ) !== false ) {
475 return true;
476 }
477 }
478
479 return false;
480 }
481 }
482