Creating Limesurvey Plugins

UPDATE 21/05/18: I am working on rewriting this for Limesurvey 3.X, hopefully get this done this week.

For our new Limesurvey management app Zest, we have created two plugins for Limesurvey, one to send a post request with certain data every time someone submits a survey, and one that anonymizes tokens after they are used. (this allows for the convenience of token based persistance while still keeping the data as anonymous as possible). In this post, I’d like to show you how to (re)create the first of these plugins, hopefully this will help make your own plugins. Another post where I do the same for the other plugin will follow shortly.

Both plugins are open source and can be freely used, you can find the code at our Github page.

Also, do not forget to take a look at the Limesurvey Wiki on Plugin development, which has a lot of useful information, as well as at the Plugin API, which shows the methods that you can use when writing your plugin.

Overview

We are going to create a plugin that makes a post request every time someone submits a survey, and the plugin can be enabled site wide but always turned on or off on a survey level, and the plugin can pass the survey id, token id, and several given answers. It will also have settings for the URL to make the request to, as well as a authorization token and a unique key for the server. While the plugin was written especially for our Zest platform, it is really easy to adapt it to whatever your implementation might need.

The final product

Global settings

On the left you can see the final global settings page. As you can see it is now geared towards use for Zest, but out of the box you can change the URL to post to, but with a few changes in the code you can adapt it to use your own tokens etcetera.

Survey settings

In the local survey settings menu you can overwrite most of the settings for each survey separately. It is also possible to pass along the users token, as well as attach an array of question codes with the given answers.

Debug respons

When debug is turned on, on each submission the data is shown on screen. Here you can see the survey id, the user’s token, and three questions that were passed along as well. ‘Test’ was a free text question, ‘q01’ an equation and ‘dsa121’ a radio list.

The basics

We are first going to create a folder in the limesurvey/plugins folder, named after the plugin we are making. We are calling it ZestHook. In this folder we will create a new PHP file with the same name as the folder. Open this file in your favorite code editor and let’s get started.

Note: Never develop plugins on production sites: once a plugin is activated it will break your entire Limesurvey application if you have errors in your code. You do not want your customers to see a broken site because you forgot a semicolon :)

Examining the example plugin

I find the easiest way to start writing a plugin is by looking at the example plugin that ships with Limesurvey, or any of the existing plugins that you can find at the plugin repository, especially if one these does something something similar to what you want to achieve. If you look at the example plugin, you can see a plugin basically exists of three parts: first we register the events we want to subscribe to (the events when we want our plugin to be called), and attach a function to the event. Next we are going to add both general settings to the plugin and settings that can be set for each survey individually. Finally, we are going to write the code that gets executed on each of the events we registered earlier. In this case, it will be the code that gets run on each survey response submission.

Extend pluginbase

We are going to start by creating our plugin class and extend the PluginBase class:

class EventlyHook extends \ls\pluginmanager\PluginBase {

}

You should make sure that the classname is exactly (case sensitive) the same as the folder name.

Registering the plugin

First we are going to add some variables we want to use, and create an init() function with the events we want to register to. in our case, we want to register to the following events: afterSurveyComplete (when we will send the post request), beforeSurveySettings (to add settings for each survey), and newSurveySettings (to save these settings). For a full list of all the events check out this Limesurvey page. The code should look like this:

protected $storage = 'DbStorage';
static protected $description = 'Webhook for Limesurvey';
static protected $name = 'EventlyHook';

public function init() {
$this->subscribe('afterSurveyComplete');
$this->subscribe('beforeSurveySettings');
$this->subscribe('newSurveySettings');
}

Adding the general settings

Our plugin is going to have several site wide options: The default URL to post to, whether to send a hook by default, our API token and unique server id and finally a global debug mode. Settings can contain several types of data, we are going to use strings and booleans for now.

The code to add these settings should look as follows:

protected $settings = array(
'bUse' => array(
'type' => 'select',
'options'=>array(
0=>'No',
1=>'Yes'
),
'default'=>1,
'label' => 'Send a hook for every survey by default?',
'help'=>'Overwritable in each Survey setting',
),
'sUrl' => array(
'type' => 'string',
'default'=>'https://zest.evently.nl/api/1/ping',
'label' => 'The default address to send the webhook to',
'help'=>'If you are using Zest, this should be https://zest.evently.nl/api/1/ping',
),
'sAuthToken' => array(
'type' => 'string',
'label' => 'Zest Platform API Token',
'help'=>'To get a token logon to your account and click on the Tokens tab',
),
'sServerToken' => array(
'type' => 'string',
'label' => 'Zest server id token',
'help'=>'To get a token logon to your account, go to your servers and copy the server id',
),
'bDebugMode' => array(
'type' => 'select',
'options'=>array(
0=>'No',
1=>'Yes'
),
'default'=>0,
'label' => 'Enable Debug Mode',
'help'=>'Enable debugmode to see what data is transmitted',
)

);

You can see that each setting is an array, where you can define the type of setting (select or string in our case), the label that should precede it, an optional help text, default value, and if the type is a select you can add an array with the options that can be selected.

