Skip to content

Conversation

@shekharnwagh
Copy link
Contributor

@shekharnwagh shekharnwagh commented Oct 1, 2025

Description

This PR adds functionality to display plugins loaded by VIP Integrations in the WordPress admin plugins list, providing better visibility.

Plugins loaded by VIP Integrations are loaded via require_once from the mu-plugins/vip-integrations/ directory. Since WordPress's get_plugins() only scans the standard wp-content/plugins/ directory, these integration plugins are invisible in the wp-admin plugins list.

To solve this, created a reusable IntegrationPluginDisplayTrait that:

  • Injects integration plugins into WordPress's plugins list via the all_plugins filter
  • Shows plugins as active with "Enabled via WPVIP Integrations" status
  • Removes activate/deactivate links (like code-activated plugins)
  • Prevents accidental database writes for integration plugins
  • Suppresses "Plugin file does not exist" error messages using output buffering

This approach is similar to how wpcom_vip_load_plugin() displays code-activated plugins, but tweaked for the plugin code being in mu-plugins/vip-integrations dir.

Screenshot 2025-10-03 at 12 17 33 AM

Changelog Description

Added

  • VIP Integrations: Added a reusable IntegrationPluginDisplayTrait that displays integration plugins in the WordPress admin plugins list
  • VIP Integrations: When Real-Time Collaboration integration is loaded, the custom build of Gutenberg and the latest version of the Real-Time Collaboration plugin activated via VIP Integrations are displayed in the WordPress admin plugins list

Pre-review checklist

Please make sure the items below have been covered before requesting a review:

  • This change works and has been tested locally or in Codespaces (or has an appropriate fallback).
  • This change works and has been tested on a sandbox.
  • This change has relevant unit tests (if applicable).
  • This change uses a rollout method to ease with deployment (if applicable - especially for large scale actions that require writes).
  • This change has relevant documentation additions / updates (if applicable).
  • I've created a changelog description that aligns with the provided examples.

Pre-deploy checklist

  • VIP staff: Ensure any alerts added/updated conform to internal standards (see internal documentation).

Steps to Test

  1. Check out PR.

  2. Copy the /vip-integrations directory from the vip-go-mu-plugins-built repo to /vip-integrations directory of this repo.

  3. Run composer install and npm install to install dependencies.

  4. Activate the 'Real-Time Collaboration' integration on one of your test sites and note down its application ID.

  5. Spin up a local dev for that site using command - vip dev-env create @<application_id>.production -s <slug> -a <path_to_app_code> -u <path_to_local_vip-go-mu-plugins_dir>

  6. Start it using the command - vip dev-env start --slug <slug> --editor vscode

  7. That should open a VS Code window. Modify the <app_name>/integrations-config/integrations.json to add following entry -

    "real-time-collaboration": {
    	"type": "real-time-collaboration",
    	"org": {
    		"status": "enabled"
    	},
    	"env": {
    		"status": "enabled",
    		"config": {
    			"web_socket_auth_secret": "test-secret-key",
    			"web_socket_url": "wss://test.example.com/_ws"
    		}
    	}
    }
  8. Navigate to the wp-admin > Plugins page and verify that the custom build of Gutenberg and the latest version of the Real-Time Collaboration plugin activated via VIP Integrations are displayed in the WordPress admin plugins list.

@shekharnwagh shekharnwagh self-assigned this Oct 1, 2025
@codecov
Copy link

codecov bot commented Oct 1, 2025

Codecov Report

❌ Patch coverage is 43.68932% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.64%. Comparing base (7a65510) to head (59863f8).

Files with missing lines Patch % Lines
integrations/integration-plugin-display-trait.php 47.25% 48 Missing ⚠️
integrations/real-time-collaboration.php 18.18% 9 Missing ⚠️
vip-integrations.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #6545      +/-   ##
=============================================
+ Coverage      34.60%   34.64%   +0.04%     
- Complexity      5028     5067      +39     
=============================================
  Files            294      295       +1     
  Lines          20827    20929     +102     
