Creating page from frontend + file upload: error.file.mime.missing

Hi,

I’m trying to combine two cookbook recipes:

Everything works fine so far – the only thing is that it throws "error.file.mime.missing" alert upon submission, even though the page is created and files are uploaded as its children successfully.
“The media type for “img-1-0.45521000-1617410187” cannot be detected”

Is there anything I’m missing? var_dump($upload[$i]) also shows me ["type"]=> string(10) "image/jpeg"

Thanks!

<?php
return function ($kirby, $page) {
  $alerts  = [];
  $success = '';
  if ($kirby->request()->is('post') === true && get('submit')) {

    // check the honeypot
    if (empty(get('website')) === false) {
      go($page->url());
      exit();
    }
    $data = [
      'firstname'     => get('firstname'),
      'lastname'      => get('lastname'),
      'name'          => get('firstname') . get('lastname'),
      'alphabetize'   => get('lastname'),
      'featured'      => false,
      'netid'         => get('email'),
      'title'         => get('title'),
      'text'          => get('text'),
      'timestamp'     => time(),
    ];
    
    $upload[0] = $kirby->request()->files()->get('file_1');
    $upload[1] = $kirby->request()->files()->get('file_2');
    $upload[2] = $kirby->request()->files()->get('file_3');
    $upload[3] = $kirby->request()->files()->get('file_4');
    $upload[4] = $kirby->request()->files()->get('file_5');

    $rules = [
    ];

    $messages = [
    ];

    // some of the data is invalid
    if ($invalid = invalid($data, $rules, $messages)) {
      $alert = $invalid;
    } 
    else {
      // authenticate as almighty
      $kirby->impersonate('kirby');
      
      try {
        // create page first
        $submission = $page->createChild([
          'title'    => $data['title'],
          'slug'     => str::slug($data['name']),
          'template' => 'project',
          'content'  => $data
        ]);
        $submission->changeStatus('listed');
        // handle uploads
        for ($i = 0; $i < count($upload); $i++) {
          $name = str::slug($data['name']).'--'. $i .'--'. microtime() .'--'. $upload[$i]['name'];
          $file = page($submission->id())->createFile([
            'source'   => $upload[$i]['tmp_name'],
            'filename' => $name,
          ]);
        }
        $success = 'Your file upload was successful';
      } 
      catch (Exception $e) {
        $alerts[$upload['name']] = $e->getMessage();
      }
    }
  }
  return compact('data', 'alerts', 'success');
};

Is the error thrown for all files you try to upload? Are all 5 file inputs required/filled?

Could you post the form as well please?

Hi! the form is below. And it seems like the error is only thrown for the last one in the array…?

snippets/upload-form.php

<form action="" method="post" enctype="multipart/form-data">

<div class="form-element half">
  <label for="firstname">First name <span class="asterisk">*</span></label>
  <input type="text" id="firstname" name="firstname" value="<?= $data['firstname'] ?? null ?>" required/>
</div>
<div class="form-element half">
  <label for="lastname">Last name <span class="asterisk">*</span></label>
  <input type="text" id="lastname" name="lastname" value="<?= $data['lastname'] ?? null ?>" required/>
</div>
<div class="form-element half">
  <label for="email">Email <span class="asterisk">*</span></label>
  <input class="sm" type="text" name="email" id="email" value="<?= $data['netid'] ?? null ?>" placeholder="netid" required/>
</div>
<div class="form-element">
  <label for="title">Project title <span class="asterisk">*</span><br></label>
  <input type="text" id="title" name="title" value="<?= $data['title'] ?? null ?>" required/>
</div>
<div class="form-element">
  <label for="text">Project description <span class="asterisk">*</span></label>
  <textarea name="text" id="text" placeholder="250 words..." required><?= $data['text'] ?? null ?></textarea>
</div>
<div class="form-element">
  <label for="file">Project Image 1 <span class="asterisk">*</span> — Hero</label>
  <img class="preview p-1" id="preview_1" src=""/>
  <input name="file_1" type="file" onchange="readURL(this,'#preview_1');" required/>
