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.
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
protectedorprivate.
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.