3

My plugin needs to allow admin to upload an image. I have the field setup and saving ok. But i cant seem to control the upload location.

This is what i have so far in my settings:

{{ forms.file({
    label: "My Image"|t,
    instructions: ""|t,
    name: 'myImage',
    value: settings.myImage
}) }}

When I output the settings.myImage, I get the file name on its own and no path. I would like it to upload to my /uploads folder. So for example, the image I uploaded was "test.jpg". When I output the result of settings.myImage, I get "test.jpg" and no path.

2 Answers 2

10

Unfortunately, a simple HTML file input on its own isn't enough to upload an image (in Craft or anywhere else) – the input simply exposes the file on your local filesystem to the browser/server. In other words, with your current setup, you're not actually uploading anything.

In my opinion, you have two choices:

  1. Use Assets and Craft's built-in Assets ElementSelect input (easy)
  2. Don't use Assets; upload the image natively using a file input (harder)

Personally, I'd recommend that unless you can't have the image be an Asset file for some reason, you should definitely go with the Assets approach. It's way easier to set up, offers a familiar user experience and you can even use image transforms and whatnot since your myImage file will be a bona fide Asset.

Either way, here's how option 1 could look:

In your plugin's settings.html file, replace your current call to the forms.file() macro with this:

{{ forms.field({
  label: "My Image"
}, include('_includes/forms/elementSelect', myImageSelectConfig)) }}

Then, fire up your plugin's primary class. Revise your getSettingsHtml() method to include the myImageSelectConfig attribute referenced in settings.html above, so the method looks something like this:

public function getSettingsHtml()
{
    return craft()->templates->render('yourpluginhandle/settings', array(
        'settings' => $this->getSettings(),
        'myImageSelectConfig' => array(
            'id'                 => 'myImage',
            'name'               => 'myImage',
            'jsClass'            => 'Craft.AssetSelectInput',
            'elementType'        => new ElementTypeVariable(craft()->elements->getElementType(ElementType::Asset)),
            'elements'           => $this->getSettings()['myImage'] ? craft()->elements->getCriteria(ElementType::Asset, array('id' => $this->getSettings()['myImage'])) : null,
            'sources'            => array('folder:2'),
            'criteria'           => array('kind' => array('image'), 'localeEnabled' => null),
            'limit'              => 1,
            'viewMode'           => 'table',
            'selectionLabel'     => Craft::t('Add image'),
        ),
    ));
}

Note: The sources attribute refers to the ID(s) for the Asset folder the admin should upload images to (and choose images from). You can look at the craft_assetfolders database table to figure out which ID(s) you want. Alternatively you can remove the sources property entirely, to enable all folders – which is probably the best option if this is going to be a publicly available plugin.

And that's it. The actual myImage setting value stored will be an array with the ID to the selected Asset file, e.g. {"myImage":["42"]}. If you need to pull the Asset in a template, you'll need to add a Variable class to your plugin, exposing the plugin settings:

class MyPluginVariable
{
    public function settings()
    {
        return craft()->plugins->getPlugin('myPlugin')->getSettings();
    }
}

...and in your template:

{% set assetId = craft.myPlugin.settings.myImage %}
{% set asset = craft.assets.id(assetId).first() %}

If you don't want to go with the Assets approach, you can keep your forms.file() macro in settings.html. However, there's a few more things you'll need to handle to get the upload working.

First, in order to actually upload a file from a file input, the enclosing form needs to have an enctype="multipart/form-data" attribute. Unfortunately, plugin settings forms don't, and there's no real way to add it. However, you can use jQuery to hack it in – add this to your plugin's settings.html file:

{% includeJs '$("form:first").attr("enctype", "multipart/form-data");' %}

Second, you'll need some server side code to actually handle the upload. Add the following to your plugin's primary class:

public function prepSettings($settings)
{

    $imageFile = UploadedFile::getInstanceByName('settings.myImage');

    if ($imageFile) {

        // Where should the image be stored?
        $basePath = $_SERVER['DOCUMENT_ROOT'];
        $uploadFolder = '/uploads/yourpluginhandle/';
        $filePath = $basePath.$uploadFolder.$imageFile->name;

        // Make sure the upload folder exists
        IOHelper::ensureFolderExists($basePath.$uploadFolder);

        // Attempt to upload the file
        if ($imageFile->saveAs($filePath)) {
            // File uploaded successfully – store the relative filepath in the plugin settings
            $settings['myImage'] = str_replace($basePath, '', $filePath);
        } else {
            // Aw.
            craft()->userSession->setError(Craft::t('Unable to upload image {filename}', array(
                'filename' => $imageFile->name,
            )));
        }
    }

    return $settings;

}

The above example is pretty much the bare minimum you'd need in order to upload a file from your plugin's settings page, to an arbitrary folder on your server's file system. The example prepSettings() script will upload the file to a folder called /uploads/yourpluginhandle in the public webroot (i.e. $_SERVER['DOCUMENT_ROOT']), before storing the relative path to the file in the settings, if the upload was successful.

Keep in mind that file uploading is actually fairly complex (lots of things can go wrong), and also a potential security risk. You should probably consider adding some code to ensure that the file's mime type ($imageFile->type), size ($imageFile->size) and other properties are legit, before calling $file->saveAs(). You might also need to create a UI for displaying and deleting the current image, and making sure old and no longer selected files are deleted from the server.

Finally, I'd posit that a vanilla browser file input is a pretty terrible user experience, compared to the Assets select input in Craft proper. There's all sorts of JavaScript libraries to help out with that, of course – in any case the above is hopefully a good starting point if you go down this road.

3
  • Seriously, though... I want to fly to Norway just to buy you a beer.
    – Brad Bell
    Commented Aug 14, 2016 at 2:03
  • @BradBell That's fortunate - I love beer! Commented Aug 14, 2016 at 5:54
  • No worries, @BillyMedia – hope it helps you out. Commented Aug 14, 2016 at 14:32
1

For anyone wanting to do this in Craft 3 there are a couple changes to the code you need to add to getSettingsHtml().

'myImageSelectConfig' => array(
            'id'                 => 'myImage',
            'name'               => 'myImage',
            'jsClass'            => 'Craft.AssetSelectInput',
            'elementType'        => 'craft\\elements\\Asset',
            'elements'           => $this->getSettings()['myImage'] && count($this->getSettings()['myImage']) ? [Craft::$app->getElements()->getElementById($this->getSettings()['myImage'][0])] : null,
            'sources'            => array('folder:2'),
            'criteria'           => array('kind' => array('image'), 'localeEnabled' => null),
            'limit'              => 1,
            'viewMode'           => 'table',
            'selectionLabel'     => Craft::t('app','Add image'),
        )

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.