Adding survey specific settings

We want to be able to overwrite the global plugin settings at the survey level, so we are going to add these settings for each survey as well. We are going to use the event that we registered earlier for: beforeSurveySettings, and create a function with the same name that will be called whenever this event is broadcast.

/**
* Add setting on survey level: send hook only for certain surveys / url setting per survey / auth code per survey / send user token / send question response
*/
public function beforeSurveySettings()
{
$oEvent = $this->event;
$oEvent->set("surveysettings.{$this->id}", array(
'name' => get_class($this),
'settings' => array(
'bUse' => array(
'type' => 'select',
'label' => 'Send a hook for this survey',
'options'=>array(
0=> 'No',
1=> 'Yes',
2=> 'Use site settings (default)'
),
'default'=>2,
'help'=>'Leave default to use global setting',
'current'=> $this->get('bUse','Survey',$oEvent->get('survey')),
),
'bUrlOverwrite' => array(
'type' => 'select',
'label' => 'Overwrite the global Hook Url',
'options'=>array(
0=> 'No',
1=> 'Yes'
),
'default'=>0,
'help'=>'Set to Yes if you want to use a specific URL for this survey',
'current'=> $this->get('bUrlOverwrite','Survey',$oEvent->get('survey')),
),
'sUrl' => array(
'type' => 'string',
'label' => 'The address to send the hook for this survey to:',
'help'=>'Leave blank to use global setting',
'current'=> $this->get('sUrl','Survey',$oEvent->get('survey')),
),
'bAuthTokenOverwrite' => array(
'type' => 'select',
'label' => 'Overwrite the global authorization token',
'options'=>array(
0=> 'No',
1=> 'Yes'
),
'default'=>0,
'help'=>'Set to Yes if you want to use a specific zest API token for this survey',
'current'=> $this->get('bAuthTokenOverwrite','Survey',$oEvent->get('survey')),
),
'sAuthToken' => array(
'type' => 'string',
'label' => 'Use a specific API Token for this survey (leave blank to use default)',
'help'=>'Leave blank to use default',
'current'=> $this->get('sAuthToken','Survey',$oEvent->get('survey')),
),
'bServerTokenOverwrite' => array(
'type' => 'select',
'label' => 'Overwrite the global server token',
'options'=>array(
0=> 'No',
1=> 'Yes'
),
'default'=>0,
'help'=>'Set to Yes if you want to use a specific Zest server-token for this survey',
'current'=> $this->get('bServerTokenOverwrite','Survey',$oEvent->get('survey')),
),
'sServerToken' => array(
'type' => 'string',
'label' => 'Zest server-token',
'help'=>'To get a token, log in to your account, go to your servers and copy the server token',
'current'=> $this->get('sServerToken','Survey',$oEvent->get('survey')),

),
'bSendToken' => array(
'type' => 'select',
'label' => 'Send the users\' token to the hook',
'options'=>array(
0=> 'No',
1=> 'Yes'
),
'default'=>0,
'help'=>'Set to Yes if you want to pass the users token along in the request',
'current'=> $this->get('bSendToken','Survey',$oEvent->get('survey')),
),
'sAnswersToSend' => array(
'type' => 'string',
'label' => 'Answers to send',
'help'=> 'Comma separated question codes of the answers you want to send along',
'current'=> $this->get('sAnswersToSend','Survey',$oEvent->get('survey')),
),
),
));
}

As you can see we have also added a few select question on whether to overwrite the global settings, as well as a Answers to send question, where you can enter a comma separated list of question codes whose answers you want to send along with the hook. (useful if you want to register a calculated score, of an e-mailadres that was entered)

We will also need to able to save these settings, so next we will create a function to do so that gets called on the newSurveySettings event.

/**
* Save the settings
*/
public function newSurveySettings()
{
$event = $this->event;
foreach ($event->get('settings') as $name => $value)
{
/* In order use survey setting, if not set, use global, if not set use default */
$default=$event->get($name,null,null,isset($this->settings[$name]['default'])?$this->settings[$name]['default']:NULL);
$this->set($name, $value, 'Survey', $event->get('survey'),$default);
}
}

This will loop through all the settings and save them.

Sending the hook

Finally, we get to write the code that will send the POST request. On firing of the afterSurveyComplete event, the afterSurveyComplete() function will execute.

/**
* Send the webhook on completion of a survey
* @return array | response
*/
public function afterSurveyComplete()
{
//do nothing if: hook is disabled on surveylevel, or survey uses site settings and site defaults to no
$oEvent = $this->getEvent();
$sSurveyId = $oEvent->get('surveyId');
if($this->isHookEnabled($sSurveyId))
{
return;
}

//more code

}

First, we will check whether we should send a webhook. The isHookEnabled function will check this, and return a boolean. If the hook is not enabled, we will return immediately. The isHookEnabled function is a simple one:

