How can I add a method to create files when in theme-editor.php

I couldn’t define an “accurate” method to implement the form I desired, so I have settled on the admin_notices action as a viable solution.

The Result Illustrations

View when in Theme Editor of the associated theme. The default style.css is selected, but is not editable. An error message is shown if Update File button is clicked.

theme editor view


Returned view after creating CSS file. When the file is successfully created, the page reloads and open the new file in the code editor.

file created


The CSS file is available for selection in the theme’s supporting plugin. If a stylesheet is selected, it will not be listed in the delete list within the Theme Editor.

supporting plugin config


The Coding

Form process function

public static function makeFile($file, $data=null)
{
    if( !is_null(mb::getPost('makecss')) ) 
    {
        // suppress default error notice since it is not related
        echo '<style>div#message.notice.notice-error {display: none;}</style>';
        
        $file = esc_html($file);
        file_put_contents(MBTHEMEDIR.'/styles/'.$file.'.css', $data);
        mb::redirect(admin_url('theme-editor.php?file=styles/'.$file.'.css&theme=thor'));
    }
    
    //delete file
    if( !is_null(mb::getPost('deletecss')) ) 
    {
        // suppress default error notice since it is not related
        echo '<style>div#message.notice.notice-error {display: none;}</style>';
        
        if( mb::getPost('csslist') != '' ) {
            unlink(MBTHEMEDIR.'/styles/'.mb::getPost('csslist'));
            mb::redirect(admin_url('theme-editor.php?theme=thor'));
        }else{
            echo '<div class="notice notice-warning is-dismissible"><p>No file was selected</p></div>';
        }
    }
    
    $newfile="
    <div class="newfile-form">
    <form action="" method="POST">
        <p>Create New CSS File</p>
        <span>File name: <input type="text" name="newfile" id="newfile" value="" placeholder="mystyle" /></span>
        <span><button type="submit" name="makecss" class="button button-primary">Create File</button></span>
        <span>
        <select name="csslist">
            <option value="">None</option>";
        foreach(mb::filelist(MBTHEMEDIR.'/styles', 'css') as $css) {
            $newfile .= '<option value="'.$css.'">'.$css.'</option>';
        }
        $newfile .= '</select>
        </span>
        <span><button type="submit" name="deletecss" class="button">Delete File</button></span>
    </form>
    </div>
    ';
    
    return $newfile;
}

Action Hook

if( current_user_can('edit_files') ) 
{
   // confirm that the specified theme is selected 
    if( strstr(mb::urlVar('theme'), 'thor') ) 
    {
       // suppress default WP missing file notice when file create request is sent
       echo '<style>div#message.notice.notice-info {display: none;}</style>';

       // implement form process action
       add_action('admin_notices', function() {
            echo mb::makeFile(sanitize_text_field(mb::getPost('newfile')));
        });

        // action to disable editing core files
        add_action('load-theme-editor.php', function()
        {
            $file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING);
            if( in_array($file, ['style.css', '404.php','index.php']) ) {
                wp_redirect(add_query_arg([],self_admin_url('theme-editor.php?theme=thor')));
                exit;
            }
        });
    }
}

I had to use a different page redirection method due to header sent errors when using wp_redirect()

public static function redirect($url) {
    if( !headers_sent() ) {
        wp_redirect(admin_url($url));
    }else{
        echo '<meta http-equiv="refresh" content="0; URL='.$url.'">';
    }
}

The filelist() method used to output the select options of the delete field. WP has a method for this but I couldn’t get it to behave as desired, so I corrected it

public static function filelist($path, $filter=null, $getpath=false)
{
    $files = new \DirectoryIterator($path);
    $filelist=[];
    foreach($files as $file) 
    {
        if( $file->isFile() && !$file->isDot() ) 
        {
            // include only files in $filter 
            // methods: 'css' or 'css|txt' or starting with '^cat' or ending with '$er'
            if( !empty($filter) && !preg_match(chr(1).$filter.chr(1), $file) ) {
                continue;
            }
            
            $filelist[] = ($getpath == true ? $file->getPath()."https://wordpress.stackexchange.com/".$file->getFilename() : $file->getFilename());
        }
    }
    
    return $filelist;
}

The getPost() method is just a set of global functions to check the form request values. The WP method to do the same task would be applicable.

The urlVar() method is a set of global functions to check the URL string for query variables and their values. The WP method to do the same would be applicable.

That’s my story and I’m sticking with it!