0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/app/views/layouts/application.html.erb
Régis Hanol f5cd83cab6
FIX: Prevent browsers from restoring stale documents in bfcache mode (#42207)
Previously, enabling `cache_control_bfcache_compatibility` (the
experiment currently running on meta, #38763) made HTML documents
storable in the browser HTTP cache, and browsers skip revalidation on
history navigations — so back/forward, session restore, and
discarded-tab reloads could resurrect a days-old document: stale topic
lists that only get older, and logged-in UI shown to logged-out sessions
(and vice versa). Reported in https://meta.discourse.org/t/400459.

This change keeps the documents out of shared caches (`no-cache,
private`), reloads any document that was served from the HTTP cache on a
history navigation (navigation entry with `type === "back_forward"` and
`transferSize === 0` — a forced reload gets type `"reload"`, so it
cannot loop), and validates the session on `pageshow` restores from the
back/forward cache, reloading when the logged-in user no longer matches
the one the page booted with. Both client-side checks only run when the
setting is enabled; the full rationale (browser-engine specifics, why
`transferSize` rather than `deliveryType`, why `fetch` rather than
`ajax`) is in the commit message.

Reproduced and verified end-to-end in Chromium and Firefox: with the
setting enabled, `goBack()` served `/latest` with zero network contact —
stale list, wrong login state, and the exact `403
/u/:username/private-message-topic-tracking-state` errors from the meta
report; with this change the same navigation heals with a single
automatic reload, and a control run with the setting disabled behaves as
before.
2026-07-31 17:24:47 +02:00

172 lines
5.9 KiB
Text
Vendored

<!DOCTYPE html>
<html lang="<%= html_lang %>" class="<%= html_classes %>">
<head>
<meta charset="utf-8">
<title><%= title_content %></title>
<meta name="description" content="<%= description_content %>">
<meta name="discourse_theme_id" content="<%= theme_id %>">
<meta name="discourse_current_homepage" content="<%= current_homepage %>">
<%- if GlobalSetting.early_hint_header_mode == "prefetch" %>
<%= render partial: "common/discourse_preload_stylesheet" %>
<%- end %>
<%= render partial: "layouts/head" %>
<%= discourse_csrf_tags %>
<%- if SiteSetting.enable_escaped_fragments? %>
<meta name="fragment" content="!">
<%- end %>
<%- if shared_session_key %>
<meta name="shared_session_key" content="<%= shared_session_key %>">
<%- end %>
<%= render("layouts/plugin_js", opts: {
include_official: allow_plugins?,
include_unofficial: allow_third_party_plugins?,
request: request,
include_admin_asset: staff?
})
%>
<%= build_plugin_html 'server:before-script-load' %>
<%- if bfcache_compatibility_mode? %>
<script id="bfcache-stale-document-check" nonce="<%= csp_nonce_placeholder %>">
{
const nav = performance.getEntriesByType("navigation")[0];
if (nav?.type === "back_forward" && nav.transferSize === 0) {
location.reload();
}
}
</script>
<%- end %>
<script nonce="<%= csp_nonce_placeholder %>">
window.EmberENV ??= {};
window.EmberENV._DEFAULT_ASYNC_OBSERVERS = true;
</script>
<%= preload_script_url ExtraLocalesController.url("main"), type_module: true %>
<%= preload_script_url ExtraLocalesController.url("mf"), type_module: true %>
<%- if ExtraLocalesController.client_overrides_exist? %>
<%= preload_script_url ExtraLocalesController.url("overrides"), type_module: true %>
<%- end %>
<%- if staff? %>
<%= preload_script_url ExtraLocalesController.url("admin"), type_module: true %>
<%- end %>
<%- if admin? %>
<%= preload_script_url ExtraLocalesController.url("wizard"), type_module: true %>
<%- end %>
<%- unless customization_disabled? %>
<%= theme_translations_lookup %>
<%- end %>
<link rel="preload" href="<%= script_asset_path "js/browser-update" %>" as="script" nonce="<%= csp_nonce_placeholder %>">
<%= preload_script 'js/browser-detect' %>
<%= preload_script "vendor", type_module: true %>
<%= module_preloads_for "discourse", (staff? ? "admin/compat-modules" : nil) %>
<%- unless customization_disabled? %>
<%- theme_js_assets.each do |asset| %>
<link rel="modulepreload" href="<%= asset[:url] %>" data-theme-id="<%= asset[:theme_id] %>" nonce="<%= csp_nonce_placeholder %>">
<%- end %>
<%= theme_lookup("head_tag") %>
<%- end %>
<%= render_google_tag_manager_head_code %>
<%= render_google_universal_analytics_code %>
<%= render_adobe_analytics_tags_code %>
<link id="manifest-link" rel="manifest" href=<%= manifest_url %> crossorigin="use-credentials">
<%- if include_ios_native_app_banner? %>
<meta name="apple-itunes-app" content="app-id=<%= SiteSetting.ios_app_id %><%= ios_app_argument %>">
<%- end %>
<%= yield :head %>
<%= build_plugin_html 'server:before-head-close' %>
<%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
<meta name="discourse/config/environment" content="<%=u discourse_config_environment %>" />
<%- if authentication_data %>
<meta id="data-authentication" data-authentication-data="<%= authentication_data %>">
<%- end %>
</head>
<body class="<%= body_classes %>">
<%- if include_splash_screen? %>
<%= render partial: "common/discourse_splash" %>
<%- end %>
<discourse-assets>
<discourse-assets-stylesheets>
<%= render partial: "common/discourse_stylesheet" %>
</discourse-assets-stylesheets>
<discourse-assets-json>
<script type="application/json" id="data-preloaded"><%= raw(preloaded_json) %></script>
</discourse-assets-json>
<discourse-assets-icons></discourse-assets-icons>
</discourse-assets>
<%- if allow_plugins? %>
<%= build_plugin_html 'server:after-body-open' %>
<%- end -%>
<%= render_google_tag_manager_body_code %>
<noscript data-path="<%= request.env['PATH_INFO'] %>">
<%= escape_noscript do %>
<%= render partial: "layouts/noscript_header" %>
<div id="main-outlet" class="wrap" role="main">
<!-- preload-content: -->
<%= yield %>
<!-- :preload-content -->
</div>
<%= render partial: "layouts/noscript_footer" %>
<% end %>
</noscript>
<%- unless customization_disabled? %>
<%= theme_lookup("header") %>
<%- end %>
<%- if allow_plugins? %>
<%= build_plugin_html 'server:header' %>
<%- end %>
<section id='main'>
</section>
<% unless current_user %>
<form id='hidden-login-form' method="post" action="<%=main_app.login_path%>" style="display: none;">
<input name="username" type="text" id="signin_username">
<input name="password" type="password" id="signin_password">
<input name="redirect" type="hidden">
<input type="submit" id="signin-button" value="<%= t 'log_in' %>">
</form>
<% end %>
<%= preload_script "discourse", type_module: true %>
<script defer src="<%= script_asset_path "js/browser-update" %>" nonce="<%= csp_nonce_placeholder %>"></script>
<%= yield :data %>
<%- unless customization_disabled? %>
<%= theme_lookup("body_tag") %>
<%- end %>
<%- if allow_plugins? %>
<%= build_plugin_html 'server:before-body-close' %>
<%- end %>
<script nonce="<%= csp_nonce_placeholder %>">/* Workaround for https://bugs.webkit.org/show_bug.cgi?id=209261 */</script>
</body>
</html>