A colleague of mine migrated a client site in preparation for taking over their account and was stunned when, upon successful migration, the site didn’t function correctly. The issue was quite frustrating in the industry. The agency built custom features inaccessible to the client – either via SFTP or WordPress administration. I’ll explain how later… and how I was able to work around the issue.
In WordPress, mu-plugins (must-use plugins) are special types of WordPress plugins that are automatically activated and cannot be deactivated from the WordPress admin panel. The mu in mu-plugins stands for must-use.
When you go to the Plugins section in the WordPress admin panel, you will only see the regular plugins that can be activated, deactivated, and managed individually. Mu-plugins, on the other hand, are automatically activated and do not appear in this list. Here are some key points about mu-plugins:
- Location: Mu-plugins are located in a separate directory named
mu-plugins
in thewp-content
directory of your WordPress installation. - Automatic Activation: Unlike regular plugins, mu-plugins are activated automatically and cannot be deactivated from the WordPress admin panel. They are always active as long as they are present in the
mu-plugins
directory. - Priority: Mu-plugins are loaded before regular plugins. This means that they have the ability to override or modify the behavior of regular plugins.
- Naming Convention: Mu-plugins do not follow the same naming convention as regular plugins. They can have any name, but the file extension must be
.php
. For example,my-custom-functions.php
can be a valid mu-plugin file name. - Use Cases: Mu-plugins are commonly used for:
- Implementing site-specific functionality that should always be active.
- Modifying core WordPress behavior or overriding default functionality.
- Enforcing certain plugins to be active on all sites in a multisite network.
- Applying custom code or modifications that should not be accessible or deactivatable by site administrators.
- Multisite: In a WordPress multisite network, mu-plugins are global and affect all sites in the network. They are loaded before regular plugins on each site.
- Updates: Mu-plugins are not updateable through the WordPress admin panel. They need to be manually updated by replacing the respective files in the
mu-plugins
directory.
In this particular instance, the agency had an mu-plugin that included a parent directory where they had custom plugins built out for the client. That parent directory was on their hosting environment but not accessible by the client at all. This is really sinister, in my opinion. Promoting WordPress for your clients comes with an expectation of having an open-source platform.
By hiding code from your clients, you’re making it impossible for them to migrate away from you or manage the instance independently. You’re basically holding them hostage unless they build a new site from scratch. I’m not alleging that the agency did anything illegal… I’m certain they had details of this in their MSA or SOW. It doesn’t make it right, though.
WordPress Plugin: How To Identify and Download mu-plugins
If you can execute PHP code, you can ultimately access that code. So, I wrote a custom plugin that could be installed on the server and created a shortcode listing the mu-plugins in the directory. I created a draft page with [listmuplugins]
in the content and previewed it in a new window. That provided me with the mu-plugin that the agency installed.
Note: The mu-plugin
directory is visible via SFTP, but they did something extra sneaky. When I opened their plugin, I found that they’d written a function to include a directory that was inaccessible from the client’s hosting environment! So, I updated my code to allow crawling they specific directory they included with [listmuplugins dir="/custom/path/"]
.
Once I updated the shortcode to the path they provided in their mu-plugin
, I could read and download every plugin the site required to operate. Here’s a screenshot of the output (I deleted any of the agency’s custom mu-plugins
).
I was then able to write customized plugins, which corrected all the issues on the site. Given that I didn’t know if they had a legal agreement, I did not want to just copy the agency’s code… despite the fact that they did not have any copyright information in the source.
How To Use This Plugin
I won’t publish this in the WordPress repository as I don’t think it should be spread without some responsibility. Here’s how you can use it if you need it, though.
- Save: Save the entire code as
listmuplugins.php
within a new folder namedlistmuplugins
inside your WordPress/wp-content/plugins/
directory. Or you can zip the directory and PHP file and upload it via the WordPress plugin administrative panel. - Activate: Activate the plugin titled List MU Plugins with Download and Directory Structure in your WordPress plugins dashboard.
- Shortcode Usage:
[listmuplugins]
: Lists MU Plugins from your site’s defaultwp-content/mu-plugins/
directory.[listmuplugins dir="/custom/path/"]
: Lists MU Plugins from a specified directory.
<?php
/*
Plugin Name: List MU Plugins with Download and Directory Structure
Description: Lists Must-Use Plugins with the ability to download their source code, and displays the directory structure. Directory can be specified in the shortcode.
Version: 2.0
Author: Douglas Karr
Author URI: https://dknewmedia.com
*/
function list_mu_plugins_shortcode( $atts ) {
$atts = shortcode_atts( array(
'dir' => WPMU_PLUGIN_DIR // Default to site's MU Plugins directory
), $atts );
$dir = $atts['dir'];
// Validate the directory path
if ( ! is_dir( $dir ) || ! is_readable( $dir ) ) {
return "<p>MU Plugins directory not found or not readable.</p>n";
}
$output = "<h2>List of Included MU Plugins:</h2>n";
$output .= list_directory_contents( $dir );
return $output;
}
add_shortcode( 'listmuplugins', 'list_mu_plugins_shortcode' );
function list_directory_contents( $dir ) {
$output = "<ul>n";
$items = scandir( $dir );
foreach ( $items as $item ) {
if ( $item === '.' || $item === '..' ) {
continue;
}
$path = $dir . '/' . $item;
if ( is_dir( $path ) ) {
$output .= '<li><strong>' . esc_html( $item ) . '/</strong>';
$output .= list_directory_contents( $path );
$output .= '</li>';
} else {
$output .= '<li>' . esc_html( $item );
if ( current_user_can( 'manage_options' ) ) {
$download_link = add_query_arg( ['mu_plugin_download' => $item] );
$output .= ' <a href="' . esc_url( $download_link ) . '">Download</a>';
}
$output .= '</li>';
}
}
$output .= "</ul>n";
return $output;
}
// Handle the download request
function handle_mu_plugin_download() {
if ( isset( $_GET['mu_plugin_download'] ) && current_user_can( 'manage_options' ) ) {
$filename = sanitize_file_name( $_GET['mu_plugin_download'] );
$filepath = WPMU_PLUGIN_DIR . '/' . $filename;
if ( file_exists( $filepath ) && is_readable( $filepath ) ) {
header( 'Content-Description: File Transfer' );
header( 'Content-Type: application/octet-stream' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Expires: 0' );
header( 'Cache-Control: must-revalidate' );
header( 'Pragma: public' );
header( 'Content-Length: ' . filesize( $filepath ) );
readfile( $filepath );
exit;
} else {
wp_die( 'File not found or not readable.' );
}
}
}
add_action( 'init', 'handle_mu_plugin_download' );
©2024 DK New Media, LLC, All rights reserved.
Originally Published on Martech Zone: WordPress: Identify and Download mu-plugins Hosted In or Outside Your WordPress Instance