0

I'm developing a plugin and I want to add a menu entry that forces a file download (a CSV).

There are a few posts in this forum and over the internet with a similar solution, all of them are quite old and that solution seems not to work anymore.

This is my current approach (based on the mentioned posts). This does force a file download, but the file is actually a generated HTML with all WP admin menu and everything.

MyPlugin::__construct()

$capability = 'manage_options';
$menu_slug  = 'myplugin-data-export';
$function   = [$this, 'exportCSVdata'];
add_submenu_page(null, '', '', $capability, $menu_slug, $function);

MyPlugin::exportCSVdata()

public function exportCSVdata()
{
    header("Content-type: application/x-msdownload");
    header("Content-Disposition: attachment; filename=data.csv");
    header("Pragma: no-cache");
    header("Expires: 0");
    echo 'data';
    exit();
}

This function is executed when I enter the URL http://example.com/wp-admin/admin.php?page=myplugin-data-export

So, the question maybe is,

how can I create a function inside my plugin class that generates a naked page? With no header, includes and all the WP html stuff?

In some ohter posts I saw a proposal to use admin-post.php instead of admin.php but didn't work either.

EDIT Plugin is being run as follows:

if ( !class_exists( 'MyPlugin' ) ) {
    class MyPlugin
    {
        static $instance = false;

        public static function getInstance()
        {
            if ( !self::$instance )
                self::$instance = new self;
            return self::$instance;
        }

        public function __construct()
        {
            register_activation_hook( __FILE__, [$this, 'mypluginInstall']);
            add_action('admin_menu', [$this, 'adminMenuEntries']);
        }

        public function adminMenuEntries()
        {
            //... some stuff
            $capability = 'manage_options';
            $menu_slug  = 'myplugin-data-export';
            $function   = [$this, 'exportCSVdata'];
            add_submenu_page(null, '', '', $capability, $menu_slug, $function);
        }
    }
}
8
  • 4
    Did you see this answer? That is still working fine.
    – fuxia
    Commented Sep 23, 2021 at 14:22
  • That's the one I mentioned with admin-post.php, it serves a blank page with no error message or log. I'll try to elaborate if you say it should work.
    – user761076
    Commented Sep 23, 2021 at 14:30
  • How s MyPlugin being constructed and ran? When/where?
    – Tom J Nowell
    Commented Sep 23, 2021 at 15:09
  • @TomJNowell I added some code to original post
    – user761076
    Commented Sep 23, 2021 at 15:16
  • @fuxia after testing more carefully again that link returns a blank page... doesn't work
    – user761076
    Commented Sep 23, 2021 at 15:28

1 Answer 1

2

how can I create a function inside my plugin class that generates a naked page? With no header, includes and all the WP html stuff?

There are many ways to do that, e.g. by using the admin_post hook just as you could see in @fuxia's answer here (which does work in WordPress 5.8.1 for what you're trying to do), or the parse_request hook as you could see in my answer here (for outputting an image data) — the WordPress REST API also uses the same hook.

But you said:

I'm developing a plugin and I want to add a menu entry that forces a file download (a CSV).

So if it has to be using a standard admin page URL like http://example.com/wp-admin/admin.php?page=myplugin-data-export, then you could hook your exportCSVdata() function on the load-<page hook> hook, where in your case, the <page hook> value would be the hook name as returned by add_submenu_page():

$hook = add_submenu_page('<parent slug>, '', '<menu title>', $capability, $menu_slug, $function);
add_action( "load-$hook", [ $this, 'exportCSVdata' ] );

Then clicking the menu item would force the file download and the (CSV) file would also have a valid data (and no admin menu HTML displayed on the page).

But then, you could actually link the menu to http://example.com/wp-admin/admin-post.php?action=print.csv by passing an empty string ('') as the 6th parameter for add_submenu_page(), and set the menu slug to admin-post.php?action=print.csv:

add_submenu_page('<parent slug>, '', '<menu title>', $capability, 'admin-post.php?action=print.csv', '');

That way, I bet the admin_post hook would work as expected.

So for example, in your class constructor, you would add add_action( 'admin_post_print.csv', [ $this, 'exportCSVdata' ] );.

3
  • 1
    Perfect, the problem was I was calling add_action( 'admin_post_print.csv', [ $this, 'exportCSVdata' ] ); in the wrong place. It's working now on the plugin __construct function, but it was failing after all the add_submenu() calls.
    – user761076
    Commented Sep 24, 2021 at 8:09
  • 1
    About if it's a menu entry or a button inside a plugin page it doesn't matter, to make it simpler I added the link in a button inside a plugin page.
    – user761076
    Commented Sep 24, 2021 at 8:10
  • 1
    "it was failing after all the add_submenu() calls" - the admin_post hook runs in a standalone PHP file, namely admin-post.php, and the admin_menu hook is not fired on that page, because it's not an admin page with UIs like the admin menu, and instead, it's an endpoint in the admin side which one can use to process form submissions or deliver a custom content like the one in question.
    – Sally CJ
    Commented Sep 24, 2021 at 8:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.