=============================================
+ Hits            7207     7251      +44     
- Misses         13620    13678      +58     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shekharnwagh shekharnwagh changed the title Display integrations in plugins list in WP-Admin [VIP-1939] Display integrations in plugins list in WP-Admin Oct 7, 2025
@shekharnwagh shekharnwagh force-pushed the add/integration-plugins-entry branch from 9937aee to f3cbfba Compare October 9, 2025 08:30
Previously, Gutenberg and RTC plugins were registered for display
immediately after require_once in RealTimeCollaborationIntegration,
which meant they could appear in the WordPress plugins list even if
they failed to load due to compatibility checks or other issues.
The trait uses static properties shared across all integrations.
Previous approach used a manual reset method between tests which
added test-specific code to trait.
The previous pattern used /s modifier and .*? which could match
across multiple error divs, causing it to remove unrelated error
notices.
@shekharnwagh shekharnwagh force-pushed the add/integration-plugins-entry branch from 6fcc440 to 52bba6a Compare October 9, 2025 17:16
@shekharnwagh shekharnwagh marked this pull request as ready for review October 9, 2025 17:35
@shekharnwagh shekharnwagh requested a review from a team as a code owner October 9, 2025 17:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds functionality to display plugins loaded by VIP Integrations in the WordPress admin plugins list. Since integration plugins are loaded from mu-plugins/vip-integrations/ via require_once, they don't appear in the standard plugins list which only scans wp-content/plugins/.

  • Created a reusable IntegrationPluginDisplayTrait for making integration plugins visible in WordPress admin
  • Integrated the trait into RealTimeCollaborationIntegration to display Gutenberg and RTC plugins
  • Added comprehensive test coverage for the trait's functionality

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
vip-integrations.php Includes the new trait file in the autoloader
integrations/integration-plugin-display-trait.php New trait providing plugin display functionality with hooks and filters
integrations/real-time-collaboration.php Integrates the trait and registers Gutenberg/RTC plugins for display
tests/integrations/test-integration-plugin-display-trait.php Comprehensive test suite for the trait functionality

Comment on lines +363 to +370
foreach ( $integration_plugins as $plugin ) {
// Remove error notices for "Plugin file does not exist"
// Pattern matches the exact WordPress error format:
// <div ... class="notice error"><p>The plugin <code>PATH</code> has been deactivated due to an error: Plugin file does not exist.</p></div>
$escaped_plugin = preg_quote( $plugin, '/' );
$pattern = '/<div[^>]*\bnotice\b[^>]*\berror\b[^>]*><p>The plugin <code>' . $escaped_plugin . '<\/code> has been deactivated due to an error: Plugin file does not exist\.<\/p><\/div>\s*/';
$buffer = preg_replace( $pattern, '', $buffer );
}
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested loop with regex operations in the output buffer callback could be inefficient for large numbers of integration plugins. Consider pre-compiling all patterns into a single regex with alternation (|) or caching the compiled patterns.

Suggested change
foreach ( $integration_plugins as $plugin ) {
// Remove error notices for "Plugin file does not exist"
// Pattern matches the exact WordPress error format:
// <div ... class="notice error"><p>The plugin <code>PATH</code> has been deactivated due to an error: Plugin file does not exist.</p></div>
$escaped_plugin = preg_quote( $plugin, '/' );
$pattern = '/<div[^>]*\bnotice\b[^>]*\berror\b[^>]*><p>The plugin <code>' . $escaped_plugin . '<\/code> has been deactivated due to an error: Plugin file does not exist\.<\/p><\/div>\s*/';
$buffer = preg_replace( $pattern, '', $buffer );
}
// Pre-compile a single regex pattern that matches any plugin error notice
if ( empty( $integration_plugins ) ) {
return $buffer;
}
$escaped_plugins = array_map( function( $plugin ) {
return preg_quote( $plugin, '/' );
}, $integration_plugins );
$alternation = implode( '|', $escaped_plugins );
$pattern = '/<div[^>]*\bnotice\b[^>]*\berror\b[^>]*><p>The plugin <code>(' . $alternation . ')<\/code> has been deactivated due to an error: Plugin file does not exist\.<\/p><\/div>\s*/';
$buffer = preg_replace( $pattern, '', $buffer );

Copilot uses AI. Check for mistakes.
Comment on lines +378 to +382
function () {
if ( ob_get_level() > 0 ) {
ob_end_flush();
}
},
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output buffer cleanup only checks if any buffer exists, but doesn't verify it's the specific buffer started by this trait. If other code also uses output buffering, this could flush the wrong buffer. Consider storing the buffer level when starting and only flush if it matches.

Copilot uses AI. Check for mistakes.
Comment on lines 355 to 359
if ( empty( $integration_plugins ) || static::$buffer_started ) {
return;
}

static::$buffer_started = true;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static flag $buffer_started could cause race conditions in concurrent requests or if multiple instances try to start buffers simultaneously. The flag is set after the check, creating a potential window where multiple buffers could be started.

Copilot uses AI. Check for mistakes.
*
* @var string
*/
private static $plugin_display_message = 'Enabled via WPVIP Integrations';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there something more concise we can use here - feels a little long, particularly when it spans two lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to ideas. Worded it so it's similar to badge - Enabled via code shown when we activate plugin from code using wpcom_vip_load_plugin.

