0

I've been coding in PHP for a long time, but I'm currently writing my first Wordpress plugin. The plugin's goals are:

  • Display a form on a public-facing page to collect simple data from site visitors (first/last name, etc)
  • Provide a way for admins export the data

I've got a plugin that successfully creates a table on activation and a shortcode that provides a form which successfully stores the submitted data in the database.

On the back-end, I have a dashboard widget that currently displays some stats about the submissions, and my last task is to provide a button to export those stats to CSV, and that's where I'm stumped. I'm not sure how to handle this in WP world...in the past, I would have had the button open a new window to a page that does the exporting and echos a CSV string to the page along with headers that indicate it's a binary file so it's downloaded. In WP, how do I accomplish this? Do I put a PHP script in my plugin directory and have my widget open that page? If so, how does that page gain access to $wpdb to handle the data access?

Here is my code (just for the dashboard widget part) as it stands now:

<?php
/*
Plugin meta details
 */
add_action('init', 'myplugin_buffer_start');
add_action('wp_footer', 'myplugin_buffer_end');

function myplugin_ob_callback($buffer) {
    // You can modify buffer here, and then return the updated code
    return $buffer;
}

/**
 * Action: init
 * Runs after WP has finished loading but before any headers are sent, user is already authenticated at this point
 * Good for intercepting $_POST/$_GET
 */
function myplugin_buffer_start() 
{ 
    ob_start("myplugin_ob_callback"); 
}

/**
 * Action wp_footer
 * Triggered near the </body> tag of the user's template by the wp_footer() function.
 */
function myplugin_buffer_end() 
{ 
    ob_end_flush(); 
}


/****************************************************************
 *  Stats Dashboard Widgets
 ***************************************************************/
function myplugin_displaytestFormWidget_process()
{
    $errors = array();

    if ( 'POST' == $_SERVER['REQUEST_METHOD'] && isset ( $_POST['myplugin_export_button'] ))
    {
        ob_end_clean(); // erase the output buffer and turn off buffering...blow away all the markup/content from the buffer up to this point

        global $wpdb;
        $tableName = $wpdb->prefix . "myplugin_test_form";
        $qry = "select Name, Date from $tableName order by Date desc";

        //ob_start();  when I uncomment this, it works!
        $result = $wpdb->get_results($qry, ARRAY_A);
        if ($wpdb->num_rows)
        {
            $date = new DateTime();
            $ts = $date->format("Y-m-d-G-i-s");
            $filename = "myCsvFile-$ts.csv";
            header( 'Content-Type: text/csv' );
            header( 'Content-Disposition: attachment;filename='.$filename);

            $fp = fopen('php://output', 'w');
            //$headrow = $result[0];
            //fputcsv($fp, array_keys($headrow));
            foreach ($result as $data) {
                fputcsv($fp, $data);
            }
            fclose($fp);

            //when I uncomment these lines along with adding ob_start above, it works
            //$contLength = ob_get_length();
            //header( 'Content-Length: '.$contLength);
        }
    }

    return myplugin_displaytestFormWidget();
}   

function myplugin_displaytestFormWidget()
{
    global $wpdb;
    $tableName = $wpdb->prefix . "myplugin_test_form";

    $submissionCount = $wpdb->get_var("select count(Id) from $tableName");
?>
    <div><strong>Last entry: </strong>John Q. test (May 5, 2013)</div>
    <div><strong>Total submissions: </strong> <?php echo $submissionCount ?></div>

    <form id="myplugin_test_export_widget" method="post" action="">
        <input type="submit" name="myplugin_export_button" value="Export All" />
    </form>
<?php
}

function myplugin_addDashboardWidgets()
{
    // widget_id, widget_name, callback, control_callback
    wp_add_dashboard_widget(
        'test-form-widget', 
        'test Form Submissions', 
        'myplugin_displaytestFormWidget_process'
    );  
}

/****************************************************************
 *  Hooks
 ***************************************************************/
//add_action('widgets_init', 'simple_widget_init');
add_action('wp_dashboard_setup', 'myplugin_addDashboardWidgets' ); 

// This shortcode will inject the form onto a page
add_shortcode('test-form', 'myplugin_displaytestForm_process');

register_activation_hook(__FILE__, 'myplugin_test_form_activate');

You can see in the myplugin_displayTestFormWidget function I'm displaying the form, I just don't know what to do with the button to make it all jive.

Screenshot of widget output

Can anyone assist?

tjans
  • 1,503
  • 2
  • 16
  • 26
  • One option I was thinking was maybe the process function could write the csv string to a file, then open another window that reads it out and offers it as a download, deleting the file on the file system when that completes. I think ideally, best case would be if there was a way to open a new page that was able to gain access to $wpdb and write out the content with the headers in place to be served as a download. – tjans May 23 '13 at 21:25

2 Answers2

2

At first add following code in your plugin

add_action('init', 'buffer_start');
add_action('wp_footer', 'buffer_end');

function callback($buffer) {
    // You can modify buffer here, and then return the updated code
    return $buffer;
}
function buffer_start() { ob_start("callback"); }
function buffer_end() { ob_end_flush(); }

Just at the top, right after the plugin meta info like

/**
 * @package Word Generator
 * @version 1.0
 * ...
 */
 // here goes the code given above, it'll solve the header sent error problem

And following code will dump a csv file

