Skip to content

Commit

Permalink
Fixed issue #17915: Error accessing general settings
Browse files Browse the repository at this point in the history
  • Loading branch information
olleharstedt committed Mar 1, 2022
1 parent 600ff00 commit 28f3890
Show file tree
Hide file tree
Showing 5 changed files with 528 additions and 0 deletions.
246 changes: 246 additions & 0 deletions modules/admin/globalsettings/README.md
@@ -0,0 +1,246 @@


# GlobalSettings admin module for LimeSurvey

## Quick introduction
Comming with LS4, it's now possible for 3rd party developpers to extends LimeSurvey controllers, and to override their views and subviews.
Here a very small example to show how to proceed.

## Directories, namespace and class name

### The core file name determines the directory name

You can extend any of the controller in the admin directory:
https://github.com/LimeSurvey/LimeSurvey/tree/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/admin

For this example, we choose to extend GlobalSettings controller:
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/admin/globalsettings.php

Since the name of the file is **globalsettings.php**, we must create a directory **modules/admin/globalsettings/**. Respecting case of original file is important.
Note that LimeSurvey files are not normalized (sometime upper case, sometime lower case, sometime using dash, sometime camel cased, etc ). If we have time, we'll normalize the names before we release LS 4.0.0.

Then, inside **modules/admin/globalsettings/controller/** we create a file with the exact same name as the core file: **globalsettings.php**

### The directory path determines the name space

LimeSurvey is based on yii1, and yii1 doesn't really use name space, but rather aliases for path. This is for historical reasons: PHP prior to 5.3.0 does not support namespace intrinsically. So yii1 rather uses a prefix for all their core classes, and uses path aliases extensively. But, for those who want to use namespace, all the Yii import methods accepts path, alias, or namespace. So ideally, in yii1, it must be possible to easely translate your namespace to aliase so we can easely translate your namespace to aliases. To be clear:

So first, in **modules/admin/globalsettings/controller/globalsettings.php** we define a namespace:

```php
namespace lsadminmodules\globalsettings\controller;
```

lsadminmodules is the Yii alias for the directory **modules/admin/**. Then **lsadminmodules\globalsettings\controller** is the namespace attached to the path **modules/admin/globalsettings/controller/**, which is indeed the path of our controller so everything is fine :)

To learn more about the relations between path, aliases and namespace in yii1:
https://www.yiiframework.com/doc/guide/1.1/en/basics.namespace

To see examples why we need to convert your name space to aliases:
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/AdminController.php#L176-L179
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/AdminController.php#L208-L213



### The class name

We give to our controller the same class name as the core controller. It extends the core controller:
```php
class GlobalSettings extends \GlobalSettings
```

# GlobalSettings admin module for LimeSurvey

## Quick introduction
Comming with LS4, it's now possible for 3rd party developpers to extends LimeSurvey controllers, and to override their views and subviews.
Here a very small example to show how to proceed.

## Directories, namespace and class name

### The core file name determines the directory name

You can extend any of the controller in the admin directory:
https://github.com/LimeSurvey/LimeSurvey/tree/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/admin

For this example, we choose to extend GlobalSettings controller:
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/admin/globalsettings.php

Since the name of the file is **globalsettings.php**, we must create a directory **modules/admin/globalsettings/**. Respecting case of original file is important.
Note that LimeSurvey files are not normalized (sometime upper case, sometime lower case, sometime using dash, sometime camel cased, etc ). If we have time, we'll normalize the names before we release LS 4.0.0.

Then, inside **modules/admin/globalsettings/controller/** we create a file with the exact same name as the core file: **globalsettings.php**

### The directory path determines the name space

LimeSurvey is based on yii1, and yii1 doesn't really use name space, but rather aliases for path. This is for historical reasons: PHP prior to 5.3.0 does not support namespace intrinsically. So yii1 rather uses a prefix for all their core classes, and uses path aliases extensively. But, for those who want to use namespace, all the Yii import methods accepts path, alias, or namespace. So ideally, in yii1, it must be possible to easely translate your namespace to aliase so we can easely translate your namespace to aliases. To be clear:

So first, in **modules/admin/globalsettings/controller/globalsettings.php** we define a namespace:

```php
namespace lsadminmodules\globalsettings\controller;
```


lsadminmodules is the Yii alias for the directory **modules/admin/**. Then **lsadminmodules\globalsettings\controller** is the namespace attached to the path **modules/admin/globalsettings/controller/**, which is indeed the path of our controller so everything is fine :)

To learn more about the relations between path, aliases and namespace in yii1:
https://www.yiiframework.com/doc/guide/1.1/en/basics.namespace

To see examples why we need to convert your name space to aliases:
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/AdminController.php#L176-L179
https://github.com/LimeSurvey/LimeSurvey/blob/85cc7c52479addbffb6a857ddc7aa10f52b0b02c/application/controllers/AdminController.php#L208-L213



### The class name

We give to our controller the same class name as the core controller. It extends the core controller:
```php
class GlobalSettings extends \GlobalSettings
```

notice the backslash in front of the second GlobalSettings: it means that we extend the GlobalSetting class from the global namespace, so from core.

Indeed, Yii1 core classes, like LimeSurvey core classes, are under the global PHP name space: **/**. Since our module use a namespace. As a consequence, we'll have to use the global name space in front of all the calls to Yii or LS classes. Eg:
```php
\Yii::getPathOfAlias();
```
instead of:
```php
Yii::getPathOfAlias();
```


## Adding new method to the GlobalSettings controller

Now, any method you'll add to your module will be accessible as if it was part of the core controller.
We added a very simple HelloWorld method that will display the content of the HelloWorld view.
You can reach it via: index.php?r=admin/globalsettings/sa/HelloWorld/

notice the backslash in front of the second GlobalSettings: it means that we extend the GlobalSetting class from the global namespace, so from core.

Indeed, Yii1 core classes, like LimeSurvey core classes, are under the global PHP name space: **/**. Since our module use a namespace. As a consequence, we'll have to use the global name space in front of all the calls to Yii or LS classes. Eg:
```php
\Yii::getPathOfAlias();
```
instead of:
```php
Yii::getPathOfAlias();
```


## Adding new method to the GlobalSettings controller

Now, any method you'll add to your module will be accessible as if it was part of the core controller.
We added a very simple HelloWorld method that will display the content of the HelloWorld view.
You can reach it via: **index.php?r=admin/globalsettings/sa/HelloWorld/**

As you can see, it's using its own view, so it's rendered in its own page like if it was a separated module. It's still availabe via the globalsettings route. So it could be a page displayed by clicking on a button or a menu in global setting, it could be an adavanced editing page for some kind of new settings, etc.

![Full page HelloWorld Module](https://account.limesurvey.org/images/github/full-page-global-setting-extension.png)

## Extending a method from the GlobalSettings controller

Of course, most of the time, when you extend a class, what you want is to override one of its methode to add some specific logic to it. Here, we did a very simple exemple.

### New class parameter
First, we added a new parameter to the GlobalSetting class:

```php
// Just an example to show how to override a parent method
public $myNewParam = "This was not in global setting core controller";
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/controller/globalsettings.php#L31

### Override \GlobalSetting::_renderWrappedTemplate()

We want to display this new param inside the overview pan of global settings. So first, we need to add $myNewParam to the array of data passed to the views.
So, first, we override the \GlobalSetting::_renderWrappedTemplate() :

```php
protected function _renderWrappedTemplate($sAction = '', $aViewUrls = array(), $aData = array(), $sRenderFile = false)
{
// We add ou new paramater to the data to parse to the view
$aData["myNewParam"] = $this->myNewParam;

// Then we just call the parent method
parent::_renderWrappedTemplate($sAction, $aViewUrls, $aData, $sRenderFile);
}
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/controller/globalsettings.php#L50-L64

As you can see, we do only one thing: we add $myNewParam to $aData, then we just call the parent method. This is a very normal way of processing. Then what ever change we do to the core method will also apply to your extension. For exemple, that what we're doing when we override the renderPartial method:
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/application/controllers/AdminController.php#L200-L221

Of course, you can also completly rewrite the logic of the parent method, and not calling at all the parent method. Sometime: you just don't have the choice. Especially when the code is not that much functional oriented, and when the method signature is poor (and let be honnest, it is often the case in LimeSurvey code). For exemple we could also have override the method \GlobalSetting::_displaySettings(). But it accepts no parameter at all, so we would have been forced to rewrite it locally:
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/application/controllers/admin/globalsettings.php#L68-L110

But good news: LimeSurvey is OpenSource. So if you feel blocked because the signature of a core method is too poor to be called as a parent method, just modify the signature, and submit a PR. Then, step by step, all the core code will become much more functionnal and easy to override from modules.

### Override the views

We made admin views override very simple for module. You just have to copy paste the views in your local module. If you have a look to the folders **/modules/admin/globalsettings/views** , you can see it contains 3 files:
```
[HelloWorld.php]
[_overview.php]
[globalSettings_view.php]
```
https://github.com/LimeSurvey/LimeSurvey/tree/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/views

**_overview.php** and **globalSettings_view.php** has been copy/paste from core views:
https://github.com/LimeSurvey/LimeSurvey/tree/98df1afb094077995e2e3b4426a4b64d06d20d60/application/views/admin/globalsettings

Now, the views from the module are the one rendered, not the views from the core. To make it clear, we added an alert in the module views.

In globalSettings_view.php:
```php
<?php if(YII_DEBUG ): ?>
<p class="alert alert-info "> This view is rendered from the global settings module. This message is shown only when debug mode is on. </p>
<?php endif;?>
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/views/globalSettings_view.php#L15-L17

In _overview.php :
```php
<?php if(YII_DEBUG ): ?>
<p class="alert alert-info "> This subview is rendered from global settings module. This message is shown only when debug mode is on. </p>
<?php endif;?>
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/views/_overview.php#L15-L17

If debug mode is on, it will show you an alert that tells you those views are the one from the module.