</div>
<div class="form-element">
  <label for="file">Project Image 2 <span class="asterisk">*</span></label>
  <img class="preview p-1" id="preview_2" src=""/>
  <input name="file_2" type="file" onchange="readURL(this,'#preview_2');" />
</div>
<div class="form-element">
  <label for="file">Project Image 3 <span class="asterisk">*</span></label>
  <img class="preview p-1" id="preview_3" src=""/>
  <input name="file_3" type="file" onchange="readURL(this,'#preview_3');" />
</div>
<div class="form-element">
  <label for="file">Project Image 4 <span class="asterisk">*</span></label>
  <img class="preview p-1" id="preview_4" src=""/>
  <input name="file_4" type="file" onchange="readURL(this,'#preview_4');" />
</div>
<div class="form-element">
  <label for="file">Project Image 5 <span class="asterisk">*</span></label>
  <img class="preview p-1" id="preview_5" src=""/>
  <input name="file_5" type="file" onchange="readURL(this,'#preview_5');" />
</div>
<div class="honey">
   <label for="message">If you are a human, leave this field empty</label>
   <input type="website" name="website" id="website" value="<?= isset($data['website']) ? esc($data['website']) : null ?>"/>
</div>
<div class="form-element p-2">
  <input class="button p-2" type="submit" name="submit" value="Submit" />
</div>
</form>

templates/upload.php

<?php snippet('header') ?>
<?php if ($success): ?>
  <div class="alert success">
    <p><?= $success ?></p>
  </div>
<?php else: ?>
  <?php if (empty($alerts) === false): ?>
    <ul>
      <?php foreach ($alerts as $alert): ?>
        <li><?= $alert ?></li>
      <?php endforeach ?>
    </ul>
  <?php endif ?>

  <article>
    <h1 class="p-2 t-l"><?= $page->title() ?></h1>
    <!-- <?= kirbytext($page->text()) ?> -->
    <?php snippet('upload-form', compact('data')); ?>
  </article>
  
<?php endif ?>

<?php snippet('footer') ?>

There are a few issues:

  • $data is not defined at page load, you would either have to initiate this variable at the top or use a default in the return array (doesn’t work with compact()):

  • try/catch

catch (Exception $e) {
        $alerts[$upload['name']] = $e->getMessage();
      }

This refers to the complete try/catch, so $upload['name'] is undefined. You need to wrap your uploads in a try/catch block as well

       // handle uploads
        for ($i = 0; $i < count($upload); $i++) {
          try {
            $name = Str::slug($data['name']).'--'. $i .'--'. microtime() .'--'. $upload[$i]['name'];
            $file = page($submission->id())->createFile([
              'source'   => $upload[$i]['tmp_name'],
              'filename' => $name,
            ]);

          } catch(Exception $e) {
            $alerts[$upload[$i]['name']] = $e->getMessage();
          }


        }
  • $upload should also be initialized at the top of the file

  • I guess you will still add error handling for missing fields/files?

Thank you! It soled the issue beautifully. One more thing if I may ask:

I am now getting into building validation rules, and wonder what’d be the best way to validate minimum and maximum image dimensions? Right now invalid() only looks at $data. I guess I can put another conditional but wondering if there’d be a better solution…

Thank you so much! I’m putting my controller as it stands below:

