Zip multiple files on server and echo back zip

Consider a site which allows user to store files (pdf, docx, jpeg, png, gif only). Part of the html:

<ul>
	<li><a href="../folder/lola.doc" target="_blank">lola.doc</a></li>
	<li><a href="../folder/lola.pdf" target="_blank">lola.pdf</a></li>
	<li><a href="../folder/lola.jpeg" target="_blank">lola.jpeg</a></li>
	<li><a href="../folder/lola.docx" target="_blank">lola.docx</a></li>
</ul>

When a user clicks on any of the above, the file either opens or a save dialpg appears. This is fine.

Now I want user to be able to select some of these files (which are on the server). The files will be zipped and echo back to user with a prompt to save. I cannot use above, so I have this option:

html:

<select class="multiple_select " multiple>
	<option value="../folder/lola.doc">lola.doc</option>
	<option value="../folder/lola.pdf">lola.pdf</option>
	<option value="../folder/lola.jpeg">lola.jpeg</option>
	<option value="../folder/lola.docx">lola.docx</option>
</select>
<button id="btn" type="button">Download</button>

js:

js:

$('#btn').on('click', function() {
	var options_selected = $('select').find('option:selected');
	options_selected_le = options_selected.length;
	var i;
	var options_selected_arr = [];
	var options_names_arr = [];
	for (i=0; i<options_selected_le; i++) {
		options_selected_arr.push(options_selected.val());
		options_names_arr.push(options_selected.text());
	}
	
	var fd = new FormData();
	fd.append('zipname', zipname);
	fd.append('options_selected_arr', JSON.stringify(options_selected_arr));
	fd.append('options_names_arr', JSON.stringify(options_names_arr));
	$.ajax({
	    url: 'download_multiple_files.php', 
	    type: 'post', 
	    data: fd,
	    cache: false,
	    contentType: false, 
	    processData: false,
	    beforeSend: function(xhr) { 
	    	xhr.setRequestHeader("X-Download", "yes"); 
	    },
	    success: function(response){
			alert(response); //I am sure this is wrong
            // Do I need js to handle zip file here. I guess php should automatically do this
	    }
	}); 
});

<?php
  session_start();
  require 'server_conn.php'; // for connection and holds test_input function
  // do some security checks ...
  
  $zipname = 'file.zip';
  $arr = json_decode($_POST['options_selected_arr']);
  $file_arr = [];
  foreach ($arr as $obj) {
    array_push($files_arr, test_input($obj));
  }  
  
  $arr = json_decode($_POST['options_names_arr']);
  $files_names_arr = [];
  foreach ($arr as $obj) {
    array_push($files_names_arr, test_input($obj));
  }

  $zip = new ZipArchive;
  $zip->open($zipname, ZipArchive::CREATE);
  for ($i=0; $i<$c; $i++) {
    $zip->addFile($file_arr[$i], $files_names_arr[$i]);
  }  
  $zip->close();
  
  header('Content-Type: application/zip');
  header('Content-Length: ' . filesize($zipname));
  header('Content-Disposition: attachment; filename="file.zip"');
  readfile($zipname);
  unlink($zipname);   
?>

Response from server is giberish and there is no error indication. I suspect my php is defective.

I have solved this using 2 methods:
Method 1:
JSZip without php (Each select option already contains file path as value)
The advantage of this method: It does not store the new zip file on the server, so storage is not a problem.
I believe using blob will also allow ziping large files, max size I don’t know.
To use this method, one needs to download Filesaver, jszip and jszip utility and add following lines to the html doc body

	<script src="../js/lib/jszip.min.js"></script>
	<script src="../js/lib/jszip-utils.min.js"></script>
	<script src="../js/lib/FileSaver.js"></script>

The js script makes use of Promisejs, which I haven’t studied before (but will now do). Below is the js:

$('#btn').on('click', function() {
	function urlToPromise(url) {
	    return new Promise(function(resolve, reject) {
	        JSZipUtils.getBinaryContent(url, function (err, data) {
	            if(err) {
	                reject(err);
	            } else {
	                resolve(data);
	            }
	        });
	    });
	}

	var options_selected = $('select').find('option:selected');
	options_selected_le = options_selected.length;

	var zipname = 'file.zip';

	var Promise = window.Promise;
	if (!Promise) {
	    Promise = JSZip.external.Promise;
	}
    
    var i;
    var zip = new JSZip();
    for (i=0; i<options_selected_le; i++) {
		var url = options_selected.eq(i).val();
        var filename = options_selected.eq(i).text();
        zip.file(filename, urlToPromise(url), {binary:true});
	}
    
    zip.generateAsync({type:"blob"}).then(function callback(blob) {
        //see FileSaver.js
        saveAs(blob, zipname);
        //alert('success'); 
    }, function (e) {
    	alert('Error zipping file(s). Retry');
    }); 
});

Method 2:
Using js and PHP:
First create a folder on the server to hold the zip file, I name the folder ‘archive’
This is why I may not vote for this method.
New js:

$('#btn').on('click', function() {
	var options_selected = $('select').find('option:selected');
	options_selected_le = options_selected.length;

	var zipname = 'file.zip';		
	var fd = new FormData();
	fd.append('zipname', zipname);
	fd.append('options_selected_arr', JSON.stringify(options_selected_arr));
	fd.append('options_names_arr', JSON.stringify(options_names_arr));

    $.ajax ({
    	url: 'download_multiple_files.php', 
	    type: 'post', 
	    data: fd,
	    cache: false,
	    contentType: false, 
	    processData: false,
	    success: function(response){
			window.location = response;
	    }
	});
});

New php:

<?php
  session_start();
  // connect to server, scan input data and do some security checks ...
  
  $zipname = 'file.zip';
  $arr = json_decode($_POST['options_selected_arr']);
  $file_arr = [];
  foreach ($arr as $obj) {
    array_push($files_arr, test_input($obj));
  }  
  
  $arr = json_decode($_POST['options_names_arr']);
  $files_names_arr = [];
  foreach ($arr as $obj) {
    array_push($files_names_arr, test_input($obj));
  }

  $zip = new ZipArchive();
  $path = '/archive/'.$zipname;
  if ($zip->open($path, ZipArchive::CREATE)!==TRUE) {
    echo 'Cannot zip files'; die;
  }

  $c = count($file_arr);
  for ($i=0; $i<$c; $i++) {
    $zip->addFile($file_arr[$i], $files_names_arr[$i]);
  }

  $zip->close();
  echo $path;
  mysqli_close($conn);
?>

This will force save dialog to appear. Two pending challenges I have for this method are:
Prevent a new window to open
The save dialog appears with download as file name but without extension .zip. So user should type .zip along with the name. I would prefer the computed zip filename to appear in the save dialog

Glad you solved your own puzzle. Just remember that Javascript is BROWSER-SIDE.
Therefore, hackers can see all the code it in. PHP is SERVER-SIDE-ONLY and therefore,
hackers can not see the code in it.

Javascript should never be used for site file handling. Always do that server-side and push the results
back out to the javascript or webpage.

Sponsor our Newsletter | Privacy Policy | Terms of Service