Then, in globalSettings_view.phop, we add the parameter $myNewParam to the data passed to the view _overview.php
```php
$this->renderPartial("./globalsettings/_overview", array(
'usercount'=>$usercount,
'surveycount'=>$surveycount,
'activesurveycount'=>$activesurveycount,
'deactivatedsurveys'=>$deactivatedsurveys,
'activetokens'=>$activetokens,
'deactivatedtokens'=>$deactivatedtokens,
// Here, we pass to the subview the new parameter
'myNewParam'=>$myNewParam,
)
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/views/globalSettings_view.php#L40-L51

That's a bit annoying. Would be better if all the data was passed to the subviews, so we would avoid to force third party coder to override the main views. Again: if you face this situtation, you can make a PR, so step by step LimeSurvey become more modular.

Now, in _overview.php, we show that data:
```php
<?php if(YII_DEBUG ): ?>
<!-- If debug mode is on, we show the new parameter -->
<tr>
<th >Value of myNewParam :</th><td><?php echo $myNewParam; ?></td>
</tr>
<?php endif;?>
```
https://github.com/LimeSurvey/LimeSurvey/blob/98df1afb094077995e2e3b4426a4b64d06d20d60/modules/admin/globalsettings/views/_overview.php#L39-L44

Now, if debug mode is on, you should see:
![Full page Global Settings view overriden](https://account.limesurvey.org/images/github/global-setting-views-override.png)

## Conclusion

That was a very brief introduction. Of course, you can do much more complex things. In global settings, you could add new settings for one of your modules (like the HelloWorld module). You would then need to override the _saveSettings method. Now, you can modify LimeSurvey deeply wihtout waiting for the team to add new events, or without modifying the core files.
86 changes: 86 additions & 0 deletions modules/admin/globalsettings/controller/globalsettings.php
@@ -0,0 +1,86 @@
<?php
/*
* LimeSurvey
* Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*
* GlobalSettings custom admin module
* This admin module extend the core GlobalSettings controller
*/


// First we define a namespace to avoid collision with core class.
// Since we do that, all our call to the core/framework classes will need the global name space: /
// For exemple: \Yii::app()->getController()->renderPartial...
namespace lsadminmodules\globalsettings\controller;

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

/* Note: Class name must identical to folder name and to the core class you want to override*/
class GlobalSettings extends \GlobalSettings
{

public $myNewParam = "This was not in global setting core controller"; // Just an example to show how to override a parent method

/**
* A brand new helloworld function for GlobalSettings !
*
* You can reach it via: index.php?r=admin/globalsettings/sa/HelloWorld/
*
* @param string $sWho who to say hello
* @return array Populated parameters ready to be rendered inside the admin interface
*/
public function HelloWorld($sWho="World")
{
// Call to Survey_Common_Action::_renderWrappedTemplate that will generate the "Layout"
$this->_renderWrappedTemplate('globalsettings', 'HelloWorld', array(
'sWho'=>$sWho,
));

}

/**
* Renders template(s) wrapped in header and footer
*
* @param string $sAction Current action, the folder to fetch views from
* @param string $aViewUrls View url(s)
* @param array $aData Data to be passed on. Optional.
*/
protected function _renderWrappedTemplate($sAction = '', $aViewUrls = array(), $aData = array(), $sRenderFile = false)
{
// We add ou new paramater to the data to parse to the view
$aData["myNewParam"] = $this->myNewParam;

// Then we just call the parent method
parent::_renderWrappedTemplate($sAction, $aViewUrls, $aData, $sRenderFile);
}

/**
* Override Survey_Common_Action::renderCentralContents
*
* If you don't understand what it does, just copy / paste it in your own admin module
* We let it here just in case you're trying to do something different
*
* NOTE: you just need to copy/paste here any view called by the core GlobalSettings to override it.
*
*/
protected function renderCentralContents($sAction, $aViewUrls, $aData = [])
{
if ( file_exists ( \Yii::getPathOfAlias('lsadminmodules.'.$sAction.'.views.' . $aViewUrls) . '.php' ) ){
// Use alias to render a view outisde of application directory.
return \Yii::app()->getController()->renderPartial('lsadminmodules.'.$sAction.'.views.' . $aViewUrls, $aData, true);
}else{
// var_dump( \Yii::getPathOfAlias('lsadminmodules.' . $sAction. '.views.' . $aViewUrls) ); die();
return parent::renderCentralContents($sAction, $aViewUrls, $aData );
}

}
}
15 changes: 15 additions & 0 deletions modules/admin/globalsettings/views/HelloWorld.php
@@ -0,0 +1,15 @@
<?php
/* @var $this AdminController */
/* @var $sWho url parameter */
?>

<div class="col-sm-12 ">

<h3 class="pagetitle"><?php eT('Hello World From Global Settings!'); ?></h3>
<div class="row">
<div class="col-sm-12 ">
<?php eT('Hello '); echo $sWho; ?> !
</div>
</div>

</div>

0 comments on commit 28f3890

Please sign in to comment.