<?php
return function ($kirby, $page) {
  $alerts  = [];
  $success = '';
  $data = [];
  $upload = [];
  
  if ($kirby->request()->is('post') === true && get('submit')) {

    // check the honeypot
    if (empty(get('website')) === false) {
      go($page->url());
      exit();
    }
    $data = [
      'firstname'     => get('firstname'),
      'lastname'      => get('lastname'),
      'name'          => get('firstname') .' '. get('lastname'),
      'alphabetize'   => get('lastname'),
      'n-number'      => get('n-number'), // add N00
      'n-number-full' => 'N00'.get('n-number'),
      'portfoliourl'  => get('portfoliourl'),
      'title'         => get('title'),
      'text'          => get('text'),
      'projecturl'    => get('projecturl'),
      'timestamp'     => time(),
      'topics'        => '',
      'disciplines'   => '',
      'questions'     => '',
    ];
    
    $upload[0] = $kirby->request()->files()->get('file_1');
    $upload[1] = $kirby->request()->files()->get('file_2');
    $upload[2] = $kirby->request()->files()->get('file_3');
    $upload[3] = $kirby->request()->files()->get('file_4');
    $upload[4] = $kirby->request()->files()->get('file_5');

    
    // TO DO: images to be Min width 5000, Min height 2500, max 10mb

    $rules = [
      'firstname'  => ['alpha'],
      'lastname'  => ['alpha'],
      'n-number'  => ['num'],
      'title'  => ['lessThan140Chars'],
      'text'  => ['lessThan250Words'],
    ];

    $messages = [
      'firstname'  => 'Please enter valid First Name',
      'lastname'  => 'Please enter valid Last Name',
      'n-number'  => 'Please enter valid N#',
      'title'  => 'Project Title needs to be shorter than 140 characters.',
      'text'  => 'Project Description needs to be shorter than 250 words.',
    ];

    // some of the data is invalid
    if ($invalid = invalid($data, $rules, $messages)) {
      $alerts = $invalid;
    } 
    else {
      // authenticate as almighty
      $kirby->impersonate('kirby');
      
      try {
        // create page first
        $submission = $page->createChild([
          'title'    => $data['title'],
          'slug'     => str::slug($data['name']),
          'template' => 'project',
          'content'  => $data
        ]);
        $submission->changeStatus('listed');
        // handle uploads
        for ($i = 0; $i < count($upload); $i++) {
          try {
            $name = str::slug($data['name']).'_'. $i .'_'. microtime() .'_'. $upload[$i]['name'];
            $file = page($submission->id())->createFile([
              'source'   => $upload[$i]['tmp_name'],
              'filename' => $name,
              'mime'     => $upload[$i]['type'],
            ]);
          }
          catch(Exception $e) {
            $alerts[$upload[$i]['name']] = $e->getMessage();
          }
        }
        $success = 'Thank you, '.$data['name'].'.<br>Your submission was successfully recorded.';
      } 
      catch (Exception $e) {
        $alerts[$data['name']] = $e->getMessage();
      }
    }
  }
  return compact('data', 'alerts', 'success');
};

You could use a file blueprints with an accept rule like in the recipe

Hi there.
I am also trying to accomplish a file upload from frontend and i followed all your introductions in the Uploading files from frontend Tutorial but every time i’m trying to use the createFile() method i get the following mime-Type error:

Argument 1 passed to Kirby\Toolkit\Mime::matches() must be of the type string, null given, called in D:\Projekte\site\kirby\src\Image\Image.php on line 214

I cannot figure out, what im doing wrong. Using PHP Version 7.4.1, MAMP as local server on Win 10.

I already dumped all the information about the uploading file:
name: test.jpg
type: image/jpeg
tmp_name: C:\Users\laptop\AppData\Local\Temp\phpB6D2.tmp
error: 0
size: 4356

Can’t find an issue there, so a advice would be nice.

Hm, what is your exact Kirby version?

Have you followed the recipe exactly as described or have you made any changes?

Is the files you are trying to upload a valid image? Somehow looks as if its mime type is not recognized. Have you tried with different files?

@texnixe I am having the same problem. i am experiencing this behaviour:

  • mac: frontend and panel uploads work fine (I am using MAMP)
  • windows: using MAMP and XAMPP, panel uploads work, frontend uploads dont work.
  • on all servers: frontend and panel uploads work fine

i read that in kirby 2 windows used to have some issues with the mime types… and it seems I might be missing something in my code to make it work on windows like it works in the panel. I followed the recipe but I upload the file via ajax so I can show a progess bar while it uploads.

this is my ajax handler (you can ignore the reload part i guess)

function fileUpload(ev) {
    var btn = ev.currentTarget;
    var form = btn.form;
    var progress = form.querySelector(".progress-bar");
    var fileInput = form.querySelector(".files");
    let url = location.protocol + '//' + location.host + location.pathname + '.json';
    var file = fileInput.files[0];

    var formData = new FormData();
    formData.append(fileInput.id, file);
    formData.set(btn.name, btn.name);

    var ajax = new XMLHttpRequest();
    ajax.responseType = 'json';
    ajax.open("POST", url, true);

    ajax.upload.onprogress = function (e) {
        if (e.lengthComputable) {
            var percentage = Math.round((e.loaded / e.total) * 100);
            progress.style.width = percentage + "%";
            progress.innerHTML = percentage + "%";
            progress.setAttribute('aria-valuenow', percentage);
        }
        else {
            console.log("Unable to compute progress information since the total size is unknown");
        }
    }

    ajax.onload = function (e) {
        // if successfull, set a message in session for an alert and reload page 
        if (this.readyState == 4 && this.status == 200) {
            sessionStorage.setItem('alert', this.response.alert);
            location.reload();
        }
    };

    ajax.send(formData);
}

In my controller.json i am saving like this:

$file = $currentLinkedExhibit->createFile([
        'source'   => $upload['tmp_name'],
        'filename' => $upload['name'],
        'template' => $_file_template,
        'content' => [
          'date' => date('Y-m-d h:m')
        ]
 ]);

and my from looks like this:

<form class="upload-form" action="xxx the url xxx" enctype="multipart/form-data" method="POST">
    <input accept=".jpg, .png, .heic, .jpeg" id="museum_preview" name="museum_preview" type="file" class="form-control files">
    <button type="submit" name="save-museum-preview" value="Vorschaubild für Museum aktualisieren" class="btn btn-primary">
        Upload
    </button>
    <div class="progress">
        <div class="progress-bar" role="progressbar" aria-label="Vorschaubild" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
    </div>
</form>

if I kirbylog($upload['type']) , on mac it says image/png, on windows it does not exist :confused:

any ideas?

I work with Mac and Linux and am not familiar with Windows peculiarities. What I would try is to set the mime types in .htaccess, see server-configs-apache/media_types.conf at main · h5bp/server-configs-apache · GitHub

No idea if that helps.

ok, thank you. but in that case the panel uploads would also not work, if it were the mime types in the htaccess, or not?

by the way, i think i had a bad reload. apparently when i call kirbylog($upload['type']) in the controller the type is there, either image/jpeg, or image/png, etc…

i will give it a try, thank you.

jesus christ… ok, it took me ages but i finally did it. the problem was the windows tmp files… somehow kirby couldnt get the mime type out of them because they look like php8457.tmp or things like that. i saw that the temp files that the panel was creating were nice jpegs or pngs, not .tmp files. after hours of trying to find this out i found this solution, but i havent tested it on mac, although it should work fine:

/*  i read the content of the php tmp file passed by the form*/
$filecontent = F::read($upload['tmp_name']); 
$temppath = sys_get_temp_dir().'\\'. time().'-'. $upload['name']; // i create a path pointing to the temp folder in the system and i add a timestamp to the name (which will include the extension of the file as well)
F::write($temppath, $filecontent); // i then write a temp file of my own, so to speak

/* and i use the path to my own temp file as the source, that will be a regular jpeg, or png, or whatever extension. with this kirby was able to get the mime type out of the file correctly*/
$file = $currentLinkedExhibit->createFile([
        'source'   => $temppath, 
        'filename' => $upload['name'],
        'template' => $_file_template,
        'content' => [
          'date' => date('Y-m-d h:m')
        ]
]);

ok, final code here so it’s a bit cleaner with the paths depending on the OS and we delete the file after we are done, since yesterday after i posted i read a kirby issue that temp files that you write on your own dont get deleted by the system, so better remove it directly. the original temp file gets removed because we dont make any changes to it:


    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
      $temppath = sys_get_temp_dir() . '\\' . time() . '-' . $upload['name'];
    } else {
      $temppath = ini_get('upload_tmp_dir') . '/' . time() . '-' . $upload['name'];
    }
    $filecontent = F::read($upload['tmp_name']);
    F::write($temppath, $filecontent);

    try {
      $file = $yourpage->createFile([
        'source'   => $temppath,
        'filename' => $upload['name'],
        'template' => $_file_template,
        'content' => [
          'date' => date('Y-m-d h:m')
        ]
      ]);

    } catch (Exception $e) {
      $alert[] = "ERROR: " . $e->getMessage();
    }

    F::remove($temppath);