Search code examples
javascriptphphtmlfile-upload

How I can use $_FILES value without input that has `file` attribute?


In my case I manipulate an image using client-side javascript, because I want to scale it before uploading it:

$("#base_img_to_compress").on("change", (e) => {
  resizeImage(source, 1440, 1080).then((imgData) => {
    appendToFormHiddenImageInput("base_img_data",imgData);
  });
});

$("#upload_form").on("submit",(e)=>{
    $.ajax({
      method:"POST",

      //How I can configure the postdata here ?
    })

});

function appendToFormHiddenImageInput(name, imgData) {
  const child = $("#upload_form").children("input[name=" + name + "]");
  if (child.length > 0) {
    child.remove();
  }
  const input = document.createElement('input');
  input.type = "hidden";
  input.setAttribute("type", "hidden");
  input.setAttribute("name", name);
  input.setAttribute("value", imgData);
  $("#upload_form").append(input);
}

function resizeImage(element, width, height) {
  const element_file = element.files[0];
  return imageConversion.compressAccurately(element_file, {
    quality: 0.7,
    type: "image/png",
    width: width,
    height: height,
    size: 80
  }).then((file) => {
    return imageConversion.filetoDataURL(file);
  })
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/WangYuLue/image-conversion/build/conversion.js"></script>

<form id="upload_form">
  <button type="submit" id="savebtn">Save me</button>
</form>
<input id="base_img_to_compress" type="file" />

The php script that handles the upload is rather simple:

$file = $_FILES['base_img_data'];

if($file['tmp_name']){
  echo "FILE UPLOADED";
  exit;
}

echo "FILE NOT UPLOADED";

But in order for $_FILES to work need the data to be uploaded as multipart/form-data where the base_img_data must be encoded as base64 string. Is there a way to do this at my javascript without using an <input type="file" /> input element as w3schools say so?

In other words the part that troubles me is this piece of javascript:

$("#upload_form").on("submit",(e)=>{
    $.ajax({
      method:"POST",

      //How I can configure the postdata here ?
    })

});

I mean, I need to populate the ajax accordingly in order to emulate a multipart form upload without using an input field that has file attribute. But in my case, I use hidden input fields with values encoded as base64, therefore the PHP won't recognize the incoming uploaded file using $_FILES magic variable.


Solution

  • Quick Answer

    You have to manually build your own FormData object. This is an approach:

    const formData = new FormData();
    
    $("#upload_form :input").each((index,element)=>{
        let value = $(element).val();
        if($(element).attr('type') == "hidden"){
            formData.append($(element).attr('name'),DataURIToBlob(value),`myfile_${index}.png`);
        }
        formData.append($(element).attr('name'),value);
    });
    
    
    function DataURIToBlob(dataURI) {
        const splitDataURI = dataURI.split(',')
        const byteString = splitDataURI[0].indexOf('base64') >= 0 ? atob(splitDataURI[1]) : decodeURI(splitDataURI[1])
        const mimeString = splitDataURI[0].split(':')[1].split(';')[0]
    
        const ia = new Uint8Array(byteString.length)
        for (let i = 0; i < byteString.length; i++){
            ia[i] = byteString.charCodeAt(i)
        }
    
        return new Blob([ia], { type: mimeString })
    }
    

    Then do the ajax call like that:

    $.ajax({
        method:"POST",
        data: formData,
        url: "./script.php",
        processData: false,
        contentType: false,
        success: ()=>{
            alert("BANZAI");
        }
    })
    

    In your example a complete submit method is:

    $("#upload_form").on("submit",(e)=>{
                    
        e.preventDefault();
        const formData = new FormData();
    
        $("#upload_form :input").each((index,element)=>{
            let value = $(element).val();
            
            if($(element).attr('type') == "hidden"){
                console.log(formData);
                formData.append($(element).attr('name'),DataURIToBlob(value),`myfile_${index}.png`);
            }
            formData.append($(element).attr('name'),value);
        });
    
        $.ajax({
            method:"POST",
            data: formData,
            url: "./script.php",
            processData: false,
            contentType: false,
            success: ()=>{
                alert("BANZAI");
            }
        })
    });
    
    function DataURIToBlob(dataURI) {
        const splitDataURI = dataURI.split(',')
        const byteString = splitDataURI[0].indexOf('base64') >= 0 ? atob(splitDataURI[1]) : decodeURI(splitDataURI[1])
        const mimeString = splitDataURI[0].split(':')[1].split(';')[0]
    
        const ia = new Uint8Array(byteString.length)
        for (let i = 0; i < byteString.length; i++){
            ia[i] = byteString.charCodeAt(i)
        }
    
        return new Blob([ia], { type: mimeString })
    }
    

    Please notice that upon ajax alongside with data option I use these options as well:

            processData: false,
            contentType: false,
    

    If they are not set to false then the upload won't happen.

    What if I also have/populate hidden input fields that do not contain file data?

    Upon placing input differentiate it using a data attribute, for example, data-file once you dynamically add it. In your example an approach should be to replace the following function:

    function appendToFormHiddenImageInput(name, imgData) {
      const child = $("#upload_form").children("input[name=" + name + "]");
      if (child.length > 0) {
        child.remove();
      }
      const input = document.createElement('input');
      input.type = "hidden";
      input.setAttribute("type", "hidden");
      input.setAttribute("name", name);
      input.setAttribute("value", imgData);
      input.setAttribute("data-file",true); // <<< This appends the distingushing indicator
      $("#upload_form").append(input);
    }
    

    The use the following submit method:

    
    $("#upload_form").on("submit",(e)=>{
                    
        e.preventDefault();
        const formData = new FormData();
    
        $("#upload_form :input").each((index,element)=>{
            let value = $(element).val();
            
            if($(element).attr('type') == "hidden" && $(element).data('file') === true ){
                console.log(formData);
                formData.append($(element).attr('name'),DataURIToBlob(value),`myfile_${index}.png`);
            }
            formData.append($(element).attr('name'),value);
        });
    
        $.ajax({
            method:"POST",
            data: formData,
            url: "./script.php",
            processData: false,
            contentType: false,
            success: ()=>{
                alert("BANZAI");
            }
        })
    });
    
    function DataURIToBlob(dataURI) {
        const splitDataURI = dataURI.split(',')
        const byteString = splitDataURI[0].indexOf('base64') >= 0 ? atob(splitDataURI[1]) : decodeURI(splitDataURI[1])
        const mimeString = splitDataURI[0].split(':')[1].split(';')[0]
    
        const ia = new Uint8Array(byteString.length)
        for (let i = 0; i < byteString.length; i++){
            ia[i] = byteString.charCodeAt(i)
        }
    
        return new Blob([ia], { type: mimeString })
    }
    
    

    Pay attention to the line:

    if($(element).attr('type') == "hidden" && $(element).data('file') === true )
    

    I also check if the field is an input file or not.

    Also pay attention to:

    input.setAttribute("data-file",true);
    

    Why does it work?

    It works because using form-data you make a POST encoded as multipart/form-data. $_FILES is a parsed value from a body that seems to be a file. In our case we re-constructed the form as multipart one at line:

    formData.append($(element).attr('name'),DataURIToBlob(value),`myfile_${index}.png`);
    

    At function appendToFormHiddenImageInput

    Miscellanous

    In my case I set a name with fixed file extension, if you want to manage it manually use the blob type. I could do it like this:

    
    const name = `myfile_${index}`
    value = DataURIToBlob(value);
    
    let suffix = 'png'
    
    swich(value.type)
    {
      // handle suffix here
    }
    name = `${name}.${suffix}`
    
    formData.append($(element).attr('name'),value,name);
    
    

    Replacing the:

       formData.append($(element).attr('name'),DataURIToBlob(value),`myfile_${index}.png`);
    

    Also, I made sure, that on the php side I had the appropriate value on upload_max_filesize setting it php.ini.