x

Main chapters

  1. LimeSurvey Cloud vs LimeSurvey CE
  2. LimeSurvey Cloud - Quick start guide
  3. LimeSurvey CE - Installation
  4. How to design a good survey (Guide)
  5. Getting started
  6. LimeSurvey configuration
  7. Introduction - Surveys
  8. View survey settings
  9. View survey menu
  10. View survey structure
  11. Introduction - Questions
  12. Introduction - Question Groups
  13. Introduction - Surveys - Management
  14. Survey toolbar options
  15. Multilingual survey
  16. Quick start guide - ExpressionScript
  17. Advanced features
  18. General FAQ
  19. Troubleshooting
  20. Workarounds
  21. License
  22. Version change log
  23. Plugins - Advanced
 Actions

Development of custom admin modules

From LimeSurvey Manual

Overview

An admin module adds pages to the LimeSurvey administration, or changes an existing admin page, without editing core files. It is a folder of PHP code inside modules/admin/ of your LimeSurvey installation.

LimeSurvey loads every sub-folder of modules/admin/ automatically. There is no install or activate step and no setting to switch a module off: to disable a module, remove its folder.

Use a plugin instead when the plugin events are enough. Plugins can be installed, activated and configured in the plugin manager. Use an admin module when you need a full page inside the LimeSurvey admin layout, or need to extend a core admin controller.

This documentation is meant for PHP developers. An admin module runs with full access to your server, so only install modules from sources you trust.

Up to LimeSurvey 7.x (open question: exact release), two example modules were shipped in modules/admin/: HelloWorld and globalsettings. They have been removed from the release, and folders with these two names are no longer loaded. Use the examples on this page instead.

Directory layout and naming

The folder name of a module is the name of its admin page in the URL. The controller file must have exactly the same name as the folder, including upper and lower case.

modules/admin/
└── HelloAdmin/
    ├── controller/
    │   └── HelloAdmin.php    (class HelloAdmin)
    └── views/
        ├── index.php
        └── surveyPage.php
Item Rule
Folder modules/admin/<ModuleName>/. The page is reached at index.php/admin/<ModuleName>.
Controller file controller/<ModuleName>.php
Controller class <ModuleName>, extending SurveyCommonAction. A module that extends a core admin controller uses the name of the core class instead.
Namespace None for a new page. Only a module that extends a core admin controller uses the namespace lsadminmodules\<ModuleName>\controller, see below.
Views views/<view>.php

The names HelloWorld and globalsettings are reserved and are skipped by LimeSurvey.

Example: a new admin page

This module adds a page that greets a name given in the URL. Only users with the global permission to read settings can open it.

modules/admin/HelloAdmin/controller/HelloAdmin.php

<?php

if (!defined('BASEPATH')) {
    exit('No direct script access allowed');
}

/* The class name must be identical to the folder name */
class HelloAdmin extends SurveyCommonAction
{
    /**
     * @param CController $controller
     * @param string $id
     * @throws CHttpException
     */
    public function __construct($controller, $id)
    {
        parent::__construct($controller, $id);
        if (!Permission::model()->hasGlobalPermission('settings', 'read')) {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }
    }

    /**
     * Default page: index.php/admin/HelloAdmin
     */
    public function index()
    {
        $this->sayHello();
    }

    /**
     * index.php/admin/HelloAdmin/sa/sayHello/sWho/Lime
     *
     * @param string $sWho Who to greet, from the URL
     */
    public function sayHello($sWho = 'World')
    {
        $this->renderWrappedTemplate('HelloAdmin', 'index', [
            'sWho' => $sWho,
        ]);
    }

    /**
     * Render the views of this module instead of core views.
     * Copy this method to your own module unchanged.
     *
     * @param string $sAction Module name
     * @param string $aViewUrls View name
     * @param array $aData Data for the view
     * @return string
     */
    protected function renderCentralContents($sAction, $aViewUrls, $aData = [])
    {
        return Yii::app()->getController()->renderPartial(
            'lsadminmodules.' . $sAction . '.views.' . $aViewUrls,
            $aData,
            true
        );
    }
}

modules/admin/HelloAdmin/views/index.php

<div class="col-12">
    <h1 class="pagetitle h3">Hello admin module</h1>
    <p>Hello <?php echo CHtml::encode($sWho); ?>!</p>
</div>

Always encode values that come from the URL or the database before output, as with CHtml::encode() above.

Reaching the page

Page URL (path format) URL (get format)
Default page index.php/admin/HelloAdmin index.php?r=admin/HelloAdmin
sayHello with a parameter index.php/admin/HelloAdmin/sa/sayHello/sWho/Lime index.php?r=admin/HelloAdmin/sa/sayHello&sWho=Lime

