I managed to get the behaviour I was looking for with the following code :
add_action('admin_menu', 'my_plugin_menu');
function my_plugin_menu() {
add_menu_page('Plugin management', 'Plugin management', 'manage_options', 'my_plugin-plugin-settings', 'my_plugin_settings_page', 'dashicons-admin-generic');
}
function my_plugin_settings_page() {
if ( !current_user_can( 'manage_options' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
}
?>
<p>Please click the button below to download the file.</p>
<form action="http://my-website-url/wp-admin/admin-post.php?action=add_foobar&data=foobarid" method="post">
<input type="hidden" name="action" value="add_foobar">
<input type="hidden" name="data" value="foobarid">
<input type="submit" value="Download the file">
</form>
</div>
<?php
}
// From : https://codex.wordpress.org/Plugin_API/Action_Reference/admin_post_(action)
add_action( 'admin_post_add_foobar', 'prefix_admin_add_foobar' );
function prefix_admin_add_foobar() {
// Handle request then generate response using echo or leaving PHP and using HTML
$filename = "file.csv";
$array = array(array(1,2,3,4), array("merle","mésange","pie","coucou"));
$delimiter = ",";
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename="'.$filename.'";');
// open the "output" stream
// see http://www.php.net/manual/en/wrappers.php.php#refsect2-wrappers.php-unknown-unknown-unknown-descriptioq
$f = fopen('php://output', 'w');
foreach ($array as $line) {
fputcsv($f, $line, $delimiter);
}
die();
}
The admin_post_(action) hook was very helpful.