Importing New Files
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.