mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-02-21 07:29:37 +01:00
refactor: utils method exports (#1008)
This commit is contained in:
parent
bc04703ce7
commit
e92c680586
@ -5,7 +5,7 @@ import Path from 'path';
|
|||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
|
|
||||||
import {API_MESSAGE, HEADERS} from '../../../lib/constants';
|
import {API_MESSAGE, HEADERS} from '../../../lib/constants';
|
||||||
import {DIST_TAGS, validate_metadata, isObject, ErrorCode} from '../../../lib/utils';
|
import {DIST_TAGS, validateMetadata, isObject, ErrorCode} from '../../../lib/utils';
|
||||||
import {media, expectJson, allow} from '../../middleware';
|
import {media, expectJson, allow} from '../../middleware';
|
||||||
import {notify} from '../../../lib/notify';
|
import {notify} from '../../../lib/notify';
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ export default function(router: Router, auth: IAuth, storage: IStorageHandler, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
metadata = validate_metadata(req.body, name);
|
metadata = validateMetadata(req.body, name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next(ErrorCode.getBadData('bad incoming package data'));
|
return next(ErrorCode.getBadData('bad incoming package data'));
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
validateName as utilValidateName,
|
validateName as utilValidateName,
|
||||||
validate_package as utilValidatePackage,
|
validatePackage as utilValidatePackage,
|
||||||
isObject,
|
isObject,
|
||||||
ErrorCode} from '../lib/utils';
|
ErrorCode} from '../lib/utils';
|
||||||
import {API_ERROR, HEADER_TYPE, HEADERS, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER} from '../lib/constants';
|
import {API_ERROR, HEADER_TYPE, HEADERS, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER} from '../lib/constants';
|
||||||
|
4
src/lib/bootstrap.js
vendored
4
src/lib/bootstrap.js
vendored
@ -9,7 +9,7 @@ import https from 'https';
|
|||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
import constants from 'constants';
|
import constants from 'constants';
|
||||||
import endPointAPI from '../api/index';
|
import endPointAPI from '../api/index';
|
||||||
import {parse_address} from './utils';
|
import {parseAddress} from './utils';
|
||||||
|
|
||||||
import type {Callback} from '@verdaccio/types';
|
import type {Callback} from '@verdaccio/types';
|
||||||
import type {$Application} from 'express';
|
import type {$Application} from 'express';
|
||||||
@ -41,7 +41,7 @@ export function getListListenAddresses(argListen: string, configListen: mixed) {
|
|||||||
addresses = [DEFAULT_PORT];
|
addresses = [DEFAULT_PORT];
|
||||||
}
|
}
|
||||||
addresses = addresses.map(function(addr) {
|
addresses = addresses.map(function(addr) {
|
||||||
const parsedAddr = parse_address(addr);
|
const parsedAddr = parseAddress(addr);
|
||||||
|
|
||||||
if (!parsedAddr) {
|
if (!parsedAddr) {
|
||||||
logger.logger.warn({addr: addr},
|
logger.logger.warn({addr: addr},
|
||||||
|
@ -6,7 +6,7 @@ import Path from 'path';
|
|||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
|
|
||||||
import {folder_exists, fileExists} from './utils';
|
import {folderExists, fileExists} from './utils';
|
||||||
|
|
||||||
const CONFIG_FILE = 'config.yaml';
|
const CONFIG_FILE = 'config.yaml';
|
||||||
const XDG = 'xdg';
|
const XDG = 'xdg';
|
||||||
@ -65,7 +65,7 @@ function updateStorageLinks(configLocation, defaultConfig) {
|
|||||||
// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
|
// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
let dataDir = process.env.XDG_DATA_HOME || Path.join(process.env.HOME, '.local', 'share');
|
let dataDir = process.env.XDG_DATA_HOME || Path.join(process.env.HOME, '.local', 'share');
|
||||||
if (folder_exists(dataDir)) {
|
if (folderExists(dataDir)) {
|
||||||
dataDir = Path.resolve(Path.join(dataDir, pkgJSON.name, 'storage'));
|
dataDir = Path.resolve(Path.join(dataDir, pkgJSON.name, 'storage'));
|
||||||
return defaultConfig.replace(/^storage: .\/storage$/m, `storage: ${dataDir}`);
|
return defaultConfig.replace(/^storage: .\/storage$/m, `storage: ${dataDir}`);
|
||||||
} else {
|
} else {
|
||||||
@ -81,7 +81,7 @@ const getXDGDirectory = () => {
|
|||||||
const XDGConfig = getXDGHome() ||
|
const XDGConfig = getXDGHome() ||
|
||||||
process.env.HOME && Path.join(process.env.HOME, '.config');
|
process.env.HOME && Path.join(process.env.HOME, '.config');
|
||||||
|
|
||||||
if (XDGConfig && folder_exists(XDGConfig)) {
|
if (XDGConfig && folderExists(XDGConfig)) {
|
||||||
return {
|
return {
|
||||||
path: Path.join(XDGConfig, pkgJSON.name, CONFIG_FILE),
|
path: Path.join(XDGConfig, pkgJSON.name, CONFIG_FILE),
|
||||||
type: XDG,
|
type: XDG,
|
||||||
@ -92,7 +92,7 @@ const getXDGDirectory = () => {
|
|||||||
const getXDGHome = () => process.env.XDG_CONFIG_HOME;
|
const getXDGHome = () => process.env.XDG_CONFIG_HOME;
|
||||||
|
|
||||||
const getWindowsDirectory = () => {
|
const getWindowsDirectory = () => {
|
||||||
if (process.platform === WIN32 && process.env.APPDATA && folder_exists(process.env.APPDATA)) {
|
if (process.platform === WIN32 && process.env.APPDATA && folderExists(process.env.APPDATA)) {
|
||||||
return {
|
return {
|
||||||
path: Path.resolve(Path.join(process.env.APPDATA, pkgJSON.name, CONFIG_FILE)),
|
path: Path.resolve(Path.join(process.env.APPDATA, pkgJSON.name, CONFIG_FILE)),
|
||||||
type: WIN,
|
type: WIN,
|
||||||
|
@ -13,6 +13,10 @@ export const HEADERS = {
|
|||||||
GZIP: 'gzip',
|
GZIP: 'gzip',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CHARACTER_ENCODING = {
|
||||||
|
UTF8: 'utf-8'
|
||||||
|
}
|
||||||
|
|
||||||
export const HEADER_TYPE = {
|
export const HEADER_TYPE = {
|
||||||
CONTENT_ENCODING: 'content-encoding',
|
CONTENT_ENCODING: 'content-encoding',
|
||||||
CONTENT_TYPE: 'content-type',
|
CONTENT_TYPE: 'content-type',
|
||||||
|
@ -13,7 +13,7 @@ import {checkPackageLocal, publishPackage, checkPackageRemote, cleanUpLinksRef,
|
|||||||
mergeUplinkTimeIntoLocal, generatePackageTemplate} from './storage-utils';
|
mergeUplinkTimeIntoLocal, generatePackageTemplate} from './storage-utils';
|
||||||
import {setupUpLinks, updateVersionsHiddenUpLink} from './uplink-util';
|
import {setupUpLinks, updateVersionsHiddenUpLink} from './uplink-util';
|
||||||
import {mergeVersions} from './metadata-utils';
|
import {mergeVersions} from './metadata-utils';
|
||||||
import {ErrorCode, normalizeDistTags, validate_metadata, isObject, DIST_TAGS} from './utils';
|
import {ErrorCode, normalizeDistTags, validateMetadata, isObject, DIST_TAGS} from './utils';
|
||||||
import type {IStorage, IProxy, IStorageHandler, ProxyList, StringValue} from '../../types';
|
import type {IStorage, IProxy, IStorageHandler, ProxyList, StringValue} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
Versions,
|
Versions,
|
||||||
@ -444,7 +444,7 @@ class Storage implements IStorageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validate_metadata(upLinkResponse, name);
|
validateMetadata(upLinkResponse, name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.logger.error({
|
self.logger.error({
|
||||||
sub: 'out',
|
sub: 'out',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
// @prettier
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
API_ERROR,
|
API_ERROR,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
DEFAULT_DOMAIN,
|
DEFAULT_DOMAIN,
|
||||||
|
CHARACTER_ENCODING
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import {generateGravatarUrl} from '../utils/user';
|
import {generateGravatarUrl} from '../utils/user';
|
||||||
|
|
||||||
@ -41,17 +43,17 @@ export function convertPayloadToBase64(payload: string): Buffer {
|
|||||||
* Validate a package.
|
* Validate a package.
|
||||||
* @return {Boolean} whether the package is valid or not
|
* @return {Boolean} whether the package is valid or not
|
||||||
*/
|
*/
|
||||||
function validate_package(name: any): boolean {
|
export function validatePackage(name: string): boolean {
|
||||||
name = name.split('/', 2);
|
const nameList = name.split('/', 2);
|
||||||
if (name.length === 1) {
|
if (nameList.length === 1) {
|
||||||
// normal package
|
// normal package
|
||||||
return validateName(name[0]);
|
return validateName(nameList[0]);
|
||||||
} else {
|
} else {
|
||||||
// scoped package
|
// scoped package
|
||||||
return (
|
return (
|
||||||
name[0][0] === '@' &&
|
nameList[0][0] === '@' &&
|
||||||
validateName(name[0].slice(1)) &&
|
validateName(nameList[0].slice(1)) &&
|
||||||
validateName(name[1])
|
validateName(nameList[1])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +63,7 @@ function validate_package(name: any): boolean {
|
|||||||
* @param {*} name the package name
|
* @param {*} name the package name
|
||||||
* @return {Boolean} whether is valid or not
|
* @return {Boolean} whether is valid or not
|
||||||
*/
|
*/
|
||||||
function validateName(name: string): boolean {
|
export function validateName(name: string): boolean {
|
||||||
if (_.isString(name) === false) {
|
if (_.isString(name) === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -84,7 +86,7 @@ function validateName(name: string): boolean {
|
|||||||
* @param {*} obj the element
|
* @param {*} obj the element
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
function isObject(obj: any): boolean {
|
export function isObject(obj: any): boolean {
|
||||||
return _.isObject(obj) && _.isNull(obj) === false && _.isArray(obj) === false;
|
return _.isObject(obj) && _.isNull(obj) === false && _.isArray(obj) === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +97,7 @@ function isObject(obj: any): boolean {
|
|||||||
* @param {*} name
|
* @param {*} name
|
||||||
* @return {Object} the object with additional properties as dist-tags ad versions
|
* @return {Object} the object with additional properties as dist-tags ad versions
|
||||||
*/
|
*/
|
||||||
function validate_metadata(object: Package, name: string) {
|
export function validateMetadata(object: Package, name: string): Object {
|
||||||
assert(isObject(object), 'not a json object');
|
assert(isObject(object), 'not a json object');
|
||||||
assert.equal(object.name, name);
|
assert.equal(object.name, name);
|
||||||
|
|
||||||
@ -118,7 +120,7 @@ function validate_metadata(object: Package, name: string) {
|
|||||||
* Create base url for registry.
|
* Create base url for registry.
|
||||||
* @return {String} base registry url
|
* @return {String} base registry url
|
||||||
*/
|
*/
|
||||||
function combineBaseUrl(
|
export function combineBaseUrl(
|
||||||
protocol: string,
|
protocol: string,
|
||||||
host: string,
|
host: string,
|
||||||
prefix?: string
|
prefix?: string
|
||||||
@ -204,15 +206,11 @@ export function getLocalRegistryTarballUri(
|
|||||||
* @param {*} tag
|
* @param {*} tag
|
||||||
* @return {Boolean} whether a package has been tagged
|
* @return {Boolean} whether a package has been tagged
|
||||||
*/
|
*/
|
||||||
function tagVersion(data: Package, version: string, tag: StringValue) {
|
export function tagVersion(data: Package, version: string, tag: StringValue): boolean {
|
||||||
if (tag) {
|
if (tag && data[DIST_TAGS][tag] !== version && semver.parse(version, true)) {
|
||||||
if (data[DIST_TAGS][tag] !== version) {
|
// valid version - store
|
||||||
if (semver.parse(version, true)) {
|
data[DIST_TAGS][tag] = version;
|
||||||
// valid version - store
|
return true;
|
||||||
data[DIST_TAGS][tag] = version;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -221,7 +219,7 @@ function tagVersion(data: Package, version: string, tag: StringValue) {
|
|||||||
* Gets version from a package object taking into account semver weirdness.
|
* Gets version from a package object taking into account semver weirdness.
|
||||||
* @return {String} return the semantic version of a package
|
* @return {String} return the semantic version of a package
|
||||||
*/
|
*/
|
||||||
function getVersion(pkg: Package, version: any) {
|
export function getVersion(pkg: Package, version: any) {
|
||||||
// this condition must allow cast
|
// this condition must allow cast
|
||||||
if (pkg.versions[version] != null) {
|
if (pkg.versions[version] != null) {
|
||||||
return pkg.versions[version];
|
return pkg.versions[version];
|
||||||
@ -254,7 +252,7 @@ function getVersion(pkg: Package, version: any) {
|
|||||||
* @param {*} urlAddress the internet address definition
|
* @param {*} urlAddress the internet address definition
|
||||||
* @return {Object|Null} literal object that represent the address parsed
|
* @return {Object|Null} literal object that represent the address parsed
|
||||||
*/
|
*/
|
||||||
function parse_address(urlAddress: any) {
|
export function parseAddress(urlAddress: any) {
|
||||||
//
|
//
|
||||||
// TODO: refactor it to something more reasonable?
|
// TODO: refactor it to something more reasonable?
|
||||||
//
|
//
|
||||||
@ -287,7 +285,7 @@ function parse_address(urlAddress: any) {
|
|||||||
* Function filters out bad semver versions and sorts the array.
|
* Function filters out bad semver versions and sorts the array.
|
||||||
* @return {Array} sorted Array
|
* @return {Array} sorted Array
|
||||||
*/
|
*/
|
||||||
function semverSort(listVersions: Array<string>): string[] {
|
export function semverSort(listVersions: Array<string>): string[] {
|
||||||
return listVersions
|
return listVersions
|
||||||
.filter(function(x) {
|
.filter(function(x) {
|
||||||
if (!semver.parse(x, true)) {
|
if (!semver.parse(x, true)) {
|
||||||
@ -353,7 +351,7 @@ const parseIntervalTable = {
|
|||||||
* @param {*} interval
|
* @param {*} interval
|
||||||
* @return {Number}
|
* @return {Number}
|
||||||
*/
|
*/
|
||||||
function parseInterval(interval: any) {
|
export function parseInterval(interval: any): number {
|
||||||
if (typeof interval === 'number') {
|
if (typeof interval === 'number') {
|
||||||
return interval * 1000;
|
return interval * 1000;
|
||||||
}
|
}
|
||||||
@ -380,16 +378,16 @@ function parseInterval(interval: any) {
|
|||||||
* @param {*} req
|
* @param {*} req
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
function getWebProtocol(req: $Request) {
|
export function getWebProtocol(req: $Request): string {
|
||||||
return req.get('X-Forwarded-Proto') || req.protocol;
|
return req.get('X-Forwarded-Proto') || req.protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLatestVersion = function(pkgInfo: Package) {
|
export function getLatestVersion(pkgInfo: Package): string {
|
||||||
return pkgInfo[DIST_TAGS].latest;
|
return pkgInfo[DIST_TAGS].latest;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ErrorCode = {
|
export const ErrorCode = {
|
||||||
getConflict: (message: string = 'this package is already present') => {
|
getConflict: (message: string = API_ERROR.PACKAGE_EXIST) => {
|
||||||
return createError(HTTP_STATUS.CONFLICT, message);
|
return createError(HTTP_STATUS.CONFLICT, message);
|
||||||
},
|
},
|
||||||
getBadData: (customMessage?: string) => {
|
getBadData: (customMessage?: string) => {
|
||||||
@ -422,15 +420,16 @@ const ErrorCode = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseConfigFile = (configPath: string) =>
|
export function parseConfigFile (configPath: string): Object {
|
||||||
YAML.safeLoad(fs.readFileSync(configPath, 'utf8'));
|
return YAML.safeLoad(fs.readFileSync(configPath, CHARACTER_ENCODING.UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the path already exist.
|
* Check whether the path already exist.
|
||||||
* @param {String} path
|
* @param {String} path
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
function folder_exists(path: string) {
|
export function folderExists(path: string) {
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(path);
|
const stat = fs.statSync(path);
|
||||||
return stat.isDirectory();
|
return stat.isDirectory();
|
||||||
@ -444,7 +443,7 @@ function folder_exists(path: string) {
|
|||||||
* @param {String} path
|
* @param {String} path
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
function fileExists(path: string) {
|
export function fileExists(path: string): boolean {
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(path);
|
const stat = fs.statSync(path);
|
||||||
return stat.isFile();
|
return stat.isFile();
|
||||||
@ -453,7 +452,7 @@ function fileExists(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByName(packages: Array<any>): string[] {
|
export function sortByName(packages: Array<any>): string[] {
|
||||||
return packages.sort(function(a, b) {
|
return packages.sort(function(a, b) {
|
||||||
if (a.name < b.name) {
|
if (a.name < b.name) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -463,11 +462,11 @@ function sortByName(packages: Array<any>): string[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addScope(scope: string, packageName: string) {
|
export function addScope(scope: string, packageName: string) {
|
||||||
return `@${scope}/${packageName}`;
|
return `@${scope}/${packageName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteProperties(propertiesToDelete: Array<string>, objectItem: any) {
|
export function deleteProperties(propertiesToDelete: Array<string>, objectItem: any) {
|
||||||
_.forEach(propertiesToDelete, (property) => {
|
_.forEach(propertiesToDelete, (property) => {
|
||||||
delete objectItem[property];
|
delete objectItem[property];
|
||||||
});
|
});
|
||||||
@ -475,7 +474,7 @@ function deleteProperties(propertiesToDelete: Array<string>, objectItem: any) {
|
|||||||
return objectItem;
|
return objectItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGravatarSupport(pkgInfo: Object): Object {
|
export function addGravatarSupport(pkgInfo: Object): Object {
|
||||||
const pkgInfoCopy = {...pkgInfo};
|
const pkgInfoCopy = {...pkgInfo};
|
||||||
const author = _.get(pkgInfo, 'latest.author', null);
|
const author = _.get(pkgInfo, 'latest.author', null);
|
||||||
const contributors = _.get(pkgInfo, 'latest.contributors', []);
|
const contributors = _.get(pkgInfo, 'latest.contributors', []);
|
||||||
@ -519,7 +518,7 @@ function addGravatarSupport(pkgInfo: Object): Object {
|
|||||||
* @param {String} readme package readme
|
* @param {String} readme package readme
|
||||||
* @return {String} converted html template
|
* @return {String} converted html template
|
||||||
*/
|
*/
|
||||||
function parseReadme(packageName: string, readme: string): string {
|
export function parseReadme(packageName: string, readme: string): string {
|
||||||
if (readme) {
|
if (readme) {
|
||||||
return marked(readme);
|
return marked(readme);
|
||||||
}
|
}
|
||||||
@ -530,30 +529,6 @@ function parseReadme(packageName: string, readme: string): string {
|
|||||||
return marked('ERROR: No README data found!');
|
return marked('ERROR: No README data found!');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildToken(type: string, token: string) {
|
export function buildToken(type: string, token: string): string {
|
||||||
return `${_.capitalize(type)} ${token}`;
|
return `${_.capitalize(type)} ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
addGravatarSupport,
|
|
||||||
deleteProperties,
|
|
||||||
addScope,
|
|
||||||
sortByName,
|
|
||||||
folder_exists,
|
|
||||||
fileExists,
|
|
||||||
parseInterval,
|
|
||||||
semverSort,
|
|
||||||
parse_address,
|
|
||||||
getVersion,
|
|
||||||
tagVersion,
|
|
||||||
combineBaseUrl,
|
|
||||||
validate_metadata,
|
|
||||||
isObject,
|
|
||||||
validateName,
|
|
||||||
validate_package,
|
|
||||||
getWebProtocol,
|
|
||||||
getLatestVersion,
|
|
||||||
ErrorCode,
|
|
||||||
parseConfigFile,
|
|
||||||
parseReadme,
|
|
||||||
};
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {parse_address as parse} from '../../../src/lib/utils';
|
import {parseAddress as parse} from '../../../src/lib/utils';
|
||||||
import {DEFAULT_DOMAIN, DEFAULT_PORT} from '../../../src/lib/constants';
|
import {DEFAULT_DOMAIN, DEFAULT_PORT} from '../../../src/lib/constants';
|
||||||
|
|
||||||
describe('Parse listen address', () => {
|
describe('Parse listen address', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user