* WordPress Plugin Administration API
* @subpackage Administration
* Parses the plugin contents to retrieve plugin's metadata.
* All plugin headers must be on their own line. Plugin description must not have
* any newlines, otherwise only parts of the description will be displayed.
* The below is formatted for printing.
* Plugin Name: Name of the plugin.
* Plugin URI: The home page of the plugin.
* Description: Plugin description.
* Author: Plugin author's name.
* Author URI: Link to the author's website.
* Version: Plugin version.
* Text Domain: Optional. Unique identifier, should be same as the one used in
* load_plugin_textdomain().
* Domain Path: Optional. Only useful if the translations are located in a
* folder above the plugin's base path. For example, if .mo files are
* located in the locale folder then Domain Path will be "/locale/" and
* must have the first slash. Defaults to the base folder the plugin is
* Network: Optional. Specify "Network: true" to require that a plugin is activated
* across all sites in an installation. This will prevent a plugin from being
* activated on a single site when Multisite is enabled.
* Requires at least: Optional. Specify the minimum required WordPress version.
* Requires PHP: Optional. Specify the minimum required PHP version.
* * / # Remove the space to close comment.
* The first 8 KB of the file will be pulled in and if the plugin data is not
* within that first 8 KB, then the plugin author should correct their plugin
* and move the plugin data headers to the top.
* The plugin file is assumed to have permissions to allow for scripts to read
* the file. This is not checked however and the file is only opened for
* @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers.
* @param string $plugin_file Absolute path to the main plugin file.
* @param bool $markup Optional. If the returned data should have HTML markup applied.
* @param bool $translate Optional. If the returned data should be translated. Default true.
* Plugin data. Values will be empty if not supplied by the plugin.
* @type string $Name Name of the plugin. Should be unique.
* @type string $Title Title of the plugin and link to the plugin's site (if set).
* @type string $Description Plugin description.
* @type string $Author Author's name.
* @type string $AuthorURI Author's website address (if set).
* @type string $Version Plugin version.
* @type string $TextDomain Plugin textdomain.
* @type string $DomainPath Plugins relative directory path to .mo files.
* @type bool $Network Whether the plugin can only be activated network-wide.
* @type string $RequiresWP Minimum required version of WordPress.
* @type string $RequiresPHP Minimum required version of PHP.
function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
$default_headers = array(
'PluginURI' => 'Plugin URI',
'Description' => 'Description',
'AuthorURI' => 'Author URI',
'TextDomain' => 'Text Domain',
'DomainPath' => 'Domain Path',
'RequiresWP' => 'Requires at least',
'RequiresPHP' => 'Requires PHP',
// Site Wide Only is deprecated in favor of Network.
'_sitewide' => 'Site Wide Only',
$plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );
// Site Wide Only is the old header for Network.
if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) {
/* translators: 1: Site Wide Only: true, 2: Network: true */
_deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), '<code>Site Wide Only: true</code>', '<code>Network: true</code>' ) );
$plugin_data['Network'] = $plugin_data['_sitewide'];
$plugin_data['Network'] = ( 'true' === strtolower( $plugin_data['Network'] ) );
unset( $plugin_data['_sitewide'] );
// If no text domain is defined fall back to the plugin slug.
if ( ! $plugin_data['TextDomain'] ) {
$plugin_slug = dirname( plugin_basename( $plugin_file ) );
if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) {
$plugin_data['TextDomain'] = $plugin_slug;
if ( $markup || $translate ) {
$plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate );
$plugin_data['Title'] = $plugin_data['Name'];
$plugin_data['AuthorName'] = $plugin_data['Author'];
* Sanitizes plugin data, optionally adds markup, optionally translates.
* @param string $plugin_file Path to the main plugin file.
* @param array $plugin_data An array of plugin data. See `get_plugin_data()`.
* @param bool $markup Optional. If the returned data should have HTML markup applied.
* @param bool $translate Optional. If the returned data should be translated. Default true.
* Plugin data. Values will be empty if not supplied by the plugin.
* @type string $Name Name of the plugin. Should be unique.
* @type string $Title Title of the plugin and link to the plugin's site (if set).
* @type string $Description Plugin description.
* @type string $Author Author's name.
* @type string $AuthorURI Author's website address (if set).
* @type string $Version Plugin version.
* @type string $TextDomain Plugin textdomain.
* @type string $DomainPath Plugins relative directory path to .mo files.
* @type bool $Network Whether the plugin can only be activated network-wide.
function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup = true, $translate = true ) {
// Sanitize the plugin filename to a WP_PLUGIN_DIR relative path.
$plugin_file = plugin_basename( $plugin_file );
$textdomain = $plugin_data['TextDomain'];
if ( ! is_textdomain_loaded( $textdomain ) ) {
if ( $plugin_data['DomainPath'] ) {
load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) . $plugin_data['DomainPath'] );
load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) );
} elseif ( 'hello.php' === basename( $plugin_file ) ) {
foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) {
// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
$plugin_data[ $field ] = translate( $plugin_data[ $field ], $textdomain );
$allowed_tags_in_links = array(
'abbr' => array( 'title' => true ),
'acronym' => array( 'title' => true ),
$allowed_tags = $allowed_tags_in_links;
$allowed_tags['a'] = array(
// Name is marked up inside <a> tags. Don't allow these.
// Author is too, but some plugins have used <a> here (omitting Author URI).
$plugin_data['Name'] = wp_kses( $plugin_data['Name'], $allowed_tags_in_links );
$plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags );
$plugin_data['Description'] = wp_kses( $plugin_data['Description'], $allowed_tags );
$plugin_data['Version'] = wp_kses( $plugin_data['Version'], $allowed_tags );
$plugin_data['PluginURI'] = esc_url( $plugin_data['PluginURI'] );
$plugin_data['AuthorURI'] = esc_url( $plugin_data['AuthorURI'] );
$plugin_data['Title'] = $plugin_data['Name'];
$plugin_data['AuthorName'] = $plugin_data['Author'];
if ( $plugin_data['PluginURI'] && $plugin_data['Name'] ) {
$plugin_data['Title'] = '<a href="' . $plugin_data['PluginURI'] . '">' . $plugin_data['Name'] . '</a>';
if ( $plugin_data['AuthorURI'] && $plugin_data['Author'] ) {
$plugin_data['Author'] = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
$plugin_data['Description'] = wptexturize( $plugin_data['Description'] );
if ( $plugin_data['Author'] ) {
$plugin_data['Description'] .= sprintf(
/* translators: %s: Plugin author. */
' <cite>' . __( 'By %s.' ) . '</cite>',
* Get a list of a plugin's files.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return string[] Array of file names relative to the plugin root.
function get_plugin_files( $plugin ) {
$plugin_file = WP_PLUGIN_DIR . '/' . $plugin;
$dir = dirname( $plugin_file );
$plugin_files = array( plugin_basename( $plugin_file ) );
if ( is_dir( $dir ) && WP_PLUGIN_DIR !== $dir ) {
* Filters the array of excluded directories and files while scanning the folder.
* @param string[] $exclusions Array of excluded directories and files.
$exclusions = (array) apply_filters( 'plugin_files_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) );
$list_files = list_files( $dir, 100, $exclusions );
$list_files = array_map( 'plugin_basename', $list_files );
$plugin_files = array_merge( $plugin_files, $list_files );
$plugin_files = array_values( array_unique( $plugin_files ) );
* Check the plugins directory and retrieve all plugin files with plugin data.
* WordPress only supports plugin files in the base plugins directory
* (wp-content/plugins) and in one directory above the plugins directory
* (wp-content/plugins/my-plugin). The file it looks for has the plugin data
* and must be found in those two locations. It is recommended to keep your
* plugin files in their own directories.
* The file with the plugin data is the file that will be included and therefore
* needs to have the main execution for the plugin. This does not mean
* everything must be contained in the file and it is recommended that the file
* be split for maintainability. Keep everything in one file for extreme
* @param string $plugin_folder Optional. Relative path to single plugin folder.
* @return array[] Array of arrays of plugin data, keyed by plugin file name. See `get_plugin_data()`.
function get_plugins( $plugin_folder = '' ) {
$cache_plugins = wp_cache_get( 'plugins', 'plugins' );
if ( ! $cache_plugins ) {
$cache_plugins = array();
if ( isset( $cache_plugins[ $plugin_folder ] ) ) {
return $cache_plugins[ $plugin_folder ];
$plugin_root = WP_PLUGIN_DIR;
if ( ! empty( $plugin_folder ) ) {
$plugin_root .= $plugin_folder;
// Files in wp-content/plugins directory.
$plugins_dir = @ opendir( $plugin_root );
while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
if ( '.' === substr( $file, 0, 1 ) ) {
if ( is_dir( $plugin_root . '/' . $file ) ) {
$plugins_subdir = @ opendir( $plugin_root . '/' . $file );
while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) {
if ( '.' === substr( $subfile, 0, 1 ) ) {
if ( '.php' === substr( $subfile, -4 ) ) {
$plugin_files[] = "$file/$subfile";
closedir( $plugins_subdir );
if ( '.php' === substr( $file, -4 ) ) {
closedir( $plugins_dir );
if ( empty( $plugin_files ) ) {
foreach ( $plugin_files as $plugin_file ) {
if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
// Do not apply markup/translate as it will be cached.
$plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false );
if ( empty( $plugin_data['Name'] ) ) {
$wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data;
uasort( $wp_plugins, '_sort_uname_callback' );
$cache_plugins[ $plugin_folder ] = $wp_plugins;
wp_cache_set( 'plugins', $cache_plugins, 'plugins' );
* Check the mu-plugins directory and retrieve all mu-plugin files with any plugin data.
* WordPress only includes mu-plugin files in the base mu-plugins directory (wp-content/mu-plugins).
* @return array[] Array of arrays of mu-plugin data, keyed by plugin file name. See `get_plugin_data()`.
function get_mu_plugins() {
if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
// Files in wp-content/mu-plugins directory.
$plugins_dir = @opendir( WPMU_PLUGIN_DIR );
while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
if ( '.php' === substr( $file, -4 ) ) {
closedir( $plugins_dir );
if ( empty( $plugin_files ) ) {
foreach ( $plugin_files as $plugin_file ) {
if ( ! is_readable( WPMU_PLUGIN_DIR . "/$plugin_file" ) ) {
// Do not apply markup/translate as it will be cached.
$plugin_data = get_plugin_data( WPMU_PLUGIN_DIR . "/$plugin_file", false, false );
if ( empty( $plugin_data['Name'] ) ) {
$plugin_data['Name'] = $plugin_file;
$wp_plugins[ $plugin_file ] = $plugin_data;
if ( isset( $wp_plugins['index.php'] ) && filesize( WPMU_PLUGIN_DIR . '/index.php' ) <= 30 ) {
unset( $wp_plugins['index.php'] );
uasort( $wp_plugins, '_sort_uname_callback' );
* Callback to sort array by a 'Name' key.
* @param array $a array with 'Name' key.
* @param array $b array with 'Name' key.
* @return int Return 0 or 1 based on two string comparison.
function _sort_uname_callback( $a, $b ) {
return strnatcasecmp( $a['Name'], $b['Name'] );
* Check the wp-content directory and retrieve all drop-ins with any plugin data.
* @return array[] Array of arrays of dropin plugin data, keyed by plugin file name. See `get_plugin_data()`.
$_dropins = _get_dropins();
// Files in wp-content directory.
$plugins_dir = @opendir( WP_CONTENT_DIR );
while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
if ( isset( $_dropins[ $file ] ) ) {
closedir( $plugins_dir );
if ( empty( $plugin_files ) ) {
foreach ( $plugin_files as $plugin_file ) {
if ( ! is_readable( WP_CONTENT_DIR . "/$plugin_file" ) ) {
// Do not apply markup/translate as it will be cached.
$plugin_data = get_plugin_data( WP_CONTENT_DIR . "/$plugin_file", false, false );
if ( empty( $plugin_data['Name'] ) ) {
$plugin_data['Name'] = $plugin_file;
$dropins[ $plugin_file ] = $plugin_data;
uksort( $dropins, 'strnatcasecmp' );
* Returns drop-ins that WordPress uses.
* Includes Multisite drop-ins only when is_multisite()
* @return array[] Key is file name. The value is an array, with the first value the
* purpose of the drop-in and the second value the name of the constant that must be
* true for the drop-in to be used, or true if no constant is required.
function _get_dropins() {
'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
'db.php' => array( __( 'Custom database class.' ), true ), // Auto on load.
'db-error.php' => array( __( 'Custom database error message.' ), true ), // Auto on error.
'install.php' => array( __( 'Custom installation script.' ), true ), // Auto on installation.
'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // Auto on maintenance.
'object-cache.php' => array( __( 'External object cache.' ), true ), // Auto on load.
'php-error.php' => array( __( 'Custom PHP error message.' ), true ), // Auto on error.