javascriptphpdownloadgridjs

Workaround to XHR request and download prohibition


I have a weird situation: I get data from a Postgres database, and from that data, I create a table in a website, using Grid.js. Each line has a "Download" button, that takes 2 arguments from that table entry and send them to a function. Originally, that function would make a XHR request to a php file, that gets files from another DB, creates a ZIP file, and should send it to the user, with readfile().

Table

I now discovered that this is not possible. XHR does not allow downloads for safety reasons. I could do something using window.location to call the PHP file, and get the download, but I am dealing with hundreds of files, so I cannot write hundreds of PHP files to get the data individually. I could, but it would be very hard to maintain and manage all those files, and it feels unprofessional.

Right now, I can:

I need to:

The current code is:

const getData = (schema, table) => {

    const xhr = new XMLHttpRequest();

    xhr.open('POST', 'php/get_data.php', true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');

    let packg = {
        schema: schema,
        table: table
    };

    const packgJSON = JSON.stringify(packg);
    // Vanilla JS XHR requires this formatting to send the data
    const data = new URLSearchParams({'content': packgJSON});

    xhr.send(data);

};
<?php

// File with connection info
$config = include('config.php');

// Connection info
$host = $config['host'];
$port = $config['port'];
$database = $config['database'];
$username = $config['username'];
$password = $config['password'];

// POST reception
$packg = json_decode($_POST['content']);
$schema = $packg->schema;
$table = $packg->table;



// File info and paths
$filename = $table;
$rootDir = "tempData/";
$fileDir = $filename . "/";
$filePath = $rootDir . $fileDir;
$completeFilePath = $filePath . $filename;
$shpfile = $filename . ".shp";
$zipped = $filename . ".zip";
$zipFile = $completeFilePath . ".zip";



// Function to send the file (PROBLEM - THIS DOES NOT WORK WITH XHR)
function sendZip($zipFile) {

    $zipName = basename($zipFile);

    header("Content-Type: application/zip");
    header("Content-Disposition: attachment; filename=$zipName");
    header("Content-Length: " . filesize($zipFile));

    ob_flush();
    flush();

    readfile($zipFile);

};


// Send the zip if it's already available (NOT PROBLEMATIC)
if (file_exists($zipFile)) {
    
    sendZip($zipFile);

};


// Get shapefile, zip it and send it, if not available (NOT PROBLEMATIC)
if (!file_exists($zipFile)) {

    // Get files
    exec("mkdir -p -m 777 $filePath");
    exec("pgsql2shp -h $host -p $port -u $username -P $password -g geom -k -f $completeFilePath $database $schema.$table");

    // ZIP the files
    $zip = new ZipArchive;

    if ($zip -> open($zipFile, ZipArchive::CREATE) === TRUE) {

        $handlerDir = opendir($filePath);

        // Iterates all files inside folder
        while ($handlerFile = readdir($handlerDir)) {
            // If the files are indeed files, and not directories
            if (is_file($filePath . $handlerFile) && $handlerFile != "." && $handlerFile != "..") {
                // Zip them
                $zip -> addFile($filePath . $handlerFile, $fileDir . $handlerFile);
            };
        };

        // Close the file
        $zip -> close();

    };

    sendZip($zipFile);

};

?>

Solution

  • As pointed out by @epascarello here, a simple GET request solves this.

    Even though I was afraid of not using POST because of an SQL injection attack, the variables pass through a pgsql2shp program, and that only accepts a valid schema and table names, so no need to worry about that as much.

    I am currently using this KISS code, and it works:

    const getData = (schema, table) => {
        
        window.location='php/get_data.php?schema=' + schema + '&table=' + table;
    
    };
    

    In PHP, it's only needed a small change from the POST reception to a GET reception. The variables are already separated, no need to decode anything:

    $schema = $_GET['schema'];
    $table = $_GET['table'];
    

    This goes to show that sometimes, we look so deep into a problem that the solution is right in front of us.