Development
This commit is contained in:
parent
7e39b077d1
commit
275ccd0bac
2 changed files with 41 additions and 33 deletions
|
|
@ -325,10 +325,12 @@ def _config_datasource(name):
|
||||||
row = dict(p)
|
row = dict(p)
|
||||||
ptype = p.get('provider', '').lower()
|
ptype = p.get('provider', '').lower()
|
||||||
if ptype == 'noip':
|
if ptype == 'noip':
|
||||||
row['credentials'] = f'<b>U:</b> {e(p.get("username", "-"))}<br><b>P:</b> •••'
|
row['credentials'] = (f'<div style="line-height:1.3">'
|
||||||
|
f'<b>U:</b> {e(p.get("username", "-"))}<br/>'
|
||||||
|
f'<b>P:</b> •••</div>')
|
||||||
elif ptype in ('cloudflare', 'duckdns'):
|
elif ptype in ('cloudflare', 'duckdns'):
|
||||||
tok = p.get('api_token', '')
|
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:
|
else:
|
||||||
row['credentials'] = '-'
|
row['credentials'] = '-'
|
||||||
row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', [])))
|
row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', [])))
|
||||||
|
|
@ -628,7 +630,7 @@ def collect_tokens():
|
||||||
f'<tbody>{rows}</tbody>'
|
f'<tbody>{rows}</tbody>'
|
||||||
'</table>'
|
'</table>'
|
||||||
'<form method="post" action="/action/general_cardpendingchanges_applyselected">'
|
'<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">'
|
'<div class="button-row">'
|
||||||
'<button type="submit" class="btn btn-primary">Apply Now</button>'
|
'<button type="submit" class="btn btn-primary">Apply Now</button>'
|
||||||
'</div></form>'
|
'</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="credential-fields" data-provider-select="{psel}">'
|
||||||
f'<div class="cred-group-token" style="display:none">'
|
f'<div class="cred-group-token" style="display:none">'
|
||||||
f'<div class="form-group"><label class="form-label">API Token</label>'
|
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>'
|
||||||
f'<div class="cred-group-noip" style="display:none">'
|
f'<div class="cred-group-noip" style="display:none">'
|
||||||
f'<div class="form-group"><label class="form-label">Username</label>'
|
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'<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>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
)
|
)
|
||||||
|
|
@ -943,15 +945,15 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
action = e(apply_tokens(item.get('action', ''), tokens))
|
action = e(apply_tokens(item.get('action', ''), tokens))
|
||||||
method = e(item.get('method', 'post'))
|
method = e(item.get('method', 'post'))
|
||||||
inner = render_items(item.get('items', []), tokens, req)
|
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))
|
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>'
|
return f'<form action="{action}" method="{method}">{hash_field}{orig_field}{inner}</form>'
|
||||||
|
|
||||||
if t == 'hidden':
|
if t == 'hidden':
|
||||||
name = e(item.get('name', ''))
|
name = e(item.get('name', ''))
|
||||||
value = e(apply_tokens(item.get('value', ''), tokens))
|
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':
|
if t == 'field':
|
||||||
return _render_field(item, tokens)
|
return _render_field(item, tokens)
|
||||||
|
|
@ -978,9 +980,9 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
f'<div class="form-group">'
|
f'<div class="form-group">'
|
||||||
f'<label class="form-label">Subnet</label>'
|
f'<label class="form-label">Subnet</label>'
|
||||||
f'<div class="subnet-row-wrap">'
|
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'<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'<span class="subnet-dotted">{e(dotted)}</span>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'<p class="form-hint field-dyn-hint" style="display:none"></p>'
|
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 ''
|
readonly = ' readonly' if item.get('readonly') else ''
|
||||||
|
|
||||||
if input_type == 'hidden':
|
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':
|
if input_type == 'checkbox':
|
||||||
checked = 'checked' if value.lower() in ('true', '1', 'yes') else ''
|
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">'
|
return (f'<div class="form-group">'
|
||||||
f'<label class="form-label">{label}</label>'
|
f'<label class="form-label">{label}</label>'
|
||||||
f'<label class="form-checkbox-row">'
|
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' <span class="form-checkbox-label">{e(cb_label)}</span>'
|
||||||
f'</label>{hint_html}</div>')
|
f'</label>{hint_html}</div>')
|
||||||
return (f'<div class="form-group">'
|
return (f'<div class="form-group">'
|
||||||
f'<label class="form-label">'
|
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>')
|
f'</label>{hint_html}</div>')
|
||||||
|
|
||||||
if input_type == 'checkbox_group':
|
if input_type == 'checkbox_group':
|
||||||
|
|
@ -1048,7 +1050,7 @@ def _render_field(item, tokens):
|
||||||
boxes = ''.join(
|
boxes = ''.join(
|
||||||
f'<label class="checkbox-group-item">'
|
f'<label class="checkbox-group-item">'
|
||||||
f'<input type="checkbox" name="{name}" value="{e(o.get("value",""))}"'
|
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>'
|
f'</label>'
|
||||||
for o in opts
|
for o in opts
|
||||||
)
|
)
|
||||||
|
|
@ -1075,7 +1077,7 @@ def _render_field(item, tokens):
|
||||||
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
||||||
max_attr = f' max="{item["max"]}"' if 'max' 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>'
|
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>')
|
f'{hint_html}</div>')
|
||||||
|
|
||||||
if input_type == 'textarea':
|
if input_type == 'textarea':
|
||||||
|
|
@ -1175,7 +1177,7 @@ def _render_field(item, tokens):
|
||||||
return (f'<div class="form-group">'
|
return (f'<div class="form-group">'
|
||||||
f'<label class="form-label">{label}</label>'
|
f'<label class="form-label">{label}</label>'
|
||||||
f'<div class="iface-picker">'
|
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'<div class="iface-picker-header">'
|
||||||
f'<button type="button" class="iface-picker-btn">{btn_label}{btn_badge}</button>'
|
f'<button type="button" class="iface-picker-btn">{btn_label}{btn_badge}</button>'
|
||||||
f'{ext_meta}'
|
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 ''
|
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>'
|
return (f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||||
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
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):
|
def _collect_form_originals(items, tokens):
|
||||||
|
|
@ -1249,7 +1251,7 @@ def _render_editable_list(item, tokens):
|
||||||
|
|
||||||
rows = ''.join(
|
rows = ''.join(
|
||||||
f'<div class="editable-list-item">'
|
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'<button type="button" class="btn btn-ghost btn-sm editable-list-remove">Remove</button>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
for v in items_list
|
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>'
|
btns += f'<button type="button" class="btn {cls}" disabled>{text}</button>'
|
||||||
continue
|
continue
|
||||||
btns += (f'<form method="post" action="{action}" style="display:inline">'
|
btns += (f'<form method="post" action="{action}" style="display:inline">'
|
||||||
f'<input type="hidden" name="row_index" value="{idx}">'
|
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="config_hash" value="{e(hash_val)}"/>'
|
||||||
f'<button type="submit" class="btn {cls}">{text}</button></form>')
|
f'<button type="submit" class="btn {cls}">{text}</button></form>')
|
||||||
elif method == 'js_edit':
|
elif method == 'js_edit':
|
||||||
target = e(ra.get('target', 'edit-form'))
|
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'
|
label = 'Disabled'; badge_cls = 'badge-disabled'
|
||||||
if toggle_action and row_idx is not None and toggle_allowed:
|
if toggle_action and row_idx is not None and toggle_allowed:
|
||||||
inner = (f'<form method="post" action="{e(toggle_action)}" style="display:inline">'
|
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'<button type="submit" class="btn-badge">'
|
||||||
f'<span class="badge {badge_cls}">{label}</span></button></form>')
|
f'<span class="badge {badge_cls}">{label}</span></button></form>')
|
||||||
else:
|
else:
|
||||||
|
|
@ -1497,8 +1499,8 @@ def render_layout(view_id, content_html, tokens):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return (f'<!DOCTYPE html>\n<html lang="en">\n<head>\n'
|
return (f'<!DOCTYPE html>\n<html lang="en">\n<head>\n'
|
||||||
f' <meta charset="UTF-8">\n'
|
f' <meta charset="UTF-8"/>\n'
|
||||||
f' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
|
f' <meta name="viewport" content="width=device-width, initial-scale=1.0"/>\n'
|
||||||
f' <title>{PRODUCT_DISPLAY_NAME}</title>\n'
|
f' <title>{PRODUCT_DISPLAY_NAME}</title>\n'
|
||||||
f' <style>{css}</style>\n'
|
f' <style>{css}</style>\n'
|
||||||
f'</head>\n<body>\n'
|
f'</head>\n<body>\n'
|
||||||
|
|
@ -1950,13 +1952,13 @@ document.addEventListener('click', function(e) {
|
||||||
if (provider === 'noip') {
|
if (provider === 'noip') {
|
||||||
return '<div class="cred-field"><span class="cred-label">U:</span>' +
|
return '<div class="cred-field"><span class="cred-label">U:</span>' +
|
||||||
'<input type="text" name="username" value="' + esc(data.username||'') +
|
'<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>' +
|
'<div class="cred-field"><span class="cred-label">P:</span>' +
|
||||||
'<input type="password" name="password" value="' + esc(data.password||'') +
|
'<input type="password" name="password" value="' + esc(data.password||'') +
|
||||||
'" class="form-input inline-edit-input"></div>';
|
'" class="form-input inline-edit-input"/></div>';
|
||||||
} else {
|
} else {
|
||||||
return '<input type="text" name="api_token" value="' + esc(data.api_token||'') +
|
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') {
|
if (inputType === 'checkbox') {
|
||||||
var checked = (val === true || val === 'true' || val === 1 || val === '1');
|
var checked = (val === true || val === 'true' || val === 1 || val === '1');
|
||||||
td.innerHTML = '<input type="checkbox" name="' + field + '"' +
|
td.innerHTML = '<input type="checkbox" name="' + field + '"' +
|
||||||
(checked ? ' checked' : '') + ' class="inline-edit-checkbox">';
|
(checked ? ' checked' : '') + ' class="inline-edit-checkbox"/>';
|
||||||
} else if (inputType === 'checkbox_multi') {
|
} else if (inputType === 'checkbox_multi') {
|
||||||
var opts = fDef.options || [];
|
var opts = fDef.options || [];
|
||||||
var checked = [];
|
var checked = [];
|
||||||
|
|
@ -1981,7 +1983,7 @@ document.addEventListener('click', function(e) {
|
||||||
var isChecked = checked.indexOf(o.value) !== -1;
|
var isChecked = checked.indexOf(o.value) !== -1;
|
||||||
cbHtml += '<label class="checkbox-multi-item">' +
|
cbHtml += '<label class="checkbox-multi-item">' +
|
||||||
'<input type="checkbox" name="' + field + '" value="' + esc(o.value) + '"' +
|
'<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>';
|
cbHtml += '</div>';
|
||||||
td.innerHTML = cbHtml;
|
td.innerHTML = cbHtml;
|
||||||
|
|
@ -1998,7 +2000,7 @@ document.addEventListener('click', function(e) {
|
||||||
var minAttr = fDef.min !== undefined ? ' min="' + esc(String(fDef.min)) + '"' : '';
|
var minAttr = fDef.min !== undefined ? ' min="' + esc(String(fDef.min)) + '"' : '';
|
||||||
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
|
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
|
||||||
td.innerHTML = '<input type="number" name="' + field + '" value="' + esc(String(val)) +
|
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') {
|
} else if (inputType === 'textarea') {
|
||||||
var textVal;
|
var textVal;
|
||||||
try { var arr = JSON.parse(val); textVal = Array.isArray(arr) ? arr.join('\n') : String(val||''); }
|
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 validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : '';
|
||||||
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint" style="display:none"></p>' : '';
|
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint" style="display:none"></p>' : '';
|
||||||
td.innerHTML = '<input type="' + inputType + '" name="' + field +
|
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'));
|
if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -210,11 +210,17 @@ def _get_ip_via_http(spec):
|
||||||
return _extract_ip(r.read().decode().strip())
|
return _extract_ip(r.read().decode().strip())
|
||||||
|
|
||||||
|
|
||||||
|
_SAFE_DIG_RE = re.compile(r'^[a-zA-Z0-9.\-_@+:\s]+$')
|
||||||
|
|
||||||
def _get_ip_via_dig(spec):
|
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.
|
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:
|
try:
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue