* Filesystem API: Top-level functionality
* Functions for reading, writing, modifying, and deleting files on the file system.
* Includes functionality for theme-specific files as well as operations for uploading,
* archiving, and rendering output when necessary.
/** The descriptions for theme files. */
$wp_file_descriptions = array(
'functions.php' => __( 'Theme Functions' ),
'header.php' => __( 'Theme Header' ),
'footer.php' => __( 'Theme Footer' ),
'sidebar.php' => __( 'Sidebar' ),
'comments.php' => __( 'Comments' ),
'searchform.php' => __( 'Search Form' ),
'404.php' => __( '404 Template' ),
'link.php' => __( 'Links Template' ),
'index.php' => __( 'Main Index Template' ),
'archive.php' => __( 'Archives' ),
'author.php' => __( 'Author Template' ),
'taxonomy.php' => __( 'Taxonomy Template' ),
'category.php' => __( 'Category Template' ),
'tag.php' => __( 'Tag Template' ),
'home.php' => __( 'Posts Page' ),
'search.php' => __( 'Search Results' ),
'date.php' => __( 'Date Template' ),
'singular.php' => __( 'Singular Template' ),
'single.php' => __( 'Single Post' ),
'page.php' => __( 'Single Page' ),
'front-page.php' => __( 'Homepage' ),
'privacy-policy.php' => __( 'Privacy Policy Page' ),
'attachment.php' => __( 'Attachment Template' ),
'image.php' => __( 'Image Attachment Template' ),
'video.php' => __( 'Video Attachment Template' ),
'audio.php' => __( 'Audio Attachment Template' ),
'application.php' => __( 'Application Attachment Template' ),
'embed.php' => __( 'Embed Template' ),
'embed-404.php' => __( 'Embed 404 Template' ),
'embed-content.php' => __( 'Embed Content Template' ),
'header-embed.php' => __( 'Embed Header Template' ),
'footer-embed.php' => __( 'Embed Footer Template' ),
'style.css' => __( 'Stylesheet' ),
'editor-style.css' => __( 'Visual Editor Stylesheet' ),
'editor-style-rtl.css' => __( 'Visual Editor RTL Stylesheet' ),
'rtl.css' => __( 'RTL Stylesheet' ),
'my-hacks.php' => __( 'my-hacks.php (legacy hacks support)' ),
'.htaccess' => __( '.htaccess (for rewrite rules )' ),
'wp-layout.css' => __( 'Stylesheet' ),
'wp-comments.php' => __( 'Comments Template' ),
'wp-comments-popup.php' => __( 'Popup Comments Template' ),
'comments-popup.php' => __( 'Popup Comments' ),
* Gets the description for standard WordPress theme files.
* @global array $wp_file_descriptions Theme file descriptions.
* @global array $allowed_files List of allowed files.
* @param string $file Filesystem path or filename.
* @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist.
* Appends 'Page Template' to basename of $file if the file is a page template.
function get_file_description( $file ) {
global $wp_file_descriptions, $allowed_files;
$dirname = pathinfo( $file, PATHINFO_DIRNAME );
$file_path = $allowed_files[ $file ];
if ( isset( $wp_file_descriptions[ basename( $file ) ] ) && '.' === $dirname ) {
return $wp_file_descriptions[ basename( $file ) ];
} elseif ( file_exists( $file_path ) && is_file( $file_path ) ) {
$template_data = implode( '', file( $file_path ) );
if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name ) ) {
/* translators: %s: Template name. */
return sprintf( __( '%s Page Template' ), _cleanup_header_comment( $name[1] ) );
return trim( basename( $file ) );
* Gets the absolute filesystem path to the root of the WordPress installation.
* @return string Full filesystem path to the root of the WordPress installation.
function get_home_path() {
$home = set_url_scheme( get_option( 'home' ), 'http' );
$siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' );
if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
$wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
$pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) );
$home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos );
$home_path = trailingslashit( $home_path );
return str_replace( '\\', '/', $home_path );
* Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep.
* The depth of the recursiveness can be controlled by the $levels param.
* @since 4.9.0 Added the `$exclusions` parameter.
* @param string $folder Optional. Full path to folder. Default empty.
* @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit).
* @param string[] $exclusions Optional. List of folders and files to skip.
* @return string[]|false Array of files on success, false on failure.
function list_files( $folder = '', $levels = 100, $exclusions = array() ) {
if ( empty( $folder ) ) {
$folder = trailingslashit( $folder );
$dir = @opendir( $folder );
while ( ( $file = readdir( $dir ) ) !== false ) {
// Skip current and parent folder links.
if ( in_array( $file, array( '.', '..' ), true ) ) {
// Skip hidden and excluded files.
if ( '.' === $file[0] || in_array( $file, $exclusions, true ) ) {
if ( is_dir( $folder . $file ) ) {
$files2 = list_files( $folder . $file, $levels - 1 );
$files = array_merge( $files, $files2 );
$files[] = $folder . $file . '/';
$files[] = $folder . $file;
* Gets the list of file extensions that are editable in plugins.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return string[] Array of editable file extensions.
function wp_get_plugin_file_editable_extensions( $plugin ) {
* Filters the list of file types allowed for editing in the plugin editor.
* @since 4.9.0 Added the `$plugin` parameter.
* @param string[] $default_types An array of editable plugin file extensions.
* @param string $plugin Path to the plugin file relative to the plugins directory.
$file_types = (array) apply_filters( 'editable_extensions', $default_types, $plugin );
* Gets the list of file extensions that are editable for a given theme.
* @param WP_Theme $theme Theme object.
* @return string[] Array of editable file extensions.
function wp_get_theme_file_editable_extensions( $theme ) {
* Filters the list of file types allowed for editing in the theme editor.
* @param string[] $default_types An array of editable theme file extensions.
* @param WP_Theme $theme The current theme object.
$file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
// Ensure that default types are still there.
return array_unique( array_merge( $file_types, $default_types ) );
* Prints file editor templates (for plugins and themes).
function wp_print_file_editor_templates() {
<script type="text/html" id="tmpl-wp-file-editor-notice">
<div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}">
<# if ( 'php_error' === data.code ) { #>
/* translators: 1: Line number, 2: File path. */
__( 'Your PHP code changes were rolled back due to an error on line %1$s of file %2$s. Please fix and try saving again.' ),
<pre>{{ data.message }}</pre>
<# } else if ( 'file_not_writable' === data.code ) { #>
/* translators: %s: Documentation URL. */
__( 'You need to make this file writable before you can save your changes. See <a href="%s">Changing File Permissions</a> for more information.' ),
__( 'https://wordpress.org/support/article/changing-file-permissions/' )
<p>{{ data.message || data.code }}</p>
<# if ( 'lint_errors' === data.code ) { #>
<# var elementId = 'el-' + String( Math.random() ); #>
<input id="{{ elementId }}" type="checkbox">
<label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
<# if ( data.dismissible ) { #>
<button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
* Attempts to edit a file for a theme or plugin.
* When editing a PHP file, loopback requests will be made to the admin and the homepage
* to attempt to see if there is a fatal error introduced. If so, the PHP change will be
* @param string[] $args {
* Args. Note that all of the arg values are already unslashed. They are, however,
* coming straight from `$_POST` and are not validated or sanitized in any way.
* @type string $file Relative path to file.
* @type string $plugin Path to the plugin file relative to the plugins directory.
* @type string $theme Theme being edited.
* @type string $newcontent New content for the file.
* @type string $nonce Nonce.
* @return true|WP_Error True on success or `WP_Error` on failure.
function wp_edit_theme_plugin_file( $args ) {
if ( empty( $args['file'] ) ) {
return new WP_Error( 'missing_file' );
if ( 0 !== validate_file( $file ) ) {
return new WP_Error( 'bad_file' );
if ( ! isset( $args['newcontent'] ) ) {
return new WP_Error( 'missing_content' );
$content = $args['newcontent'];
if ( ! isset( $args['nonce'] ) ) {
return new WP_Error( 'missing_nonce' );
if ( ! empty( $args['plugin'] ) ) {
$plugin = $args['plugin'];
if ( ! current_user_can( 'edit_plugins' ) ) {
return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) );
if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
return new WP_Error( 'nonce_failure' );
if ( ! array_key_exists( $plugin, get_plugins() ) ) {
return new WP_Error( 'invalid_plugin' );
if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) );
$editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
$real_file = WP_PLUGIN_DIR . '/' . $file;
(array) get_option( 'active_plugins', array() ),
} elseif ( ! empty( $args['theme'] ) ) {
$stylesheet = $args['theme'];
if ( 0 !== validate_file( $stylesheet ) ) {
return new WP_Error( 'bad_theme_path' );
if ( ! current_user_can( 'edit_themes' ) ) {
return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) );
$theme = wp_get_theme( $stylesheet );
if ( ! $theme->exists() ) {
return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) );
if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $stylesheet . '_' . $file ) ) {
return new WP_Error( 'nonce_failure' );
if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
__( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message()
$editable_extensions = wp_get_theme_file_editable_extensions( $theme );
$allowed_files = array();
foreach ( $editable_extensions as $type ) {
$allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
$style_files = $theme->get_files( 'css', -1 );
$allowed_files['style.css'] = $style_files['style.css'];
$allowed_files = array_merge( $allowed_files, $style_files );
$allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
// Compare based on relative paths.
if ( 0 !== validate_file( $file, array_keys( $allowed_files ) ) ) {
return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) );
$real_file = $theme->get_stylesheet_directory() . '/' . $file;
$is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet );
return new WP_Error( 'missing_theme_or_plugin' );
if ( ! is_file( $real_file ) ) {
return new WP_Error( 'file_does_not_exist', __( 'File does not exist! Please double check the name and try again.' ) );
// Ensure file extension is allowed.
if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
$extension = strtolower( $matches[1] );
if ( ! in_array( $extension, $editable_extensions, true ) ) {
return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) );
$previous_content = file_get_contents( $real_file );
if ( ! is_writable( $real_file ) ) {
return new WP_Error( 'file_not_writable' );
$f = fopen( $real_file, 'w+' );
return new WP_Error( 'file_not_writable' );