What do we think using just WPVIP Integration ? But that might be slightly confusing when that shows up for Gutenberg plugin.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially something as simple as a link to the integrations center via a link Settings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-10-10 at 09 09 46

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just took a quick look at the code in this repo and I couldn't find any references to dashboard.wpvip.com. If we have add it, it'll be some work constructing the url and maintaining it in unlikely scenario the routing in dashboard changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we get away with a link to the docs? like: https://docs.wpvip.com/integrations/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about Enabled via integrations and integrations links to https://docs.wpvip.com/integrations/?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be against adding a link or something better down the road, but the simple but clear text that you already have in there is fine with me even though it splits to two lines.

Copy link
Contributor

@rinatkhaziev rinatkhaziev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So using regex and ob doesn't seem entirely safe. There are better ways, for example:

https://developer.wordpress.org/reference/hooks/plugin_action_links/


// Cleanup and error suppression
add_action( 'admin_init', [ $this, 'cleanup_recently_activated' ], 1 );
add_action( 'admin_notices', [ $this, 'suppress_deactivation_notices' ], 1 );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious as to when these get triggered, as it looks like they would only appear when the plugin activation/deactivation links are present and a user clicks on them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking through this again, yeah we won't need this since we've already removed the links to activate/deactivate plugin and the activation happens via code. Removed in a38323e

*
* @var string
*/
private static $plugin_display_message = 'Enabled via WPVIP Integrations';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be against adding a link or something better down the road, but the simple but clear text that you already have in there is fine with me even though it splits to two lines.

maxschmeling and others added 3 commits October 22, 2025 21:59
Integration plugins never enter the recently_activated list since
they're filtered out of active_plugins and users can't deactivate
them via the UI. This method is therefore unnecessary.
@shekharnwagh
Copy link
Contributor Author

shekharnwagh commented Oct 24, 2025

So using regex and ob doesn't seem entirely safe. There are better ways, for example:

https://developer.wordpress.org/reference/hooks/plugin_action_links/

The plugin_action_links only customizes the action buttons in each plugin row. The "Plugin file does not exist" error is generated during WordPress's plugin validation. We are already hooking into that filer to remove the activation and deactivation links and adding the Enabled via WPVIP Integrations message.
That's why I ended up using output buffering to suppress the error notice.

I'm open to better solutions. Is there a specific WordPress hook or filter that prevents the validation check for plugins in non-standard directories? @rinatkhaziev

@sonarqubecloud
Copy link

@rinatkhaziev
Copy link
Contributor

I'm open to better solutions. Is there a specific WordPress hook or filter that prevents the validation check for plugins in non-standard directories? @rinatkhaziev

OIC, I misunderstood the diff.

Okay, in this particular case simply removing filter_active_plugins_for_display gets rid of the notice.

diff --git a/integrations/integration-plugin-display-trait.php b/integrations/integration-plugin-display-trait.php
index 9376cfb55..ce872a255 100644
--- a/integrations/integration-plugin-display-trait.php
+++ b/integrations/integration-plugin-display-trait.php
@@ -122,7 +122,7 @@ trait IntegrationPluginDisplayTrait {
 		add_filter( 'network_admin_plugin_action_links', [ $this, 'filter_plugin_action_links' ], 10, 2 );
 
 		// Active plugin management (display only)
-		add_filter( 'option_active_plugins', [ $this, 'filter_active_plugins_for_display' ] );
+		// add_filter( 'option_active_plugins', [ $this, 'filter_active_plugins_for_display' ] );
 		add_filter( 'site_option_active_sitewide_plugins', [ $this, 'filter_network_active_plugins_for_display' ] );
 
 		// Prevent accidental database writes
@@ -130,8 +130,8 @@ trait IntegrationPluginDisplayTrait {
 		add_filter( 'pre_update_site_option_active_sitewide_plugins', [ $this, 'filter_update_network_active_plugins' ] );
 
 		// Cleanup and error suppression
-		add_action( 'admin_notices', [ $this, 'suppress_deactivation_notices' ], 1 );
-		add_action( 'network_admin_notices', [ $this, 'suppress_deactivation_notices' ], 1 );
+		// add_action( 'admin_notices', [ $this, 'suppress_deactivation_notices' ], 1 );
+		// add_action( 'network_admin_notices', [ $this, 'suppress_deactivation_notices' ], 1 );
 	}
 
 	/**
CleanShot 2025-10-24 at 18 13 53@2x

@shekharnwagh
Copy link
Contributor Author

shekharnwagh commented Oct 25, 2025

Okay, in this particular case simply removing filter_active_plugins_for_display gets rid of the notice.

Removing filter_active_plugins_for_display means they now show up in inactive plugins list which isn't what we'd want. We do have a text saying "Enabled via WPVIP Integrations", but they not being in active list might indicate they aren't loaded.

Screenshot 2025-10-25 at 10 40 00 AM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants