Development

This commit is contained in:
Matthew Grotke 2026-05-24 00:08:14 -04:00
parent 7e39b077d1
commit 275ccd0bac
2 changed files with 41 additions and 33 deletions

View file

@ -325,10 +325,12 @@ def _config_datasource(name):
row = dict(p)
ptype = p.get('provider', '').lower()
if ptype == 'noip':
row['credentials'] = f'<b>U:</b> {e(p.get("username", "-"))}<br><b>P:</b> &bull;&bull;&bull;'
row['credentials'] = (f'<div style="line-height:1.3">'
f'<b>U:</b> {e(p.get("username", "-"))}<br/>'
f'<b>P:</b> &bull;&bull;&bull;</div>')
elif ptype in ('cloudflare', 'duckdns'):
tok = p.get('api_token', '')
row['credentials'] = f'API Token: {tok[:8]}...' if tok else '(not set)'
row['credentials'] = f'<b>API Token:</b> {e(tok[:16])}...' if tok else '(not set)'
else:
row['credentials'] = '-'
row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', [])))
@ -628,7 +630,7 @@ def collect_tokens():
f'<tbody>{rows}</tbody>'
'</table>'
'<form method="post" action="/action/general_cardpendingchanges_applyselected">'
f'<input type="hidden" name="config_hash" value="{e(core_hash())}">'
f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
'<div class="button-row">'
'<button type="submit" class="btn btn-primary">Apply Now</button>'
'</div></form>'
@ -915,13 +917,13 @@ def _render_item(item, tokens, inherited_req=None):
f'<div class="credential-fields" data-provider-select="{psel}">'
f'<div class="cred-group-token" style="display:none">'
f'<div class="form-group"><label class="form-label">API Token</label>'
f'<input type="text" name="api_token" class="form-input"></div>'
f'<input type="text" name="api_token" class="form-input"/></div>'
f'</div>'
f'<div class="cred-group-noip" style="display:none">'
f'<div class="form-group"><label class="form-label">Username</label>'
f'<input type="text" name="username" class="form-input"></div>'
f'<input type="text" name="username" class="form-input"/></div>'
f'<div class="form-group"><label class="form-label">Password</label>'
f'<input type="password" name="password" class="form-input"></div>'
f'<input type="password" name="password" class="form-input"/></div>'
f'</div>'
f'</div>'
)
@ -943,15 +945,15 @@ def _render_item(item, tokens, inherited_req=None):
action = e(apply_tokens(item.get('action', ''), tokens))
method = e(item.get('method', 'post'))
inner = render_items(item.get('items', []), tokens, req)
hash_field = f'<input type="hidden" name="config_hash" value="{e(core_hash())}">'
hash_field = f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
originals = json.dumps(_collect_form_originals(item.get('items', []), tokens))
orig_field = f'<input type="hidden" name="original_values" value="{e(originals)}">'
orig_field = f'<input type="hidden" name="original_values" value="{e(originals)}"/>'
return f'<form action="{action}" method="{method}">{hash_field}{orig_field}{inner}</form>'
if t == 'hidden':
name = e(item.get('name', ''))
value = e(apply_tokens(item.get('value', ''), tokens))
return f'<input type="hidden" name="{name}" value="{value}">'
return f'<input type="hidden" name="{name}" value="{value}"/>'
if t == 'field':
return _render_field(item, tokens)
@ -978,9 +980,9 @@ def _render_item(item, tokens, inherited_req=None):
f'<div class="form-group">'
f'<label class="form-label">Subnet</label>'
f'<div class="subnet-row-wrap">'
f'<input type="text" name="{subnet_name}" value="{e(subnet_val)}" placeholder="{subnet_ph}" class="form-input">'
f'<input type="text" name="{subnet_name}" value="{e(subnet_val)}" placeholder="{subnet_ph}" class="form-input"/>'
f'<span class="subnet-sep">/</span>'
f'<input type="number" name="{prefix_name}" value="{pf}" min="1" max="30" class="form-input subnet-prefix-input">'
f'<input type="number" name="{prefix_name}" value="{pf}" min="1" max="30" class="form-input subnet-prefix-input"/>'
f'<span class="subnet-dotted">{e(dotted)}</span>'
f'</div>'
f'<p class="form-hint field-dyn-hint" style="display:none"></p>'
@ -1022,7 +1024,7 @@ def _render_field(item, tokens):
readonly = ' readonly' if item.get('readonly') else ''
if input_type == 'hidden':
return f'<input type="hidden" name="{name}" value="{e(value)}">'
return f'<input type="hidden" name="{name}" value="{e(value)}"/>'
if input_type == 'checkbox':
checked = 'checked' if value.lower() in ('true', '1', 'yes') else ''
@ -1031,12 +1033,12 @@ def _render_field(item, tokens):
return (f'<div class="form-group">'
f'<label class="form-label">{label}</label>'
f'<label class="form-checkbox-row">'
f'<input type="checkbox" name="{name}" {checked} class="form-checkbox">'
f'<input type="checkbox" name="{name}" {checked} class="form-checkbox"/>'
f' <span class="form-checkbox-label">{e(cb_label)}</span>'
f'</label>{hint_html}</div>')
return (f'<div class="form-group">'
f'<label class="form-label">'
f'<input type="checkbox" name="{name}" {checked} class="form-checkbox"> {label}'
f'<input type="checkbox" name="{name}" {checked} class="form-checkbox"/> {label}'
f'</label>{hint_html}</div>')
if input_type == 'checkbox_group':
@ -1048,7 +1050,7 @@ def _render_field(item, tokens):
boxes = ''.join(
f'<label class="checkbox-group-item">'
f'<input type="checkbox" name="{name}" value="{e(o.get("value",""))}"'
f'{"checked" if o.get("value") in selected else ""}> {e(o.get("label",""))}'
f'{"checked" if o.get("value") in selected else ""}/> {e(o.get("label",""))}'
f'</label>'
for o in opts
)
@ -1075,7 +1077,7 @@ def _render_field(item, tokens):
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
return (f'<div class="form-group"><label class="form-label">{label}</label>'
f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr} class="form-input{extra_cls}"{readonly}>'
f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr} class="form-input{extra_cls}"{readonly}/>'
f'{hint_html}</div>')
if input_type == 'textarea':
@ -1175,7 +1177,7 @@ def _render_field(item, tokens):
return (f'<div class="form-group">'
f'<label class="form-label">{label}</label>'
f'<div class="iface-picker">'
f'<input type="hidden" name="{name}" value="{e(current)}">'
f'<input type="hidden" name="{name}" value="{e(current)}"/>'
f'<div class="iface-picker-header">'
f'<button type="button" class="iface-picker-btn">{btn_label}{btn_badge}</button>'
f'{ext_meta}'
@ -1190,7 +1192,7 @@ def _render_field(item, tokens):
dyn_hint = '<p class="form-hint field-dyn-hint" style="display:none"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
return (f'<div class="form-group"><label class="form-label">{label}</label>'
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}>{hint_html}{dyn_hint}</div>')
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}</div>')
def _collect_form_originals(items, tokens):
@ -1249,7 +1251,7 @@ def _render_editable_list(item, tokens):
rows = ''.join(
f'<div class="editable-list-item">'
f'<input type="text" name="{name}" value="{e(v)}" class="form-input">'
f'<input type="text" name="{name}" value="{e(v)}" class="form-input"/>'
f'<button type="button" class="btn btn-ghost btn-sm editable-list-remove">Remove</button>'
f'</div>'
for v in items_list
@ -1323,8 +1325,8 @@ def _render_table(item, tokens, inherited_req=None):
btns += f'<button type="button" class="btn {cls}" disabled>{text}</button>'
continue
btns += (f'<form method="post" action="{action}" style="display:inline">'
f'<input type="hidden" name="row_index" value="{idx}">'
f'<input type="hidden" name="config_hash" value="{e(hash_val)}">'
f'<input type="hidden" name="row_index" value="{idx}"/>'
f'<input type="hidden" name="config_hash" value="{e(hash_val)}"/>'
f'<button type="submit" class="btn {cls}">{text}</button></form>')
elif method == 'js_edit':
target = e(ra.get('target', 'edit-form'))
@ -1377,7 +1379,7 @@ def _render_table_cell(value, render_fn, col_class='', field='', row_idx=None,
label = 'Disabled'; badge_cls = 'badge-disabled'
if toggle_action and row_idx is not None and toggle_allowed:
inner = (f'<form method="post" action="{e(toggle_action)}" style="display:inline">'
f'<input type="hidden" name="row_index" value="{row_idx}">'
f'<input type="hidden" name="row_index" value="{row_idx}"/>'
f'<button type="submit" class="btn-badge">'
f'<span class="badge {badge_cls}">{label}</span></button></form>')
else:
@ -1497,8 +1499,8 @@ def render_layout(view_id, content_html, tokens):
pass
return (f'<!DOCTYPE html>\n<html lang="en">\n<head>\n'
f' <meta charset="UTF-8">\n'
f' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
f' <meta charset="UTF-8"/>\n'
f' <meta name="viewport" content="width=device-width, initial-scale=1.0"/>\n'
f' <title>{PRODUCT_DISPLAY_NAME}</title>\n'
f' <style>{css}</style>\n'
f'</head>\n<body>\n'
@ -1950,13 +1952,13 @@ document.addEventListener('click', function(e) {
if (provider === 'noip') {
return '<div class="cred-field"><span class="cred-label">U:</span>' +
'<input type="text" name="username" value="' + esc(data.username||'') +
'" class="form-input inline-edit-input"></div>' +
'" class="form-input inline-edit-input"/></div>' +
'<div class="cred-field"><span class="cred-label">P:</span>' +
'<input type="password" name="password" value="' + esc(data.password||'') +
'" class="form-input inline-edit-input"></div>';
'" class="form-input inline-edit-input"/></div>';
} else {
return '<input type="text" name="api_token" value="' + esc(data.api_token||'') +
'" class="form-input inline-edit-input" placeholder="API Token">';
'" class="form-input inline-edit-input" placeholder="API Token"/>';
}
}
@ -1971,7 +1973,7 @@ document.addEventListener('click', function(e) {
if (inputType === 'checkbox') {
var checked = (val === true || val === 'true' || val === 1 || val === '1');
td.innerHTML = '<input type="checkbox" name="' + field + '"' +
(checked ? ' checked' : '') + ' class="inline-edit-checkbox">';
(checked ? ' checked' : '') + ' class="inline-edit-checkbox"/>';
} else if (inputType === 'checkbox_multi') {
var opts = fDef.options || [];
var checked = [];
@ -1981,7 +1983,7 @@ document.addEventListener('click', function(e) {
var isChecked = checked.indexOf(o.value) !== -1;
cbHtml += '<label class="checkbox-multi-item">' +
'<input type="checkbox" name="' + field + '" value="' + esc(o.value) + '"' +
(isChecked ? ' checked' : '') + ' class="inline-edit-checkbox-multi"> ' + esc(o.label) + '</label>';
(isChecked ? ' checked' : '') + ' class="inline-edit-checkbox-multi"/> ' + esc(o.label) + '</label>';
});
cbHtml += '</div>';
td.innerHTML = cbHtml;
@ -1998,7 +2000,7 @@ document.addEventListener('click', function(e) {
var minAttr = fDef.min !== undefined ? ' min="' + esc(String(fDef.min)) + '"' : '';
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
td.innerHTML = '<input type="number" name="' + field + '" value="' + esc(String(val)) +
'"' + minAttr + maxAttr + ' class="form-input inline-edit-input">';
'"' + minAttr + maxAttr + ' class="form-input inline-edit-input"/>';
} else if (inputType === 'textarea') {
var textVal;
try { var arr = JSON.parse(val); textVal = Array.isArray(arr) ? arr.join('\n') : String(val||''); }
@ -2011,7 +2013,7 @@ document.addEventListener('click', function(e) {
var validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : '';
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint" style="display:none"></p>' : '';
td.innerHTML = '<input type="' + inputType + '" name="' + field +
'" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '>' + hintHtml;
'" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '/>' + hintHtml;
if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input'));
}
});

View file

@ -210,11 +210,17 @@ def _get_ip_via_http(spec):
return _extract_ip(r.read().decode().strip())
_SAFE_DIG_RE = re.compile(r'^[a-zA-Z0-9.\-_@+:\s]+$')
def _get_ip_via_dig(spec):
"""Query public IP via dig. spec: {"type": "dig", "command": "<dig args>"}
"""Query public IP via dig. spec: {"type": "dig", "url": "<dig args>"}
Requires the 'dig' utility to be installed.
"""
cmd = ["dig", "+short"] + spec["url"].split()
url = spec["url"]
if not _SAFE_DIG_RE.match(url):
log.warning(f"Skipping dig service with disallowed characters: {url!r}")
return None
cmd = ["dig", "+short"] + url.split()
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0: