Importing New Files

Jul 29, 2015

When working with blocks, it's typical for developers to hook into the File Manager to select files that exist. The File Manager provides upload functionality, after all, so developers with access to the block interface will be able to upload files and then choose the uploaded file.

This is not always the case, however. For example, what if you're developing a custom application or a custom Dashboard page, and need to store files as part of this process? You should definitely be using the Concrete CMS File manager for this, as it provides version control, searching, metadata support and more. But what if your custom interface just using a simple <input type="file" /> element? You need the ability to take a custom file upload and import that uploaded file into the File Manager. Fortunately this is pretty easy.

Consider this form, in a single page template. This will post back to the submit() method in the single page controller.

<form method="post" enctype="multipart/form-data" action="<?=$view->action('submit')?>">
    <div class="form-group">
        <label class="control-label">Upload Photo</label>
        <input type="file" name="photo">
    </div>
    <div class="form-group">
        <button type="submit" name="upload">Upload</button>
    </div>
</form>

This is a standard form for uploading files in PHP. This file will be posted in the $_FILES array to the submit() method, per the PHP file uploading guidelines. Here is our submit method

public function submit()
{
    $file = $_FILES['photo']['tmp_name'];
    $filename = $_FILES['photo']['name'];
}

Normally, we would use something like move_uploaded_file() to move the file to a file storage directory, then have to save the filename somewhere. And we wouldn't know anything about the file, or really be able to work with it in an extensible way. But if we import the file into the file manager, we'll get a File object back, which gives us lots of functionality.

public function submit()
{
    $file = $_FILES['photo']['tmp_name'];
    $filename = $_FILES['photo']['name'];
    $importer = new \Concrete\Core\File\Importer();
    $result = $importer->import($file, $filename);
}

That's it! Assuming a successful file upload, $result will be an instance of the \Concrete\Core\File\Version object for the newly uploaded file.

Handling Errors

Unfortunately, we can't always assume a successful file upload. Sometimes users attempt to upload files with disallowed file extensions, or something goes wrong on the server like we run out of space, or the file is too large. In these instances we need to provide feedback to the user.

public function submit()
{
    $error = \Concrete\Core\File\Importer::E_PHP_FILE_ERROR_DEFAULT;
    if (isset($_FILES['photo']) && is_uploaded_file($_FILES['photo']['tmp_name'])) {
        $file = $_FILES['photo']['tmp_name'];
        $filename = $_FILES['photo']['name'];
        $importer = new \Concrete\Core\File\Importer();
        $result = $importer->import($file, $filename);
        if ($result instanceof \Concrete\Core\Entity\File\Version) {
            $this->redirect('/my/success/page');
        } else {
            $error = $result;
        }
    } else if (isset($_FILES['photo'])) {
        $error = $_FILES['photo']['error'];
    }

    $this->set('errorMessage', \Concrete\Core\File\Importer::getErrorMessage($error));                
}

This is pretty self-explanatory. First, we seed the $error code variable with the general PHP error code in Concrete. If everything fails, this is the code we will use. Next, we make sure that the $_FILES array actually contains data. If it does, but there's no upload, we seed the $error code with the $_FILES['photo']['error'] code from PHP. Finally, if that all works, but for some reason $result isn't an instance of the Version object, we seed the $error code with whatever error code Concrete returned from the import() method. This will likely be a code telling us that we uploaded a file with an invalid extension, or something similar. Finally, whatever code we got, we use the getErrorMessage() method to translate that code into a human-readable error string, and send it into the page template.

Handling Permissions

If we want to handle upload permissions as well as errors, we need to add a little bit more code to our example. In this example, we're only going to allow file uploads if the user has access to the file manager, and then we're going to check permissions against the file extension to ensure that the extension is also something allowed by the user.

public function submit()
{
    $fp = \FilePermissions::getGlobal();
    if ($fp->canAddFiles()) {
        $error = \Concrete\Core\File\Importer::E_PHP_FILE_ERROR_DEFAULT;
        if (isset($_FILES['photo']) && is_uploaded_file($_FILES['photo']['tmp_name'])) {
            $validator = \Core::make('helper/file');
            if (!$fp->canAddFileType($validator->getExtension($_FILES['photo']['name']))) {
                $error = \Concrete\Core\File\Importer::E_FILE_INVALID_EXTENSION;
            } else {
                $file = $_FILES['photo']['tmp_name'];
                $filename = $_FILES['photo']['name'];
                $importer = new \Concrete\Core\File\Importer();
                $result = $importer->import($file, $filename);
                if ($result instanceof \Concrete\Core\File\Version) {
                    $this->redirect('/my/success/page');
                } else {
                    $error = $result;
                }
            }
        } else if (isset($_FILES['photo'])) {
            $error = $_FILES['photo']['error'];
        }

        $this->set('errorMessage', \Concrete\Core\File\Importer::getErrorMessage($error));                
    } else {
        $this->set('errorMessage', 'You do not have access to add files to the file manager.');
    }
}

First, we retrieve the global file permissions object. We then ask that permissions object whether the current user has access to add files. If they don't, we exit immediately with the error message. Otherwise, we use a special file helper method to retrieve the extension of the uploaded file, and we pass that to the canAddFileType() method of the same object. This method queries the detailed file permissions for the current user, ensuring that they have access to upload files of this particular type.

Validating files

In the past, we had file inspectors which would let you parse PDFs on upload/replace, or run special functions based on any kind of file that was uploaded. In fact, they’re still in the core even though they’re not really being used – they need to removed or officially marked as deprecated. Unfortunately, those only let you run logic or set attributes or do additional processing. They could not validate and discard a file, for example.

Thankfully, we have since overhauled that, thanks to members of the community who wanted to have better validation of problematic files like SVGs. So now in recent versions of the core (it’s present in 8.5.5 and perhaps several point releases earlier) we have “file processors”. You configure them through your application/config/app.php file, by defining custom classes within the import_processors array. So to do something like scan every PDF which is uploaded to ensure it contains readable text, you’d do something like this:

return [
    'import_processors' => [
        'ccm.pdf.customvalidator' => My\Custom\Class\In\Some\Package\File\Import\Processor\IsPdfReadableValidator::class,
    ]
  ];

The key of the array doesn’t matter; it just needs something that doesn’t clash with what the core is doing. You can see what the core is doing by checking the contents of the import_processors array in the concrete/config/app.php directory.

Within the IsPdfReadableValidator class, you’d make it implement the Concrete\Core\File\Import\Processor\ValidatorInterface.

Using the Concrete\Core\File\Import\Processor\FileExtensionValidator in the core as a guide is probably good. Just make the custom validator only fire when PDF files are uploaded, and put your custom logic within the validate method.

All these methods fire automatically on new uploads as well as replace, so implementing this validator in this one spot should be enough to validate the files both when they are uploaded for the first time as a new file, or when creating a new version of an existing file.

Was this information useful?
Thank you for your feedback.