fix(extensions): keep runtime settings gated on owned storage (#5094)
This commit is contained in:
parent
a76c794d6d
commit
07ed67a793
6 changed files with 73 additions and 30 deletions
|
|
@ -7,6 +7,7 @@ It is disabled by default and never executes or fetches third-party URLs.
|
|||
|
||||
import html
|
||||
import json
|
||||
import math
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
|
@ -559,26 +560,32 @@ def _normalize_enum_options(options: object) -> Optional[List[Dict[str, str]]]:
|
|||
normalized.append({"value": value, "label": label})
|
||||
return normalized
|
||||
|
||||
_SETTINGS_DEFAULT_MISSING = object()
|
||||
|
||||
def _normalize_settings_default(field_type: str, raw_default: object, options: Optional[List[Dict[str, str]]] = None) -> Tuple[bool, object]:
|
||||
if field_type == "boolean":
|
||||
if raw_default is None:
|
||||
if raw_default is _SETTINGS_DEFAULT_MISSING:
|
||||
return True, False
|
||||
return (True, raw_default) if isinstance(raw_default, bool) else (False, None)
|
||||
if field_type == "string":
|
||||
if raw_default is None:
|
||||
if raw_default is _SETTINGS_DEFAULT_MISSING:
|
||||
return True, ""
|
||||
return (True, raw_default) if isinstance(raw_default, str) else (False, None)
|
||||
if field_type == "number":
|
||||
if raw_default is None:
|
||||
if raw_default is _SETTINGS_DEFAULT_MISSING:
|
||||
return True, 0
|
||||
return (True, raw_default) if isinstance(raw_default, (int, float)) and not isinstance(raw_default, bool) else (False, None)
|
||||
return (
|
||||
(True, raw_default)
|
||||
if isinstance(raw_default, (int, float)) and not isinstance(raw_default, bool) and math.isfinite(raw_default)
|
||||
else (False, None)
|
||||
)
|
||||
if field_type == "integer":
|
||||
if raw_default is None:
|
||||
if raw_default is _SETTINGS_DEFAULT_MISSING:
|
||||
return True, 0
|
||||
return (True, raw_default) if isinstance(raw_default, int) and not isinstance(raw_default, bool) else (False, None)
|
||||
if field_type == "enum" and options:
|
||||
values = [option["value"] for option in options]
|
||||
if raw_default is None:
|
||||
if raw_default is _SETTINGS_DEFAULT_MISSING:
|
||||
return True, values[0]
|
||||
return (True, raw_default) if isinstance(raw_default, str) and raw_default in values else (False, None)
|
||||
return False, None
|
||||
|
|
@ -615,7 +622,7 @@ def _sanitize_settings_schema(entry: Dict[str, object]) -> List[Dict[str, object
|
|||
options = _normalize_enum_options(raw_field.get("options")) if field_type == "enum" else None
|
||||
if field_type == "enum" and options is None:
|
||||
continue
|
||||
ok, default = _normalize_settings_default(field_type, raw_field.get("default"), options)
|
||||
ok, default = _normalize_settings_default(field_type, raw_field.get("default", _SETTINGS_DEFAULT_MISSING), options)
|
||||
if not ok:
|
||||
continue
|
||||
if key in seen_keys:
|
||||
|
|
@ -1545,7 +1552,7 @@ def inject_extension_tags(index_html: str) -> str:
|
|||
runtime_json = json.dumps(runtime_config, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
|
||||
runtime_tag = (
|
||||
"<script>window.__HERMES_EXTENSION_CONFIG__={};"
|
||||
"if(window.HermesExtensionSettings)window.HermesExtensionSettings.configure(window.__HERMES_EXTENSION_CONFIG__);"
|
||||
"if(window.HermesExtensionSettings)window.HermesExtensionSettings.primeFromStatus(window.__HERMES_EXTENSION_CONFIG__);"
|
||||
"</script>"
|
||||
).format(runtime_json)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,19 +97,28 @@
|
|||
return fields;
|
||||
}
|
||||
|
||||
function configure(config){
|
||||
const list=Array.isArray(config&&config.extensions)?config.extensions:[];
|
||||
schemas.clear();
|
||||
function normalizeSchemas(rawRows){
|
||||
const list=Array.isArray(rawRows)?rawRows:[];
|
||||
const entries=[];
|
||||
for(const entry of list){
|
||||
const id=extensionId(entry&&entry.id);
|
||||
if(!id) continue;
|
||||
schemas.set(id, {
|
||||
const storageOwned=!!(entry&&entry.storage_owned);
|
||||
entries.push({
|
||||
id,
|
||||
name:text(entry&&entry.name,id),
|
||||
storage_owned:!!(entry&&entry.storage_owned),
|
||||
settings_schema:normalizeSchema(entry&&entry.settings_schema),
|
||||
storage_owned:storageOwned,
|
||||
settings_schema:storageOwned?normalizeSchema(entry&&entry.settings_schema):[],
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function primeFromStatus(statusPayload){
|
||||
schemas.clear();
|
||||
for(const entry of normalizeSchemas(statusPayload&&statusPayload.extensions)){
|
||||
schemas.set(entry.id,entry);
|
||||
}
|
||||
}
|
||||
|
||||
function safeRead(key){
|
||||
|
|
@ -179,15 +188,20 @@
|
|||
return overrides;
|
||||
}
|
||||
|
||||
function supportsSettings(meta){
|
||||
return !!(meta&&meta.storage_owned&&Array.isArray(meta.settings_schema)&&meta.settings_schema.length);
|
||||
}
|
||||
|
||||
function settingsForExtension(id){
|
||||
const clean=extensionId(id);
|
||||
const meta=schemas.get(clean)||{id:clean,name:clean,storage_owned:false,settings_schema:[]};
|
||||
const schema=meta.settings_schema||[];
|
||||
const schema=supportsSettings(meta)?meta.settings_schema:[];
|
||||
const key=settingsKey(clean);
|
||||
function current(){
|
||||
return validate(schema,safeRead(key)).values;
|
||||
}
|
||||
function setAll(values){
|
||||
if(!supportsSettings(meta)) return {ok:false,values:current(),errors:{extension:'unsupported'}};
|
||||
const checked=validate(schema,values);
|
||||
if(!checked.ok) return checked;
|
||||
const saved=safeWrite(key,overridesFromValues(schema,checked.values));
|
||||
|
|
@ -209,10 +223,12 @@
|
|||
},
|
||||
setAll,
|
||||
reset(){
|
||||
if(!supportsSettings(meta)) return current();
|
||||
safeWrite(key,{});
|
||||
return current();
|
||||
},
|
||||
clear(){
|
||||
if(!supportsSettings(meta)) return false;
|
||||
safeWrite(key,{});
|
||||
return true;
|
||||
},
|
||||
|
|
@ -220,24 +236,31 @@
|
|||
}
|
||||
|
||||
function storageForExtension(id){
|
||||
const key=storageKey(id);
|
||||
const clean=extensionId(id);
|
||||
const meta=schemas.get(clean)||{id:clean,name:clean,storage_owned:false,settings_schema:[]};
|
||||
const allowed=!!meta.storage_owned;
|
||||
const key=storageKey(clean);
|
||||
return {
|
||||
getAll(){return safeRead(key);},
|
||||
getAll(){return allowed?safeRead(key):{};},
|
||||
get(name,defaultValue){
|
||||
if(!allowed) return defaultValue;
|
||||
const data=safeRead(key);
|
||||
return Object.prototype.hasOwnProperty.call(data,name)?data[name]:defaultValue;
|
||||
},
|
||||
set(name,value){
|
||||
if(!allowed) return false;
|
||||
const data=safeRead(key);
|
||||
data[name]=value;
|
||||
return safeWrite(key,data);
|
||||
},
|
||||
remove(name){
|
||||
if(!allowed) return false;
|
||||
const data=safeRead(key);
|
||||
delete data[name];
|
||||
return safeWrite(key,data);
|
||||
},
|
||||
clear(){
|
||||
if(!allowed) return false;
|
||||
safeWrite(key,{});
|
||||
return true;
|
||||
},
|
||||
|
|
@ -245,8 +268,8 @@
|
|||
}
|
||||
|
||||
const api={
|
||||
configure,
|
||||
normalizeSchema,
|
||||
normalizeSchemas,
|
||||
primeFromStatus,
|
||||
namespaceForExtension,
|
||||
settingsForExtension,
|
||||
storageForExtension,
|
||||
|
|
@ -260,5 +283,5 @@
|
|||
window.hermesExt.storage=window.hermesExt.storage||{};
|
||||
window.hermesExt.settings.forExtension=settingsForExtension;
|
||||
window.hermesExt.storage.forExtension=storageForExtension;
|
||||
configure(window.__HERMES_EXTENSION_CONFIG__||{});
|
||||
primeFromStatus(window.__HERMES_EXTENSION_CONFIG__||{});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -8170,7 +8170,7 @@ function _extensionEntryBadge(entry){
|
|||
|
||||
function _configureExtensionSettingsFromStatus(data){
|
||||
if(!window.HermesExtensionSettings||!data||!Array.isArray(data.extensions)) return;
|
||||
window.HermesExtensionSettings.configure({extensions:data.extensions});
|
||||
window.HermesExtensionSettings.primeFromStatus({extensions:data.extensions});
|
||||
}
|
||||
|
||||
function _extensionSettingsFieldHtml(field,value){
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ def test_extension_settings_runtime_config_injects_before_extension_scripts(tmp_
|
|||
injected = inject_extension_tags("<html><head></head><body></body></html>")
|
||||
|
||||
assert "window.__HERMES_EXTENSION_CONFIG__" in injected
|
||||
assert "window.HermesExtensionSettings.configure(window.__HERMES_EXTENSION_CONFIG__)" in injected
|
||||
assert "window.HermesExtensionSettings.primeFromStatus(window.__HERMES_EXTENSION_CONFIG__)" in injected
|
||||
assert '"storage_owned":true' in injected
|
||||
assert '"settings_schema":[{"key":"flag","type":"boolean"' in injected
|
||||
assert injected.index("window.__HERMES_EXTENSION_CONFIG__") < injected.index("/extensions/settings-ok.js")
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears():
|
|||
}};
|
||||
eval(fs.readFileSync({str(EXTENSION_SETTINGS_JS)!r}, 'utf8'));
|
||||
|
||||
window.HermesExtensionSettings.configure({{
|
||||
window.HermesExtensionSettings.primeFromStatus({{
|
||||
extensions: [{{
|
||||
id: 'demo.ext',
|
||||
name: 'Demo',
|
||||
|
|
@ -75,6 +75,23 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears():
|
|||
assert.strictEqual(store.has('hermes.ext.settings.demo.ext'), false);
|
||||
assert.strictEqual(storage.clear(), true);
|
||||
assert.strictEqual(store.has('hermes.ext.storage.demo.ext'), false);
|
||||
|
||||
window.HermesExtensionSettings.primeFromStatus({{
|
||||
extensions: [{{
|
||||
id: 'denied.ext',
|
||||
name: 'Denied',
|
||||
storage_owned: false,
|
||||
settings_schema: [{{key: 'flag', type: 'boolean', default: false}}],
|
||||
}}]
|
||||
}});
|
||||
|
||||
const deniedSettings = window.HermesExtensionSettings.settingsForExtension('denied.ext');
|
||||
assert.strictEqual(deniedSettings.setAll({{flag: true}}).ok, false);
|
||||
assert.strictEqual(store.has('hermes.ext.settings.denied.ext'), false);
|
||||
|
||||
const deniedStorage = window.HermesExtensionSettings.storageForExtension('denied.ext');
|
||||
assert.strictEqual(deniedStorage.set('note', 'blocked'), false);
|
||||
assert.strictEqual(store.has('hermes.ext.storage.denied.ext'), false);
|
||||
"""
|
||||
)
|
||||
_run_node(script)
|
||||
|
|
|
|||
|
|
@ -223,6 +223,8 @@ def test_extension_status_sanitizes_settings_schema_only_for_owned_storage(tmp_p
|
|||
{"key": "bad_enum", "type": "enum", "options": [{"label": "No value"}]},
|
||||
{"key": "flag", "type": "boolean", "default": False},
|
||||
{"key": "bad_default", "type": "integer", "default": 1.2},
|
||||
{"key": "null_flag", "type": "boolean", "default": None},
|
||||
{"key": "null_name", "type": "string", "default": None},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -243,13 +245,7 @@ def test_extension_status_sanitizes_settings_schema_only_for_owned_storage(tmp_p
|
|||
status = get_extension_status()
|
||||
by_id = {entry["id"]: entry for entry in status["extensions"]}
|
||||
assert by_id["settings-ok"]["storage_owned"] is True
|
||||
assert [field["key"] for field in by_id["settings-ok"]["settings_schema"]] == [
|
||||
"flag",
|
||||
"name",
|
||||
"ratio",
|
||||
"count",
|
||||
"mode",
|
||||
]
|
||||
assert [field["key"] for field in by_id["settings-ok"]["settings_schema"]] == ["flag", "name", "ratio", "count", "mode"]
|
||||
assert by_id["settings-ok"]["settings_schema"][0]["default"] is True
|
||||
assert by_id["settings-ok"]["settings_schema"][4]["options"] == [
|
||||
{"value": "compact", "label": "Compact"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue