Commit 0435213c authored by Carsten  Rose's avatar Carsten Rose
Browse files

#3466 / Input Typeahead: optional only allow specified input.

Mode: typeAheadPedantic
TypeAhead.php: Implemented new parameter  TYPEAHEAD_API_PREFETCH.
Ldap.php: add new mode MODE_LDAP_PREFETCH
AbstractBuildForm.php: If FE_TYPEAHEAD_PEDANTIC is specified, add attribute DATA_TYPEAHEAD_PEDANTIC,'true'
parent dd608b0e
......@@ -226,8 +226,8 @@ config.qfq.ini
+-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+
| EDIT_FORM_PAGE | EDIT_FORM_PAGE = form | T3 Pagealias to edit a form. |
+-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+
| LDAP_1_RDN | LDAP_1_RDN='ou=Admin,ou=example,dc=com' | Credentials for non-anonymous LDAP access |
+-----------------------------+-------------------------------------------------+ |
| LDAP_1_RDN | LDAP_1_RDN='ou=Admin,ou=example,dc=com' | Credentials for non-anonymous LDAP access. At the moment only one set of |
+-----------------------------+-------------------------------------------------+ crendentials is supported. |
| LDAP_1_PASSWORD | LDAP_1_PASSWORD=mySecurePassword | |
+-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+
......@@ -702,7 +702,7 @@ Store: *LDAP* - L
^^^^^^^^^^^^^^^^^
* Sanatized: *yes*
* See :ref:`STORE_LDAP`:
* See also :ref:`LDAP`:
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| Name | Explanation |
......@@ -811,46 +811,51 @@ SQL Statement
LDAP
====
A form can retrieve values from an LDAP server to display or to save them. Configuration options for LDAP will be specified
A form can retrieve data from LDAP server(s) to display or to save them. Configuration options for LDAP will be specified
in the *parameter* field of the *Form* and/or the *FormElement*. Definitions of the *FormElement* will overwrite definitions
of the *Form*.
of the *Form*. One LDAP Server can be configured per *FormElement*. Multiple *FormElements* might use individual LDAP
Server configurations.
Best practice for configuration - if LDAP access is:
To decide which Parameter should be placed on *Form.parameter* and which on *FormElement.parameter*: If LDAP access is ...
* only necessary in one *FormElement*, most usefull setup is to specify all values in that specific *FormElement*,
* needed on multiple *FormElement*s (of the same *Form*, e.g. one *input* with *typeAhead*, one *note* and one *action*), it's more
efficient to specify the base parameter *ldapServer*, *ldapBaseDn* in *Form.parameter* and the rest on the current
*FormElement*.
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| Parameter | Example | Description | Form | FormElement | Used for |
+==========================+==================================+===============================================================+======+=============+==========+
| ldapServer | directory.example.com | Hostname. For LDAPS: `ldaps://directory.example.com:636` | x | x | TA, FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapBaseDn | ou=Addressbook,dc=example,dc=com | Base DN to start the search | x | x | TA, FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapAttributes | cn, email | List of attributes to save in STORE_LDAP | x | x | FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapSearch | (mail=john.doe@example.com) | Regular LDAP search expresssion | x | x | FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapTimeLimit | 3 (default) | Maximum time to wait for an answer of the LDAP Server | x | x | TA, FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapUseBindCredentials | ldapUseBindCredentials=1 | Use LDAP_1_* crendentials from config.qfq.ini for ldap_bind() | x | x | TA, FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdap | - | Enable LDAP as 'Typeahead' data source | | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapSearch | `(|(cn=*?*)(mail=*?*))` | Regular LDAP search expresssion | x | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapValuePrintf | `'%s / %s', cn, email` | Custom format to display attributes, as `value` | x | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapIdPrintf | `'%s', email` | Custom format to display attributes, as `id` | x | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLimit | 20 (default) | Result will be limited to this number of entries | x | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadMinLength | 2 (default) | Minimum number of characters before starting the search | x | x | TA |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| fillStoreLdap | - | Activate `Fill STORE LDAP` with the first retrieved record | | x | FSL |
+--------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| Parameter | Example | Description | Form | FormElement | Used for |
+=============================+==================================+===============================================================+======+=============+==========+
| ldapServer | directory.example.com | Hostname. For LDAPS: `ldaps://directory.example.com:636` | x | x | TA, FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapBaseDn | ou=Addressbook,dc=example,dc=com | Base DN to start the search | x | x | TA, FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapAttributes | cn, email | List of attributes to save in STORE_LDAP | x | x | FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapSearch | (mail=john.doe@example.com) | Regular LDAP search expresssion | x | x | FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapTimeLimit | 3 (default) | Maximum time to wait for an answer of the LDAP Server | x | x | TA, FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| ldapUseBindCredentials | ldapUseBindCredentials=1 | Use LDAP_1_* crendentials from config.qfq.ini for ldap_bind() | x | x | TA, FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdap | - | Enable LDAP as 'Typeahead' data source | | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapSearch | `(|(cn=*?*)(mail=*?*))` | Regular LDAP search expresssion, returns upto typeAheadLimit | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapSearchPrefetch | `(mail=?)` | Regular LDAP search expresssion, typically return one record | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapValuePrintf | `'%s / %s', cn, mail` | Custom format to display attributes, as `value` | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLdapIdPrintf | `'%s', mail` | Custom format to display attributes, as `id` | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadLimit | 20 (default) | Result will be limited to this number of entries | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadPedantic | typeAheadPedantic | Activate 'pedantic' mode - only valid keys are allowed | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| typeAheadMinLength | 2 (default) | Minimum number of characters before starting the search | x | x | TA |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
| fillStoreLdap | - | Activate `Fill STORE LDAP` with the first retrieved record | | x | FSL |
+-----------------------------+----------------------------------+---------------------------------------------------------------+------+-------------+----------+
* *typeAheadLimit*: there might be a hard limit on the server side (e.g. 100) - which can't be extended.
* *ldapUseBindCredentials* is only necessary if `anonymous` access is not possible. RDN and password has to be configured in
......@@ -891,6 +896,20 @@ The typed data will be escaped to fullfill LDAP search limitations.
Regular *Form* variables might be used on all parameter and will be evaluated during form load (!) - *not* at the time when
the user types something.
Pedantic
^^^^^^^^
In case the typed value (technically this is the value of the *id*, latest in the moment when loosing the focus) have
to be a valid (= exist on the LDAP server), the *typeAheadPedantic* mode can be activated.
If the user typed something and that is not a valid *id*, the client will delete the input when loosing the focus.
To identify the exact *id*, an additional search filter is necessary.
* *Form.parameter* or *FormElement.parameter*:
* *typeAheadPedantic*
* *typeAheadLdapSearchPrefetch* = `(mail=?)`
.. _Fill_LDAP_STORE:
Fill STORE LDAP (FSL)
......
......@@ -515,8 +515,9 @@ abstract class AbstractBuildForm {
$config = array();
if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
$keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES, F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH, F_TYPEAHEAD_LIMIT,
F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF, F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
$keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES, F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH,
F_TYPEAHEAD_LDAP_SEARCH_PREFETCH, F_TYPEAHEAD_LIMIT, F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF,
F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
$formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
} else {
return $formElement; // nothing to do.
......@@ -815,6 +816,9 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
if (isset($formElement[FE_TYPEAHEAD_PEDANTIC])) {
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_PEDANTIC, 'true');
}
}
if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
......@@ -908,6 +912,7 @@ abstract class AbstractBuildForm {
$formElement[FE_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_LDAP_SERVER);
$formElement[FE_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_LDAP_BASE_DN);
$formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH);
$formElement[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH);
$formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF);
$formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_KEY_PRINTF);
$formElement[FE_LDAP_USE_BIND_CREDENTIALS] = Support::setIfNotSet($formElement, FE_LDAP_USE_BIND_CREDENTIALS);
......@@ -926,6 +931,7 @@ abstract class AbstractBuildForm {
FE_LDAP_SERVER => $formElement[FE_LDAP_SERVER],
FE_LDAP_BASE_DN => $formElement[FE_LDAP_BASE_DN],
FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH],
FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH],
FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF],
FE_TYPEAHEAD_LDAP_KEY_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF],
FE_TYPEAHEAD_LIMIT => $formElement[FE_TYPEAHEAD_LIMIT],
......
......@@ -207,7 +207,8 @@ const ERROR_NO_TARGET_PATH_FILE_NAME = 1503;
const ERROR_LDAP_CONNECT = 1600;
const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH = 1601;
const ERROR_LDAP_BIND = 1602;
const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH_PREFETCH = 1602;
const ERROR_LDAP_BIND = 1603;
// KeyValueParser
const ERROR_KVP_VALUE_HAS_NO_KEY = 1900;
......@@ -456,6 +457,7 @@ const SQL_LOG_MODE_ALL = 'all';
const SQL_LOG_MODE_MODIFY = 'modify';
const SQL_LOG_MODE_ERROR = 'error'; // write log entry, independent of global setting (e.g. broken Query)
const MODE_LDAP_PREFETCH = 'ldapPrefetch';
const MODE_LDAP_SINGLE = 'ldapSingle';
const MODE_LDAP_MULTI = 'ldapMulti';
......@@ -493,6 +495,7 @@ const DATA_REQUIRED = 'data-required';
const CLASS_TYPEAHEAD = 'qfq-typeahead';
const DATA_TYPEAHEAD_SIP = 'data-typeahead-sip'; // Used for typeAhead
const CLASS_NOTE = 'qfq-note';
const DATA_ENABLE_SAVE_BUTTON = 'data-enable-save-button';
......@@ -502,6 +505,7 @@ const DATA_ENABLE_SAVE_BUTTON = 'data-enable-save-button';
const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit';
const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength';
const DATA_TYPEAHEAD_PEDANTIC = 'data-typeahead-pedantic';
const CLASS_CHARACTER_COUNT = 'qfq-character-count';
const DATA_CHARACTER_COUNT_ID = 'data-character-count-id';
......@@ -579,9 +583,11 @@ const F_LDAP_TIME_LIMIT = 'ldapTimeLimit';
const F_LDAP_USE_BIND_CREDENTIALS = 'ldapUseBindCredentials';
const F_TYPEAHEAD_LIMIT = 'typeAheadLimit';
const F_TYPEAHEAD_MINLENGTH = 'typeAheadMinLength';
const F_TYPEAHEAD_PEDANTIC = 'typeAheadPedantic';
const F_TYPEAHEAD_LDAP_VALUE_PRINTF = 'typeAheadLdapValuePrintf';
const F_TYPEAHEAD_LDAP_ID_PRINTF = 'typeAheadLdapIdPrintf';
const F_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch';
const F_TYPEAHEAD_LDAP_SEARCH_PREFETCH = 'typeAheadLdapSearchPrefetch';
const F_MODE = 'mode';
const F_MODE_READONLY = 'readonly';
......@@ -676,11 +682,13 @@ const FE_LDAP_TIME_LIMIT = F_LDAP_TIME_LIMIT;
const FE_LDAP_USE_BIND_CREDENTIALS = F_LDAP_USE_BIND_CREDENTIALS;
const FE_TYPEAHEAD_LIMIT = F_TYPEAHEAD_LIMIT;
const FE_TYPEAHEAD_MINLENGTH = F_TYPEAHEAD_MINLENGTH;
const FE_TYPEAHEAD_PEDANTIC = F_TYPEAHEAD_PEDANTIC;
const FE_TYPEAHEAD_SQL = 'typeAheadSql';
const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = F_TYPEAHEAD_LDAP_VALUE_PRINTF;
const FE_TYPEAHEAD_LDAP_KEY_PRINTF = F_TYPEAHEAD_LDAP_ID_PRINTF;
const FE_TYPEAHEAD_LDAP = 'typeAheadLdap';
const FE_TYPEAHEAD_LDAP_SEARCH = F_TYPEAHEAD_LDAP_SEARCH;
const FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH = F_TYPEAHEAD_LDAP_SEARCH_PREFETCH;
const FE_FILL_STORE_LDAP = 'fillStoreLdap';
const FE_CHARACTER_COUNT_WRAP = 'characterCountWrap';
const RETYPE_FE_NAME_EXTENSION = 'RETYPE';
......
......@@ -34,13 +34,17 @@ class TypeAhead {
*/
public function __construct($phpUnit = false) {
if ((!isset($_GET[TYPEAHEAD_API_QUERY]) && !isset($_GET[TYPEAHEAD_API_PREFETCH])) || !isset($_GET[TYPEAHEAD_API_SIP])) {
throw new CodeException('Missing GET parameter "' . TYPEAHEAD_API_SIP . '" or "' . TYPEAHEAD_API_QUERY . '" or "' . TYPEAHEAD_API_PREFETCH . '"');
}
$this->vars[TYPEAHEAD_API_QUERY] = isset($_GET[TYPEAHEAD_API_QUERY]) ? $_GET[TYPEAHEAD_API_QUERY] : '';
$this->vars[TYPEAHEAD_API_PREFETCH] = isset($_GET[TYPEAHEAD_API_PREFETCH]) ? $_GET[TYPEAHEAD_API_PREFETCH] : '';
$this->vars[TYPEAHEAD_API_SIP] = $_GET[TYPEAHEAD_API_SIP];
$this->vars[TYPEAHEAD_API_SIP] = isset($_GET[TYPEAHEAD_API_SIP]) ? $_GET[TYPEAHEAD_API_SIP] : '';
if ($this->vars[TYPEAHEAD_API_SIP] == '') {
throw new CodeException('Missing GET parameter "' . TYPEAHEAD_API_SIP);
}
if ($this->vars[TYPEAHEAD_API_QUERY] == '' && $this->vars[TYPEAHEAD_API_PREFETCH] == '') {
throw new CodeException('Missing GET parameter "' . TYPEAHEAD_API_QUERY . '" or "' . TYPEAHEAD_API_PREFETCH . '"');
}
$session = Session::getInstance($phpUnit);
......@@ -67,8 +71,8 @@ class TypeAhead {
$arr = $this->typeAheadSql($sipVars, $this->vars[TYPEAHEAD_API_QUERY]);
} elseif (isset($sipVars[FE_LDAP_SERVER])) {
$ldap = new Ldap();
//TODO hier weiter machen mit Implementierung PREFETCH
$arr = $ldap->process($sipVars, $this->vars[TYPEAHEAD_API_QUERY]);
$mode = ($this->vars[TYPEAHEAD_API_PREFETCH] == '') ? MODE_LDAP_MULTI : MODE_LDAP_PREFETCH;
$arr = $ldap->process($sipVars, $this->vars[TYPEAHEAD_API_QUERY], $mode);
}
return $arr;
......
......@@ -106,7 +106,7 @@ class Ldap {
*
* @param array $config [FE_LDAP_SERVER , FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_LDAP_KEY_PRINTF, FE_TYPEAHEAD_LDAP_VALUE_PRINTF]
* @param string $searchValue value to search via $config[FE_LDAP_SEARCH]
* @param string $mode MODE_LDAP_SINGLE | MODE_LDAP_MULTI
* @param string $mode MODE_LDAP_SINGLE | MODE_LDAP_MULTI | MODE_LDAP_PREFETCH
* @return array Array: [ [ 'key' => '...', 'value' => '...' ], ]
* @throws UserFormException
*/
......@@ -114,15 +114,29 @@ class Ldap {
$arr = array();
// For TypeAhead, use an optional given F_TYPEAHEAD_LDAP_SEARCH
if ($mode == MODE_LDAP_MULTI) {
if (!isset($config[F_TYPEAHEAD_LDAP_SEARCH]) || $config[F_TYPEAHEAD_LDAP_SEARCH] == '') {
throw new UserFormException("Missing definition for `" . F_TYPEAHEAD_LDAP_SEARCH . "`", ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH);
}
$config[F_LDAP_SEARCH] = $config[F_TYPEAHEAD_LDAP_SEARCH];
} else {
if (!isset($config[F_LDAP_SEARCH]) || $config[F_LDAP_SEARCH] == '') {
throw new UserFormException("Missing definition for `" . F_LDAP_SEARCH . "`", ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH);
}
switch ($mode) {
case MODE_LDAP_PREFETCH:
if (!isset($config[F_TYPEAHEAD_LDAP_SEARCH_PREFETCH]) || $config[F_TYPEAHEAD_LDAP_SEARCH_PREFETCH] == '') {
throw new UserFormException("Missing definition for `" . F_TYPEAHEAD_LDAP_SEARCH_PREFETCH . "`", ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH_PREFETCH);
}
$config[F_LDAP_SEARCH] = $config[F_TYPEAHEAD_LDAP_SEARCH_PREFETCH];
break;
case MODE_LDAP_MULTI:
if (!isset($config[F_TYPEAHEAD_LDAP_SEARCH]) || $config[F_TYPEAHEAD_LDAP_SEARCH] == '') {
throw new UserFormException("Missing definition for `" . F_TYPEAHEAD_LDAP_SEARCH . "`", ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH);
}
$config[F_LDAP_SEARCH] = $config[F_TYPEAHEAD_LDAP_SEARCH];
break;
case MODE_LDAP_SINGLE:
if (!isset($config[F_LDAP_SEARCH]) || $config[F_LDAP_SEARCH] == '') {
throw new UserFormException("Missing definition for `" . F_LDAP_SEARCH . "`", ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH);
}
break;
default:
throw new UserFormException("Unknown mode: " . $mode, ERROR_UNKNOWN_MODE);
}
$searchValue = Support::ldap_escape($searchValue, null, LDAP_ESCAPE_FILTER);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment