Commit 3f83ed2a authored by Carsten  Rose's avatar Carsten Rose
Browse files

Fixes #6914. Final implemention of customized typeMessageViolation. Incl. unit...

Fixes #6914. Final implemention of customized typeMessageViolation. Incl. unit tests. New escape mode 'C' - escapes ':' by '\'.' - useful for variables in variable definition.
parent ed1865bf
Pipeline #1420 passed with stage
in 2 minutes and 3 seconds
...@@ -1142,28 +1142,144 @@ Types ...@@ -1142,28 +1142,144 @@ Types
Store variables Store variables
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
Syntax: *{{VarName[:<store / prio>[:<sanitize class>[:<escape>[:<default>]]]]}}* Syntax: *{{VarName[:<store(s)[:<sanitize class>[:<escape>[:<default value>[:type violate message]]]]]}}*
See also:
* `store`_
* `sanitize-class`_
* `variable-escape`_
* `variable-default`_
* `variable-type-message-violate`_
* Example:: * Example::
{{pId}} {{pId}}
{{pId:FSE}} {{pId:FSE}}
{{pId:FSE:digit}} {{pId:FSE:digit}}
{{pId::digit}}
{{name:FSE:alnumx:m}} {{name:FSE:alnumx:m}}
{{name:::m}}
{{name:FSE:alnumx:m:John Doe}} {{name:FSE:alnumx:m:John Doe}}
{{name::::John Doe}}
{{name:FSE:alnumx:m:John Doe:forbidden characters}}
{{name:::::forbidden characters}}
* Zero or more stores might be specified to be searched for the given VarName. * Zero or more stores might be specified to be searched for the given VarName.
* If no store is specified, the by default searched stores are: **FSRVD** (=FORM > SIP > RECORD > VARS > DEFAULT). * If no store is specified, the default for the searched stores are: **FSRVD** (=FORM > SIP > RECORD > VARS > DEFAULT).
* If the VarName is not found in one store, the next store is searched, up to the last specified store. * If the VarName is not found in one store, the next store is searched, up to the last specified store.
* If the VarName is not found and a default value is given, the default is returned. * If the VarName is not found and a default value is given, the default is returned.
* If no value is found, nothing is replaced - the string '{{<VarName>}}' remains. * If no value is found, nothing is replaced - the string '{{<VarName>}}' remains.
* If anywhere along the line an empty string is found, this **is** a value: therefore, the search will stop. * If anywhere along the line an empty string is found, this **is** a value: therefore, the search will stop.
See also: .. _`sanitize-class`:
Sanitize class
--------------
Values in STORE_CLIENT *C* (Client=Browser) and STORE_FORM *F* (Form, HTTP 'post') are checked against a
sanitize class. Values from other stores are *not* checked against any sanitize class, even if a sanitize class is specified.
* Variables get by default the sanitize class defined in the corresponding `FormElement`. If not defined,
the default class is 'digit'.
* A default sanitize class can be overwritten by individual definition: *{{a:C:alnumx}}*
* If a value violates the specific sanitize class, see `variable-type-message-violate`_ for default or customized message.
By default the value becomes `!!<name of sanitize class>!!`. E.g. `!!digit!!`.
For QFQ variables and FormElements:
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| Name | Form | Query | Pattern |
+==================+======+=======+=========================================================================================+
| **alnumx** | Form | Query | [A-Za-z][0-9]@-_.,;: /() ÀÈÌÒÙàèìòùÁÉÍÓÚÝáéíóúýÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿç |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **digit** | Form | Query | [0-9] |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **numerical** | Form | Query | [0-9.-+] |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **allbut** | Form | Query | All characters allowed, but not [ ] { } % \ #. The used regexp: '^[^\[\]{}%\\#]+$', |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **all** | Form | Query | no sanitizing |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
* `store`_
* `variable-escape`_ Only in FormElement:
* `sanitize-class`_
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **auto** | Form | | Only supported for FormElements. Most suitable checktype is dynamically evaluated based |
| | | | on native column definition, the FormElement type, and other info. See below for details. |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **email** | Form | Query | [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,} |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **pattern** | Form | | Compares the value against a regexp. |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
Rules for CheckType Auto (by priority):
* TypeAheadSQL or TypeAheadLDAP defined: **alnumx**
* Table definition
* integer type: **digit**
* floating point number: **numerical**
* FE Type
* 'password', 'note': **all**
* 'editor', 'text' and encode = 'specialchar': **all**
* None of the above: **alnumx**
.. _`variable-escape`:
Escape
------
Variables used in SQL Statements might cause trouble by using: NUL (ASCII 0), \\n, \\r, \\, ', ", and Control-Z.
To protect the web application the following `escape` types are available:
* 'm' - `real_escape_string() <http://php.net/manual/en/mysqli.real-escape-string.php>`_ (m = mysql)
* 'l' - LDAP search filter values will be escaped: `ldap-escape() <http://php.net/manual/en/function.ldap-escape.php>`_ (LDAP_ESCAPE_FILTER).
* 'L' - LDAP DN values will be escaped. `ldap-escape() <http://php.net/manual/en/function.ldap-escape.php>`_ (LDAP_ESCAPE_DN).
* 's' - single ticks will be escaped. str_replace() of ' against \\'.
* 'd' - double ticks will be escaped: str_replace() of " against \\".
* 'C' - colon ':' will be escaped: str_replace() of : against \\:.
* 'c' - config - the escape type configured in `configuration`_.
* '' - the escape type configured in `configuration`_.
* '-' - no escaping.
* The `escape` type is defined by the fourth parameter of the variable. E.g.: `{{name:FE:alnumx:m}}` (m = mysql).
* It's possible to combine different `escape` types, they will be processed in the order given. E.g. `{{name:FE:alnumx:Ls}}` (L, s).
* Escaping is typically necessary for SQL or LDAP queries.
* Be careful when escaping nested variables. Best is to escape **only** the most outer variable.
* In configuration_ a global `escapeTypeDefault` can be defined. The configured escape type applies to all substituted
variables, who *do not* contain a *specific* escape type.
* Additionally a `defaultEscapeType` can be defined per `Form` (separate field in the *Form editor*). This overwrites the
global definition of `configuration`. By default, every `Form.defaultEscapeType` = 'c' (=config), which means the setting
in `configuration`_.
* To suppress a default escape type, define the `escape type` = '-' on the specific variable. E.g.: `{{name:FE:alnumx:-}}`.
.. _`variable-default`:
Default
-------
* Any string can be given to define a default value.
* If a default value is given, it makes no sense to define more than one store: with a default value given, only the
first store is considered.
* If the default value contains a ':', that one needs to be escaped by '\'.
.. _`variable-type-message-violate`:
Type message violate
--------------------
typeMessageViolate:
* 'c' - The violated class will shown, surrounded by '!!'. E.g. '!!digit!!'. This is the default.
* 'e' - Instead of the value an empty string will be shown.
* '0' - Instead of the value the string '0' will be shown.
* 'custom text ...' - Instead of the value the custom text will be shown. If the text contains a ':', that one needs to
be escaped by '\'.
.. _`sql-variables`: .. _`sql-variables`:
...@@ -1269,90 +1385,6 @@ These variables are especially helpful in: ...@@ -1269,90 +1385,6 @@ These variables are especially helpful in:
* `report`, to create create links or buttons outside of a SQL statement. E.g. in `head`, `rbeg`, ... * `report`, to create create links or buttons outside of a SQL statement. E.g. in `head`, `rbeg`, ...
* `form`, to create links and buttons in labels or notes. * `form`, to create links and buttons in labels or notes.
.. _`sanitize-class`:
Sanitize class
--------------
Values in STORE_CLIENT *C* (Client=Browser) and STORE_FORM *F* (Form, HTTP 'post') are checked against a
sanitize class. Values from other stores are not checked against any sanitize class.
* If a value violates the specific sanitize class, the value becomes `!!<name of sanitize class>!!`. E.g. `!!digit!!`.
* Variables get by default the sanitize class defined in the corresponding `FormElement`. If not defined,
the default class is 'digit'.
* A default sanitize class can be overwritten by individual definition: *{{a:C:alnumx}}*
For QFQ variables and FormElements:
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| Name | Form | Query | Pattern |
+==================+======+=======+=========================================================================================+
| **alnumx** | Form | Query | [A-Za-z][0-9]@-_.,;: /() ÀÈÌÒÙàèìòùÁÉÍÓÚÝáéíóúýÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿç |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **digit** | Form | Query | [0-9] |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **numerical** | Form | Query | [0-9.-+] |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **allbut** | Form | Query | All characters allowed, but not [ ] { } % \ #. The used regexp: '^[^\[\]{}%\\#]+$', |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
| **all** | Form | Query | no sanitizing |
+------------------+------+-------+-----------------------------------------------------------------------------------------+
Only in FormElement:
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **auto** | Form | | Only supported for FormElements. Most suitable checktype is dynamically evaluated based |
| | | | on native column definition, the FormElement type, and other info. See below for details. |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **email** | Form | Query | [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,} |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
| **pattern** | Form | | Compares the value against a regexp. |
+------------------+------+-------+-------------------------------------------------------------------------------------------+
Rules for CheckType Auto (by priority):
* TypeAheadSQL or TypeAheadLDAP defined: **alnumx**
* Table definition
* integer type: **digit**
* floating point number: **numerical**
* FE Type
* 'password', 'note': **all**
* 'editor', 'text' and encode = 'specialchar': **all**
* None of the above: **alnumx**
.. _`variable-escape`:
Escape
------
Variables used in SQL Statements might cause trouble by using: NUL (ASCII 0), \\n, \\r, \\, ', ", and Control-Z.
To protect the web application the following `escape` types are available:
* 'm' - `real_escape_string() <http://php.net/manual/en/mysqli.real-escape-string.php>`_ (m = mysql)
* 'l' - LDAP search filter values will be escaped: `ldap-escape() <http://php.net/manual/en/function.ldap-escape.php>`_ (LDAP_ESCAPE_FILTER).
* 'L' - LDAP DN values will be escaped. `ldap-escape() <http://php.net/manual/en/function.ldap-escape.php>`_ (LDAP_ESCAPE_DN).
* 's' - single ticks will be escaped. str_replace() of ' against \\'.
* 'd' - double ticks will be escaped: str_replace() of " against \\".
* 'c' - config - the escape type configured in `configuration`_.
* '' - the escape type configured in `configuration`_.
* '-' - no escaping.
* The `escape` type is defined by the fourth parameter of the variable. E.g.: `{{name:FE:alnumx:m}}` (m = mysql).
* It's possible to combine different `escape` types, they will be processed in the order given. E.g. `{{name:FE:alnumx:Ls}}` (L, s).
* Escaping is typically necessary for SQL or LDAP queries.
* Be careful when escaping nested variables. Best is to escape **only** the most outer variable.
* In configuration_ a global `escapeTypeDefault` can be defined. The configured escape type applies to all substituted
variables, who *do not* contain a *specific* escape type.
* Additionally a `defaultEscapeType` can be defined per `Form` (separate field in the *Form editor*). This overwrites the
global definition of `configuration`. By default, every `Form.defaultEscapeType` = 'c' (=config), which means the setting
in `configuration`_.
* To suppress a default escape type, define the `escape type` = '-' on the specific variable. E.g.: `{{name:FE:alnumx:-}}`.
Security Security
======== ========
......
...@@ -708,6 +708,7 @@ const DOUBLE_TICK = '"'; ...@@ -708,6 +708,7 @@ const DOUBLE_TICK = '"';
const TOKEN_ESCAPE_CONFIG = 'c'; const TOKEN_ESCAPE_CONFIG = 'c';
const TOKEN_ESCAPE_SINGLE_TICK = 's'; const TOKEN_ESCAPE_SINGLE_TICK = 's';
const TOKEN_ESCAPE_DOUBLE_TICK = 'd'; const TOKEN_ESCAPE_DOUBLE_TICK = 'd';
const TOKEN_ESCAPE_COLON = 'C';
const TOKEN_ESCAPE_LDAP_FILTER = 'l'; const TOKEN_ESCAPE_LDAP_FILTER = 'l';
const TOKEN_ESCAPE_LDAP_DN = 'L'; const TOKEN_ESCAPE_LDAP_DN = 'L';
const TOKEN_ESCAPE_MYSQL = 'm'; const TOKEN_ESCAPE_MYSQL = 'm';
......
...@@ -375,6 +375,9 @@ class Evaluate { ...@@ -375,6 +375,9 @@ class Evaluate {
case TOKEN_ESCAPE_DOUBLE_TICK: case TOKEN_ESCAPE_DOUBLE_TICK:
$value = str_replace('"', '\\"', $value); $value = str_replace('"', '\\"', $value);
break; break;
case TOKEN_ESCAPE_COLON:
$value = str_replace(':', '\\:', $value);
break;
case TOKEN_ESCAPE_LDAP_FILTER: case TOKEN_ESCAPE_LDAP_FILTER:
$value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER); $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
break; break;
......
...@@ -684,8 +684,8 @@ class Store { ...@@ -684,8 +684,8 @@ class Store {
$len = strlen(SIP_PREFIX_BASE64); $len = strlen(SIP_PREFIX_BASE64);
while ($useStores !== false) { while ($useStores !== false && $useStores!=='') {
$store = substr($useStores, 0, 1); // next store $store = $useStores[0]; // current store
$finalKey = $key; $finalKey = $key;
if ($store == STORE_LDAP) { if ($store == STORE_LDAP) {
......
...@@ -103,6 +103,62 @@ class EvaluateTest extends AbstractDatabaseTest { ...@@ -103,6 +103,62 @@ class EvaluateTest extends AbstractDatabaseTest {
$this->assertEquals($expected, $eval->parse('{{SHOW COLUMNS FROM Person}}')); $this->assertEquals($expected, $eval->parse('{{SHOW COLUMNS FROM Person}}'));
} }
/**
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
public function testParseStore() {
$eval = new Evaluate($this->store, $this->dbArray[DB_INDEX_DEFAULT]);
$allStores = [STORE_BEFORE, STORE_CLIENT, STORE_TABLE_DEFAULT, STORE_FORM, STORE_LDAP,
STORE_TABLE_COLUMN_TYPES, STORE_PARENT_RECORD, STORE_SIP, STORE_TYPO3, STORE_USER, STORE_VAR, STORE_SYSTEM];
// Not found anywhere
foreach ($allStores as $store) {
$this->assertEquals("$store - start {{not-set-in-any-store:$store}} end", $eval->parse("$store - start {{not-set-in-any-store:$store}} end"));
}
$this->assertEquals("E - start end", $eval->parse('E - start {{not-set-in-any-store:E}} end'));
$this->assertEquals("0 - start 0 end", $eval->parse('0 - start {{not-set-in-any-store:0}} end'));
// All stores (but empty,zero): Check default
foreach ($allStores as $store) {
if ($store == STORE_EMPTY || $store == STORE_ZERO) {
continue;
}
$this->assertEquals("$store - start Jane end", $eval->parse("$store - start {{not-set-in-any-store:$store:::Jane}} end"));
}
// Set and retrieve
foreach ($allStores as $store) {
$this->store::setVar('value', '0123', $store);
$this->assertEquals("$store - start 0123 end", $eval->parse("$store - start {{value:$store}} end"));
}
// Check default sanitize class
// Set and retrieve
foreach ($allStores as $store) {
if ($store == STORE_CLIENT || $store == STORE_FORM) {
continue;
}
$this->store::setVar('value', 'John', $store);
$this->assertEquals("$store - start John end", $eval->parse("$store - start {{value:$store}} end"));
}
$this->store::setVar('value', 'John', STORE_CLIENT);
$this->assertEquals("start !!digit!! end", $eval->parse('start {{value:C}} end'));
$this->store::setVar('value', 'John', STORE_FORM);
$this->assertEquals("start !!digit!! end", $eval->parse('start {{value:F}} end'));
// All Stores: Sanitize Class explizit set: alnumx
foreach ($allStores as $store) {
$this->assertEquals("start John end", $eval->parse('start {{value:' . $store . ':alnumx}} end'));
}
}
/** /**
* @throws CodeException * @throws CodeException
* @throws DbException * @throws DbException
...@@ -335,6 +391,11 @@ class EvaluateTest extends AbstractDatabaseTest { ...@@ -335,6 +391,11 @@ class EvaluateTest extends AbstractDatabaseTest {
$this->assertEquals('hello', $eval->substitute('a:F:all:d', $foundInStore)); $this->assertEquals('hello', $eval->substitute('a:F:all:d', $foundInStore));
$this->assertEquals(STORE_FORM, $foundInStore); $this->assertEquals(STORE_FORM, $foundInStore);
// None, Colon
$this->store->setVar('a', 'hello', STORE_FORM, true);
$this->assertEquals('hello', $eval->substitute('a:F:all:C', $foundInStore));
$this->assertEquals(STORE_FORM, $foundInStore);
// ', Single Tick // ', Single Tick
$this->store->setVar('a', 'hel\'lo', STORE_FORM, true); $this->store->setVar('a', 'hel\'lo', STORE_FORM, true);
...@@ -346,6 +407,11 @@ class EvaluateTest extends AbstractDatabaseTest { ...@@ -346,6 +407,11 @@ class EvaluateTest extends AbstractDatabaseTest {
$this->assertEquals('hel\'lo', $eval->substitute('a:F:all:d', $foundInStore)); $this->assertEquals('hel\'lo', $eval->substitute('a:F:all:d', $foundInStore));
$this->assertEquals(STORE_FORM, $foundInStore); $this->assertEquals(STORE_FORM, $foundInStore);
// Colon
$this->store->setVar('a', 'hel:lo', STORE_FORM, true);
$this->assertEquals('hel\:lo', $eval->substitute('a:F:all:C', $foundInStore));
$this->assertEquals(STORE_FORM, $foundInStore);
// ", Single Tick // ", Single Tick
$this->store->setVar('a', 'hel"lo', STORE_FORM, true); $this->store->setVar('a', 'hel"lo', STORE_FORM, true);
...@@ -430,6 +496,57 @@ class EvaluateTest extends AbstractDatabaseTest { ...@@ -430,6 +496,57 @@ class EvaluateTest extends AbstractDatabaseTest {
} }
/**
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
public function testTypeViolateMessage() {
$eval = new Evaluate($this->store, $this->dbArray[DB_INDEX_DEFAULT]);
$store = STORE_CLIENT;
$this->store::setVar('value', 'Joh%n', $store);
$this->assertEquals("$store - start 0 end", $eval->parse("$store - start {{value:$store:digit:::0}} end"));
$this->store::setVar('value', 'Joh%n', $store);
$this->assertEquals("$store - start end", $eval->parse("$store - start {{value:$store:digit:::e}} end"));
$this->store::setVar('value', 'Joh%n', $store);
$this->assertEquals("$store - start !!digit!! end", $eval->parse("$store - start {{value:$store:digit:::c}} end"));
$this->store::setVar('value', 'Joh%n', $store);
$this->assertEquals("$store - start custom message end", $eval->parse("$store - start {{value:$store:digit:::custom message}} end"));
}
/**
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
public function testParseSpecialCharInParameter() {
$eval = new Evaluate($this->store, $this->dbArray[DB_INDEX_DEFAULT]);
$store = STORE_CLIENT;
// default
$this->assertEquals("$store - start default mit : in string end", $eval->parse("$store - start {{not in store:$store:alnumx::default mit \: in string}} end"));
$this->assertEquals("$store - start default mit ' in string end", $eval->parse("$store - start {{not in store:$store:alnumx::default mit ' in string}} end"));
$this->assertEquals("$store - start default mit \" in string end", $eval->parse("$store - start {{not in store:$store:alnumx::default mit \" in string}} end"));
// typeMessageViolate
$this->store::setVar('value', 'John', $store);
$this->assertEquals("$store - start violate standard end", $eval->parse("$store - start {{value:$store::::violate standard}} end"));
$this->assertEquals("$store - start violate with : in string end", $eval->parse("$store - start {{value:$store::::violate with \: in string}} end"));
$this->assertEquals("$store - start violate with \' in string end", $eval->parse("$store - start {{value:$store::::violate with ' in string}} end"));
$this->assertEquals("$store - start violate with \\\" in string end", $eval->parse("$store - start {{value:$store::::violate with \" in string}} end"));
}
/** /**
* @throws CodeException * @throws CodeException
* @throws DbException * @throws DbException
...@@ -448,7 +565,5 @@ class EvaluateTest extends AbstractDatabaseTest { ...@@ -448,7 +565,5 @@ class EvaluateTest extends AbstractDatabaseTest {
} catch (\Exception $e) { } catch (\Exception $e) {
echo $e->getMessage(); echo $e->getMessage();
} }
} }
} }
...@@ -202,6 +202,7 @@ class SanitizeTest extends TestCase { ...@@ -202,6 +202,7 @@ class SanitizeTest extends TestCase {
} }
//[ ] { } % & \ # //[ ] { } % & \ #
/** /**
*/ */
public function testSanitizeExceptionAllBut() { public function testSanitizeExceptionAllBut() {
...@@ -371,5 +372,35 @@ class SanitizeTest extends TestCase { ...@@ -371,5 +372,35 @@ class SanitizeTest extends TestCase {
$this->assertEquals($_GET[CLIENT_PAGE_LANGUAGE], '5'); $this->assertEquals($_GET[CLIENT_PAGE_LANGUAGE], '5');
} }
/**
* @throws CodeException
* @throws UserFormException
*/
public function testTypeMessageViolate() {
// Default
$result = Sanitize::sanitize('badstring', 'digit');
$this->assertEquals('!!digit!!', $result);
// SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS
$result = Sanitize::sanitize('badstring', 'digit', '', '', SANITIZE_EMPTY_STRING,
'', SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS);
$this->assertEquals('!!digit!!', $result);
// SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY
$result = Sanitize::sanitize('badstring', 'digit', '', '', SANITIZE_EMPTY_STRING,
'', SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY);
$this->assertEquals('', $result);
// SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO
$result = Sanitize::sanitize('badstring', 'digit', '', '', SANITIZE_EMPTY_STRING,
'', SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO);
$this->assertEquals('0', $result);
// SANITIZE_TYPE_MESSAGE_VIOLATE_ ... custom
$result = Sanitize::sanitize('badstring', 'digit', '', '', SANITIZE_EMPTY_STRING,
'', 'custom message');
$this->assertEquals('custom message', $result);
}
} }
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