v2.2.2: Fix bootstrap permission denied - chmod after install, shell fallback, fixPermissions plugin method
This commit is contained in:
@@ -631,6 +631,14 @@ data: [DONE]
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v2.2.2 (2026-05-19)
|
||||||
|
- **Permission Fix** — `chmod -R 755` on all bootstrap binaries after extraction (fixes "Permission denied" on bash)
|
||||||
|
- **Shell Auto-Fallback** — if bash fails with permission error, auto-falls back to `/system/bin/sh`
|
||||||
|
- **fixPermissions Plugin** — new `BootstrapPlugin.fixPermissions()` for JS to call after install
|
||||||
|
- `ensureBuildTools()` uses full paths to `pkg`/`apt` with `chmod 755` before execution
|
||||||
|
- `setPermissionsRecursive()` ensures all nested binaries get execute permission
|
||||||
|
- Shell auto-refreshes path on each execute call, re-chmods if needed
|
||||||
|
|
||||||
### v2.2.1 (2026-05-19)
|
### v2.2.1 (2026-05-19)
|
||||||
- **Auto-Install Build Tools** — `aapt2`, `ecj`, `d8`, `apksigner` auto-installed via `pkg` with full paths and retry logic
|
- **Auto-Install Build Tools** — `aapt2`, `ecj`, `d8`, `apksigner` auto-installed via `pkg` with full paths and retry logic
|
||||||
- **Dev Tools Banner** — yellow warning banner on Coding/Agentic mode if tools missing, one-tap Install button
|
- **Dev Tools Banner** — yellow warning banner on Coding/Agentic mode if tools missing, one-tap Install button
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "ai.z.chat"
|
applicationId "ai.z.chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 14
|
versionCode 15
|
||||||
versionName "2.2.1"
|
versionName "2.2.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
sendProgress(call, "Setting permissions...", 85);
|
sendProgress(call, "Setting permissions...", 85);
|
||||||
setPermissions(new File(stagingDir, "bin"));
|
setPermissions(new File(stagingDir, "bin"));
|
||||||
setPermissions(new File(stagingDir, "libexec"));
|
setPermissions(new File(stagingDir, "libexec"));
|
||||||
|
setPermissionsRecursive(new File(stagingDir, "bin"));
|
||||||
|
setPermissionsRecursive(new File(stagingDir, "libexec"));
|
||||||
|
|
||||||
new File(homeDir).mkdirs();
|
new File(homeDir).mkdirs();
|
||||||
new File(prefixDir + "/tmp").mkdirs();
|
new File(prefixDir + "/tmp").mkdirs();
|
||||||
@@ -147,6 +149,15 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
throw new RuntimeException("Failed to rename staging to prefix");
|
throw new RuntimeException("Failed to rename staging to prefix");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/bin"}).waitFor();
|
||||||
|
rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/libexec"}).waitFor();
|
||||||
|
rt.exec(new String[]{"chmod", "755", prefixDir + "/lib"}).waitFor();
|
||||||
|
} catch (Exception ce) {
|
||||||
|
Log.w(TAG, "chmod after rename failed: " + ce.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
writeEnvFile();
|
writeEnvFile();
|
||||||
writeProfileFile();
|
writeProfileFile();
|
||||||
|
|
||||||
@@ -316,6 +327,37 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setPermissionsRecursive(File dir) {
|
||||||
|
if (!dir.exists() || !dir.isDirectory()) return;
|
||||||
|
File[] children = dir.listFiles();
|
||||||
|
if (children == null) return;
|
||||||
|
for (File f : children) {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
f.setExecutable(true, false);
|
||||||
|
setPermissionsRecursive(f);
|
||||||
|
} else if (f.isFile()) {
|
||||||
|
f.setExecutable(true, false);
|
||||||
|
f.setReadable(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void fixPermissions(PluginCall call) {
|
||||||
|
try {
|
||||||
|
File binDir = new File(prefixDir + "/bin");
|
||||||
|
if (binDir.exists()) {
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/bin"}).waitFor();
|
||||||
|
rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/libexec"}).waitFor();
|
||||||
|
setPermissionsRecursive(binDir);
|
||||||
|
}
|
||||||
|
call.resolve(new JSObject().put("fixed", true));
|
||||||
|
} catch (Exception e) {
|
||||||
|
call.reject("Fix permissions failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void writeEnvFile() {
|
private void writeEnvFile() {
|
||||||
try {
|
try {
|
||||||
File envFile = new File(prefixDir + "/etc/termux.env");
|
File envFile = new File(prefixDir + "/etc/termux.env");
|
||||||
|
|||||||
@@ -44,11 +44,27 @@ public class ShellPlugin extends Plugin {
|
|||||||
new File(homeDir + "/bin").mkdirs();
|
new File(homeDir + "/bin").mkdirs();
|
||||||
new File(homeDir + "/tmp").mkdirs();
|
new File(homeDir + "/tmp").mkdirs();
|
||||||
|
|
||||||
|
refreshShell();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshShell() {
|
||||||
File bash = new File(prefixDir + "/bin/bash");
|
File bash = new File(prefixDir + "/bin/bash");
|
||||||
File sh = new File(prefixDir + "/bin/sh");
|
File sh = new File(prefixDir + "/bin/sh");
|
||||||
if (bash.exists()) {
|
if (bash.exists()) {
|
||||||
|
if (!bash.canExecute()) {
|
||||||
|
bash.setExecutable(true, false);
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().exec(new String[]{"chmod", "755", bash.getAbsolutePath()}).waitFor();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
shellPath = bash.getAbsolutePath();
|
shellPath = bash.getAbsolutePath();
|
||||||
} else if (sh.exists()) {
|
} else if (sh.exists()) {
|
||||||
|
if (!sh.canExecute()) {
|
||||||
|
sh.setExecutable(true, false);
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().exec(new String[]{"chmod", "755", sh.getAbsolutePath()}).waitFor();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
shellPath = sh.getAbsolutePath();
|
shellPath = sh.getAbsolutePath();
|
||||||
} else {
|
} else {
|
||||||
shellPath = "/system/bin/sh";
|
shellPath = "/system/bin/sh";
|
||||||
@@ -70,6 +86,7 @@ public class ShellPlugin extends Plugin {
|
|||||||
if (cwd == null || cwd.isEmpty()) cwd = homeDir;
|
if (cwd == null || cwd.isEmpty()) cwd = homeDir;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
refreshShell();
|
||||||
String[] env = buildEnv();
|
String[] env = buildEnv();
|
||||||
String shell = shellPath != null ? shellPath : "sh";
|
String shell = shellPath != null ? shellPath : "sh";
|
||||||
ProcessBuilder pb = new ProcessBuilder(shell, "-c", command);
|
ProcessBuilder pb = new ProcessBuilder(shell, "-c", command);
|
||||||
@@ -77,7 +94,21 @@ public class ShellPlugin extends Plugin {
|
|||||||
pb.environment().putAll(toEnvMap(env));
|
pb.environment().putAll(toEnvMap(env));
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
|
|
||||||
Process process = pb.start();
|
Process process;
|
||||||
|
try {
|
||||||
|
process = pb.start();
|
||||||
|
} catch (java.io.IOException ioe) {
|
||||||
|
if (shell != null && !shell.equals("/system/bin/sh")) {
|
||||||
|
Log.w(TAG, "Shell " + shell + " failed, falling back to /system/bin/sh: " + ioe.getMessage());
|
||||||
|
pb = new ProcessBuilder("/system/bin/sh", "-c", command);
|
||||||
|
pb.directory(new File(cwd));
|
||||||
|
pb.environment().putAll(toEnvMap(env));
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
process = pb.start();
|
||||||
|
} else {
|
||||||
|
throw ioe;
|
||||||
|
}
|
||||||
|
}
|
||||||
String processId = String.valueOf(System.currentTimeMillis());
|
String processId = String.valueOf(System.currentTimeMillis());
|
||||||
activeProcesses.put(processId, process);
|
activeProcesses.put(processId, process);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -327,13 +327,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
<p class="about-text">Z.AI Chat v2.2.1</p>
|
<p class="about-text">Z.AI Chat v2.2.2</p>
|
||||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||||
<p class="about-text">Compatible with Android 15/16</p>
|
<p class="about-text">Compatible with Android 15/16</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
<ul class="changelog-list">
|
<ul class="changelog-list">
|
||||||
|
<li>
|
||||||
|
<span class="changelog-version">v2.2.2</span>
|
||||||
|
<span class="changelog-date">2026-05-19</span>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Permission Fix</strong> — chmod 755 on all bootstrap binaries after extraction (fixes "Permission denied")</li>
|
||||||
|
<li><strong>Shell Auto-Fallback</strong> — if bash permission denied, auto-falls back to system sh</li>
|
||||||
|
<li><strong>fixPermissions Plugin</strong> — new BootstrapPlugin.fixPermissions() callable from JS</li>
|
||||||
|
<li>ensureBuildTools uses full paths to pkg/apt with chmod before execution</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="changelog-version">v2.2.1</span>
|
<span class="changelog-version">v2.2.1</span>
|
||||||
<span class="changelog-date">2026-05-19</span>
|
<span class="changelog-date">2026-05-19</span>
|
||||||
|
|||||||
@@ -1911,6 +1911,8 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||||
|
|
||||||
if (Shell) {
|
if (Shell) {
|
||||||
var env = await Shell.getEnv();
|
var env = await Shell.getEnv();
|
||||||
termState.homeDir = env.HOME;
|
termState.homeDir = env.HOME;
|
||||||
@@ -1919,31 +1921,41 @@
|
|||||||
termState.cwd = env.CWD || env.HOME;
|
termState.cwd = env.CWD || env.HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') + '/usr' : '';
|
var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : termState.homeDir || '';
|
||||||
var pkgBin = prefix + '/bin/pkg';
|
var prefixUsr = prefix + '/usr';
|
||||||
var aptBin = prefix + '/bin/apt';
|
var pkgBin = prefixUsr + '/bin/pkg';
|
||||||
|
var aptBin = prefixUsr + '/bin/apt';
|
||||||
|
|
||||||
var pkgExists = await shellExec('test -x "' + pkgBin + '"', termState.homeDir, false);
|
var pkgTest = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false);
|
||||||
var aptExists = await shellExec('test -x "' + aptBin + '"', termState.homeDir, false);
|
var aptTest = await shellExec('test -f "' + aptBin + '"', termState.homeDir, false);
|
||||||
|
|
||||||
if (pkgExists.exitCode !== 0 && aptExists.exitCode !== 0) {
|
if (pkgTest.exitCode !== 0 && aptTest.exitCode !== 0) {
|
||||||
termPrint('[!] No package manager found. Install Termux bootstrap first.', 'err');
|
var bsStatus;
|
||||||
|
try { bsStatus = await Bootstrap.getStatus(); } catch(e) { bsStatus = { installed: false }; }
|
||||||
|
if (!bsStatus.installed) {
|
||||||
|
termPrint('[!] Termux bootstrap not installed yet.', 'err');
|
||||||
|
} else {
|
||||||
|
termPrint('[!] Package manager not found at ' + pkgBin, 'err');
|
||||||
|
}
|
||||||
termState.devToolsInstalled = false;
|
termState.devToolsInstalled = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
termPrint('[*] Installing build tools (aapt2, ecj, dx, apksigner)...', 'info');
|
termPrint('[*] Installing build tools (aapt2, ecj, dx, apksigner)...', 'info');
|
||||||
termPrint('[*] This may take a few minutes on first run...', 'info');
|
termPrint('[*] This may take a few minutes...', 'info');
|
||||||
showStatusToast('Installing build tools...', 'info');
|
showStatusToast('Installing build tools...', 'info');
|
||||||
|
|
||||||
var installCmd;
|
var methods = [];
|
||||||
if (pkgExists.exitCode === 0) {
|
if (pkgTest.exitCode === 0) {
|
||||||
installCmd = pkgBin + ' update -y 2>&1 && ' + pkgBin + ' install -y aapt2 ecj dx apksigner 2>&1';
|
methods.push('chmod 755 "' + pkgBin + '" 2>/dev/null; "' + pkgBin + '" update -y 2>&1 && "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1');
|
||||||
} else {
|
}
|
||||||
installCmd = aptBin + ' update -y 2>&1 && ' + aptBin + ' install -y aapt2 ecj dx apksigner 2>&1';
|
if (aptTest.exitCode === 0) {
|
||||||
|
methods.push('chmod 755 "' + aptBin + '" 2>/dev/null; "' + aptBin + '" update -y 2>&1 && "' + aptBin + '" install -y aapt2 ecj dx apksigner 2>&1');
|
||||||
}
|
}
|
||||||
|
|
||||||
var installResult = await shellExec(installCmd, termState.homeDir, false);
|
for (var m = 0; m < methods.length; m++) {
|
||||||
|
termPrint('[*] Attempt ' + (m + 1) + '...', 'info');
|
||||||
|
var installResult = await shellExec(methods[m], termState.homeDir, false);
|
||||||
if (installResult.output) {
|
if (installResult.output) {
|
||||||
var out = installResult.output;
|
var out = installResult.output;
|
||||||
if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800);
|
if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800);
|
||||||
@@ -1957,19 +1969,6 @@
|
|||||||
termState.devToolsInstalled = true;
|
termState.devToolsInstalled = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkgExists.exitCode === 0 && installResult.exitCode !== 0) {
|
|
||||||
termPrint('[*] Retrying with apt directly...', 'info');
|
|
||||||
var retryResult = await shellExec(aptBin + ' update 2>&1 && ' + aptBin + ' install -y aapt2 ecj dx apksigner 2>&1', termState.homeDir, false);
|
|
||||||
if (retryResult.output) termPrint(retryResult.output.substring(0, 1500).replace(/\n$/, ''), '');
|
|
||||||
|
|
||||||
var recheck2 = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false);
|
|
||||||
if (recheck2.exitCode === 0) {
|
|
||||||
termPrint('[OK] Build tools installed!', 'success');
|
|
||||||
showStatusToast('Build tools installed!', 'success');
|
|
||||||
termState.devToolsInstalled = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
termPrint('[!] Auto-install failed. Open Terminal and run:', 'err');
|
termPrint('[!] Auto-install failed. Open Terminal and run:', 'err');
|
||||||
@@ -2029,6 +2028,7 @@
|
|||||||
if (!bsStatus.installed) {
|
if (!bsStatus.installed) {
|
||||||
try {
|
try {
|
||||||
await Bootstrap.install();
|
await Bootstrap.install();
|
||||||
|
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||||
if (Shell) {
|
if (Shell) {
|
||||||
var env = await Shell.getEnv();
|
var env = await Shell.getEnv();
|
||||||
termState.homeDir = env.HOME;
|
termState.homeDir = env.HOME;
|
||||||
@@ -2037,8 +2037,9 @@
|
|||||||
termState.cwd = env.CWD || env.HOME;
|
termState.cwd = env.CWD || env.HOME;
|
||||||
}
|
}
|
||||||
updateCwdDisplay();
|
updateCwdDisplay();
|
||||||
|
await shellExec('echo shell-ok', termState.homeDir, false);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
btn.textContent = 'Bootstrap failed';
|
btn.textContent = 'Bootstrap failed: ' + e.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2355,14 +2356,10 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var result = await Bootstrap.install();
|
var result = await Bootstrap.install();
|
||||||
statusEl.innerHTML = '<p style="color:var(--success);font-size:16px;font-weight:700">✔ Termux environment installed!</p>' +
|
|
||||||
'<p>Full Linux shell with bash, coreutils, and package manager ready.</p>' +
|
|
||||||
'<p id="devsetup-tools-status">Installing build tools (aapt2, ecj, d8, apksigner)...</p>';
|
|
||||||
btn.querySelector('.btn-text').textContent = 'Installed';
|
|
||||||
btn.querySelector('.btn-loader').style.display = 'none';
|
|
||||||
|
|
||||||
termState.homeDir = result.prefixDir ? result.prefixDir.replace('/usr', '') : termState.homeDir;
|
progressText.textContent = 'Fixing file permissions...';
|
||||||
termState.cwd = termState.homeDir + '/home';
|
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||||
|
|
||||||
if (Shell) {
|
if (Shell) {
|
||||||
var env = await Shell.getEnv();
|
var env = await Shell.getEnv();
|
||||||
termState.homeDir = env.HOME;
|
termState.homeDir = env.HOME;
|
||||||
@@ -2372,13 +2369,29 @@
|
|||||||
}
|
}
|
||||||
updateCwdDisplay();
|
updateCwdDisplay();
|
||||||
|
|
||||||
|
var shellTest = await shellExec('echo OK', termState.homeDir, false);
|
||||||
|
if (shellTest.exitCode !== 0) {
|
||||||
|
statusEl.innerHTML = '<p style="color:var(--danger)">Shell test failed: ' + (shellTest.output || 'unknown error') + '</p>' +
|
||||||
|
'<p>Try restarting the app.</p>';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.querySelector('.btn-text').textContent = 'Retry Install';
|
||||||
|
btn.querySelector('.btn-loader').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.innerHTML = '<p style="color:var(--success);font-size:16px;font-weight:700">✔ Termux environment installed!</p>' +
|
||||||
|
'<p id="devsetup-tools-status">Installing build tools (aapt2, ecj, d8, apksigner)...</p>';
|
||||||
|
|
||||||
var toolsOk = await ensureBuildTools();
|
var toolsOk = await ensureBuildTools();
|
||||||
var toolsStatus = $('#devsetup-tools-status');
|
var toolsStatus = $('#devsetup-tools-status');
|
||||||
if (toolsStatus) {
|
if (toolsStatus) {
|
||||||
toolsStatus.innerHTML = toolsOk
|
toolsStatus.innerHTML = toolsOk
|
||||||
? '<span style="color:var(--success)">✔ Build tools installed (aapt2, ecj, d8, apksigner)</span>'
|
? '<span style="color:var(--success)">✔ All tools installed — ready to build!</span>'
|
||||||
: '<span style="color:var(--warning)">Build tools not installed. Run: pkg install aapt2 ecj dx apksigner</span>';
|
: '<span style="color:var(--warning)">Build tools not installed. Open Terminal and run: pkg install aapt2 ecj dx apksigner</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
btn.querySelector('.btn-text').textContent = 'Installed';
|
||||||
|
btn.querySelector('.btn-loader').style.display = 'none';
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
statusEl.innerHTML = '<p style="color:var(--danger)">Install failed: ' + e.message + '</p>' +
|
statusEl.innerHTML = '<p style="color:var(--danger)">Install failed: ' + e.message + '</p>' +
|
||||||
'<p>Check your internet connection and try again.</p>';
|
'<p>Check your internet connection and try again.</p>';
|
||||||
|
|||||||
Reference in New Issue
Block a user