mirror of
https://github.com/javascript-obfuscator/javascript-obfuscator
synced 2026-08-09 12:42:29 +00:00
Added ThroughIdentifierReplacer for code separate between ScopeIdentifierReplacer and ScopeThroughIdentifiersTransformer
This commit is contained in:
@@ -56,6 +56,7 @@ export enum ServiceIdentifiers {
|
||||
IStringArrayScopeCallsWrapperNamesDataStorage = 'IStringArrayScopeCallsWrapperNamesDataStorage',
|
||||
IStringArrayStorage = 'IStringArrayStorage',
|
||||
IStringArrayStorageAnalyzer = 'IStringArrayStorageAnalyzer',
|
||||
IThroughIdentifierReplacer = 'IThroughIdentifierReplacer',
|
||||
IVisitedLexicalScopeNodesStackStorage = 'IVisitedLexicalScopeNodesStackStorage',
|
||||
Newable__ICustomNode = 'Newable<ICustomNode>',
|
||||
Newable__TControlFlowStorage = 'Newable<TControlFlowStorage>',
|
||||
|
||||
@@ -11,6 +11,8 @@ import { IdentifierReplacer } from '../../../node-transformers/rename-identifier
|
||||
import { LabeledStatementTransformer } from '../../../node-transformers/rename-identifiers-transformers/LabeledStatementTransformer';
|
||||
import { ScopeIdentifiersTransformer } from '../../../node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer';
|
||||
import { ScopeThroughIdentifiersTransformer } from '../../../node-transformers/rename-identifiers-transformers/ScopeThroughIdentifiersTransformer';
|
||||
import { ThroughIdentifierReplacer } from '../../../node-transformers/rename-identifiers-transformers/through-replacer/ThroughIdentifierReplacer';
|
||||
import { IThroughIdentifierReplacer } from '../../../interfaces/node-transformers/rename-identifiers-transformers/replacer/IThroughIdentifierReplacer';
|
||||
|
||||
export const renameIdentifiersTransformersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
|
||||
// rename identifiers transformers
|
||||
@@ -34,4 +36,8 @@ export const renameIdentifiersTransformersModule: interfaces.ContainerModule = n
|
||||
bind<IIdentifierReplacer>(ServiceIdentifiers.IIdentifierReplacer)
|
||||
.to(IdentifierReplacer)
|
||||
.inSingletonScope();
|
||||
|
||||
bind<IThroughIdentifierReplacer>(ServiceIdentifiers.IThroughIdentifierReplacer)
|
||||
.to(ThroughIdentifierReplacer)
|
||||
.inSingletonScope();
|
||||
});
|
||||
|
||||
-6
@@ -15,12 +15,6 @@ export interface IIdentifierReplacer {
|
||||
*/
|
||||
storeLocalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
*/
|
||||
storeThroughName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import * as ESTree from 'estree';
|
||||
|
||||
export interface IThroughIdentifierReplacer {
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
*/
|
||||
store (identifierNode: ESTree.Identifier): void;
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @returns {Identifier}
|
||||
*/
|
||||
replace (identifierNode: ESTree.Identifier): ESTree.Identifier;
|
||||
}
|
||||
+102
-19
@@ -1,58 +1,141 @@
|
||||
import { inject, injectable, } from 'inversify';
|
||||
import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
|
||||
|
||||
import * as eslintScope from 'eslint-scope';
|
||||
import * as ESTree from 'estree';
|
||||
|
||||
import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
|
||||
|
||||
import { IIdentifierNamesCacheStorage } from '../../interfaces/storages/identifier-names-cache/IIdentifierNamesCacheStorage';
|
||||
import { IIdentifierReplacer } from '../../interfaces/node-transformers/rename-identifiers-transformers/replacer/IIdentifierReplacer';
|
||||
import { IOptions } from '../../interfaces/options/IOptions';
|
||||
import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
|
||||
import { IScopeIdentifiersTraverser } from '../../interfaces/node/IScopeIdentifiersTraverser';
|
||||
import { IScopeThroughIdentifiersTraverserCallbackData } from '../../interfaces/node/IScopeThroughIdentifiersTraverserCallbackData';
|
||||
import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
|
||||
|
||||
import { ScopeThroughIdentifiersTransformer } from '../rename-identifiers-transformers/ScopeThroughIdentifiersTransformer';
|
||||
import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
|
||||
|
||||
import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
|
||||
import { NodeGuards } from '../../node/NodeGuards';
|
||||
|
||||
/**
|
||||
* Renames all scope through identifiers for Dead Code Injection
|
||||
*/
|
||||
@injectable()
|
||||
export class DeadCodeInjectionIdentifiersTransformer extends ScopeThroughIdentifiersTransformer {
|
||||
export class DeadCodeInjectionIdentifiersTransformer extends AbstractNodeTransformer {
|
||||
/**
|
||||
* @type {IIdentifierReplacer}
|
||||
*/
|
||||
private readonly identifierReplacer: IIdentifierReplacer;
|
||||
|
||||
/**
|
||||
* @type {IScopeIdentifiersTraverser}
|
||||
*/
|
||||
private readonly scopeIdentifiersTraverser: IScopeIdentifiersTraverser;
|
||||
|
||||
/**
|
||||
* @param {IIdentifierReplacer} identifierReplacer
|
||||
* @param {IRandomGenerator} randomGenerator
|
||||
* @param {IScopeIdentifiersTraverser} scopeIdentifiersTraverser
|
||||
* @param {IIdentifierNamesCacheStorage} identifierNamesCacheStorage
|
||||
* @param {IOptions} options
|
||||
* @param {IScopeIdentifiersTraverser} scopeIdentifiersTraverser
|
||||
*/
|
||||
public constructor (
|
||||
@inject(ServiceIdentifiers.IIdentifierReplacer) identifierReplacer: IIdentifierReplacer,
|
||||
@inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
|
||||
@inject(ServiceIdentifiers.IScopeIdentifiersTraverser)
|
||||
scopeIdentifiersTraverser: IScopeIdentifiersTraverser,
|
||||
@inject(ServiceIdentifiers.IIdentifierNamesCacheStorage)
|
||||
identifierNamesCacheStorage: IIdentifierNamesCacheStorage,
|
||||
@inject(ServiceIdentifiers.IOptions) options: IOptions
|
||||
@inject(ServiceIdentifiers.IOptions) options: IOptions,
|
||||
@inject(ServiceIdentifiers.IScopeIdentifiersTraverser) scopeIdentifiersTraverser: IScopeIdentifiersTraverser
|
||||
) {
|
||||
super(
|
||||
identifierReplacer,
|
||||
randomGenerator,
|
||||
scopeIdentifiersTraverser,
|
||||
identifierNamesCacheStorage,
|
||||
options
|
||||
super(randomGenerator, options);
|
||||
|
||||
this.identifierReplacer = identifierReplacer;
|
||||
this.scopeIdentifiersTraverser = scopeIdentifiersTraverser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeTransformationStage} nodeTransformationStage
|
||||
* @returns {IVisitor | null}
|
||||
*/
|
||||
public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
|
||||
switch (nodeTransformationStage) {
|
||||
case NodeTransformationStage.RenameIdentifiers:
|
||||
return {
|
||||
enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
|
||||
if (parentNode && NodeGuards.isProgramNode(node)) {
|
||||
return this.transformNode(node, parentNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VariableDeclaration} programNode
|
||||
* @param {NodeGuards} parentNode
|
||||
* @returns {NodeGuards}
|
||||
*/
|
||||
public transformNode (programNode: ESTree.Program, parentNode: ESTree.Node): ESTree.Node {
|
||||
this.scopeIdentifiersTraverser.traverseScopeThroughIdentifiers(
|
||||
programNode,
|
||||
parentNode,
|
||||
(data: IScopeThroughIdentifiersTraverserCallbackData) => {
|
||||
const {
|
||||
reference,
|
||||
variableLexicalScopeNode
|
||||
} = data;
|
||||
|
||||
this.transformScopeThroughIdentifiers(reference, variableLexicalScopeNode);
|
||||
}
|
||||
);
|
||||
|
||||
return programNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Reference} reference
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
*/
|
||||
private transformScopeThroughIdentifiers (
|
||||
reference: eslintScope.Reference,
|
||||
lexicalScopeNode: TNodeWithLexicalScope,
|
||||
): void {
|
||||
if (reference.resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const identifier: ESTree.Identifier = reference.identifier;
|
||||
|
||||
this.storeIdentifierName(identifier, lexicalScopeNode);
|
||||
this.replaceIdentifierName(identifier, lexicalScopeNode, reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent method, because we have to store only local scope names
|
||||
*
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
*/
|
||||
protected override storeIdentifierName (
|
||||
private storeIdentifierName (
|
||||
identifierNode: ESTree.Identifier,
|
||||
lexicalScopeNode: TNodeWithLexicalScope
|
||||
): void {
|
||||
this.identifierReplacer.storeLocalName(identifierNode, lexicalScopeNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
* @param {Variable} reference
|
||||
*/
|
||||
private replaceIdentifierName (
|
||||
identifierNode: ESTree.Identifier,
|
||||
lexicalScopeNode: TNodeWithLexicalScope,
|
||||
reference: eslintScope.Reference
|
||||
): void {
|
||||
const newIdentifier: ESTree.Identifier = this.identifierReplacer
|
||||
.replace(identifierNode, lexicalScopeNode);
|
||||
|
||||
// rename of identifier
|
||||
reference.identifier.name = newIdentifier.name;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-40
@@ -6,12 +6,11 @@ import * as ESTree from 'estree';
|
||||
|
||||
import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
|
||||
|
||||
import { IIdentifierNamesCacheStorage } from '../../interfaces/storages/identifier-names-cache/IIdentifierNamesCacheStorage';
|
||||
import { IIdentifierReplacer } from '../../interfaces/node-transformers/rename-identifiers-transformers/replacer/IIdentifierReplacer';
|
||||
import { IOptions } from '../../interfaces/options/IOptions';
|
||||
import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
|
||||
import { IScopeIdentifiersTraverser } from '../../interfaces/node/IScopeIdentifiersTraverser';
|
||||
import { IScopeThroughIdentifiersTraverserCallbackData } from '../../interfaces/node/IScopeThroughIdentifiersTraverserCallbackData';
|
||||
import { IThroughIdentifierReplacer } from '../../interfaces/node-transformers/rename-identifiers-transformers/replacer/IThroughIdentifierReplacer';
|
||||
import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
|
||||
|
||||
import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
|
||||
@@ -24,42 +23,32 @@ import { NodeGuards } from '../../node/NodeGuards';
|
||||
*/
|
||||
@injectable()
|
||||
export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer {
|
||||
/**
|
||||
* @type {IIdentifierNamesCacheStorage}
|
||||
*/
|
||||
protected readonly identifierNamesCacheStorage: IIdentifierNamesCacheStorage;
|
||||
|
||||
/**
|
||||
* @type {IIdentifierReplacer}
|
||||
*/
|
||||
protected readonly identifierReplacer: IIdentifierReplacer;
|
||||
|
||||
/**
|
||||
* @type {IScopeIdentifiersTraverser}
|
||||
*/
|
||||
protected readonly scopeIdentifiersTraverser: IScopeIdentifiersTraverser;
|
||||
|
||||
/**
|
||||
* @param {IIdentifierReplacer} identifierReplacer
|
||||
* @param {IRandomGenerator} randomGenerator
|
||||
* @type {IThroughIdentifierReplacer}
|
||||
*/
|
||||
protected readonly throughIdentifierReplacer: IThroughIdentifierReplacer;
|
||||
|
||||
/**
|
||||
* @param {IThroughIdentifierReplacer} throughIdentifierReplacer
|
||||
* @param {IScopeIdentifiersTraverser} scopeIdentifiersTraverser
|
||||
* @param {IIdentifierNamesCacheStorage} identifierNamesCacheStorage
|
||||
* @param {IRandomGenerator} randomGenerator
|
||||
* @param {IOptions} options
|
||||
*/
|
||||
public constructor (
|
||||
@inject(ServiceIdentifiers.IIdentifierReplacer) identifierReplacer: IIdentifierReplacer,
|
||||
@inject(ServiceIdentifiers.IThroughIdentifierReplacer) throughIdentifierReplacer: IThroughIdentifierReplacer,
|
||||
@inject(ServiceIdentifiers.IScopeIdentifiersTraverser) scopeIdentifiersTraverser: IScopeIdentifiersTraverser,
|
||||
@inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
|
||||
@inject(ServiceIdentifiers.IScopeIdentifiersTraverser)
|
||||
scopeIdentifiersTraverser: IScopeIdentifiersTraverser,
|
||||
@inject(ServiceIdentifiers.IIdentifierNamesCacheStorage)
|
||||
identifierNamesCacheStorage: IIdentifierNamesCacheStorage,
|
||||
@inject(ServiceIdentifiers.IOptions) options: IOptions
|
||||
) {
|
||||
super(randomGenerator, options);
|
||||
|
||||
this.identifierReplacer = identifierReplacer;
|
||||
this.throughIdentifierReplacer = throughIdentifierReplacer;
|
||||
this.scopeIdentifiersTraverser = scopeIdentifiersTraverser;
|
||||
this.identifierNamesCacheStorage = identifierNamesCacheStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,15 +82,13 @@ export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer
|
||||
parentNode,
|
||||
(data: IScopeThroughIdentifiersTraverserCallbackData) => {
|
||||
const {
|
||||
isGlobalDeclaration,
|
||||
reference,
|
||||
variableLexicalScopeNode
|
||||
} = data;
|
||||
|
||||
this.transformScopeThroughIdentifiers(
|
||||
reference,
|
||||
variableLexicalScopeNode,
|
||||
isGlobalDeclaration
|
||||
variableLexicalScopeNode
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -112,12 +99,10 @@ export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer
|
||||
/**
|
||||
* @param {Reference} reference
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
* @param {boolean} isGlobalDeclaration
|
||||
*/
|
||||
protected transformScopeThroughIdentifiers (
|
||||
reference: eslintScope.Reference,
|
||||
lexicalScopeNode: TNodeWithLexicalScope,
|
||||
isGlobalDeclaration: boolean
|
||||
lexicalScopeNode: TNodeWithLexicalScope
|
||||
): void {
|
||||
if (reference.resolved) {
|
||||
return;
|
||||
@@ -125,35 +110,28 @@ export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer
|
||||
|
||||
const identifier: ESTree.Identifier = reference.identifier;
|
||||
|
||||
this.storeIdentifierName(identifier, lexicalScopeNode, isGlobalDeclaration);
|
||||
this.replaceIdentifierName(identifier, lexicalScopeNode, reference);
|
||||
this.storeIdentifierName(identifier);
|
||||
this.replaceIdentifierName(identifier, reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
* @param {boolean} isGlobalDeclaration
|
||||
*/
|
||||
protected storeIdentifierName (
|
||||
identifierNode: ESTree.Identifier,
|
||||
lexicalScopeNode: TNodeWithLexicalScope,
|
||||
isGlobalDeclaration: boolean
|
||||
identifierNode: ESTree.Identifier
|
||||
): void {
|
||||
this.identifierReplacer.storeThroughName(identifierNode, lexicalScopeNode);
|
||||
this.throughIdentifierReplacer.store(identifierNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
* @param {Variable} reference
|
||||
*/
|
||||
protected replaceIdentifierName (
|
||||
identifierNode: ESTree.Identifier,
|
||||
lexicalScopeNode: TNodeWithLexicalScope,
|
||||
reference: eslintScope.Reference
|
||||
): void {
|
||||
const newIdentifier: ESTree.Identifier = this.identifierReplacer
|
||||
.replace(identifierNode, lexicalScopeNode);
|
||||
const newIdentifier: ESTree.Identifier = this.throughIdentifierReplacer.replace(identifierNode);
|
||||
|
||||
// rename of identifier
|
||||
reference.identifier.name = newIdentifier.name;
|
||||
|
||||
@@ -105,37 +105,6 @@ export class IdentifierReplacer implements IIdentifierReplacer {
|
||||
namesMap.set(identifierName, newIdentifierName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store identifier node `name` of `through` identifiers as key in map with value from identifier names cache.
|
||||
* Reserved name will be ignored.
|
||||
*
|
||||
* @param {Node} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
*/
|
||||
public storeThroughName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void {
|
||||
const identifierName: string = identifierNode.name;
|
||||
|
||||
if (this.isReservedName(identifierName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIdentifierName: string | null = this.identifierNamesCacheStorage.get(identifierName) ?? null;
|
||||
|
||||
if (!newIdentifierName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.blockScopesMap.has(lexicalScopeNode)) {
|
||||
this.blockScopesMap.set(lexicalScopeNode, new Map());
|
||||
}
|
||||
|
||||
const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
|
||||
|
||||
namesMap.set(identifierName, newIdentifierName);
|
||||
|
||||
this.identifierNamesCacheStorage.set(identifierName, newIdentifierName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @param {TNodeWithLexicalScope} lexicalScopeNode
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { inject, injectable, } from 'inversify';
|
||||
import { ServiceIdentifiers } from '../../../container/ServiceIdentifiers';
|
||||
|
||||
import * as ESTree from 'estree';
|
||||
|
||||
import { IIdentifierNamesCacheStorage } from '../../../interfaces/storages/identifier-names-cache/IIdentifierNamesCacheStorage';
|
||||
import { IOptions } from '../../../interfaces/options/IOptions';
|
||||
import { IThroughIdentifierReplacer } from '../../../interfaces/node-transformers/rename-identifiers-transformers/replacer/IThroughIdentifierReplacer';
|
||||
|
||||
import { NodeFactory } from '../../../node/NodeFactory';
|
||||
|
||||
@injectable()
|
||||
export class ThroughIdentifierReplacer implements IThroughIdentifierReplacer {
|
||||
/**
|
||||
* @type {IIdentifierNamesCacheStorage}
|
||||
*/
|
||||
private readonly identifierNamesCacheStorage: IIdentifierNamesCacheStorage;
|
||||
|
||||
/**
|
||||
* @type {IOptions}
|
||||
*/
|
||||
private readonly options: IOptions;
|
||||
|
||||
/**
|
||||
* @param {IIdentifierNamesCacheStorage} identifierNamesCacheStorage
|
||||
* @param {IOptions} options
|
||||
*/
|
||||
public constructor (
|
||||
@inject(ServiceIdentifiers.IIdentifierNamesCacheStorage)
|
||||
identifierNamesCacheStorage: IIdentifierNamesCacheStorage,
|
||||
@inject(ServiceIdentifiers.IOptions) options: IOptions
|
||||
) {
|
||||
this.identifierNamesCacheStorage = identifierNamesCacheStorage;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store identifier node `name` of `through` identifiers as key in map with value from identifier names cache.
|
||||
* Reserved name will be ignored.
|
||||
*
|
||||
* @param {Node} identifierNode
|
||||
*/
|
||||
public store (identifierNode: ESTree.Identifier): void {
|
||||
const identifierName: string = identifierNode.name;
|
||||
|
||||
if (this.isReservedName(identifierName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIdentifierName: string | null = this.identifierNamesCacheStorage.get(identifierName) ?? null;
|
||||
|
||||
if (!newIdentifierName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.identifierNamesCacheStorage.set(identifierName, newIdentifierName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} identifierNode
|
||||
* @returns {Identifier}
|
||||
*/
|
||||
public replace (identifierNode: ESTree.Identifier): ESTree.Identifier {
|
||||
const identifierName: string = this.identifierNamesCacheStorage.get(identifierNode.name)
|
||||
?? identifierNode.name;
|
||||
|
||||
return NodeFactory.identifierNode(identifierName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isReservedName (name: string): boolean {
|
||||
if (!this.options.reservedNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.options.reservedNames
|
||||
.some((reservedName: string) => {
|
||||
return new RegExp(reservedName, 'g').exec(name) !== null;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user