0

I am currently working on a project that uses HTML and PHP to allow a user to enter information into a web form in order to generate an Excel file. On submission, the form runs a PHP file, main.php:

<?php
    // output headers so that the file is downloaded rather than displayed
    $order = $_POST["order"];
    header('Content-Type: text/plain; charset=utf-8');
    header('Content-Disposition: attachment; filename="'.$_POST['order'].'.xls"');
    exec('php xl.php');
    sleep(2);
    readfile('xl_template.xls');
    error_reporting(E_ALL);
    ini_set("display_errors", 1);
?>

where "xl.php" is another PHP file and "xl_template" is a template for the Excel sheet I wish to modify. The purpose of main.php is to grab the modified template and download it to the user's computer, while xl.php actually modifies the Excel template and saves it to the server computer (using PHPExcel library):

<?php
    // this file will be called by main.php
    // after execution, there should be a newfile.xls
    // for the main.php to read from
    error_reporting(E_ALL);
    require('Classes/PHPExcel.php');

    // variable definitions
    $template = "xl_template.xls";

    $objPHPExcel = PHPExcel_IOFactory::load($template);
    $objWorksheet = $objPHPExcel->getActiveSheet();
    $objWorksheet->getCell('A2')->setValue('123400000000000000000001');
    $objWorksheet->getCell('B2')->setValue('it worked!');
    $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');
    $objWriter->save($template);
?>

This works as expected when the xl.php file is called from terminal and then the form is filled out. However, when xl.php is called from main.php through the exec('php xl.php') method, the Excel file does not get updated, which I assume to mean that xl.php was not successfully executed.

In addition to exec(), I have tried system(),shell_exec, the backtick operator, and passthru(), with the same results. Also, I have tried Javascript and JQuery methods, calling the xl.php file with $.get('xl.php') and $.ajax({url: 'xl.php'}) with no luck.

Any insight into this problem would be greatly appreciated, as I am still new to using PHP. Thanks.

Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
R. Hilyer
  • 13
  • 4
  • So much text - whats your problem? you can't call xl.php from a php script? – Xatenev Jun 02 '16 at 15:34
  • why fire up a seperate php to execute that external script? can't you write that second script as a function call, then simply `include()`, then do `run_excel_stuff()`? – Marc B Jun 02 '16 at 15:37
  • @Xatenev: I can call it, but it does not modify the Excel file that I have on the server side. – R. Hilyer Jun 02 '16 at 15:38
  • @MarcB: I tried the `include()` method also, with the same results. The reason I had to separate the files was because the `readfile()` method in main.php was causing errors if a function was called before it. So I need the Excel file to be modified before the `readfile()` is called. – R. Hilyer Jun 02 '16 at 15:41
  • System calls might be forbidden. Why should `readfile` cause errors when previously a function has been executed? Errors in file format occur when you output error and warning messages instead of logging them. If the modification is only for this single request, there's no need to store something to disk. – Pinke Helga Jun 02 '16 at 15:51

2 Answers2

1

I would suggest making two separate functions and calling them in the order you need to to prevent the error from occurring. This is the most efficient way to do this.

If you really want to use a different page, you could could possibly use the header() function in php to redirect to the second php page, and include any variables you need to pass in the URL and retrieve them using $_GET().

DMort
  • 347
  • 1
  • 2
  • 10
  • I agree on this. I think having just one php file and 2 functions called in order is the easiest way. If you want to have them in two files, you can import one file into another and just make a call to the function in the second file. – Biribu Jun 02 '16 at 15:53
  • Like i said, I'm fairly new to PHP so I'll give the function calls another try just in case my errors were caused by me instead of PHP. – R. Hilyer Jun 02 '16 at 15:55
  • Got it working. I had to move some stuff around, create some functions, and change some file permissions, but it finally works. Thanks a lot for your help. – R. Hilyer Jun 02 '16 at 16:20
  • However, this is not how you are supposed to do it! Avoid temporary files where possible, they introduce unnecessary overhead. And in your case, you are even saving it in the script folder instead of a temp folder, and you even have a race condition due to the fixed filename! Imagine what happens if two people download the excel file at the same time... The right way to do it is to use `$objWriter->save("php://output");`, where `php://output`is a fake-filename which will stream it to the user directly. Therefore, though, you would need to use `require("xl.php");` and not `exec("php xl.php");`. – CherryDT Jun 02 '16 at 16:22
  • Plus, you are sending it with the wrong mime type. I don't think an excel file is plain text. You would need `header('Content-type: application/vnd.ms-excel');`. – CherryDT Jun 02 '16 at 16:22
0

Problem 1

You are using a pretty weird way of loading your second PHP file. While exec("php xl.php") will indeed execute xl.php, it will do it as if you were executing it from a command line. Your xl.php will not have access to any variables or other information from your outer script, and it can't write anything to the output which is sent to the user. Also, it is much harder to debug any errors occuring in the second script this way.

You should instead use require("xl.php"); which will run the xl.php file at this point as if its content were directly part of your outer script.

Problem 2

You are writing to a file on the server. This is something you should avoid doing unless absolutely necessary (e.g. for user uploads).

  • In your case, the write probably fails in the first place because you write it into the same directory your PHP file resides in. Usually, PHP doesn't have permission to write in there - for a good reason, because if your webserver (e.g. Apache) will only run PHP files in this directory, and PHP doesn't have the rights to write into it, then it is a lot harder for an attacker to put their own malicious PHP code in there and run it, or modify one of your scripts to add malicious code to it.
  • Assuming you got the permissions issue solved (which should be done by using a separate directory outside of the PHP script directory, possibly just the temporary directory - not by changing the permissions to allow writing into the script directory!), then you have another problem: Your filename is constant. Now assume two users download an Excel file at the same time... Everything will get messed up because two sessions will try writing/reading to/from the same file.
  • Assuming you fixed this by using a random temporary name (and then also delete the file again at the end), then there is still the issue that you are accessing the hard disk when it's not really needed. You could instead just directly stream the content of the Excel file from memory to the user's browser.

So, the best solution would be not using a file at all, but directly sending the data to the user. This can be done using the pseudo-filename php://output which basically sends all data written to it directly to the user.

Problem 3

Your content type header specifies text/plain and UTF-8 as charset, but you are sending an Excel file. Since an Excel file is not plain text (and probably not UTF-8 either), you should use the right MIME type instead, which is application/vnd.ms-excel.

Problem 4

You are turning error reporting on only after all the action already happened. This means that if an error occured, it will not be reported the way you expect, because it happens before the reporting is enabled.

Final example

Incorporating the fixes mentioned above, your code could look something like this:

main.php:

<?php

    error_reporting(E_ALL);
    ini_set("display_errors", 1);

    // output headers so that the file is downloaded rather than displayed
    $order = $_POST["order"];
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment; filename="'.$_POST['order'].'.xls"');

    require("xl.php");
?>

xl.php:

<?php
    // this file will be called by main.php
    require('Classes/PHPExcel.php');

    $objPHPExcel = PHPExcel_IOFactory::load($template);
    $objWorksheet = $objPHPExcel->getActiveSheet();
    $objWorksheet->getCell('A2')->setValue('123400000000000000000001');
    $objWorksheet->getCell('B2')->setValue('it worked!');
    $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');
    $objWriter->save("php://output");
?>

Final thoughts

Now that you change the code like this, it would probably be best to think about whether this construct with the second PHP file is even needed like this, and if it is, whether you maybe want to wrap the code in a function and call it from the outer script so you can nicely pass parameters as well, because I assume you are not always going to write just staticly "It works!" in there.

CherryDT
  • 25,571
  • 5
  • 49
  • 74