if ( 'POST' == $_SERVER['REQUEST_METHOD'] && isset ( $_POST['myplugin_export_button'] ))
{
    // Somehow, perform the export 
    ob_clean();
    global $wpdb;
    $qry = 'your query';
    $result = $wpdb->get_results($qry, ARRAY_A);
    if ($wpdb->num_rows){
        $date = new DateTime();
        $ts = $date->format("Y-m-d-G-i-s");
        $filename = "myCsvFile-$ts.csv";
        header( 'Content-Type: text/csv' );
        header( 'Content-Disposition: attachment;filename='.$filename);

        $fp = fopen('php://output', 'w');
        $headrow = $result[0];
        fputcsv($fp, array_keys($headrow));
        foreach ($result as $data) {
            fputcsv($fp, $data);
        }
        fclose($fp);
        $contLength = ob_get_length();
        header( 'Content-Length: '.$contLength);
        exit();
    }
}
The Alpha
  • 143,660
  • 29
  • 287
  • 307
  • Thanks, this is getting me closer. Just have to work through a few issues...for one, why is the ob_end_clean needed on line 4? Wouldn't that kill the previously opened buffer (opened on buffer_start hook)? Also, while this code does download a csv file, the error log and the CSV still has a warning on line 135 that headers were already started at line 131, which 135 is the header('Content-Length:') line and 131 is the fputcsv line... – tjans May 24 '13 at 13:53
  • Actually, it's also including the remaining markup for the entire wordpress dashboard page in the CSV file too...must be something out of order here. Also, when I remove the header( 'Content-Length: '.$contLength); line, it writes it to the CSV without error... – tjans May 24 '13 at 15:09
  • Actually `ob_end_clean();` removes the other `content/markup` and this code is taken from my working plugin. – The Alpha May 24 '13 at 15:24
  • I just did my own test and I see what the ob_end_clean does (removes the markup from the beginning of the doctype/html tag down to where my widget begins), so I understand that. What I don't understand is why that header('content-length line causes the headers to be sent warning, and also my CSV file is getting my csv data plus all the content/markup that follows...I have to be missing some small, but significant crucial step that is obviously working correctly in your plugin but not mine... – tjans May 24 '13 at 15:37
  • Was I supposed to include something in the callback where you specify that you can modify the buffer? I just left it as is, and my code to create the csv is still in my process function... – tjans May 24 '13 at 15:41
  • It's ok, `$buffer` variable contains the output so you return it after some modification or without any modification. – The Alpha May 24 '13 at 15:44
  • I've updated my code as it stands now...I know I probably just have one piece out of place...seems like if I understand it right, on init, it starts an output buffer...when it gets to my widget, it cleans the output buffer, outputs the CSV to a file for download, and then tries to get the length of the content using ob_get_length()...is there still a buffer going? Didn't the ob_end_clean stop buffering that was started in init? I think my issue is that of a lack of knowledge of the lifecycle... – tjans May 24 '13 at 15:48
  • I think I figured it out...i need the content length so that it doesn't include all the markup following the CSV content. The problem is, when I include that, I get the header warning, but when I add a "ob_start()" right after the ob_end_clean, I don't get the header error and the CSV only includes the intended content...why doesn't yours need an ob_start but mine does? – tjans May 24 '13 at 15:54
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/30582/discussion-between-tjans-and-sheikh-heera) – tjans May 24 '13 at 15:59
  • http://stackoverflow.com/questions/7186189/what-is-the-meaning-of-php-input-php-output-and-when-it-needs-to-use – The Alpha May 24 '13 at 16:08
  • I'm having a minor issue with this solution on our live server, and rather than fill up this discussion with even more, I posted a code-review stack exchange post and posted my code: http://codereview.stackexchange.com/questions/27734/exporting-data-in-a-wordpress-widget – tjans Jun 24 '13 at 16:10
  • @tjans, Updated with `exit()` at the end, you fixed it already. – The Alpha Jun 24 '13 at 19:03
0

I've implemented similar functionality in another plugin I developed a while ago. I won't claim it's the best practice (I'm not 100% sure if there is such a thing in this instance) but it seemed like a clean and reliable solution for me at the time.

Picking up from where you left off, inside your myplugin_displayTestFormWidget_process function, let me just put some real and pseudo code that should get you rolling.

if ( 'POST' == $_SERVER['REQUEST_METHOD'] && isset ( $_POST['myplugin_export_button'] ))
{
    // clear out the buffer
    ob_clean();
    // get the $wpdb variable into scope so you may use it
    global $wpdb;

    // define some filename using a timestamp maybe
    // $csv_file_name = 'export_' . date('Ymd') . '.csv';

    // get the results of your query
    $result = $wpdb->get_results("SELECT * FROM your_table");

    // loop your results and build your csv file
    foreach($result as $row){
        // put your csv data into something like $csv_string or whatever
    }

    header("Content-type: text/x-csv");
    header("Content-Transfer-Encoding: binary");
    header("Content-Disposition: attachment; filename=".$csv_file_name);
    header("Pragma: no-cache");
    header("Expires: 0");

    echo $csv_string;
    exit;
}

You mentioned you are pretty comfortable with PHP so I didn't really dig into the PHP aspects of getting this done.

Hope that helps, have fun!

Jared Cobb
  • 5,167
  • 3
  • 27
  • 36
  • Thanks for the reply...I'm very familiar with the guts of what you're doing there, I've done that kind of thing before, but can you actually do that within a WP widget? Just for testing I tried adding a header("Location: http://www.google.com") and it gave me the ol' familiar error about headers already being sent by previous output...and what in this example are you using the output buffering for? I see you never close it and you exit the script...that'll kill the loading of the rest of the page... – tjans May 23 '13 at 20:54
  • Not fully understanding what you were aiming at, I thought I'd still give your idea a shot...as I suspected, I got the Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\wordpress\wp-admin\includes\template.php:1642) in... – tjans May 23 '13 at 20:59