The sa part of the URL names the method to run. Rules:

  • Only public methods declared in your module class can be called this way. Any other name opens the default page, index().
  • URL parameters are passed to the method parameters with the same name.

Example: a page inside a survey

A method that takes a $surveyid parameter is shown inside the survey layout, with the survey side menu and top bar. Add this method to the HelloAdmin class above:

    /**
     * index.php/admin/HelloAdmin/sa/surveyPage/surveyid/123456
     *
     * @param int $surveyid Survey ID, from the URL
     */
    public function surveyPage($surveyid)
    {
        $oSurvey = Survey::model()->findByPk($surveyid);

        $aData = [];
        // A survey ID shows the survey layout
        $aData['surveyid'] = $surveyid;
        // The title bar and its breadcrumb
        $aData['title_bar']['title'] = $oSurvey->currentLanguageSettings->surveyls_title . " (" . gT("ID") . ":" . $surveyid . ")";
        $aData['title_bar']['module_subaction'] = 'HelloAdmin';
        $aData['title_bar']['module_subaction_url'] = App()->createUrl('admin/HelloAdmin/sa/surveyPage', ['surveyid' => $surveyid]);

        $this->renderWrappedTemplate('HelloAdmin', 'surveyPage', $aData);
    }

modules/admin/HelloAdmin/views/surveyPage.php

<div class="col-12">
    <h1 class="pagetitle h3">Hello admin module</h1>
    <p>This page is shown inside the survey layout.</p>
</div>

When the URL contains a survey ID, LimeSurvey checks it before your method runs:

  • An ID that is not a number is refused with error 403, an unknown survey with error 404.
  • The user needs at least read permission on the survey, otherwise error 403.
  • Any further permission, for example to change the survey, must be checked in your method with Permission::model()->hasSurveyPermission().

Extending a core admin controller

A module can extend a core admin controller: add methods to it, change its methods, or replace its views. The folder name must be the name of the core admin page, for example labels for the label sets page.

The module class has the same name as the core class, so it must use a namespace, and extend the core class from the global namespace:

modules/admin/labels/controller/labels.php

<?php

// The namespace avoids a collision with the core class of the same name
namespace lsadminmodules\labels\controller;

if (!defined('BASEPATH')) {
    exit('No direct script access allowed');
}

class Labels extends \Labels
{
    /**
     * A new page: index.php/admin/labels/sa/hello
     */
    public function hello()
    {
        $this->renderWrappedTemplate('labels', 'hello');
    }

}

The view of the new page goes into modules/admin/labels/views/hello.php.

Because the class is in a namespace, every call to a LimeSurvey or Yii class needs a leading backslash, for example \Yii::app() or \Permission::model().

All public methods of the core class stay available at their usual URLs, and the new public methods of your module are added to them. To change a core method, override it and call parent:: where you can, so your module keeps receiving the changes made to the core method.

Overriding core views

To replace a view of the extended core page, copy it from application/views/admin/<page>/ to the views/ folder of your module, with the same file name and sub-folder, and change the copy. LimeSurvey then renders your copy instead of the core view. Views you do not copy are still taken from the core.

Your copy does not receive later changes to the core view, for example fixes and new options after a LimeSurvey update. Copy only the views you need to change, and compare them with the core views after each update.

To pass extra data to a view, override renderWrappedTemplate(), add your data and call the parent method:

    protected function renderWrappedTemplate($sAction = '', $aViewUrls = array(), $aData = array(), $sRenderFile = false)
    {
        $aData['myNewParam'] = 'Value for the view';
        parent::renderWrappedTemplate($sAction, $aViewUrls, $aData, $sRenderFile);
    }

Security checklist

  • Permissions: Check the permission your page needs in the constructor or in each method. LimeSurvey only checks that the user is logged in, and has read permission on a survey when the URL contains a survey ID.
  • Output: Encode every value from the URL, a form or the database with CHtml::encode() before output.
  • Changes: Change data only on POST requests. Forms built with CHtml::form() include the CSRF token that LimeSurvey checks.
  • Methods: Every public method declared in your module class can be called from a URL. Make helper methods protected or private.

Yii root modules

LimeSurvey can also load real Yii modules from modules/root/. They are not shown inside the LimeSurvey admin layout, and they have no access check: their pages can be opened without logging in. They are useful for plain output, for example JSON.

A root module must be added to application/config/internal.php, in the modules array:

'modules' => array(
    'MyRootModule' => array(
        'class' => 'modules.root.MyRootModule.MyRootModuleModule',
    ),
),

internal.php is a core file and is replaced on every update, so you have to add the entry again after each update.