Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5723aab03 | |||
| f51f5dd19f | |||
| aae5f0789d | |||
| 586fe3950c | |||
| 241c907c65 | |||
| aab4244d84 | |||
| 767a3a987c | |||
| a2924d872a | |||
| 54baf21490 | |||
| 5a3790a7d3 | |||
| 236e099b81 |
@@ -62,7 +62,8 @@ import { resolve } from 'node:path';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// On Windows, the path must be absolute
|
||||
chromiumArgs: [`--user-data-dir="${resolve('.wxt/chrome-data')}"`],
|
||||
chromiumProfile: resolve('.wxt/chrome-data'),
|
||||
keepProfileChanges: true,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -12,15 +12,18 @@ When using `browser.scripting.executeScript`, you can execute content scripts or
|
||||
// entrypoints/background.ts
|
||||
const res = await browser.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['injected.js'],
|
||||
files: ['content-scripts/example.js'],
|
||||
});
|
||||
console.log(res); // "Hello John!"
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/injected.js
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('Script was injected!');
|
||||
return 'Hello John!';
|
||||
// entrypoints/example.content.ts
|
||||
export default defineContentScript({
|
||||
registration: 'runtime',
|
||||
main(ctx) {
|
||||
console.log('Script was executed!');
|
||||
return 'Hello John!';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
+9
-2
@@ -2,11 +2,18 @@
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPointStrategy": "packages",
|
||||
"entryPoints": ["../packages/wxt"],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"plugin": [
|
||||
"typedoc-plugin-markdown",
|
||||
"typedoc-vitepress-theme",
|
||||
"typedoc-plugin-frontmatter"
|
||||
],
|
||||
"out": "./api/reference",
|
||||
"githubPages": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none"
|
||||
"readme": "none",
|
||||
"frontmatterGlobals": {
|
||||
"editLink": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "4.15.7",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.1.0",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.6.3",
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.17
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.16...wxt-v0.19.17)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- New `server:created`, `server:started`, and `server:closed` hooks ([#1179](https://github.com/wxt-dev/wxt/pull/1179))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Re-initialize WXT modules correctly during development ([#1176](https://github.com/wxt-dev/wxt/pull/1176))
|
||||
- ESLint config being generated when ESLint is not installed. ([#1198](https://github.com/wxt-dev/wxt/pull/1198))
|
||||
- Validate entrypoints after hooks have modified them ([54baf21](https://github.com/wxt-dev/wxt/commit/54baf21))
|
||||
- Revert "Validate entrypoints after hooks have modified them (54baf21)" ([a2924d8](https://github.com/wxt-dev/wxt/commit/a2924d8))
|
||||
- Update `vite` dependency range to support v6 ([#1215](https://github.com/wxt-dev/wxt/pull/1215))
|
||||
- Automatically convert MV3 `content_security_policy` to MV2 ([#1168](https://github.com/wxt-dev/wxt/pull/1168))
|
||||
- Correctly remove child elements with integrated UI remove ([#1219](https://github.com/wxt-dev/wxt/pull/1219))
|
||||
- Make content script `matches` optional ([#1220](https://github.com/wxt-dev/wxt/pull/1220))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix analyze typo in type ([#1187](https://github.com/wxt-dev/wxt/pull/1187))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- 1natsu ([@1natsu172](http://github.com/1natsu172))
|
||||
- Nishu <nnmurmu@enacton.com>
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.19.16
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.15...wxt-v0.19.16)
|
||||
|
||||
@@ -152,6 +152,40 @@ describe('Auto Imports', () => {
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('"enabled: false" should NOT output an ESlint config file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT output an ESlint config file by default', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow customizing the output', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.16",
|
||||
"version": "0.19.17",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -122,7 +122,7 @@
|
||||
"publish-browser-extension": "^2.2.2",
|
||||
"scule": "^1.3.0",
|
||||
"unimport": "^3.13.1",
|
||||
"vite": "^5.4.11",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "^2.1.4",
|
||||
"web-ext-run": "^0.2.1",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
|
||||
@@ -34,6 +34,7 @@ export function createIntegratedUi<TMounted>(
|
||||
};
|
||||
const remove = () => {
|
||||
options.onRemove?.(mounted);
|
||||
wrapper.replaceChildren();
|
||||
wrapper.remove();
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
@@ -392,10 +392,11 @@ async function getUnimportEslintOptions(
|
||||
case 'auto':
|
||||
const version = await getEslintVersion();
|
||||
let major = parseInt(version[0]);
|
||||
if (isNaN(major)) eslintEnabled = false;
|
||||
if (major <= 8) eslintEnabled = 8;
|
||||
else if (major >= 9) eslintEnabled = 9;
|
||||
// NaN
|
||||
else eslintEnabled = 8;
|
||||
else eslintEnabled = false;
|
||||
break;
|
||||
case true:
|
||||
eslintEnabled = 8;
|
||||
|
||||
@@ -1581,6 +1581,41 @@ describe('Manifest Utils', () => {
|
||||
permissions: ['tabs', 'scripting'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert MV3 CSP object to MV2 CSP string with localhost for MV2', async () => {
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const inputCsp =
|
||||
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';";
|
||||
const expectedCsp =
|
||||
"script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';";
|
||||
|
||||
// Setup WXT for Firefox and serve command
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'firefox',
|
||||
command: 'serve',
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
content_security_policy: {
|
||||
extension_pages: inputCsp,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: fakeWxtDevServer({
|
||||
port: 3000,
|
||||
hostname: 'localhost',
|
||||
origin: 'http://localhost:3000',
|
||||
}),
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual.content_security_policy).toEqual(expectedCsp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export function mapWxtOptionsToContentScript(
|
||||
css: string[] | undefined,
|
||||
): Manifest.ContentScript {
|
||||
return {
|
||||
matches: options.matches,
|
||||
matches: options.matches ?? [],
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
|
||||
@@ -117,11 +117,12 @@ export async function generateManifest(
|
||||
if (wxt.config.manifestVersion === 2) {
|
||||
convertWebAccessibleResourcesToMv2(manifest);
|
||||
convertActionToMv2(manifest);
|
||||
convertCspToMv2(manifest);
|
||||
moveHostPermissionsToPermissions(manifest);
|
||||
}
|
||||
|
||||
if (wxt.config.manifestVersion === 3) {
|
||||
validateMv3WebAccessbileResources(manifest);
|
||||
validateMv3WebAccessibleResources(manifest);
|
||||
}
|
||||
|
||||
stripKeys(manifest);
|
||||
@@ -143,7 +144,7 @@ export async function generateManifest(
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes suffixes from the version, like X.Y.Z-alpha1 (which brosers don't allow), so it's a
|
||||
* Removes suffixes from the version, like X.Y.Z-alpha1 (which browsers don't allow), so it's a
|
||||
* simple version number, like X or X.Y or X.Y.Z, which browsers allow.
|
||||
*/
|
||||
function simplifyVersion(versionName: string): string {
|
||||
@@ -362,7 +363,7 @@ function addEntrypoints(
|
||||
// at runtime
|
||||
if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {
|
||||
contentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
script.options.matches?.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
@@ -405,7 +406,7 @@ function addEntrypoints(
|
||||
);
|
||||
}
|
||||
runtimeContentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
script.options.matches?.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
@@ -467,34 +468,28 @@ function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
|
||||
}
|
||||
|
||||
const extensionPagesCsp = new ContentSecurityPolicy(
|
||||
manifest.manifest_version === 3
|
||||
? // @ts-expect-error: extension_pages is not typed
|
||||
(manifest.content_security_policy?.extension_pages ??
|
||||
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';") // default extension_pages CSP for MV3
|
||||
: (manifest.content_security_policy ??
|
||||
"script-src 'self'; object-src 'self';"), // default CSP for MV2
|
||||
// @ts-expect-error: extension_pages exists, we convert MV2 CSPs to this earlier in the process
|
||||
manifest.content_security_policy?.extension_pages ??
|
||||
(manifest.manifest_version === 3
|
||||
? DEFAULT_MV3_EXTENSION_PAGES_CSP
|
||||
: DEFAULT_MV2_CSP),
|
||||
);
|
||||
const sandboxCsp = new ContentSecurityPolicy(
|
||||
// @ts-expect-error: sandbox is not typed
|
||||
manifest.content_security_policy?.sandbox ??
|
||||
"sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';", // default sandbox CSP for MV3
|
||||
manifest.content_security_policy?.sandbox ?? DEFAULT_MV3_SANDBOX_CSP,
|
||||
);
|
||||
|
||||
if (wxt.server) {
|
||||
if (wxt.config.command === 'serve') {
|
||||
extensionPagesCsp.add('script-src', allowedCsp);
|
||||
sandboxCsp.add('script-src', allowedCsp);
|
||||
}
|
||||
|
||||
if (manifest.manifest_version === 3) {
|
||||
manifest.content_security_policy ??= {};
|
||||
// @ts-expect-error: extension_pages is not typed
|
||||
manifest.content_security_policy.extension_pages =
|
||||
extensionPagesCsp.toString();
|
||||
// @ts-expect-error: sandbox is not typed
|
||||
manifest.content_security_policy.sandbox = sandboxCsp.toString();
|
||||
} else {
|
||||
manifest.content_security_policy = extensionPagesCsp.toString();
|
||||
}
|
||||
manifest.content_security_policy ??= {};
|
||||
// @ts-expect-error: extension_pages is not typed
|
||||
manifest.content_security_policy.extension_pages =
|
||||
extensionPagesCsp.toString();
|
||||
// @ts-expect-error: sandbox is not typed
|
||||
manifest.content_security_policy.sandbox = sandboxCsp.toString();
|
||||
}
|
||||
|
||||
function addDevModePermissions(manifest: Manifest.WebExtensionManifest) {
|
||||
@@ -552,7 +547,7 @@ export function getContentScriptCssWebAccessibleResources(
|
||||
|
||||
resources.push({
|
||||
resources: [cssFile],
|
||||
matches: script.options.matches.map((matchPattern) =>
|
||||
matches: script.options.matches?.map((matchPattern) =>
|
||||
stripPathFromMatchPattern(matchPattern),
|
||||
),
|
||||
});
|
||||
@@ -613,7 +608,7 @@ export function stripPathFromMatchPattern(pattern: string) {
|
||||
/**
|
||||
* Converts all MV3 web accessible resources to their MV2 forms. MV3 web accessible resources are
|
||||
* generated in this file, and may be defined by the user in their manifest. In both cases, when
|
||||
* targetting MV2, automatically convert their definitions down to the basic MV2 array.
|
||||
* targeting MV2, automatically convert their definitions down to the basic MV2 array.
|
||||
*/
|
||||
export function convertWebAccessibleResourcesToMv2(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
@@ -652,10 +647,21 @@ function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void {
|
||||
manifest.browser_action = manifest.action;
|
||||
}
|
||||
|
||||
function convertCspToMv2(manifest: Manifest.WebExtensionManifest): void {
|
||||
if (
|
||||
typeof manifest.content_security_policy === 'string' ||
|
||||
manifest.content_security_policy?.extension_pages == null
|
||||
)
|
||||
return;
|
||||
|
||||
manifest.content_security_policy =
|
||||
manifest.content_security_policy.extension_pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure all resources are in MV3 format. If not, add a wanring
|
||||
*/
|
||||
export function validateMv3WebAccessbileResources(
|
||||
export function validateMv3WebAccessibleResources(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
): void {
|
||||
if (manifest.web_accessible_resources == null) return;
|
||||
@@ -718,3 +724,9 @@ const mv3OnlyKeys = [
|
||||
'side_panel',
|
||||
];
|
||||
const firefoxMv3OnlyKeys = ['host_permissions'];
|
||||
|
||||
const DEFAULT_MV3_EXTENSION_PAGES_CSP =
|
||||
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';";
|
||||
const DEFAULT_MV3_SANDBOX_CSP =
|
||||
"sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';";
|
||||
const DEFAULT_MV2_CSP = "script-src 'self'; object-src 'self';";
|
||||
|
||||
@@ -588,7 +588,7 @@ export interface BackgroundEntrypointOptions extends BaseEntrypointOptions {
|
||||
|
||||
export interface BaseContentScriptEntrypointOptions
|
||||
extends BaseEntrypointOptions {
|
||||
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
|
||||
matches?: PerBrowserOption<Manifest.ContentScript['matches']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "documentIdle"
|
||||
|
||||
Generated
+18
-1
@@ -58,6 +58,9 @@ importers:
|
||||
typedoc:
|
||||
specifier: ^0.25.4
|
||||
version: 0.25.4(typescript@5.6.3)
|
||||
typedoc-plugin-frontmatter:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(typedoc-plugin-markdown@4.0.0-next.23(typedoc@0.25.4(typescript@5.6.3)))
|
||||
typedoc-plugin-markdown:
|
||||
specifier: 4.0.0-next.23
|
||||
version: 4.0.0-next.23(typedoc@0.25.4(typescript@5.6.3))
|
||||
@@ -451,7 +454,7 @@ importers:
|
||||
specifier: ^3.13.1
|
||||
version: 3.13.1(rollup@4.24.0)(webpack-sources@3.2.3)
|
||||
vite:
|
||||
specifier: ^5.4.11
|
||||
specifier: ^5.0.0 || ^6.0.0
|
||||
version: 5.4.11(@types/node@20.17.6)(sass@1.80.7)
|
||||
vite-node:
|
||||
specifier: ^2.1.4
|
||||
@@ -3041,10 +3044,12 @@ packages:
|
||||
|
||||
glob@6.0.4:
|
||||
resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
global-dirs@3.0.1:
|
||||
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
|
||||
@@ -3174,6 +3179,7 @@ packages:
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
@@ -4283,6 +4289,7 @@ packages:
|
||||
|
||||
rimraf@2.4.5:
|
||||
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-dts@6.1.1:
|
||||
@@ -4678,6 +4685,11 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typedoc-plugin-frontmatter@1.1.0:
|
||||
resolution: {integrity: sha512-4PW4V2xDY2hw+fEWmg7g6FBCIWZdiEE+tzjJ5K4JhurvJ0t0Vp0IE/0nuHGGIZVtV5WxPIed+GpiH1uZrpDquQ==}
|
||||
peerDependencies:
|
||||
typedoc-plugin-markdown: '>=4.3.0'
|
||||
|
||||
typedoc-plugin-markdown@4.0.0-next.23:
|
||||
resolution: {integrity: sha512-bKD0LnrQxUTbuDRiJfCWYLlzyERuskWldJ5eWKKuNY38xBOyBhSN8P+vD5KxlJoAifTOm76MK58sNrl4wgOoWg==}
|
||||
peerDependencies:
|
||||
@@ -9290,6 +9302,11 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typedoc-plugin-frontmatter@1.1.0(typedoc-plugin-markdown@4.0.0-next.23(typedoc@0.25.4(typescript@5.6.3))):
|
||||
dependencies:
|
||||
typedoc-plugin-markdown: 4.0.0-next.23(typedoc@0.25.4(typescript@5.6.3))
|
||||
yaml: 2.5.1
|
||||
|
||||
typedoc-plugin-markdown@4.0.0-next.23(typedoc@0.25.4(typescript@5.6.3)):
|
||||
dependencies:
|
||||
typedoc: 0.25.4(typescript@5.6.3)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@wxt-dev/module-vue": "^1.0.1",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript": "5.6.3",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user