export csv functionality into my plugin

I need to build a “export as .csv” functionality into my plugin. Problem is: i get the header already sent warning, because by the time my “export” function is called, WordPress has already started rendering.

The way i set it up is via a small form in the Settings screen, which action attribute points to a file (download.csv.php) in my plugin folder, that should not be shown, just prompt a file download dialog for the csv dump.

Read More

Here is my code.

In my plugin Settings screen:

<form method="post" id="download_form"  action="<?php echo plugins_url( 'download.csv.php' , __FILE__ ); ?>">
            <input type="hidden" name="download" value="<?php echo get_home_path(); ?>" />
            <input type="submit" name="download_csv"  class="button-primary" value="<?php _e('Download the log (.csv)', $this->localizationDomain); ?>" />
        </form>

And the download.csv.php file:

$core = $_POST['download'].'wp-load.php';

if(isset($_POST['download']) && is_file($core)){

    require_once( $core );

    global $wpdb;

    $rows = $wpdb->get_results('SELECT * FROM `'.$this->dbName.'`;',ARRAY_A);

    if($rows){

        require_once('classes/csvmaker.class.php');
        $CSV = new CSVMaker();

        $CSVHeader = array();
        $CSVHeader['id'] = "ID";
        $CSVHeader['when'] = "WHEN";
        $CSVHeader['who'] = "WHO";
        $CSVHeader['description'] = "DESCRIPTION";
        $CSVHeader['type'] = "TYPE";

        foreach($rows as $r){
            $CSV->addEntry($r);
        }
        $file_name = plugin_dir_path(__FILE__).'data.csv';
        file_put_contents($file_name,$CSV->buildDoc);
        ob_start();
        header("Expires: Mon, 1 Apr 1974 05:00:00 GMT");
        header("Last-Modified: " . gmdate("D,d M YH:i:s") . " GMT");
        header("Cache-Control: no-cache, must-revalidate");
        header("Pragma: no-cache");
        header("Content-type: application/CSV");
        header("Content-Disposition: attachment; filename=$file_name.csv");
        echo $CSV->buildDoc;
        return ob_get_clean();
        exit;

    } 

What am i doing wrong?

Related posts

Leave a Reply

3 comments

  1. The start of what you’re doing wrong can be explained by this quote:

    which action attribute points to a file (download.csv.php) in my
    plugin folder, that should not be shown, just prompt a file download
    dialog for the csv dump.

    and this code:

    $core = $_POST['download'].'wp-load.php';
    if(isset($_POST['download']) && is_file($core)){
        require_once( $core );
    

    What you’re basically doing is a) stepping outside of the WordPress environment and accessing some part of your plugin directly and b) then trying to load the WordPress environment from inside that file (and doing it in an extremely insecure manner, I might add).

    Instead of doing that, it’s better to stay inside the WordPress environment to begin with and to override the output to be the way you want it to be.

    A better way would be to stay in the admin, hook to admin_init, and detect when you need to get your CSV output and return that instead.

    So for your form, do something like this:

    <form method="post" id="download_form" action="">
                <input type="submit" name="download_csv" class="button-primary" value="<?php _e('Download the log (.csv)', $this->localizationDomain); ?>" />
        </form>
    

    Notice no action is used here. This means it’s submitting back to your same admin page, with no changes. Now, you can detect that in whatever function you have connected to the admin_init action hook, like so:

    global $plugin_page;
    if ( isset($_POST['download_csv']) && $plugin_page == 'whatever' ) {
        echo "HELLO"; die;
    }
    

    The $plugin_page global will be set to the page=whatever you have in your normal plugin’s settings screen. Instead of echoing “HELLO” like I did here, call a function in your plugin to generate and output that CSV properly, headers and all, and then die. Or something to that effect.

    Note that this is simplistic, and may be insecure. You may also want to do a nonce and a capabilities check here, to ensure that the user is allowed to download this CSV and intended to do so.

  2. Putting the whole updated code.

    In your plugin update

    <form method="post" id="download_form" action="">
    
    <input type="submit" name="download_csv" class="button-primary" value="<?php _e('Download the log (.csv)', $this->localizationDomain); ?>" />
    
    </form>
    

    Now in your function.php

    add_action("admin_init", "download_csv");
    
    function download_csv() {
    
      if (isset($_POST['download_csv'])) {
    
        global $wpdb;
    
        $sql = "SELECT * FROM {$wpdb->prefix}table_name";
    
        $rows = $wpdb->get_results($sql, 'ARRAY_A');
    
        if ($rows) {
    
            $csv_fields = array();
            $csv_fields[] = "first_column";
            $csv_fields[] = 'second_column';
    
            $output_filename = 'file_name' .'.csv';
            $output_handle = @fopen('php://output', 'w');
    
            header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
            header('Content-Description: File Transfer');
            header('Content-type: text/csv');
            header('Content-Disposition: attachment; filename=' . 
            $output_filename);
            header('Expires: 0');
            header('Pragma: public');
    
            $first = true;
           // Parse results to csv format
            foreach ($rows as $row) {
    
           // Add table headers
                if ($first) {
    
                   $titles = array();
    
                    foreach ($row as $key => $val) {
    
                        $titles[] = $key;
    
                    }
    
                    fputcsv($output_handle, $titles);
    
                    $first = false;
                }
    
                $leadArray = (array) $row; // Cast the Object to an array
                // Add row to file
                fputcsv($output_handle, $leadArray);
            }
    
            //echo '<a href="'.$output_handle.'">test</a>';
    
            // Close output file stream
            fclose($output_handle);
    
            die();
        }
      }
    }
    
  3. I ran into some problems with the above methods – add_action. No did not want the plugin to perform this functionality automatically. Instead I needed a admin solution.

    1.Discovered clean the cache or the output of HTML goes into the csv file.
    2. You have to “exit” the process.

    here is my solution. Change the tables and fields according to your own needs:

     function Export()
     {
      global $wpdb; 
    
      header('Content-Type: text/csv');
      header('Content-Disposition: attachment; filename="export.csv"');
    
     // clean output buffer
     ob_end_clean();
    
     $fp = fopen('php://output', 'w');
    
     $header_row = array(
        0 => 'field 1 label',
        1 => 'field 2 label',
        2 => 'field 3 label',
         );
    
    
     fputcsv($fp, $header_row); 
    
     $Table_Name   = $wpdb->prefix.'YourTable'; 
     $sql_query    = $wpdb->prepare("SELECT * FROM $Table_Name", 1) ;
     $rows         = $wpdb->get_results($sql_query, ARRAY_A);
     if(!empty($rows)) 
       {
        foreach($rows as $Record)
          {  
    
     $OutputRecord = array($Record['field1'],$Record['field2'],$Record['field3']);   
      fputcsv($fp, $OutputRecord);       
       }
      }
    
    fclose( $fp );
    exit;
    }
    

    UPDATE: Since I wrote this, HTML header data goes into the file, if you use the code via shortcode or direct call, on the front end. Using this on the backend / admin section, it works perfectly.