Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
3c27c697c1
5 files changed
+176
-4
No files matched your search
@@ -5,6 +5,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **Settings → Extensions gallery cards now link back to their source and show permissions in readable groups.** Gallery and Installed cards now surface a safe "Source" link from registry metadata (falling back to the extension registry path when needed), and the permissions disclosure renders WebUI API access, storage, DOM, sidecar/native-host, filesystem, and external-network declarations as plain rows instead of raw JSON. Thanks @franksong2702. (#5093, #5095)
|
||||
|
||||
- **Opt-in CSP `frame-src` allowlist so an extension can embed a self-hosted web app in an iframe.** A new `HERMES_WEBUI_CSP_FRAME_EXTRA` env var widens what the WebUI page may embed in an `<iframe>` (default is same-origin only), mirroring the existing `HERMES_WEBUI_CSP_CONNECT_EXTRA` knob — space-separated `http(s)` origins with optional wildcard subdomain and port, validated and ignored if malformed. This unblocks an "external app tab" extension (pin Grafana / Vaultwarden / your own dashboard as a tab). It only governs what *this* page can embed; `frame-ancestors` stays `'none'`, so it does not let anyone embed the WebUI itself. Default-off; with the var unset, behavior is unchanged. Documented in `docs/EXTENSIONS.md`.
|
||||
|
||||
- **`/moa` now works in the WebUI, routing the turn through your configured Mixture-of-Agents preset.** Typing `/moa <prompt>` runs that turn through the agent's MoA config (the same preset/usage you'd get from the CLI) instead of a plain single-model turn; `/moa` with no prompt shows the available usage. The MoA config is resolved **server-side** from your profile config — the browser only signals that an override is wanted, it can't supply the config — and it's applied per-turn (not persisted). It fails closed on gateway-backed sessions. Thanks @rodboev. (#5070, #5057)
|
||||
|
||||
@@ -681,6 +681,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'No extra permissions declared.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -2316,6 +2317,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nessuna autorizzazione extra dichiarata.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -3942,6 +3944,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '追加の権限は宣言されていません。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -6276,6 +6279,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Дополнительные разрешения не указаны.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -7827,6 +7831,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'No se declararon permisos adicionales.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -9062,6 +9067,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Keine zusätzlichen Berechtigungen deklariert.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -10964,6 +10970,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '未声明额外权限。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -11821,6 +11828,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '未宣告額外權限。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -13343,6 +13351,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nenhuma permissão extra declarada.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -14871,6 +14880,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '추가 권한이 선언되지 않았습니다.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -16509,6 +16519,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Aucune permission supplémentaire déclarée.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -18103,6 +18114,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Ek izin bildirilmedi.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -19743,6 +19755,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nie zadeklarowano dodatkowych uprawnień.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -21271,6 +21284,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Không khai báo quyền bổ sung nào.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
|
||||
+120
-2
@@ -8512,6 +8512,117 @@ function _extensionSafeHttpUrl(value){
|
||||
}
|
||||
}
|
||||
|
||||
function _extensionRegistrySourceUrl(entryPath){
|
||||
const raw=String(entryPath||'').trim();
|
||||
if(!raw||raw.startsWith('/')||raw.includes('\\')||raw.includes('\0')) return '';
|
||||
const parts=raw.split('/').filter(Boolean);
|
||||
if(parts.length===0||parts.some(part=>part==='.'||part==='..')) return '';
|
||||
const folder=parts.length>1?parts.slice(0,-1):parts;
|
||||
return 'https://github.com/hermes-webui/hermes-webui-extensions/tree/main/'+folder.map(encodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
function _extensionSourceUrl(entry){
|
||||
if(!entry||typeof entry!=='object') return '';
|
||||
const candidates=[
|
||||
entry.homepage,
|
||||
entry.repository_url,
|
||||
entry.repo_url,
|
||||
entry.source_url,
|
||||
entry.source,
|
||||
];
|
||||
const repository=entry.repository;
|
||||
if(typeof repository==='string'){
|
||||
candidates.push(repository);
|
||||
}else if(repository&&typeof repository==='object'){
|
||||
candidates.push(repository.url,repository.html_url);
|
||||
}
|
||||
for(const candidate of candidates){
|
||||
const safe=_extensionSafeHttpUrl(candidate);
|
||||
if(safe) return safe;
|
||||
}
|
||||
return _extensionSafeHttpUrl(_extensionRegistrySourceUrl(entry.entry_path||entry.runtime_manifest_path));
|
||||
}
|
||||
|
||||
function _extensionSourceLink(entry){
|
||||
const url=_extensionSourceUrl(entry);
|
||||
if(!url) return '';
|
||||
return `<a class="extension-gallery-source-link" href="${esc(url)}" target="_blank" rel="noopener noreferrer">Source</a>`;
|
||||
}
|
||||
|
||||
function _extensionPermissionList(value){
|
||||
if(!Array.isArray(value)) return '';
|
||||
const items=value
|
||||
.map(item=>String(item||'').trim())
|
||||
.filter(Boolean);
|
||||
return items.length?items.join(', '):'';
|
||||
}
|
||||
|
||||
function _extensionPermissionRows(perms){
|
||||
if(!perms||typeof perms!=='object') return [];
|
||||
const rows=[];
|
||||
const api=(perms.webui_api&&typeof perms.webui_api==='object')?perms.webui_api:{};
|
||||
const apiRead=_extensionPermissionList(api.read);
|
||||
const apiWrite=_extensionPermissionList(api.write);
|
||||
if(apiRead) rows.push(['WebUI API reads',apiRead]);
|
||||
if(apiWrite) rows.push(['WebUI API writes',apiWrite]);
|
||||
if(perms.webui_navigation===true) rows.push(['Navigation','Can open or switch WebUI views']);
|
||||
|
||||
const sidecarCommands=(perms.sidecar_commands&&typeof perms.sidecar_commands==='object')?perms.sidecar_commands:{};
|
||||
const commandLabels=[
|
||||
['from_loopback','accepts loopback commands'],
|
||||
['can_switch_sessions','switch sessions'],
|
||||
['can_write_drafts','write drafts'],
|
||||
['can_autosend','auto-send drafts'],
|
||||
['can_respond_approval','respond to approvals'],
|
||||
['can_respond_clarify','respond to clarifications'],
|
||||
];
|
||||
const commands=commandLabels
|
||||
.filter(([key])=>sidecarCommands[key]===true)
|
||||
.map(([,label])=>label);
|
||||
if(commands.length) rows.push(['Sidecar commands',commands.join(', ')]);
|
||||
|
||||
const dom=(perms.dom&&typeof perms.dom==='object')?perms.dom:{};
|
||||
const domItems=[];
|
||||
if(dom.owned===true) domItems.push('renders extension-owned UI');
|
||||
if(dom.mutates_core_views===true) domItems.push('can alter core WebUI views');
|
||||
if(domItems.length) rows.push(['DOM access',domItems.join(', ')]);
|
||||
|
||||
const storage=(perms.storage&&typeof perms.storage==='object')?perms.storage:{};
|
||||
const ownedStorage=_extensionPermissionList(storage.owned||storage.owned_keys);
|
||||
const sharedStorage=_extensionPermissionList(storage.shared_webui_keys);
|
||||
if(ownedStorage) rows.push(['Owned storage keys',ownedStorage]);
|
||||
if(sharedStorage) rows.push(['Shared WebUI storage',sharedStorage]);
|
||||
|
||||
if(perms.loopback_sidecar===true) rows.push(['Loopback sidecar','Can contact a declared local loopback helper']);
|
||||
if(perms.native_host===true) rows.push(['Native host','Requires a local native host or desktop app']);
|
||||
|
||||
const filesystem=(perms.filesystem&&typeof perms.filesystem==='object')?perms.filesystem:{};
|
||||
if(filesystem.arbitrary===true){
|
||||
rows.push(['Filesystem','Can access arbitrary filesystem paths']);
|
||||
}else if(filesystem.serves_bundled_assets===true){
|
||||
rows.push(['Filesystem','Serves bundled extension assets only']);
|
||||
}
|
||||
if(perms.network_external===true||perms.external_network===true){
|
||||
rows.push(['External network','Can contact external network origins']);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function _extensionPermissionSummary(perms){
|
||||
const rows=_extensionPermissionRows(perms);
|
||||
const body=rows.length
|
||||
? '<div class="extension-gallery-permission-list">'+rows.map(([label,value])=>`
|
||||
<div class="extension-gallery-permission-row">
|
||||
<span class="extension-gallery-permission-label">${esc(label)}</span>
|
||||
<span class="extension-gallery-permission-value">${esc(value)}</span>
|
||||
</div>`).join('')+'</div>'
|
||||
: `<div class="extension-gallery-permission-empty">${esc(t('ext_gallery_permissions_empty'))}</div>`;
|
||||
return `<details class="extension-gallery-perms">
|
||||
<summary>${esc(t('ext_gallery_permissions_show'))}</summary>
|
||||
${body}
|
||||
</details>`;
|
||||
}
|
||||
|
||||
function _extensionPostInstallNote(entry,isInstalled){
|
||||
const lifecycle=(entry&&entry.lifecycle&&typeof entry.lifecycle==='object')?entry.lifecycle:{};
|
||||
const post=(entry&&entry.post_install&&typeof entry.post_install==='object')?entry.post_install:null;
|
||||
@@ -8592,7 +8703,14 @@ function _renderExtensionsGallery(entries,statusData){
|
||||
const isInstalled=installedIds.has(String(entry.id||''));
|
||||
const restartRequired=!!(entry.lifecycle&&(entry.lifecycle.restart_required||entry.lifecycle.webui_restart_required));
|
||||
const badgesHtml=caps.map(c=>`<span class="extension-gallery-badge">${esc(String(c))}</span>`).join('');
|
||||
const permsHtml=perms?`<details class="extension-gallery-perms"><summary data-i18n="ext_gallery_permissions_show">Permissions</summary><pre>${esc(JSON.stringify(perms,null,2))}</pre></details>`:'';
|
||||
const metaBits=[];
|
||||
if(author) metaBits.push('by '+author);
|
||||
if(version) metaBits.push('v'+version);
|
||||
const sourceLinkHtml=_extensionSourceLink(entry);
|
||||
const metaHtml=(metaBits.length||sourceLinkHtml)
|
||||
? `<div class="extension-gallery-meta">${metaBits.length?`<span>${metaBits.join(' · ')}</span>`:''}${sourceLinkHtml}</div>`
|
||||
: '';
|
||||
const permsHtml=perms?_extensionPermissionSummary(perms):'';
|
||||
const postInstallHtml=_extensionPostInstallNote(entry,isInstalled);
|
||||
const actionBtn=isInstalled
|
||||
?`<button class="extension-gallery-uninstall-btn" data-ext-uninstall-id="${id}" type="button" data-i18n="ext_gallery_uninstall">Uninstall</button>`
|
||||
@@ -8602,7 +8720,7 @@ function _renderExtensionsGallery(entries,statusData){
|
||||
<div class="extension-gallery-head">
|
||||
<div class="extension-gallery-info">
|
||||
<div class="extension-gallery-name">${name}${installedBadge}</div>
|
||||
<div class="extension-gallery-meta">${author?'by '+author:''}${version?' · v'+version:''}</div>
|
||||
${metaHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="extension-gallery-desc">${desc}</div>
|
||||
|
||||
+9
-2
@@ -5144,7 +5144,9 @@ main.main > #mainPlugin{display:none;}
|
||||
.extension-gallery-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;}
|
||||
.extension-gallery-info{flex:1;min-width:0;}
|
||||
.extension-gallery-name{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
|
||||
.extension-gallery-meta{font-size:11px;color:var(--muted);margin-top:2px;}
|
||||
.extension-gallery-meta{font-size:11px;color:var(--muted);margin-top:2px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
|
||||
.extension-gallery-source-link{display:inline-flex;align-items:center;color:var(--accent);font-weight:600;text-decoration:none;}
|
||||
.extension-gallery-source-link:hover{text-decoration:underline;}
|
||||
.extension-gallery-desc{font-size:12px;color:var(--muted);margin-top:6px;line-height:1.5;}
|
||||
.extension-gallery-badge-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px;}
|
||||
.extension-gallery-badge{display:inline-flex;align-items:center;background:var(--code-bg);color:var(--muted);border-radius:999px;padding:2px 8px;font-size:10.5px;font-weight:600;font-family:var(--font-mono);}
|
||||
@@ -5157,7 +5159,11 @@ main.main > #mainPlugin{display:none;}
|
||||
.extension-gallery-next-link:hover{text-decoration:underline;}
|
||||
.extension-gallery-perms{margin-top:8px;font-size:12px;}
|
||||
.extension-gallery-perms summary{cursor:pointer;color:var(--muted);user-select:none;}
|
||||
.extension-gallery-perms pre{margin:6px 0 0;padding:8px;background:var(--code-bg);border-radius:4px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-all;}
|
||||
.extension-gallery-permission-list{display:flex;flex-direction:column;gap:6px;margin-top:7px;padding:8px 9px;border:1px solid var(--border);border-radius:8px;background:var(--subtle-bg,rgba(127,127,127,.06));}
|
||||
.extension-gallery-permission-row{display:grid;grid-template-columns:minmax(110px,.45fr) 1fr;gap:8px;align-items:start;min-width:0;}
|
||||
.extension-gallery-permission-label{font-size:10.5px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:0;}
|
||||
.extension-gallery-permission-value{font-size:12px;color:var(--text);line-height:1.35;overflow-wrap:anywhere;}
|
||||
.extension-gallery-permission-empty{margin-top:7px;color:var(--muted);font-size:12px;}
|
||||
.extension-gallery-actions{display:flex;gap:8px;margin-top:10px;align-items:center;flex-wrap:wrap;}
|
||||
.extension-gallery-install-btn,.extension-gallery-uninstall-btn{padding:4px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid transparent;transition:background .15s,color .15s;}
|
||||
.extension-gallery-install-btn{background:var(--accent-bg);color:var(--accent-text);border-color:var(--accent);}
|
||||
@@ -5176,6 +5182,7 @@ main.main > #mainPlugin{display:none;}
|
||||
.extension-sidecar-row-head{align-items:flex-start;flex-direction:column;}
|
||||
.extension-sidecar-fields>div{flex-direction:column;gap:2px;}
|
||||
.extension-sidecar-fields span{flex:0 0 auto;}
|
||||
.extension-gallery-permission-row{grid-template-columns:1fr;gap:2px;}
|
||||
}
|
||||
|
||||
/* ── Provider model tags ── */
|
||||
|
||||
@@ -247,6 +247,33 @@ def test_extensions_gallery_renders_post_install_guidance():
|
||||
assert "webui_restart_required" in install_block
|
||||
|
||||
|
||||
def test_extensions_gallery_links_sources_and_humanizes_permissions():
|
||||
helper_block = _between("function _extensionRegistrySourceUrl", "function _extensionPostInstallNote")
|
||||
render_block = _between("function _renderExtensionsGallery", "function _bindExtensionGalleryButtons")
|
||||
|
||||
assert "entry.homepage" in helper_block
|
||||
assert "entry.repository_url" in helper_block
|
||||
assert "entry.entry_path||entry.runtime_manifest_path" in helper_block
|
||||
assert "hermes-webui/hermes-webui-extensions/tree/main" in helper_block
|
||||
assert "encodeURIComponent" in helper_block
|
||||
assert "extension-gallery-source-link" in helper_block
|
||||
assert "target=\"_blank\"" in helper_block
|
||||
assert "rel=\"noopener noreferrer\"" in helper_block
|
||||
assert "t('ext_gallery_permissions_empty')" in helper_block
|
||||
assert "webui_api" in helper_block
|
||||
assert "sidecar_commands" in helper_block
|
||||
assert "dom.mutates_core_views" in helper_block
|
||||
assert "storage.shared_webui_keys" in helper_block
|
||||
assert "loopback_sidecar" in helper_block
|
||||
assert "native_host" in helper_block
|
||||
assert "network_external" in helper_block
|
||||
assert "extension-gallery-permission-row" in helper_block
|
||||
assert "_extensionSourceLink(entry)" in render_block
|
||||
assert "_extensionPermissionSummary(perms)" in render_block
|
||||
assert "JSON.stringify(perms" not in render_block
|
||||
assert "<pre>" not in render_block
|
||||
|
||||
|
||||
def test_copy_extensions_diagnostics_copies_current_sanitized_payload():
|
||||
copy_block = _between("function copyExtensionsDiagnostics", "// ── Plugins panel")
|
||||
|
||||
@@ -268,8 +295,11 @@ def test_extensions_styles_are_scoped_to_extensions_panel():
|
||||
assert ".extension-sidecar-list" in STYLE_CSS
|
||||
assert ".extension-sidecar-runtime" in STYLE_CSS
|
||||
assert ".extension-sidecar-status-badge" in STYLE_CSS
|
||||
assert ".extension-gallery-source-link" in STYLE_CSS
|
||||
assert ".extension-gallery-next-step" in STYLE_CSS
|
||||
assert ".extension-gallery-next-link" in STYLE_CSS
|
||||
assert ".extension-gallery-permission-list" in STYLE_CSS
|
||||
assert ".extension-gallery-permission-row" in STYLE_CSS
|
||||
|
||||
|
||||
def test_extensions_i18n_keys_exist_for_all_locales():
|
||||
@@ -280,6 +310,7 @@ def test_extensions_i18n_keys_exist_for_all_locales():
|
||||
"settings_tab_extensions",
|
||||
"ext_gallery_next_step",
|
||||
"ext_gallery_after_install",
|
||||
"ext_gallery_permissions_empty",
|
||||
"ext_gallery_local_component_required",
|
||||
"ext_gallery_local_app_label",
|
||||
"ext_gallery_required_suffix",
|
||||
|
||||
Reference in new issue
Block a user