Convert config.coffee to JS
This commit is contained in:
1 parent
88d5239fb9
commit
852b7f38ca
3 files changed
+238
-210
No files matched your search
@@ -2,7 +2,6 @@
|
|||||||
lib/*.map
|
lib/*.map
|
||||||
lib/api-functions.js
|
lib/api-functions.js
|
||||||
lib/auth.js
|
lib/auth.js
|
||||||
lib/config.js
|
|
||||||
lib/cwmp.js
|
lib/cwmp.js
|
||||||
lib/db.js
|
lib/db.js
|
||||||
lib/nbi.js
|
lib/nbi.js
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
###
|
|
||||||
# Copyright 2013-2017 Zaid Abdulla
|
|
||||||
#
|
|
||||||
# This file is part of GenieACS.
|
|
||||||
#
|
|
||||||
# GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# GenieACS is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
###
|
|
||||||
|
|
||||||
path = require 'path'
|
|
||||||
common = require './common'
|
|
||||||
|
|
||||||
options = {
|
|
||||||
CONFIG_DIR : {type : 'path', default : 'config'},
|
|
||||||
MONGODB_CONNECTION_URL : {type : 'string', default : 'mongodb://127.0.0.1/genieacs'},
|
|
||||||
REDIS_PORT : {type : 'int', default : 6379},
|
|
||||||
REDIS_HOST : {type : 'string', default : ''},
|
|
||||||
REDIS_DB : {type : 'int', default : 0},
|
|
||||||
|
|
||||||
CWMP_WORKER_PROCESSES : {type : 'int', default : 0},
|
|
||||||
CWMP_PORT : {type : 'int', default : 7547},
|
|
||||||
CWMP_INTERFACE : {type : 'string', default : '0.0.0.0'},
|
|
||||||
CWMP_SSL : {type : 'bool', default : false},
|
|
||||||
CWMP_LOG_FILE : {type: 'path', default : ''},
|
|
||||||
CWMP_ACCESS_LOG_FILE : {type : 'path', default : ''},
|
|
||||||
|
|
||||||
NBI_WORKER_PROCESSES : {type : 'int', default : 0},
|
|
||||||
NBI_PORT : {type : 'int', default : 7557},
|
|
||||||
NBI_INTERFACE : {type : 'string', default : '0.0.0.0'},
|
|
||||||
NBI_SSL : {type : 'bool', default : false},
|
|
||||||
NBI_LOG_FILE : {type: 'path', default : ''},
|
|
||||||
NBI_ACCESS_LOG_FILE : {type : 'path', default : ''},
|
|
||||||
|
|
||||||
FS_WORKER_PROCESSES : {type : 'int', default : 0},
|
|
||||||
FS_PORT : {type : 'int', default : 7567},
|
|
||||||
FS_INTERFACE : {type : 'string', default : '0.0.0.0'},
|
|
||||||
FS_SSL : {type : 'bool', default : false},
|
|
||||||
FS_HOSTNAME : {type : 'string', default : 'acs.example.com'},
|
|
||||||
FS_LOG_FILE : {type: 'path', default : ''},
|
|
||||||
FS_ACCESS_LOG_FILE : {type : 'path', default : ''},
|
|
||||||
|
|
||||||
UDP_CONNECTION_REQUEST_PORT : {type : 'int', default : 0},
|
|
||||||
|
|
||||||
DOWNLOAD_TIMEOUT: {type : 'int', default : 3600},
|
|
||||||
EXT_TIMEOUT: {type: 'int', default: 3000},
|
|
||||||
MAX_CACHE_TTL : {type : 'int', default : 86400},
|
|
||||||
DEBUG : {type : 'bool', default : false},
|
|
||||||
RETRY_DELAY : {type : 'int', default : 300},
|
|
||||||
SESSION_TIMEOUT : {type : 'int', default : 30},
|
|
||||||
CONNECTION_REQUEST_TIMEOUT : {type : 'int', default: 2000},
|
|
||||||
GPN_NEXT_LEVEL : {type : 'int', default : 0},
|
|
||||||
GPV_BATCH_SIZE : {type : 'int', default : 32},
|
|
||||||
MAX_DEPTH : {type: 'int', default : 16},
|
|
||||||
COOKIES_PATH : {type : 'string'},
|
|
||||||
LOG_FORMAT : {type : 'string', default : 'simple'},
|
|
||||||
ACCESS_LOG_FORMAT : {type : 'string', default : ''},
|
|
||||||
MAX_CONCURRENT_REQUESTS : {type : 'int', default: 20},
|
|
||||||
DATETIME_MILLISECONDS : {type: 'bool', default: true},
|
|
||||||
BOOLEAN_LITERAL : {type: 'bool', default: true},
|
|
||||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH: {type: 'bool', default: false},
|
|
||||||
MAX_COMMIT_ITERATIONS : {type : 'int', default: 32},
|
|
||||||
|
|
||||||
# XML configuration
|
|
||||||
XML_RECOVER : {type : 'bool', default : false},
|
|
||||||
XML_IGNORE_ENC : {type : 'bool', default : false},
|
|
||||||
XML_FORMAT : {type : 'bool', default : false},
|
|
||||||
XML_NO_DECL : {type : 'bool', default : false},
|
|
||||||
XML_NO_EMPTY : {type : 'bool', default : false},
|
|
||||||
XML_IGNORE_NAMESPACE : {type : 'bool', default : false},
|
|
||||||
|
|
||||||
# Should probably never be changed
|
|
||||||
DEVICE_ONLINE_THRESHOLD : {type : 'int', default : 4000}
|
|
||||||
}
|
|
||||||
|
|
||||||
allConfig = {}
|
|
||||||
|
|
||||||
|
|
||||||
setConfig = (name, value, commandLineArgument) ->
|
|
||||||
return true if allConfig[name]?
|
|
||||||
|
|
||||||
# For compatibility with v1.0
|
|
||||||
if name in ['PRESETS_CACHE_DURATION', 'presets-cache-duration']
|
|
||||||
setConfig('MAX_CACHE_TTL', value)
|
|
||||||
|
|
||||||
if name in ['GET_PARAMETER_NAMES_DEPTH_THRESHOLD', 'get-parameter-names-depth-threshold']
|
|
||||||
setConfig('GPN_NEXT_LEVEL', value)
|
|
||||||
|
|
||||||
if name in ['TASK_PARAMETERS_BATCH_SIZE', 'task-parameters-batch-size']
|
|
||||||
setConfig('GPV_BATCH_SIZE', value)
|
|
||||||
|
|
||||||
if name in ['IGNORE_XML_NAMESPACES', 'ignore-xml-namespaces']
|
|
||||||
setConfig('XML_IGNORE_NAMESPACE', value)
|
|
||||||
|
|
||||||
if name in ['XML_PARSE_IGNORE_ENC', 'xml-parse-ignore-enc']
|
|
||||||
setConfig('XML_IGNORE_ENC', value)
|
|
||||||
|
|
||||||
if name in ['XML_PARSE_RECOVER', 'xml-parse-recover']
|
|
||||||
setConfig('XML_RECOVER', value)
|
|
||||||
|
|
||||||
if name in ['FS_IP', 'fs-ip']
|
|
||||||
setConfig('FS_HOSTNAME', value)
|
|
||||||
|
|
||||||
cast = (val, type) ->
|
|
||||||
switch type
|
|
||||||
when 'int'
|
|
||||||
Number(val)
|
|
||||||
when 'bool'
|
|
||||||
String(val).trim().toLowerCase() in ['true', 'on', 'yes', '1']
|
|
||||||
when 'string'
|
|
||||||
String(val)
|
|
||||||
when 'path'
|
|
||||||
if val then path.resolve(val) else ''
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
_value = null
|
|
||||||
|
|
||||||
for optionName, optionDetails of options
|
|
||||||
n = optionName
|
|
||||||
if commandLineArgument
|
|
||||||
n = n.toLowerCase().replace(/_/g, '-')
|
|
||||||
|
|
||||||
if name == n
|
|
||||||
_value = cast(value, optionDetails.type)
|
|
||||||
n = optionName
|
|
||||||
else if name.startsWith("#{n}-")
|
|
||||||
_value = cast(value, optionDetails.type)
|
|
||||||
n = "#{optionName}-#{name[optionName.length+1..]}"
|
|
||||||
|
|
||||||
if _value?
|
|
||||||
allConfig[n] = _value
|
|
||||||
# Save as environmnet variable to pass on to any child processes
|
|
||||||
process.env["GENIEACS_#{n}"] = _value
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
# Command line arguments
|
|
||||||
argv = process.argv[2..]
|
|
||||||
while argv.length
|
|
||||||
arg = argv.shift()
|
|
||||||
if arg[0] == '-'
|
|
||||||
v = argv.shift()
|
|
||||||
setConfig(arg[2..], v, true)
|
|
||||||
|
|
||||||
|
|
||||||
# Environment variable
|
|
||||||
for k, v of process.env
|
|
||||||
continue if k.lastIndexOf('GENIEACS_', 0) != 0
|
|
||||||
k = k[9..] # remove "GENIEACS_" prefix
|
|
||||||
setConfig(k, v)
|
|
||||||
|
|
||||||
|
|
||||||
# Use default config dir if none defined
|
|
||||||
setConfig('CONFIG_DIR', options['CONFIG_DIR'].default)
|
|
||||||
|
|
||||||
# Configuration file
|
|
||||||
for k, v of require(path.resolve(allConfig.CONFIG_DIR, 'config'))
|
|
||||||
if not setConfig(k, v)
|
|
||||||
# Pass as environment variable to be accessable by extensions
|
|
||||||
process.env["GENIEACS_#{k}"] = v
|
|
||||||
|
|
||||||
|
|
||||||
# Defaults
|
|
||||||
for k, v of options
|
|
||||||
setConfig(k, v.default) if v.default?
|
|
||||||
|
|
||||||
|
|
||||||
get = (option, deviceId) ->
|
|
||||||
if deviceId?
|
|
||||||
name = "#{option}-#{deviceId}"
|
|
||||||
v = allConfig[name]
|
|
||||||
return v if v?
|
|
||||||
i = name.lastIndexOf('-')
|
|
||||||
v = allConfig[name[0...i]]
|
|
||||||
return v if v?
|
|
||||||
i = name.lastIndexOf('-', i-1)
|
|
||||||
v = allConfig[name[0...i]]
|
|
||||||
return v if v? or i == -1
|
|
||||||
|
|
||||||
return allConfig[option]
|
|
||||||
|
|
||||||
|
|
||||||
getDefault = (optionName) ->
|
|
||||||
option = options[optionName]
|
|
||||||
return null if not option
|
|
||||||
val = option.default
|
|
||||||
if val and option.type == 'path'
|
|
||||||
val = path.resolve(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
# load authentication scripts
|
|
||||||
try
|
|
||||||
exports.auth = require(path.resolve(allConfig.CONFIG_DIR, 'auth'))
|
|
||||||
|
|
||||||
exports.get = get
|
|
||||||
exports.getDefault = getDefault
|
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2018 Zaid Abdulla
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
CONFIG_DIR: {type: "path", "default": "config"},
|
||||||
|
MONGODB_CONNECTION_URL: {type: "string", "default": "mongodb://127.0.0.1/genieacs"},
|
||||||
|
REDIS_PORT: {type: "int", "default": 6379},
|
||||||
|
REDIS_HOST: {type: "string", "default": ""},
|
||||||
|
REDIS_DB: {type: "int", "default": 0},
|
||||||
|
|
||||||
|
CWMP_WORKER_PROCESSES: {type: "int", "default": 0},
|
||||||
|
CWMP_PORT: {type: "int", "default": 7547},
|
||||||
|
CWMP_INTERFACE: {type: "string", "default": "0.0.0.0"},
|
||||||
|
CWMP_SSL: {type: "bool", "default": false},
|
||||||
|
CWMP_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
CWMP_ACCESS_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
|
||||||
|
NBI_WORKER_PROCESSES: {type: "int", "default": 0},
|
||||||
|
NBI_PORT: {type: "int", "default": 7557},
|
||||||
|
NBI_INTERFACE: {type: "string", "default": "0.0.0.0"},
|
||||||
|
NBI_SSL: {type: "bool", "default": false},
|
||||||
|
NBI_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
NBI_ACCESS_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
|
||||||
|
FS_WORKER_PROCESSES: {type: "int", "default": 0},
|
||||||
|
FS_PORT: {type: "int", "default": 7567},
|
||||||
|
FS_INTERFACE: {type: "string", "default": "0.0.0.0"},
|
||||||
|
FS_SSL: {type: "bool", "default": false},
|
||||||
|
FS_HOSTNAME: {type: "string", "default": "acs.example.com"},
|
||||||
|
FS_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
FS_ACCESS_LOG_FILE: {type: "path", "default": ""},
|
||||||
|
|
||||||
|
UDP_CONNECTION_REQUEST_PORT: {type: "int", "default": 0},
|
||||||
|
|
||||||
|
DOWNLOAD_TIMEOUT: {type: "int", "default": 3600},
|
||||||
|
EXT_TIMEOUT: {type: "int", "default": 3000},
|
||||||
|
MAX_CACHE_TTL: {type: "int", "default": 86400},
|
||||||
|
DEBUG: {type: "bool", "default": false},
|
||||||
|
RETRY_DELAY: {type: "int", "default": 300},
|
||||||
|
SESSION_TIMEOUT: {type: "int", "default": 30},
|
||||||
|
CONNECTION_REQUEST_TIMEOUT: {type: "int", "default": 2000},
|
||||||
|
GPN_NEXT_LEVEL: {type: "int", "default": 0},
|
||||||
|
GPV_BATCH_SIZE: {type: "int", "default": 32},
|
||||||
|
MAX_DEPTH: {type: "int", "default": 16},
|
||||||
|
COOKIES_PATH: {type: "string"},
|
||||||
|
LOG_FORMAT: {type: "string", "default": "simple"},
|
||||||
|
ACCESS_LOG_FORMAT: {type: "string", "default": ""},
|
||||||
|
MAX_CONCURRENT_REQUESTS: {type: "int", "default": 20},
|
||||||
|
DATETIME_MILLISECONDS: {type: "bool", "default": true},
|
||||||
|
BOOLEAN_LITERAL: {type: "bool", "default": true},
|
||||||
|
CONNECTION_REQUEST_ALLOW_BASIC_AUTH: {type: "bool", "default": false},
|
||||||
|
MAX_COMMIT_ITERATIONS: {type: "int", "default": 32},
|
||||||
|
|
||||||
|
// XML configuration
|
||||||
|
XML_RECOVER: {type: "bool", "default": false},
|
||||||
|
XML_IGNORE_ENC: {type: "bool", "default": false},
|
||||||
|
XML_FORMAT: {type: "bool", "default": false},
|
||||||
|
XML_NO_DECL: {type: "bool", "default": false},
|
||||||
|
XML_NO_EMPTY: {type: "bool", "default": false},
|
||||||
|
XML_IGNORE_NAMESPACE: {type: "bool", "default": false},
|
||||||
|
|
||||||
|
// Should probably never be changed
|
||||||
|
DEVICE_ONLINE_THRESHOLD: {type: "int", "default": 4000}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allConfig = {};
|
||||||
|
|
||||||
|
function setConfig(name, value, commandLineArgument) {
|
||||||
|
if (allConfig[name] != null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// For compatibility with v1.0
|
||||||
|
if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration")
|
||||||
|
setConfig("MAX_CACHE_TTL", value);
|
||||||
|
|
||||||
|
if (name === "GET_PARAMETER_NAMES_DEPTH_THRESHOLD" || name === "get-parameter-names-depth-threshold")
|
||||||
|
setConfig("GPN_NEXT_LEVEL", value);
|
||||||
|
|
||||||
|
if (name === "TASK_PARAMETERS_BATCH_SIZE" || name === "task-parameters-batch-size")
|
||||||
|
setConfig("GPV_BATCH_SIZE", value);
|
||||||
|
|
||||||
|
if (name === "IGNORE_XML_NAMESPACES" || name === "ignore-xml-namespaces")
|
||||||
|
setConfig("XML_IGNORE_NAMESPACE", value);
|
||||||
|
|
||||||
|
if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc")
|
||||||
|
setConfig("XML_IGNORE_ENC", value);
|
||||||
|
|
||||||
|
if (name === "XML_PARSE_RECOVER" || name === "xml-parse-recover")
|
||||||
|
setConfig("XML_RECOVER", value);
|
||||||
|
|
||||||
|
if (name === "FS_IP" || name === "fs-ip")
|
||||||
|
setConfig("FS_HOSTNAME", value);
|
||||||
|
|
||||||
|
function cast(val, type) {
|
||||||
|
switch (type) {
|
||||||
|
case "int":
|
||||||
|
return Number(val);
|
||||||
|
case "bool":
|
||||||
|
return ["true", "on", "yes", "1"].includes(String(val).trim().toLowerCase());
|
||||||
|
case "string":
|
||||||
|
return String(val);
|
||||||
|
case "path":
|
||||||
|
if (!val)
|
||||||
|
return "";
|
||||||
|
return path.resolve(val);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value = null;
|
||||||
|
for (let [optionName, optionDetails] of Object.entries(options)) {
|
||||||
|
let n = optionName;
|
||||||
|
if (commandLineArgument)
|
||||||
|
n = n.toLowerCase().replace(/_/g, "-");
|
||||||
|
|
||||||
|
if (name === n) {
|
||||||
|
_value = cast(value, optionDetails.type);
|
||||||
|
n = optionName;
|
||||||
|
} else if (name.startsWith(`${n}-`)) {
|
||||||
|
_value = cast(value, optionDetails.type);
|
||||||
|
n = `${optionName}-${name.slice(optionName.length + 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_value != null) {
|
||||||
|
allConfig[n] = _value;
|
||||||
|
// Save as environmnet variable to pass on to any child process
|
||||||
|
process.env[`GENIEACS_${n}`] = _value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command line arguments
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
while (argv.length) {
|
||||||
|
const arg = argv.shift();
|
||||||
|
if (arg[0] === "-") {
|
||||||
|
const v = argv.shift();
|
||||||
|
setConfig(arg.slice(2), v, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variable
|
||||||
|
for (const [k, v] of Object.entries(process.env)) {
|
||||||
|
if (k.startsWith("GENIEACS_"))
|
||||||
|
setConfig(k.slice(9), v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default config dir if none defined
|
||||||
|
setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]);
|
||||||
|
|
||||||
|
// Configuration file
|
||||||
|
const configFile = require(path.resolve(allConfig.CONFIG_DIR, "config"));
|
||||||
|
for (const [k, v] of Object.entries(configFile))
|
||||||
|
if (!setConfig(k, v))
|
||||||
|
// Pass as environment variable to be accessable by extensions
|
||||||
|
process.env[`GENIEACS_${k}`] = `${v}`;
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
for (const [k, v] of Object.entries(options))
|
||||||
|
if (v["default"] != null)
|
||||||
|
setConfig(k, v["default"]);
|
||||||
|
|
||||||
|
function get(optionName, deviceId) {
|
||||||
|
if (!deviceId)
|
||||||
|
return allConfig[optionName];
|
||||||
|
|
||||||
|
optionName = `${optionName}-${deviceId}`;
|
||||||
|
let v = allConfig[optionName];
|
||||||
|
if (v != null)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
let i = optionName.lastIndexOf("-");
|
||||||
|
v = allConfig[optionName.slice(0, i)];
|
||||||
|
if (v != null)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
i = optionName.lastIndexOf("-", i - 1);
|
||||||
|
v = allConfig[optionName.slice(0, i)];
|
||||||
|
if (v != null)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
i = optionName.lastIndexOf("-", i - 1);
|
||||||
|
v = allConfig[optionName.slice(0, i)];
|
||||||
|
if (v != null)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
i = optionName.lastIndexOf("-", i - 1);
|
||||||
|
if (i > 0) {
|
||||||
|
v = allConfig[optionName.slice(0, i)];
|
||||||
|
if (v != null)
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefault(optionName) {
|
||||||
|
const option = options[optionName];
|
||||||
|
if (!option)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let val = option["default"];
|
||||||
|
if (val && option.type === "path")
|
||||||
|
val = path.resolve(val);
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load authentication scripts
|
||||||
|
try {
|
||||||
|
exports.auth = require(path.resolve(allConfig.CONFIG_DIR, "auth"));
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
exports.get = get;
|
||||||
|
exports.getDefault = getDefault;
|
||||||
Reference in new issue
Block a user