mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-13 15:30:27 +00:00
Merge branch 'master' into GHSA-56fq-63vj-9992-add-watch-ui-snapshot
This commit is contained in:
commit
ca41d7ca9e
23 files changed
+94
-55
No files matched your search
@@ -98,7 +98,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
backups_blueprint.register_blueprint(construct_restore_blueprint(datastore))
|
||||
backup_threads = []
|
||||
|
||||
@backups_blueprint.route("/request-backup", methods=['GET'])
|
||||
@backups_blueprint.route("/request-backup", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def request_backup():
|
||||
if any(thread.is_alive() for thread in backup_threads):
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<a class="pure-button pure-button-primary"
|
||||
href="{{ url_for('backups.request_backup') }}">{{ _('Create backup') }}</a>
|
||||
<form method="POST" action="{{ url_for('backups.request_backup') }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pure-button pure-button-primary">{{ _('Create backup') }}</button>
|
||||
</form>
|
||||
{% if available_backups %}
|
||||
{# POST + CSRF token: this permanently deletes every backup archive, so it must
|
||||
not be reachable from a bare GET (an <img src=...> on any page the operator
|
||||
|
||||
@@ -296,7 +296,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
return browsersteps_start_session
|
||||
|
||||
|
||||
@browser_steps_blueprint.route("/browsersteps_start_session", methods=['GET'])
|
||||
@browser_steps_blueprint.route("/browsersteps_start_session", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def browsersteps_start_session():
|
||||
# A new session was requested, return sessionID
|
||||
|
||||
@@ -100,7 +100,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
results = _recalc_check_status(uuid=uuid)
|
||||
return results
|
||||
|
||||
@check_proxies_blueprint.route("/<uuid_str:uuid>/start", methods=['GET'])
|
||||
@check_proxies_blueprint.route("/<uuid_str:uuid>/start", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def start_check(uuid):
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ def construct_llm_blueprint(datastore: ChangeDetectionStore):
|
||||
logger.exception("LLM model list full traceback:")
|
||||
return jsonify({'models': [], 'error': str(e)}), 400
|
||||
|
||||
@llm_blueprint.route("/test", methods=['GET'])
|
||||
@llm_blueprint.route("/test", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def llm_test():
|
||||
from flask import request
|
||||
|
||||
@@ -577,7 +577,10 @@
|
||||
if (mult.trim()) params.set('local_token_multiplier', mult.trim());
|
||||
|
||||
try {
|
||||
const resp = await fetch('{{ url_for("settings.llm.llm_test") }}?' + params);
|
||||
const resp = await fetch('{{ url_for("settings.llm.llm_test") }}?' + params, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRFToken': csrftoken}
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
result.style.cssText = 'display:block; background:rgba(39,174,96,0.08); border:1px solid rgba(39,174,96,0.3); border-radius:5px; padding:0.6em 0.85em; font-size:0.88em; line-height:1.45;';
|
||||
|
||||
@@ -455,7 +455,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_pool,
|
||||
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
@ui_blueprint.route("/language/auto-detect", methods=['GET'])
|
||||
@ui_blueprint.route("/language/auto-detect", methods=['POST'])
|
||||
def delete_locale_language_session_var_if_it_exists():
|
||||
"""Clear the session locale preference to auto-detect from browser Accept-Language header"""
|
||||
if 'locale' in session:
|
||||
|
||||
@@ -836,7 +836,7 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
# Pass the current request path so users are redirected back after login
|
||||
return redirect(url_for('login', redirect=request.path))
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
|
||||
@@ -850,7 +850,7 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
# Otherwise just go to watchlist
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
@app.route('/set-language/<locale>')
|
||||
@app.route('/set-language/<locale>', methods=['POST'])
|
||||
def set_language(locale):
|
||||
"""Set the user's preferred language in the session"""
|
||||
if not request.cookies:
|
||||
|
||||
@@ -284,7 +284,7 @@ $(document).ready(function () {
|
||||
$('#browser-steps-ui .loader .spinner').show();
|
||||
// Request a new session
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
type: "POST",
|
||||
url: browser_steps_start_url,
|
||||
statusCode: {
|
||||
400: function () {
|
||||
|
||||
@@ -73,7 +73,7 @@ $(function () {
|
||||
|
||||
// Request start, needs CSRF?
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
type: "POST",
|
||||
url: recheck_proxy_start_url,
|
||||
}).done(function (data) {
|
||||
$.each(data, function (proxy_key, state) {
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
li {
|
||||
border-bottom: 1px solid var(--color-border-table-cell);
|
||||
|
||||
>* {
|
||||
>*, >form>button {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
color: var(--color-text);
|
||||
@@ -134,6 +134,14 @@
|
||||
background: var(--color-background-menu-link-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons shrink-wrap and centre their label; anchors don't. No global
|
||||
// border-box reset here, so this must not be folded into the rule above.
|
||||
>form>button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
&#menu-pause, &#menu-mute {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,20 @@
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
// The csrf mini-form around each option is layout-transparent, so the buttons
|
||||
// stay the flex items.
|
||||
> form {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
|
||||
.pure-menu-item {
|
||||
height: initial;
|
||||
|
||||
// Mini POST forms (pause/mute/log out need a csrf_token) are layout-transparent,
|
||||
// so the button inside sits where the plain <a> used to.
|
||||
> form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
.header{color:var(--color-text-menu-heading)}.header a{color:var(--color-text-menu-heading)}.header::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;pointer-events:none;background:linear-gradient(to right, rgba(200, 200, 200, 0.02), rgba(200, 200, 200, 0.6))}ul#top-right-menu{list-style:none;margin-left:auto;padding:0;margin-top:0;margin-right:0;margin-bottom:0;display:grid;gap:1.1rem;grid-auto-flow:column;grid-auto-columns:max-content;align-items:center}ul#top-right-menu .toggle-button{padding:0}.current-diff-url{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-align:left;margin:0 .55rem;-webkit-mask-image:linear-gradient(to right, #000 calc(100% - 2.5em), transparent);mask-image:linear-gradient(to right, #000 calc(100% - 2.5em), transparent)}.current-diff-url span{overflow:visible;white-space:nowrap}.pure-menu-horizontal{padding:.55rem;display:flex;justify-content:space-between;align-items:center}.pure-menu-horizontal svg{height:1.3rem}.fi{height:1.3rem;cursor:pointer}#pure-menu-horizontal-spinner{height:2px;background:linear-gradient(-75deg, #ff6000, #ff8f00, #ffdd00, #ed0000);background-size:400% 400%;animation:gradient 200s ease infinite;opacity:.8;position:fixed;bottom:0;left:0;width:100%;z-index:100;pointer-events:none}.status-pill{display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 12px;border-radius:var(--common-round-border);border:1px solid hsla(0,0%,100%,.25);background:hsla(0,0%,100%,.1);color:var(--color-text-menu-heading);font-size:.78rem;font-weight:600;white-space:nowrap;text-decoration:none;font-family:inherit;cursor:pointer}.status-pill:hover{background:hsla(0,0%,100%,.18)}.status-pill .live-dot{width:8px;height:8px;border-radius:50%;background:#42dd53;box-shadow:0 0 0 3px rgba(66,221,83,.3);animation:status-pill-pulse 2s infinite}.status-pill.paused .live-dot{background:#e8a33d;box-shadow:0 0 0 3px rgba(232,163,61,.3);animation:none}.status-pill .action-icon{width:16px;height:16px}.status-pill.muted{opacity:.8}.status-pill.muted .action-icon{color:#e8a33d}@keyframes status-pill-pulse{0%,100%{opacity:1}50%{opacity:.4}}.menu-pop-wrap{position:relative}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:0;border-radius:var(--common-round-border);background:rgba(0,0,0,0);color:var(--color-text-menu-heading);cursor:pointer}.icon-btn svg{width:18px;height:18px;fill:none;stroke:currentColor}.icon-btn:hover{background:hsla(0,0%,100%,.14)}.menu-pop{display:none;position:absolute;top:calc(100% + 8px);right:0;min-width:220px;padding:6px;border:1px solid var(--color-border-table-cell);border-radius:12px;background:var(--color-background);box-shadow:0 18px 50px rgba(0,0,0,.18);z-index:80}.menu-pop.open{display:block}.menu-pop .mi{display:flex;align-items:center;gap:10px;width:100%;padding:9px 10px;border:0;border-radius:8px;background:rgba(0,0,0,0);color:var(--color-text);font-size:.85rem;text-align:left;text-decoration:none;white-space:nowrap;cursor:pointer}.menu-pop .mi:hover{background:var(--color-background-menu-link-hover)}.menu-pop .mi .ico{display:inline-flex;color:var(--color-text-input-description)}.menu-pop .mi .ico svg{width:16px;height:16px;fill:none;stroke:currentColor}.menu-pop .mi .right{margin-left:auto;font-size:.75rem;color:var(--color-text-input-description)}.top-menu-list .action-label{color:var(--color-text-menu-heading)}@media only screen and (max-width: 768px){.top-menu-list .action-label{display:none}}#inline-menu-extras-group{list-style:none;margin:0;padding:0;display:grid;gap:.55rem;grid-auto-flow:column;grid-auto-columns:max-content;align-items:center}@media only screen and (max-width: 980px){#nav-menu #menu-settings span{display:none}}:root{--body-main-text-size: 0.9rem;--color-white: #fff;--color-grey-50: #111;--color-grey-100: #262626;--color-grey-200: #333;--color-grey-300: #444;--color-grey-325: #555;--color-grey-350: #565d64;--color-grey-400: #666;--color-grey-500: #777;--color-grey-600: #999;--color-grey-700: #cbcbcb;--color-grey-750: #ddd;--color-grey-800: #e0e0e0;--color-grey-850: #eee;--color-grey-900: #f2f2f2;--color-black: #000;--color-dark-red: #a00;--color-light-red: #dd0000;--color-background-page: var(--color-grey-100);--color-background-gradient-first: #5ad8f7;--color-background-gradient-second: #2f50af;--color-background-gradient-third: #9150bf;--color-background: var(--color-white);--color-text: var(--color-grey-200);--color-link: #1b98f8;--color-menu-accent: #ed5900;--color-background-code: var(--color-grey-850);--color-error: var(--color-dark-red);--color-error-input: #ffebeb;--color-error-list: var(--color-light-red);--color-table-background: var(--color-background);--color-table-stripe: var(--color-grey-900);--watchlist-row-selected: #e3f0ff;--watchlist-row-hover: #f2f2f2;--color-text-tab: var(--color-white);--color-background-tab: rgba(255, 255, 255, 0.2);--color-background-tab-hover: rgba(255, 255, 255, 0.5);--color-text-tab-active: #222;--coloLine truncated
|
||||
.header{color:var(--color-text-menu-heading)}.header a{color:var(--color-text-menu-heading)}.header::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;pointer-events:none;background:linear-gradient(to right, rgba(200, 200, 200, 0.02), rgba(200, 200, 200, 0.6))}ul#top-right-menu{list-style:none;margin-left:auto;padding:0;margin-top:0;margin-right:0;margin-bottom:0;display:grid;gap:1.1rem;grid-auto-flow:column;grid-auto-columns:max-content;align-items:center}ul#top-right-menu .toggle-button{padding:0}.current-diff-url{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-align:left;margin:0 .55rem;-webkit-mask-image:linear-gradient(to right, #000 calc(100% - 2.5em), transparent);mask-image:linear-gradient(to right, #000 calc(100% - 2.5em), transparent)}.current-diff-url span{overflow:visible;white-space:nowrap}.pure-menu-horizontal{padding:.55rem;display:flex;justify-content:space-between;align-items:center}.pure-menu-horizontal svg{height:1.3rem}.fi{height:1.3rem;cursor:pointer}#pure-menu-horizontal-spinner{height:2px;background:linear-gradient(-75deg, #ff6000, #ff8f00, #ffdd00, #ed0000);background-size:400% 400%;animation:gradient 200s ease infinite;opacity:.8;position:fixed;bottom:0;left:0;width:100%;z-index:100;pointer-events:none}.status-pill{display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 12px;border-radius:var(--common-round-border);border:1px solid hsla(0,0%,100%,.25);background:hsla(0,0%,100%,.1);color:var(--color-text-menu-heading);font-size:.78rem;font-weight:600;white-space:nowrap;text-decoration:none;font-family:inherit;cursor:pointer}.status-pill:hover{background:hsla(0,0%,100%,.18)}.status-pill .live-dot{width:8px;height:8px;border-radius:50%;background:#42dd53;box-shadow:0 0 0 3px rgba(66,221,83,.3);animation:status-pill-pulse 2s infinite}.status-pill.paused .live-dot{background:#e8a33d;box-shadow:0 0 0 3px rgba(232,163,61,.3);animation:none}.status-pill .action-icon{width:16px;height:16px}.status-pill.muted{opacity:.8}.status-pill.muted .action-icon{color:#e8a33d}@keyframes status-pill-pulse{0%,100%{opacity:1}50%{opacity:.4}}.menu-pop-wrap{position:relative}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:0;border-radius:var(--common-round-border);background:rgba(0,0,0,0);color:var(--color-text-menu-heading);cursor:pointer}.icon-btn svg{width:18px;height:18px;fill:none;stroke:currentColor}.icon-btn:hover{background:hsla(0,0%,100%,.14)}.menu-pop{display:none;position:absolute;top:calc(100% + 8px);right:0;min-width:220px;padding:6px;border:1px solid var(--color-border-table-cell);border-radius:12px;background:var(--color-background);box-shadow:0 18px 50px rgba(0,0,0,.18);z-index:80}.menu-pop.open{display:block}.menu-pop .mi{display:flex;align-items:center;gap:10px;width:100%;padding:9px 10px;border:0;border-radius:8px;background:rgba(0,0,0,0);color:var(--color-text);font-size:.85rem;text-align:left;text-decoration:none;white-space:nowrap;cursor:pointer}.menu-pop .mi:hover{background:var(--color-background-menu-link-hover)}.menu-pop .mi .ico{display:inline-flex;color:var(--color-text-input-description)}.menu-pop .mi .ico svg{width:16px;height:16px;fill:none;stroke:currentColor}.menu-pop .mi .right{margin-left:auto;font-size:.75rem;color:var(--color-text-input-description)}.top-menu-list .action-label{color:var(--color-text-menu-heading)}@media only screen and (max-width: 768px){.top-menu-list .action-label{display:none}}#inline-menu-extras-group{list-style:none;margin:0;padding:0;display:grid;gap:.55rem;grid-auto-flow:column;grid-auto-columns:max-content;align-items:center}@media only screen and (max-width: 980px){#nav-menu #menu-settings span{display:none}}:root{--body-main-text-size: 0.9rem;--color-white: #fff;--color-grey-50: #111;--color-grey-100: #262626;--color-grey-200: #333;--color-grey-300: #444;--color-grey-325: #555;--color-grey-350: #565d64;--color-grey-400: #666;--color-grey-500: #777;--color-grey-600: #999;--color-grey-700: #cbcbcb;--color-grey-750: #ddd;--color-grey-800: #e0e0e0;--color-grey-850: #eee;--color-grey-900: #f2f2f2;--color-black: #000;--color-dark-red: #a00;--color-light-red: #dd0000;--color-background-page: var(--color-grey-100);--color-background-gradient-first: #5ad8f7;--color-background-gradient-second: #2f50af;--color-background-gradient-third: #9150bf;--color-background: var(--color-white);--color-text: var(--color-grey-200);--color-link: #1b98f8;--color-menu-accent: #ed5900;--color-background-code: var(--color-grey-850);--color-error: var(--color-dark-red);--color-error-input: #ffebeb;--color-error-list: var(--color-light-red);--color-table-background: var(--color-background);--color-table-stripe: var(--color-grey-900);--watchlist-row-selected: #e3f0ff;--watchlist-row-hover: #f2f2f2;--color-text-tab: var(--color-white);--color-background-tab: rgba(255, 255, 255, 0.2);--color-background-tab-hover: rgba(255, 255, 255, 0.5);--color-text-tab-active: #222;--coloLine truncated
|
||||
@@ -271,13 +271,19 @@
|
||||
<div class="modal-body">
|
||||
<div class="language-list">
|
||||
{% for locale, lang_data in available_languages.items()|sort %}
|
||||
<a href="{{ url_for('set_language', locale=locale, redirect=request.path) }}" class="language-option" data-locale="{{ locale }}">
|
||||
<span class="lang-option {{ lang_data.flag }}"></span> <span class="language-name">{{ lang_data.name }}</span>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('set_language', locale=locale, redirect=request.path) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="language-option" data-locale="{{ locale }}">
|
||||
<span class="lang-option {{ lang_data.flag }}"></span> <span class="language-name">{{ lang_data.name }}</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('ui.delete_locale_language_session_var_if_it_exists', redirect=request.path) }}" >{{ _('Auto-detect from browser') }}</a>
|
||||
<form method="POST" action="{{ url_for('ui.delete_locale_language_session_var_if_it_exists', redirect=request.path) }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="bare-btn">{{ _('Auto-detect from browser') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ _('Language support is in beta, please help us improve by opening a PR on GitHub with any updates.') }}
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
</li>
|
||||
{%- if current_user.is_authenticated -%}
|
||||
<li class="pure-menu-item menu-collapsible">
|
||||
<a href="{{ url_for('logout', redirect=request.path) }}" ><i data-feather="log-out" class="action-icon"></i> {{ _('Log out') }}</a>
|
||||
<form method="POST" action="{{ url_for('logout', redirect=request.path) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="bare-btn"><i data-feather="log-out" class="action-icon"></i> {{ _('Log out') }}</button>
|
||||
</form>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ def test_check_access_control(app, client, live_server, measure_memory_usage, da
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
res = c.get(url_for("logout"),
|
||||
res = c.post(url_for("logout"),
|
||||
follow_redirects=True)
|
||||
|
||||
assert b"Login" in res.data
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_backup(client, live_server, measure_memory_usage, datastore_path):
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Launch the thread in the background to create the backup
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("backups.request_backup"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -136,7 +136,7 @@ def test_backup_restore(client, live_server, measure_memory_usage, datastore_pat
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Create a full backup
|
||||
client.get(url_for("backups.request_backup"), follow_redirects=True)
|
||||
client.post(url_for("backups.request_backup"), follow_redirects=True)
|
||||
time.sleep(4)
|
||||
|
||||
# Download the latest backup zip
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_zh_TW(client, live_server, measure_memory_usage, datastore_path):
|
||||
# Be sure we got a session cookie
|
||||
res = client.get(url_for("watchlist.index"), follow_redirects=True)
|
||||
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="zh_Hant_TW"), # Traditional
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -21,7 +21,7 @@ def test_zh_TW(client, live_server, measure_memory_usage, datastore_path):
|
||||
assert '選擇語言'.encode() in res.data
|
||||
|
||||
# Check second set works
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="en_GB"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -30,7 +30,7 @@ def test_zh_TW(client, live_server, measure_memory_usage, datastore_path):
|
||||
assert b"Select Language" in res.data, "Second set of language worked"
|
||||
|
||||
# Check arbitration between zh_Hant_TW<->zh
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="zh"), # Simplified chinese
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -89,7 +89,7 @@ def test_language_switching(client, live_server, measure_memory_usage, datastore
|
||||
client.get(url_for("add_watch_ui.add_watch_ui_index"), follow_redirects=True)
|
||||
|
||||
# Step 1: Set the language to Italian using the /set-language endpoint
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="it"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -119,7 +119,7 @@ def test_language_switching(client, live_server, measure_memory_usage, datastore
|
||||
# NB: use 'en_GB' not 'en' — only the variants are in language_codes; the
|
||||
# plain 'en' code is silently rejected by set_language and the locale would
|
||||
# remain at 'it', defeating the round-trip assertion below.
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="en_GB"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -152,7 +152,7 @@ def test_invalid_locale(client, live_server, measure_memory_usage, datastore_pat
|
||||
# bare 'en' is NOT in language_codes and is silently rejected by
|
||||
# set_language, so passing it here would leave the session locale unset
|
||||
# and let the (unrelated) Accept-Language fallback decide what renders.
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="en_GB"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -160,7 +160,7 @@ def test_invalid_locale(client, live_server, measure_memory_usage, datastore_pat
|
||||
assert res.status_code == 200
|
||||
|
||||
# Try to set an invalid locale
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="invalid_locale_xyz"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -190,7 +190,7 @@ def test_language_persistence_in_session(client, live_server, measure_memory_usa
|
||||
client.get(url_for("watchlist.index"), follow_redirects=True)
|
||||
|
||||
# Set language to Italian
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="it"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -215,7 +215,7 @@ def test_language_persistence_in_session(client, live_server, measure_memory_usa
|
||||
assert sess.get('locale') == 'it', "Locale should be set in session"
|
||||
|
||||
# Call auto-detect to clear the locale
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("ui.delete_locale_language_session_var_if_it_exists"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -254,7 +254,7 @@ def test_set_language_with_redirect(client, live_server, measure_memory_usage, d
|
||||
client.get(url_for("watchlist.index"), follow_redirects=True)
|
||||
|
||||
# Set language with a redirect parameter (simulating language change from /settings)
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="de", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
@@ -268,7 +268,7 @@ def test_set_language_with_redirect(client, live_server, measure_memory_usage, d
|
||||
assert sess.get('locale') == 'de'
|
||||
|
||||
# Test with invalid locale (should still redirect safely)
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="invalid_locale", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
@@ -276,7 +276,7 @@ def test_set_language_with_redirect(client, live_server, measure_memory_usage, d
|
||||
assert '/settings' in res.location
|
||||
|
||||
# Test with malicious redirect (should default to watchlist)
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="en", redirect="https://evil.com"),
|
||||
follow_redirects=False
|
||||
)
|
||||
@@ -296,7 +296,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
client.get(url_for("watchlist.index"), follow_redirects=True)
|
||||
|
||||
# Test Italian translations
|
||||
res = client.get(url_for("set_language", locale="it"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="it"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -312,7 +312,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
assert b"Time Between Check" not in res.data, "Should not have English 'Time Between Check'"
|
||||
|
||||
# Test Korean translations
|
||||
res = client.get(url_for("set_language", locale="ko"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="ko"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -332,7 +332,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
assert b"Time Between Check" not in res.data, "Should not have English 'Time Between Check'"
|
||||
|
||||
# Test Chinese Simplified translations
|
||||
res = client.get(url_for("set_language", locale="zh"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="zh"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -348,7 +348,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
assert b"Time Between Check" not in res.data, "Should not have English 'Time Between Check'"
|
||||
|
||||
# Test German translations
|
||||
res = client.get(url_for("set_language", locale="de"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="de"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -363,7 +363,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
assert b"Time Between Check" not in res.data, "Should not have English 'Time Between Check'"
|
||||
|
||||
# Test Russian translations
|
||||
res = client.get(url_for("set_language", locale="ru"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="ru"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -378,7 +378,7 @@ def test_time_unit_translations(client, live_server, measure_memory_usage, datas
|
||||
assert b"Time Between Check" not in res.data, "Should not have English 'Time Between Check'"
|
||||
|
||||
# Test Traditional Chinese (zh_Hant_TW) translations
|
||||
res = client.get(url_for("set_language", locale="zh_Hant_TW"), follow_redirects=True)
|
||||
res = client.post(url_for("set_language", locale="zh_Hant_TW"), follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
res = client.get(url_for("settings.settings_page"), follow_redirects=True)
|
||||
@@ -627,7 +627,7 @@ def test_session_locale_overrides_accept_language(client, live_server, measure_m
|
||||
"Expected Taiwan flag 'fi fi-tw' from auto-detect"
|
||||
|
||||
# Step 2: User explicitly selects Korean language
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="ko"),
|
||||
headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, # Browser still sends zh-TW
|
||||
follow_redirects=True
|
||||
@@ -700,7 +700,7 @@ def test_clear_history_translated_confirmation(client, live_server, measure_memo
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Set language to German
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="de"),
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -726,7 +726,7 @@ def test_clear_history_translated_confirmation(client, live_server, measure_memo
|
||||
"German confirmation word 'loschen' should be accepted (issue #3865)"
|
||||
|
||||
# Switch back to English and verify English word still works
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("set_language", locale="en_US"),
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_language_endpoints_work_for_anonymous_users(client, live_server, measur
|
||||
follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
|
||||
client.get(url_for("logout"), follow_redirects=True)
|
||||
client.post(url_for("logout"), follow_redirects=True)
|
||||
|
||||
# Both language links are rendered on the login page, so both must be reachable
|
||||
res = client.get(url_for("login"))
|
||||
@@ -33,13 +33,13 @@ def test_language_endpoints_work_for_anonymous_users(client, live_server, measur
|
||||
assert b'language-selector' in res.data, "Language modal trigger should render for anonymous users"
|
||||
|
||||
# Picking a specific language must not redirect to the login page
|
||||
res = client.get(url_for("set_language", locale="de"), follow_redirects=False)
|
||||
res = client.post(url_for("set_language", locale="de"), follow_redirects=False)
|
||||
assert res.status_code == 302
|
||||
assert '/login' not in res.headers.get("Location", ""), \
|
||||
"set_language must not bounce anonymous users to /login"
|
||||
|
||||
# ...and neither must clearing it back to auto-detect
|
||||
res = client.get(url_for("ui.delete_locale_language_session_var_if_it_exists"), follow_redirects=False)
|
||||
res = client.post(url_for("ui.delete_locale_language_session_var_if_it_exists"), follow_redirects=False)
|
||||
assert res.status_code == 302
|
||||
assert '/login' not in res.headers.get("Location", ""), \
|
||||
"Auto-detect must not bounce anonymous users to /login (it renders on the login page)"
|
||||
@@ -393,12 +393,12 @@ def test_llm_models_endpoint_blocks_private_api_base(
|
||||
|
||||
def test_llm_test_endpoint_blocks_private_api_base(
|
||||
client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""GET /settings/llm/test must refuse api_base pointing at private/loopback
|
||||
"""POST /settings/llm/test must refuse api_base pointing at private/loopback
|
||||
hosts and must never reach litellm.completion()."""
|
||||
monkeypatch.delenv('ALLOW_IANA_RESTRICTED_ADDRESSES', raising=False)
|
||||
|
||||
for bad in _SSRF_PRIVATE_HOSTS:
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for('settings.llm.llm_test'),
|
||||
query_string={'model': 'openai/gpt-4', 'api_base': bad},
|
||||
)
|
||||
@@ -530,7 +530,7 @@ def test_llm_test_refuses_to_leak_stored_key_to_different_api_base(
|
||||
monkeypatch.setattr(llm_client, 'completion',
|
||||
lambda **kw: calls.append(kw) or ('', 0, 0, 0))
|
||||
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for('settings.llm.llm_test'),
|
||||
query_string={
|
||||
'model': 'gpt-4o-mini',
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_rss_tag_feed_ignores_security_token(client, live_server, datastore_path
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Logout
|
||||
client.get(url_for("logout"), follow_redirects=True)
|
||||
client.post(url_for("logout"), follow_redirects=True)
|
||||
|
||||
# Request the tag RSS feed WITH the token
|
||||
res = client.get(
|
||||
|
||||
@@ -440,7 +440,7 @@ def test_login_redirect_with_password(client, live_server, measure_memory_usage,
|
||||
assert b"evil.com" not in res.data
|
||||
|
||||
# Logout for cleanup
|
||||
client.get(url_for("logout"))
|
||||
client.post(url_for("logout"))
|
||||
|
||||
# Test 5: Incorrect password with redirect should stay on login page
|
||||
res = client.post(
|
||||
@@ -483,7 +483,7 @@ def test_login_redirect_from_protected_page(client, live_server, measure_memory_
|
||||
client.application.config['DATASTORE'].data['settings']['application']['password'] = salted_pass
|
||||
|
||||
# Logout to ensure we're not authenticated
|
||||
client.get(url_for("logout"))
|
||||
client.post(url_for("logout"))
|
||||
|
||||
# Try to access a protected page (edit page for first watch)
|
||||
res = client.get(
|
||||
@@ -524,7 +524,7 @@ def test_login_redirect_from_protected_page(client, live_server, measure_memory_
|
||||
assert b'Edit' in res.data or b'Watching' in res.data
|
||||
|
||||
# Cleanup
|
||||
client.get(url_for("logout"))
|
||||
client.post(url_for("logout"))
|
||||
del client.application.config['DATASTORE'].data['settings']['application']['password']
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ def test_logout_with_redirect(client, live_server, measure_memory_usage, datasto
|
||||
assert res.status_code == 200
|
||||
|
||||
# Now logout with a redirect parameter (simulating logout from /settings)
|
||||
res = client.get(
|
||||
res = client.post(
|
||||
url_for("logout", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
@@ -961,7 +961,7 @@ def test_ghsa_8757_69j2_hx56_backup_restore_history_path_traversal(client, live_
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Download a legitimate backup to use as a template
|
||||
client.get(url_for("backups.request_backup"), follow_redirects=True)
|
||||
client.post(url_for("backups.request_backup"), follow_redirects=True)
|
||||
time.sleep(4)
|
||||
res = client.get(url_for("backups.download_backup", filename="latest"), follow_redirects=True)
|
||||
assert res.content_type == "application/zip"
|
||||
|
||||
Reference in new issue
Block a user