private function isHookEnabled($sSurveyId)
{
return ($this->get('bUse','Survey',$sSurveyId)==0)||(($this->get('bUse','Survey',$sSurveyId)==2) && ($this->get('bUse',null,null,$this->settings['bUse'])==0));
}

It first checks if the webhook is enabled on the survey level:

$this->get('bUse','Survey',$sSurveyId)

This retrieves the value for bUse for the survey with this survey id. If this is set to yes, it returns true.

If the value is not set to yes, we will then check if the survey settings is set to ‘use global settings’ and the global setting is set to yes.

The code will then continue to get the relevant data, and eventually send the post request / webhook. The finished code looks like this:

/**
* Send the webhook on completion of a survey
* @return array | response
*/
public function afterSurveyComplete()
{
$time_start = microtime(true);
$oEvent = $this->getEvent();
$sSurveyId = $oEvent->get('surveyId');
if($this->isHookEnabled($sSurveyId))
{
return;
}

$url = ($this->get('bUrlOverwrite','Survey',$sSurveyId)==='1') ? $this->get('sUrl','Survey',$sSurveyId) : $this->get('sUrl',null,null,$this->settings['sUrl']);
$auth = ($this->get('bAuthTokenOverwrite','Survey',$sSurveyId)==='1') ? $this->get('sAuthToken','Survey',$sSurveyId) : $this->get('sAuthToken',null,null,$this->settings['sAuthToken']);
$serverToken = ($this->get('bServerTokenOverwrite','Survey',$sSurveyId)==='1') ? $this->get('sServerToken','Survey',$sSurveyId) : $this->get('sServerToken',null,null,$this->settings['sServerToken']);
$additionalFields = $this->getAdditionalFields($sSurveyId);

if(($this->get('bSendToken','Survey',$sSurveyId)==='1')||(count($additionalFields) > 0))
{
$responseId = $oEvent->get('responseId');
$response = $this->api->getResponse($sSurveyId, $responseId);
$sToken = $this->getTokenString($sSurveyId,$response);
$additionalAnswers = $this->getAdditionalAnswers($additionalFields, $response);
}

$parameters = array(
"survey" => $sSurveyId,
"token" => (isset($sToken)) ? $sToken : null,
"api_token" => $auth,
"server_key" => $serverToken,
"additionalFields" => "additionalFields" => ($additionalFields) ? json_encode($additionalAnswers) : null
);

$hookSent = $this->httpPost($url,$parameters);

$this->debug($parameters, $hookSent, $time_start);

return;
}


/**
* httpPost function http://hayageek.com/php-curl-post-get/
* creates and executes a POST request
* returns the output
*/
private function httpPost($url,$params)
{
$postData = '';
//create name value pairs seperated by &
foreach($params as $k => $v)
{
$postData .= $k . '='.$v.'&';
}
$postData = rtrim($postData, '&');
$fp = fopen(dirname(__FILE__).'/errorlog.txt', 'w');
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_POST, count($postData));
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
$output=curl_exec($ch);
curl_close($ch);
return $output;
}

/**
* check if the hook should be sent
* returns a boolean
*/

private function isHookEnabled($sSurveyId)
{
return ($this->get('bUse','Survey',$sSurveyId)==0)||(($this->get('bUse','Survey',$sSurveyId)==2) && ($this->get('bUse',null,null,$this->settings['bUse'])==0));
}


/**
*
*
*/
private function getAdditionalFields($sSurveyId)
{
$additionalFieldsString = $this->get('sAnswersToSend','Survey',$sSurveyId);
if($additionalFieldsString != ''||$additionalFieldsString != null)
{
return explode(',',$this->get('sAnswersToSend','Survey',$sSurveyId));
}
return null;
}


private function debug($parameters, $hookSent, $time_start)
{
if($this->get('bDebugMode',null,null,$this->settings['bDebugMode'])==1)
{
echo '<pre>';
var_dump($parameters);
echo "<br><br> ----------------------------- <br><br>";
var_dump($hookSent);
echo "<br><br> ----------------------------- <br><br>";
echo 'Total execution time in seconds: ' . (microtime(true) - $time_start);
echo '</pre>';
}
}

private function getTokenString($sSurveyId,$response)
{
return ($this->get('bSendToken','Survey',$sSurveyId)==='1') ? $response['token'] : null;
}

private function getAdditionalAnswers($additionalFields, $response)
{
if($additionalFields)
{
$additionalAnswers = array();
foreach($additionalFields as $field)
{
$additionalAnswers[$field] = htmlspecialchars($response[$field]);
}
return $additionalAnswers;
}
return null;
}

As you can we retrieve the necessary data such as the url to post to, the authentication and server token, the users token, and any additional fields and answers that need to be send along. This data then gets posted to the url, and if the debug setting is on, the response is printed on the screen. If you look at the code you can see how to retrieve certain fields, both on a global server lever or specific survey settings.

Afterthoughts

Hopefully this example plugin can help you to create your own plugins for Limesurvey! The full and complete code can be found on Github, feel free to use it, fork it, improve it or create your own plugins with it. If you have any questions feel free to reach me at stefan [at] evently.nl