Skip to content
Snippets Groups Projects
Commit eb62556b authored by James Walker's avatar James Walker
Browse files

Merge branch 'v2-php8' into 'v2'

PHP 8 support in v2

See merge request !89
parents 29e5c421 f512a3dd
No related branches found
Tags 2.15.0
1 merge request!89PHP 8 support in v2
Showing
with 864 additions and 848 deletions
......@@ -14,7 +14,7 @@ before_script:
- mkdir -p .tmp
- apt-get update && apt-get install -y git zip unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- composer install --ignore-platform-reqs
phpstan:
stage: analyse
......
......@@ -21,15 +21,15 @@
]
},
"require": {
"php": "^7.3",
"ezyang/htmlpurifier": "^4.12",
"setasign/fpdi-fpdf": "^2.2",
"setasign/fpdi-tfpdf": "^2.2",
"setasign/fpdi-tcpdf": "^2.2"
"php": "^7.4|^8.0",
"ezyang/htmlpurifier": "^4",
"setasign/fpdi-fpdf": "^2",
"setasign/fpdi-tfpdf": "^2",
"setasign/fpdi-tcpdf": "^2"
},
"require-dev": {
"ilmiont/trainline": "^1.1",
"phpstan/phpstan": "^0.12"
"ilmiont/trainline": "^1.2",
"phpstan/phpstan": "^1.3"
},
"scripts": {
"phpstan": "vendor/bin/phpstan analyse",
......
This diff is collapsed.
This diff is collapsed.
......@@ -5,6 +5,8 @@ parameters:
paths:
- src
- tests
bootstrapFiles:
- phpstan.php
parallel:
processTimeout: 300.0
tmpDir: .tmp
......@@ -14,7 +16,6 @@ parameters:
checkExplicitMixedMissingReturn: true
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
checkMissingClosureNativeReturnTypehintRule: true
checkMissingIterableValueType: false
checkUninitializedProperties: true
polluteScopeWithAlwaysIterableForeach: false
......
<?php
define("ARDEIDAE_DNC_ERROR_REPORTING", true);
define("ARDEIDAE_DNC_DATE_DEFAULT_TIMEZONE_SET", true);
define("ARDEIDAE_DNC_SET_ERROR_HANDLER", true);
define("ARDEIDAE_DNC_SET_EXCEPTION_HANDLER", true);
?>
<?php
namespace Ardeidae\Exceptions;
use function Ardeidae\is_php8;
/**
* File read exception.
......@@ -18,24 +19,28 @@ class FileReadException extends \Exception {
*
* @var string
*/
public $file;
public $filename;
/**
* Construct exception.
*
* @param string $file File
* @param string $filename File
* @param int $id optional User defined exception code
* @param \Exception $e optional Previous exception
* @return self
*/
public function __construct(
string $file, int $id=0, \Exception $e=null) {
string $filename,
int $id=0,
\Exception $e=null) {
$this -> file = $file;
parent::__construct("Failed to read \"$file\".", $id, $e);
$this -> filename = $filename;
if (!is_php8()) $this -> file = $filename;
parent::__construct("Failed to read \"$filename\".", $id, $e);
}
}
?>
\ No newline at end of file
?>
......@@ -2,6 +2,7 @@
namespace Ardeidae\Exceptions;
use Ardeidae\Runtime\HTTP\Files\FileUpload;
use function Ardeidae\is_php8;
/**
* File upload error – file upload failed
......@@ -19,24 +20,26 @@ class FileUploadError extends \Exception {
*
* @var \Ardeidae\Runtime\HTTP\Files\FileUpload
*/
public $file;
public $upload;
/**
* Construct the exception.
*
* @param \Ardeidae\Runtime\HTTP\Files\FileUpload $file
* @param \Ardeidae\Runtime\HTTP\Files\FileUpload $upload
* @param int $id optional User defined exception code
* @param \Exception $e Previous exception
* @return self
*/
public function __construct(
FileUpload $file, int $id=0, \Exception $e=null) {
FileUpload $upload,
int $id=0,
\Exception $e=null) {
$this -> file = $file;
$this -> upload = $upload;
parent::__construct("File upload failed.", $id, $e);
}
}
?>
\ No newline at end of file
?>
......@@ -2,6 +2,7 @@
namespace Ardeidae\Exceptions;
use Ardeidae\Runtime\HTTP\Files\FileUpload;
use function Ardeidae\is_php8;
/**
* File upload exception – failed to save file
......@@ -19,7 +20,7 @@ class FileUploadException extends \Exception {
*
* @var \Ardeidae\Runtime\HTTP\Files\FileUpload
*/
public $file;
public $upload;
/**
* Path attempted to save to
......@@ -32,21 +33,23 @@ class FileUploadException extends \Exception {
/**
* Construct the exception.
*
* @param \Ardeidae\Runtime\HTTP\Files\FileUpload $file
* @param \Ardeidae\Runtime\HTTP\Files\FileUpload $upload
* @param string $path Path attempted to save to
* @param int $id optional User defined exception code
* @param \Exception $e optional Previous exception
* @return self
*/
public function __construct(
FileUpload $file, string $path,
int $id=0, \Exception $e=null) {
FileUpload $upload,
string $path,
int $id=0,
\Exception $e=null) {
$this -> file = $file;
$this -> upload = $upload;
$this -> path = $path;
parent::__construct("Failed to save uploaded file.", $id, $e);
}
}
?>
\ No newline at end of file
?>
<?php
namespace Ardeidae\Exceptions;
use function Ardeidae\is_php8;
/**
* File write exception.
......@@ -18,24 +19,28 @@ class FileWriteException extends \Exception {
*
* @var string
*/
public $file;
public $filename;
/**
* Construct exception.
*
* @param string $file File
* @param string $filename File
* @param int $id optional User defined exception code
* @param \Exception optional $e Previous exception
* @param \Exception $e optional Previous exception
* @return self
*/
public function __construct(
string $file, int $id=0, \Exception $e=null) {
string $filename,
int $id=0,
\Exception $e=null) {
$this -> file = $file;
parent::__construct("Failed to write \"$file\".", $id, $e);
$this -> filename = $filename;
if (!is_php8()) $this -> file = $filename;
parent::__construct("Failed to write \"$filename\".", $id, $e);
}
}
?>
\ No newline at end of file
?>
<?php
namespace Ardeidae;
/**
* Set error report level.
*
* @param int $level
* @return void
*/
function config_errors(int $level) : void {
error_reporting($level);
}
/**
* Set timezone.
*
* @param string $timezone PHP timezone string.
* @return void
*/
function config_timezone(string $timezone) : void {
date_default_timezone_set($timezone);
}
/**
* Get whether a string "starts with" another string.
*
* @param string $needle
* @param string $haystack
* @return bool
*/
function starts_with(string $needle, string $haystack) : bool {
return substr($haystack, 0, strlen($needle)) === $needle;
}
/**
* Get whether a string "ends with" another string.
*
* @param string $needle
* @param string $haystack
* @return bool
*/
function ends_with(string $needle, string $haystack) : bool {
return substr($haystack, -strlen($needle)) === $needle;
}
/**
* Check whether an array is associative.
*
* Deemed associative if contains at least one string key.
*
* @param array $array
* @return bool
*/
function is_assoc_array(array $array) : bool {
return count(array_filter(array_keys($array), "is_string")) > 0;
}
/**
* Get all the public properties of an object.
*
* Works because this function will be outside the scope
* of the caller, so can be used even within a class.
*
* @param object $object
* @return array Associative array of properties to values
*/
function get_object_vars_public(object $object) : array {
return get_object_vars($object);
}
/**
* Get whether a given property name of an object is publicly available.
*
* Returns `true` if the property does not already exist.
*
* @param object $object
* @param string $prop Property name
* @return bool
*/
function is_public_object_var(object $object, string $prop) : bool {
$exists = property_exists($object, $prop);
$scoped = in_array($prop, array_keys(get_object_vars_public($object)));
return (!$exists || $scoped);
}
/**
* Map an array to include only keys which are specified in `$keys`.
*
* E.g. `input` is `["foo" => "bar", "abc" => "xyz"]` and `$keys` is
* `["foo", "foobar"]` – return will be `["foo" => "bar"]`.
*
* @param array $input
* @param array $keys
* @return array
*/
function array_mapk(array $input, array $keys) : array {
$arr = [];
foreach ($keys as $key) {
if (array_key_exists($key, $input)) {
$arr[$key] = $input[$key];
}
}
return $arr;
}
/**
* Compare two arrays to determine if all keys in one are present in both.
*
* `$params` is input, `$values` needs to match it.
*
* If `$params` is associative, matches values as described above.
*
* Otherwise parameters just need to exist in the array.
*
* Returns:
* 0 - Key in `$params` not in `$values`
* 1 - Key in `$params` has truthy value, falsey in `$values`
* 2 - Key in `$params` has array value, and `$values` value not in it
* -1 - All OK
*
* @param array $param Input array, determining requirements
* @param array $values Comparison array of values to compare
* @return array [{success}, {param that caused failure}]
*/
function compare_array_values(array $params, array $values) : array {
foreach ($params as $param => $value) {
if (is_numeric($param)) {
$param = $value;
if (!in_array($param, $values)) return [0, $param];
}
else if (!array_key_exists($param, $values)) return [0, $param];
if (is_string($param) && is_assoc_array($params)) {
if ($value && !$values[$param]) return [1, $param];
if (is_array($value)) {
if (!in_array($values[$param], $value)) return [2, $param];
}
}
}
return [-1, null];
}
/**
* Resolve an array of arguments for unpacking and passing to a method.
*
* Avoids type issues when an associative array, i.e. one argument, is
* passed to a method that expects an array for unpacking.
*
* If `$args` is associative, subclasses it into a new array.
*
* Otherwise returns the array, or the resulting array, for assumed
* unpacking as arguments for a method.
*
* @param array $args
* @return array Array of values of the resulting arguments.
*/
function resolve_array_args(array $args) : array {
if (is_assoc_array($args)) {
$args = [$args];
}
return array_values($args);
}
/**
* `echo(...)` safe variant.
*
* Sanitises output using `filter_var(...)`.
*
* Defaults to `FILTER_SANITIZE_FULL_SPECIAL_CHARS` if no filter given.
*
* @param mixed $value
* @param string $filter optional `filter_var(...)` filter name to use
* @param bool $echo optional Actually echo (`true`)
* @return string Sanitised output
*/
function echos($value, string $filter=null, bool $echo=true) : string {
if (!$filter) {
$filter = "FILTER_SANITIZE_FULL_SPECIAL_CHARS";
}
$value = filter_var($value, constant($filter));
if ($echo) echo $value;
return $value;
}
/**
* Get input from CLI stdin.
*
* You can optionally restrict the returned values with `$limit`.
* Default allows any.
* If `$limit` is truthy, user must enter a value (no empty strings)
* If `$limit` is an array, user must enter a value in the array.
* The prompt is automatically edited to include the available options.
* The prompt will recurse until the user supplies a valid input.
*
* @param string $prompt
* @param mixed $limit See above.
* @return string
*/
function stdin(string $prompt, $limit=null) : string {
$value = null;
$accepted = false;
$acceptable = null;
if (is_array($limit)) $acceptable = $limit;
elseif ($limit) $acceptable = true;
else $acceptable = "*";
if ($acceptable === true) $prompt .= " (*)";
elseif (is_array($acceptable)) {
$prompt .= " " . "(" . implode("|", $acceptable) . ")";
}
while (!$accepted) {
stdout($prompt);
$value = trim(fgets(fopen("php://stdin", "r")));
if ($acceptable === "*") $accepted = true;
elseif ($acceptable === true && $value !== "") $accepted = true;
elseif (is_array($acceptable) && in_array($value, $acceptable)) {
return true;
}
}
return $value;
}
/**
* Output text to stdout with echo.
*
* Only outputs if in a CLI interface.
*
* @param string $txt
* @param string $in optional Indent multipler as groups of 8 spaces
* @param bool $eol optional Whether to add newline after output (true)
* @param bool $cl optional Whether to clear CLI before output (false)
* @return void
*/
function stdout(
string $txt, int $in=0, bool $eol=true, bool $cl=false) : void {
if (!ARDEIDAE_CLI) return;
if ($cl) stdclr();
$indent = 8 * $in;
$eol = $eol ? "\n" : "";
echo "Ardeidae: " . str_repeat(" ", $indent) . $txt . $eol;
}
/**
* Clear CLI if in CLI interface.
*
* @return void
*/
function stdclr() : void {
if (ARDEIDAE_CLI) system("clear");
}
?>
\ No newline at end of file
<?php
namespace Ardeidae;
/**
* Set error report level.
*
* @param int $level
* @return void
*/
function config_errors(int $level) : void {
error_reporting($level);
}
/**
* Set timezone.
*
* @param string $timezone PHP timezone string.
* @return void
*/
function config_timezone(string $timezone) : void {
date_default_timezone_set($timezone);
}
/**
* Get whether we're running on PHP 8 or newer.
*
* @return bool
*/
function is_php8() : bool {
return (((int) explode(".", phpversion())[0]) >= 8);
}
/**
* Get whether a string "starts with" another string.
*
* @param string $needle
* @param string $haystack
* @return bool
*/
function starts_with(string $needle, string $haystack) : bool {
return substr($haystack, 0, strlen($needle)) === $needle;
}
/**
* Get whether a string "ends with" another string.
*
* @param string $needle
* @param string $haystack
* @return bool
*/
function ends_with(string $needle, string $haystack) : bool {
return substr($haystack, -strlen($needle)) === $needle;
}
/**
* Check whether an array is associative.
*
* Deemed associative if contains at least one string key.
*
* @param array $array
* @return bool
*/
function is_assoc_array(array $array) : bool {
return count(array_filter(array_keys($array), "is_string")) > 0;
}
/**
* Get all the public properties of an object.
*
* Works because this function will be outside the scope
* of the caller, so can be used even within a class.
*
* @param object $object
* @return array Associative array of properties to values
*/
function get_object_vars_public(object $object) : array {
return get_object_vars($object);
}
/**
* Get whether a given property name of an object is publicly available.
*
* Returns `true` if the property does not already exist.
*
* @param object $object
* @param string $prop Property name
* @return bool
*/
function is_public_object_var(object $object, string $prop) : bool {
$exists = property_exists($object, $prop);
$scoped = in_array($prop, array_keys(get_object_vars_public($object)));
return (!$exists || $scoped);
}
/**
* Map an array to include only keys which are specified in `$keys`.
*
* E.g. `input` is `["foo" => "bar", "abc" => "xyz"]` and `$keys` is
* `["foo", "foobar"]` – return will be `["foo" => "bar"]`.
*
* @param array $input
* @param array $keys
* @return array
*/
function array_mapk(array $input, array $keys) : array {
$arr = [];
foreach ($keys as $key) {
if (array_key_exists($key, $input)) {
$arr[$key] = $input[$key];
}
}
return $arr;
}
/**
* Compare two arrays to determine if all keys in one are present in both.
*
* `$params` is input, `$values` needs to match it.
*
* If `$params` is associative, matches values as described above.
*
* Otherwise parameters just need to exist in the array.
*
* Returns:
* 0 - Key in `$params` not in `$values`
* 1 - Key in `$params` has truthy value, falsey in `$values`
* 2 - Key in `$params` has array value, and `$values` value not in it
* -1 - All OK
*
* @param array $param Input array, determining requirements
* @param array $values Comparison array of values to compare
* @return array [{success}, {param that caused failure}]
*/
function compare_array_values(array $params, array $values) : array {
foreach ($params as $param => $value) {
if (is_numeric($param)) {
$param = $value;
if (!in_array($param, $values)) return [0, $param];
}
else if (!array_key_exists($param, $values)) return [0, $param];
if (is_string($param) && is_assoc_array($params)) {
if ($value && !$values[$param]) return [1, $param];
if (is_array($value)) {
if (!in_array($values[$param], $value)) return [2, $param];
}
}
}
return [-1, null];
}
/**
* Resolve an array of arguments for unpacking and passing to a method.
*
* Avoids type issues when an associative array, i.e. one argument, is
* passed to a method that expects an array for unpacking.
*
* If `$args` is associative, subclasses it into a new array.
*
* Otherwise returns the array, or the resulting array, for assumed
* unpacking as arguments for a method.
*
* @param array $args
* @return array Array of values of the resulting arguments.
*/
function resolve_array_args(array $args) : array {
if (is_assoc_array($args)) {
$args = [$args];
}
return array_values($args);
}
/**
* `echo(...)` safe variant.
*
* Sanitises output using `filter_var(...)`.
*
* Defaults to `FILTER_SANITIZE_FULL_SPECIAL_CHARS` if no filter given.
*
* @param mixed $value
* @param string $filter optional `filter_var(...)` filter name to use
* @param bool $echo optional Actually echo (`true`)
* @return string Sanitised output
*/
function echos($value, string $filter=null, bool $echo=true) : string {
if (!$filter) {
$filter = "FILTER_SANITIZE_FULL_SPECIAL_CHARS";
}
$value = filter_var($value, constant($filter));
if ($echo) echo $value;
return $value;
}
/**
* Get input from CLI stdin.
*
* You can optionally restrict the returned values with `$limit`.
* Default allows any.
* If `$limit` is truthy, user must enter a value (no empty strings)
* If `$limit` is an array, user must enter a value in the array.
* The prompt is automatically edited to include the available options.
* The prompt will recurse until the user supplies a valid input.
*
* @param string $prompt
* @param mixed $limit See above.
* @return string
*/
function stdin(string $prompt, $limit=null) : string {
$value = null;
$accepted = false;
$acceptable = null;
if (is_array($limit)) $acceptable = $limit;
elseif ($limit) $acceptable = true;
else $acceptable = "*";
if ($acceptable === true) $prompt .= " (*)";
elseif (is_array($acceptable)) {
$prompt .= " " . "(" . implode("|", $acceptable) . ")";
}
while (!$accepted) {
stdout($prompt);
$value = trim(fgets(fopen("php://stdin", "r")));
if ($acceptable === "*") $accepted = true;
elseif ($acceptable === true && $value !== "") $accepted = true;
elseif (is_array($acceptable) && in_array($value, $acceptable)) {
return true;
}
}
return $value;
}
/**
* Output text to stdout with echo.
*
* Only outputs if in a CLI interface.
*
* @param string $txt
* @param string $in optional Indent multipler as groups of 8 spaces
* @param bool $eol optional Whether to add newline after output (true)
* @param bool $cl optional Whether to clear CLI before output (false)
* @return void
*/
function stdout(
string $txt, int $in=0, bool $eol=true, bool $cl=false) : void {
if (!ARDEIDAE_CLI) return;
if ($cl) stdclr();
$indent = 8 * $in;
$eol = $eol ? "\n" : "";
echo "Ardeidae: " . str_repeat(" ", $indent) . $txt . $eol;
}
/**
* Clear CLI if in CLI interface.
*
* @return void
*/
function stdclr() : void {
if (ARDEIDAE_CLI) system("clear");
}
?>
......@@ -30,7 +30,9 @@ class PasswordHelper {
* @return string Password encrypted hash
*/
public static function encrypt(string $password, $algo=null) : string {
return password_hash($password, ($algo ? $algo : PASSWORD_DEFAULT));
$hash = password_hash($password, ($algo ? $algo : PASSWORD_DEFAULT));
if ($hash) return $hash;
else throw new \RuntimeException("Encryption error (check hashing algorithm available).");
}
......
......@@ -135,7 +135,7 @@ class SQLHelper {
public static function createCountSubquery(
string $query, string $var="count") : string {
$q = trim(preg_replace("/LIMIT.*?(?=\INTO|\\)|)$/mi", "", $query));
$q = trim(preg_replace("/LIMIT.*?(?=INTO|\\)|)$/mi", "", $query));
return "SELECT COUNT(*) AS $var FROM ($q) count_selection";
}
......@@ -233,4 +233,4 @@ class SQLHelper {
}
?>
\ No newline at end of file
?>
......@@ -60,9 +60,9 @@ class SlugHelper {
* @return string
*/
public static function generate(int $length=255) : string {
return substr(md5(uniqid(rand(), true)), 0, $length);
return substr(md5(uniqid(((string) rand()), true)), 0, $length);
}
}
?>
\ No newline at end of file
?>
......@@ -41,8 +41,8 @@ class Session extends \Ardeidae\App\Registry {
* @param \Ardeidae\Runtime\App $app Active app object
* @return self
*/
public function __construct(bool $init=true, App $app) {
$this -> name = "{$app -> name}-{$app -> version}";
public function __construct(bool $init=true, App $app=null) {
$this -> name = ($app ? "{$app -> name}-{$app -> version}" : "ardeidae");
$foff = defined("ARDEIDAE_SESSION_REGISTRY_DISABLE_AUTO_INIT");
if ($init && (session_status() === PHP_SESSION_NONE) && !$foff) {
session_name($this -> name());
......
......@@ -151,7 +151,7 @@ class Cors {
* @return bool
*/
public function isCorsPreflightRequest() : bool {
return (strtoupper($this -> Request -> METHOD) === self::CORS);
return (strtoupper(($this -> Request -> METHOD ?? "")) === self::CORS);
}
......
......@@ -233,7 +233,7 @@ class Request extends \Ardeidae\Runtime\Request {
* @return boolean
*/
public function isJsonRequest() : bool {
return ((explode(";", ($this -> header("CONTENT_TYPE") ?? null))[0] ?? null) === "application/json");
return ((explode(";", ($this -> header("CONTENT_TYPE") ?? ""))[0] ?? null) === "application/json");
}
......
......@@ -5,18 +5,14 @@ use Ardeidae\Exceptions\PropertyInaccessible;
/**
* Abstract type class
*
* This class implements `\JsonSerializable` to return its
* current value when it is serialized to JSON, see the
* implementation of `jsonSerialize(...)` for further details.
*
*
* @package Ardeidae
* @subpackage Types
* @author James Walker
* @copyright James Walker 2019
* @license MIT License
*/
abstract class Type implements TypeInterface, \JsonSerializable {
abstract class Type implements TypeInterface {
/**
* Method to invoke when getting value
......@@ -91,16 +87,6 @@ abstract class Type implements TypeInterface, \JsonSerializable {
$this -> $method($value);
}
/**
* JSON serialize – return the type's value using `getValue(...)`.
*
* @return mixed
*/
public function jsonSerialize() {
return $this -> getValue();
}
}
?>
\ No newline at end of file
?>
......@@ -62,17 +62,17 @@ class TestController extends Ardeidae\App\Controller {
*
* @param \Ardeidae\Runtime\Request $request
* @param \Ardeidae\Runtime\Response $response
* @param array $headers optional `HTTP_DEFAULT_HEADERS`
* @param \Ardeidae\Runtime\Runtime $runtime
* @param array $headers optional `HTTP_DEFAULT_HEADERS`
* @return self
*/
public function __construct(
Request $request,
Response $response,
$headers=[],
Runtime $runtime) {
Runtime $runtime,
$headers=[]) {
if ($headers) $this -> HTTP_DEFAULT_HEADERS = $headers;
$this -> HTTP_DEFAULT_HEADERS = $headers;
parent::__construct($request, $response, $runtime);
}
......@@ -229,8 +229,8 @@ class ControllerTests extends Unit {
new Ardeidae\Registries\Env()
),
new Ardeidae\Runtime\CLI\Response(),
[],
new Ardeidae\Runtime\CLI\Runtime(new App())
new Ardeidae\Runtime\CLI\Runtime(new App()),
[]
);
}
......@@ -276,8 +276,8 @@ class ControllerTests extends Unit {
$c = new TestController(
$r,
new Ardeidae\Runtime\HTTP\Response(),
["foo" => "bar", "test" => "true"],
new Ardeidae\Runtime\HTTP\Runtime($app, new Cors($app, $r))
new Ardeidae\Runtime\HTTP\Runtime($app, new Cors($app, $r)),
["foo" => "bar", "test" => "true"]
);
assertions\ident($c -> Response -> headers["foo"], "bar");
assertions\ident($c -> Response -> headers["test"], "true");
......
......@@ -396,8 +396,11 @@ class ArdeidaeTests extends Trainline\Framework\Unit {
* Getting app public properties as array.
*/
public function app() : void {
$a = new ArdeidaeTest(false, false);
assertions\ident($a -> app(), [
$res = $a -> app();
$expected = [
"name" => $a -> name,
"autoload" => $a -> autoload,
"namespace" => $a -> namespace,
......@@ -413,7 +416,12 @@ class ArdeidaeTests extends Trainline\Framework\Unit {
"providers" => [],
"install_file" => $a -> install_file,
"migrations" => $a -> migrations
]);
];
foreach ($expected as $prop => $val) {
assertions\ident($res[$prop], $val);
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment