Recent Blog Posts

REST API for file upload in Drupal 8 (multipart/form-data)

By Ronald van Belzen | November 30, 2018

This article is related to a former article in which I claimed that "there is no support to directly upload images using REST in Drupal 8". Since Drupal 8.6 this in no longer true (https://www.drupal.org/node/1927648). You can read how to use this new functionality at https://wimleers.com/blog/api-first-drupal-file-uploads and https://wimleers.com/blog/api-first-drupal-8.6.

This article is, however, about an entirely different method to upload a file in Drupal 8 for the somewhat unusual use case of passing the uploaded file to a third party REST api. Not that the method described here will be limited to that use case, but in most cases the new Drupal 8.6 functionality will be the preferred choice.

The situation was that for a decoupled set-up the front-end needed to upload a file to a third party REST api. For that upload the third party needed info about the user present in Drupal that the front-end was not allowed to retrieve or expose in any way. Also, as a temporary solution Drupal needed to perform a virusscan on the uploaded files. The front-end strongly preferred the use of a multipart/form-data content type for the POST request.

So, the uploaded file is not meant for storing in the Drupal application and not meant to be saved as File object. The method shown below does do that anyway, because the clamav module, used for the virus scan, requires it.

As usual I start with a new module:

# my_api.info.yml

name: My REST Service
type: module
description: "REST Service for passing file upload"
package: Web services
core: '8.x'

And since I need to create the end-point for this REST api myself, I create the route for this end-point:

How to set a private or protected property without a setter

By Ronald van Belzen | July 7, 2018

When you have a php class with private or protected properties without setters that you want to set for testing purposes, there are two possible ways to tackle that problem.

As an example I create the following class:

class Example {

  protected $var;

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

}

The first approach uses reflection:

$ex = new Example('relection');

$reflectionClass = new \ReflectionClass('\Example');

$reflectionProperty = $reflectionClass->getProperty('var');
$reflectionProperty->setAccessible(true);

$reflectionProperty->setValue($ex, 'another value');

echo $reflectionProperty->getValue($ex);
// Outputs: another value

The second approach uses closure (PHP 5 >= 5.3.0, PHP 7) :

$ex = new Example('closure');
 
$changePropertyClosure = function () {
    $this->var = 'another value';
};
 
$setProperty = $changePropertyClosure->bindTo($ex, get_class($ex));
$setProperty();

$getPropertyClosure = function () {
    return $this->var;
};

$getProperty = $getPropertyClosure->bindTo($ex, get_class($ex));
echo $getProperty();
// Outputs: another value

 

How to override a local task in Drupal 8

By Ronald van Belzen | June 16, 2018

In the previous blog post I added a local task next to the "Published comments" and "Unapproved comments" under the Content "Comments" menu.

The new local task was for the display of a list of comments that were designated as spam comments, and a count of the number of these comments was uncluded. However, this count has in common with the count of unapproved comments that spam comment are unpublished. So the count of unpublished comments will include the number of spam comments.

To correct this we will need to override the local task menu for unapproved comments.

The mechanism for altering the local tasks of another module is by using hook_menu_local_tasks_alter(). The example shown in the documentation adds a top-level menu to all pages, but here we are going to override an existing sub-level menu on only 3 pages.

/* mycomment.module */

/**
 * Set the unapproved comments count.
 *
 * Implements hook_menu_local_tasks_alter().
 */
function mycomment_menu_local_tasks_alter(&$data, $route_name, &$cacheability) {
  if ($route_name == 'comment.admin' ||
    $route_name == 'comment.admin_approval' ||
    $route_name == 'mycomment.admin_comment_spam'
   ) {
    $data['tabs'][1]['comment.admin_approval']['#link']['title'] =
      t('Unapproved comments (@count)', [
        '@count' => get_actual_unapproved_comments(),
    ]);
  }
}

The routes are those of the "Published comments", "Unapproved comments" and "Spam comments". Each level of menus in the $data array is enclosed in each own array item. Index 0 is for the top level menus, while the "Unapproved comments" local task can be found in the $data item with index 1.

The title of the link of the "Unapproved comment" is overridden. The new count of unapproved comments without spam comments is delivered by the function get_actual_unapproved_comments().

How to define a local task in Drupal 8

By Ronald van Belzen | June 2, 2018

A local task in Drupal is a callback displayed as a tab. A local task must have a parent item in order for the tab to be rendered.

A well-known example that is part of Drupal core are the local taks for comments with the menu titles "Published comments" and "Unapproved comments". To demonstrate I am going to add an extra local task to those of comments. Let's say I made a new view that will display spam comments and in the advanced settings I have given that view the machine name "page_comment_spam".

The first step would be to create the routing to that view. In the routing the machine name of the view is given to the defaults _view parameter.

# mycomment.routing.yml

mycomment.admin_comment_spam:
  path: /admin/content/comment/spam
  defaults: 
    _title: 'Comment spam'
    _view: page_comment_spam
  requirements:
    _permission: 'administer comments'

This routing defines the callback I will need to define the local task. Instead of a view, I could also have define a controller function (with a defaults _controller parameter) or a form (with a defaults _form parameter), which is used more often. For this example it does not really make a difference.

The definition for the local task is as follows.

# mycomment.links.tasks.yml

mycomment.admin_comment_spam:
  title: 'Spam comments'
  route_name: mycomment.admin_comment_spam
  class: Drupal\mycomment\Plugin\Menu\LocalTask\SpamComments
  parent_id: comment.admin
  weight: 10

I hope that the fact that I named the routing name and the task name the same, does not confuse you in thinking that it needs to be the same name. It does not need to be.

Special in the above is that a class is being defined. This class definition is optional. I included it here to demonstrate how you can dynamically change the title of the tab with the help of a local task plugin.

What is important is that I defined the parent_id the same as the existing local tasks for comments, and that I defined the route_name to be used for the callback of the tab. I added some weight to the definition to make sure the tab is displayed to the right of the existing local tasks.

The plugin definition I adapted from the one being used in the (core) comments module. It adds a count to the tab link equal to the number of comments that will be displayed in the view.

Checking existing users with SFS

By Ronald van Belzen | May 27, 2018

At the moment that you start using Stop Forum Spam some of your subscribers may be known spammers at stopforumspam.com. They may have been dormant for some to time and start spamming when you least expect it. Also some subscribers to your site may have become known as spammers after they registered.

For this reason it may be wise to check your subscribers against the stopforumspam.com database. This is what the function checkUsers() of the SfsRequest class.

/* /src/SfsRequest */ 

 /**
   * Check registered user accounts of being spammers at www.stopforumspam.com.
   */
  public function checkUsers() {
    if (!$this->config->get('sfs_cron_job')) {
      return FALSE;
    }
    $lastUid = $this->config->get('sfs_cron_last_uid');
    $limit = $this->config->get('sfs_cron_account_limit');
    if ($limit > 0) {
      $query = $this->connection->select('users', 'e');
      $query->fields('e', ['uid']);
      $query->condition('uid', $lastUid, '>');
      $query->range(0, $limit);
      $query->orderBy('uid', 'ASC');
      $uids = $query->execute()->fetchCol();
      foreach ($uids as $uid) {
        $lastUid = $uid;
        $user = User::load($uid);
        $include = ($user->isActive() || $this->config->get('sfs_cron_blocked_accounts'));
        if ($include && !$user->hasPermission('exclude from sfs scans') && $this->userIsSpammer($user)) {
          try {
            $user->block();
            $user->save();
            $this->log->notice('User acount @uid has been disabled.', ['@uid' => $uid]);
          }
          catch (EntityStorageException $e) {
            $this->log->error('Failed to disable user acount @uid: @error', ['@uid' => $uid, '@error' => $e->getMessage()]);
          }
        }
      }
      $this->config->set('sfs_cron_last_uid', $lastUid);
      $this->config->save();
    }
    return TRUE;
  }

The function does not scan all subscribers in one run, but scans a limited amount of subscribers that the configuration setting 'sfs_cron_account_limit' allows. Inactive users can be excluded from the scan and users with the permission 'exclude from scans' are skipped too. User accounts of known spammers are disabled.

The actual check is done by the function userIsSpammer() for each individual user.