Upload an Image File using REST API in Drupal 8

By Ronald van Belzen | December 8, 2017

Currently there is no support to directly upload images using REST in Drupal 8 (https://www.drupal.org/node/1927648). The work-around that I describe here uses Base64 encoded images to accomplish the upload of an image using REST.

For a decoupled Drupal 8 installation I needed to upload an avatar image for a user. Since I started with a minimal installation of Drupal 8, I first had to enable the image and field_ui modules and added an image field to the user entity that I named 'avatar' (with the machine name 'field_avatar'). I also created three new image styles specifically to be used for this image style (machine names: 'avatar_large', 'avatar_medium' and 'avatar_small').

Next, I installed the restui module and enabled the modules basic_auth, rest, restui and serialization before starting on my own module. The info file 'web/modules/custom/mymodule/mymodule.info.yml':

name: MyModule REST Services
type: module
description: "MyModule REST Service Resources"
package: Web services
dependencies:
  - rest
core: '8.x'

The helper class that I created to handle most of the functionality is displayed in full below, but will be explained as we make use of its functionality ('web/modules/custom/mymodule/src/Base64Image.php'):

<?php
namespace Drupal\mymodule;

use Drupal\image\Entity\ImageStyle;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class Base64Image {
  protected $base64Image;
  protected $fileData;
  protected $fileName;
  protected $directory;

  public function __construct($base64Image) {
    $this->base64Image = $base64Image;
    $this->decodeBase64Image();
  }

  protected function decodeBase64Image() {
    $this->fileData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $this->base64Image));
    if ($this->fileData === FALSE) {
      throw new BadRequestHttpException('Avatar image could not be processed.');
    }
    // Determine image type
    $f = finfo_open();
    $mimeType = finfo_buffer($f, $this->fileData, FILEINFO_MIME_TYPE);
    // Generate fileName
    $ext = $this->getMimeTypeExtension($mimeType);
    $this->fileName = uniqid(rand()) . $ext;
  }

  /**
   * @param string $mimeType
   *
   * @return string
   */
  protected function getMimeTypeExtension($mimeType) {
    $mimeTypes = [
      'image/png' => 'png',
      'image/jpeg' => 'jpg',
      'image/gif' => 'gif',
      'image/bmp' => 'bmp',
      'image/vnd.microsoft.icon' => 'ico',
      'image/tiff' => 'tiff',
      'image/svg+xml' => 'svg',
    ];
    if (isset($mimeTypes[$mimeType])) {
      return '.' . $mimeTypes[$mimeType];
    }
    else {
      $split = explode('/', $mimeType);
      return '.' . $split[1];
    }
  }

  /**
   * @return mixed
   */
  public function getFileData() {
    return $this->fileData;
  }

  /**
   * @return mixed
   */
  public function getFileName() {
    return $this->fileName;
  }

  /**
   * @param string $path
   */
  public function setFileDirectory($path) {
    $this->directory = \Drupal::service('file_system')->realpath(file_default_scheme() . "://");
    $this->directory .= '/' . $path;
    file_prepare_directory($this->directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
  }

  /**
   * @param $path
   */
  public function setImageStyleImages($path) {
    $styles = ['avatar_large', 'avatar_medium', 'avatar_small'];
    foreach ($styles as $style) {
      $imageStyle = ImageStyle::load($style);
      $uri = $imageStyle->buildUri($path . '/' . $this->fileName);
      $imageStyle->createDerivative($this->directory . '/' . $this->fileName, $uri);
    }
  }
}

The REST resource 'web/modules/custom/mymodule/src/Plugin/rest/resource/UserResetRestResource.php' has a double functionality. It can be used by a cookie authenticated user to reset his profile. It can also be used by a not authenticated user to confirm his account request send to him by e-mail (and re-routed to  and to be handled by a front-end script when the user clicks the link in the confirmatiom e-mail). For the latter it needs the parameters 'uid', 'timestamp' and 'hash' in the body of the posted request in a json, for example:

{
    "uid": { "value": "33" }, 
    "timestamp": { "value": "1511340939"}, 
    "hash": { "value": "9qn4J2HumMJfbwIet3r1IiE-ZMCE81CvnFxjRCDdIhY"}, 
    "pass": { "value": "password"},
    "firstname": {"value": "T."}, 
    "lastname": {"value": "Tester"}, 
    "prefix": {"value": "de"}, 
    "company": {"value": "My Company"}, 
    "gender": {"value": "M"},
    "avatar": {"data": "HGFYFHJNVTYIKKNVDDQGHMBFG:L:..."}
}

The full source of the REST resource will be displayed at the end of this article. I will be concentrating on the handling of the image upload now.

The function post() first calls the function ensureUserCanReset() that validates the user request. This function returns the User object for which the information needs to be updated. None of the fields are mandatory. The presence of each field is checked, and, when present, is updated. The update of the image needs a bit more work than the other fields:

    if (isset($data['avatar']['data']) && !empty($data['avatar']['data'])) {
      $path = 'avatar/' . date('Y-m');
      $img = new Base64Image($data['avatar']['data']);
      $img->setFileDirectory($path);
      $file = file_save_data($img->getFileData(), 'public://' . $path . '/' . $img->getFileName() , FILE_EXISTS_REPLACE);
      $user->field_avatar->setValue(['target_id' => $file->id()]);
      $img->setImageStyleImages($path);
    }

The $path parameter is used for determining where the image and its derived image style images need to be saved.

In the next line a Base64Image object is instantiated by passing the Base64 endoded image to the constructor. The constructor of the Base64Image calls the function to decode the image. When the decoding was successful, the image mime-type is determined and used to create an unique file name.

The third line calls the object method setFileDirectory() that makes sure that the directory in which the image will be saved is created with the proper access rights.

The file_save_data() takes care of the actual saving of the image and returns the \Drupal\file\FileInterface object of the saved file.

$file->id() is used to set the reference to the saved image file.

The last line in which the image style Images are set is just an extra and not needed at this point, since the image style images will also be created when they are not available on retrieval of the images.

The full source:

<?php

namespace Drupal\mymodule\Plugin\rest\resource;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Psr\Log\LoggerInterface;
use Drupal\mymodule\Base64Image;

/**
 * Provides a resource to post User update data.
 *
 * @RestResource(
 *   id = "user_reset_rest_resource",
 *   label = @Translation("User Reset rest resource"),
 *   uri_paths = {
 *     "https://www.drupal.org/link-relations/create" = "/service/user/reset",
 *   },
 * )
 */
class UserResetRestResource extends ResourceBase {

  /**
   * A current user instance.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Constructs a Drupal\rest\Plugin\ResourceBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param array $serializer_formats
   *   The available serialization formats.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   A current user instance.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    array $serializer_formats,
    LoggerInterface $logger,
    AccountProxyInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);

    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('myport_api'),
      $container->get('current_user')
    );
  }

  /**
   * Responds to POST requests.
   *
   * @param array $data
   *
   * @return \Drupal\rest\ResourceResponse
   */
  public function post($data = []) {
    $user = $this->ensureUserCanReset($data);

    if (isset($data['pass']['value']) && !empty($data['pass']['value'])) {
      $user->setPassword($data['pass']['value']);
    }
    if (isset($data['firstname']['value']) && !empty($data['firstname']['value'])) {
      $user->set('field_first_name', $data['firstname']['value']);
    }
    if (isset($data['lastname']['value']) && !empty($data['lastname']['value'])) {
      $user->set('field_last_name', $data['lastname']['value']);
    }
    if (isset($data['prefix']['value'])) {
      $user->set('field_name_prefix', $data['prefix']['value']);
    }
    if (isset($data['gender']['value']) && !empty($data['gender']['value'])) {
      $gender = (($data['gender']['value'] == "F") ? 0 : 1);
      $user->set('field_gender', $gender);
    }
    if (isset($data['company']['value']) && !empty($data['company']['value'])) {
      $user->set('field_company_name', $data['company']['value']);
    }
    if (isset($data['avatar']['data']) && !empty($data['avatar']['data'])) {
      $path = 'avatar/' . date('Y-m');
      $img = new Base64Image($data['avatar']['data']);
      $img->setFileDirectory($path);
      $file = file_save_data($img->getFileData(), 'public://' . $path . '/' . $img->getFileName() , FILE_EXISTS_REPLACE);
      $user->field_avatar->setValue(['target_id' => $file->id()]);
      $img->setImageStyleImages($path);
    }

    $user->save();

    $response = array(
      "uid" => ['value' => $user->id()],
      "status" => ['value' => 'OK'],
    );

    return new ResourceResponse($response);
  }

  /**
   * @param array $data
   *
   * @return \Drupal\Core\Entity\EntityInterface|null|static
   */
  protected function ensureUserCanReset($data) {
    $current = \Drupal::time()->getRequestTime();
    $uid_valid = isset($data['uid']['value']) && !empty($data['uid']['value']);
    $timestamp_valid = isset($data['timestamp']['value']) && !empty($data['timestamp']['value']);
    $hash_valid = isset($data['hash']['value']) && !empty($data['hash']['value']);

    // For anonymous users parameters uid, timestamp and hash need to be validated
    if ($this->currentUser->isAnonymous()) {
      if ($uid_valid && $timestamp_valid && $hash_valid) {
        $user = User::load($data['uid']['value']);
        if ($user === NULL || !$user->isActive()) {
          throw new AccessDeniedHttpException();
        }
        if ($current > ($data['timestamp']['value'] + 3600)) {
          throw new UnprocessableEntityHttpException('Reset request has expired. Reset your password (again).');
        }
        if (!Crypt::hashEquals($data['hash']['value'], user_pass_rehash($user, $data['timestamp']['value']))) {
          throw new UnprocessableEntityHttpException('Invalid credentials.');
        }
      }
      else {
        throw new UnprocessableEntityHttpException('Anonymous user cannot be validated (parameter(s) missing).');
      }
    }
    else {
      $user = User::load($this->currentUser->id());
      if ($user === NULL) {
        throw new AccessDeniedHttpException();
      }
    }

    return $user;
  }
}

 

Add new comment