Skip to content

Commit 69f34c2

Browse files
committed
Add custom 404 page support for sites
Allow sites to configure a custom entry to display when visitors access non-existent pages. Custom 404 entries must be published and not password-protected to be used. Falls back to default 404 behavior when no custom entry is configured or when the custom entry is not publicly accessible. REDMINE-21049
1 parent 175a9f4 commit 69f34c2

File tree

9 files changed

+186
-11
lines changed

9 files changed

+186
-11
lines changed

app/controllers/concerns/pageflow/controller_delegation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module ControllerDelegation
88
# the response.
99
def delegate_to_rack_app!(app)
1010
result = app.call(request.env)
11-
yield(*result) if block_given?
11+
result = yield(result) if block_given?
1212

1313
throw(:delegate, result)
1414
end

app/controllers/pageflow/application_controller.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,9 @@ class ApplicationController < ActionController::Base
2222

2323
rescue_from ActiveRecord::RecordNotFound do |exception|
2424
debug_log_with_backtrace(exception)
25+
2526
respond_to do |format|
26-
format.html do
27-
begin
28-
render file: Rails.public_path.join('pageflow', 'error_pages', '404.html'), status: :not_found
29-
rescue ActionView::MissingTemplate => exception
30-
debug_log_with_backtrace(exception)
31-
head :not_found
32-
end
33-
end
27+
format.html { render_static_404_error_page }
3428
format.any { head :not_found }
3529
end
3630
end
@@ -61,6 +55,14 @@ def locale_from_accept_language_header
6155
http_accept_language.compatible_language_from(I18n.available_locales)
6256
end
6357

58+
def render_static_404_error_page
59+
render file: Rails.public_path.join('pageflow', 'error_pages', '404.html'),
60+
status: :not_found
61+
rescue ActionView::MissingTemplate => e
62+
debug_log_with_backtrace(e)
63+
head :not_found
64+
end
65+
6466
def debug_log_with_backtrace(exception)
6567
Rails.logger.debug exception
6668
backtrace = ''

app/controllers/pageflow/entries_controller.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def show
2525
return unless check_entry_password_protection(entry)
2626

2727
delegate_to_entry_type_frontend_app!(entry)
28+
rescue ActiveRecord::RecordNotFound
29+
render_custom_or_static_404_error_page
2830
end
2931
end
3032
end
@@ -100,15 +102,18 @@ def entry_redirect(entry)
100102
Pageflow.config.public_entry_redirect.call(entry, request)
101103
end
102104

103-
def delegate_to_entry_type_frontend_app!(entry)
105+
def delegate_to_entry_type_frontend_app!(entry, override_status: nil)
104106
EntriesControllerEnvHelper.add_entry_info_to_env(request.env, entry: entry, mode: :published)
105107

106-
delegate_to_rack_app!(entry.entry_type.frontend_app) do |_status, headers, _body|
108+
delegate_to_rack_app!(entry.entry_type.frontend_app) do |result|
109+
status, headers, body = result
107110
config = Pageflow.config_for(entry)
108111

109112
allow_iframe_for_embed(headers)
110113
apply_additional_headers(entry, config, headers)
111114
apply_cache_control(entry, config, headers)
115+
116+
[override_status || status, headers, body]
112117
end
113118
end
114119

@@ -128,5 +133,17 @@ def apply_additional_headers(entry, config, headers)
128133
config.additional_public_entry_headers.for(entry, request)
129134
)
130135
end
136+
137+
def render_custom_or_static_404_error_page
138+
site = Site.for_request(request).first
139+
140+
if site&.custom_404_entry&.published_without_password_protection?
141+
entry = PublishedEntry.new(site.custom_404_entry)
142+
delegate_to_entry_type_frontend_app!(entry, override_status: 404)
143+
else
144+
# Fallback to ApplicationController's handler method
145+
render_static_404_error_page
146+
end
147+
end
131148
end
132149
end

app/models/concerns/pageflow/entry_publication_states.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def published_with_password_protection?
3838
published? && published_revision.password_protected?
3939
end
4040

41+
def published_without_password_protection?
42+
published? && !published_revision.password_protected?
43+
end
44+
4145
def published?
4246
published_revision.present?
4347
end

app/models/pageflow/site.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module Pageflow
22
class Site < ApplicationRecord
33
belongs_to :account
4+
belongs_to :custom_404_entry, class_name: 'Entry', optional: true
45

56
has_many :entry_templates, dependent: :destroy
67
has_many :entries
@@ -11,6 +12,7 @@ class Site < ApplicationRecord
1112

1213
validates :account, presence: true
1314
validates_inclusion_of :cutoff_mode_name, in: :available_cutoff_mode_names, allow_blank: true
15+
validate :custom_404_entry_belongs_to_same_site
1416

1517
delegate :enabled_feature_names, to: :account
1618

@@ -74,5 +76,11 @@ def self.ransackable_associations(_auth_object = nil)
7476
def available_cutoff_mode_names
7577
Pageflow.config_for(account).cutoff_modes.names
7678
end
79+
80+
def custom_404_entry_belongs_to_same_site
81+
return unless custom_404_entry.present?
82+
83+
errors.add(:custom_404_entry, :must_belong_to_same_site) if custom_404_entry.site_id != id
84+
end
7785
end
7886
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddCustom404EntryToSites < ActiveRecord::Migration[7.1]
2+
def change
3+
add_reference :pageflow_sites, :custom_404_entry, null: true
4+
end
5+
end

spec/models/concerns/pageflow/entry_publication_states_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,5 +249,45 @@ module Pageflow
249249
end
250250
end
251251
end
252+
253+
describe '#published_with_password_protection?' do
254+
it 'returns true for entry published with password protection' do
255+
entry = create(:entry, :published_with_password)
256+
257+
expect(entry.published_with_password_protection?).to eq(true)
258+
end
259+
260+
it 'returns false for entry published without password protection' do
261+
entry = create(:entry, :published)
262+
263+
expect(entry.published_with_password_protection?).to eq(false)
264+
end
265+
266+
it 'returns false for non-published entry' do
267+
entry = create(:entry)
268+
269+
expect(entry.published_with_password_protection?).to eq(false)
270+
end
271+
end
272+
273+
describe '#published_without_password_protection?' do
274+
it 'returns true for entry published without password protection' do
275+
entry = create(:entry, :published)
276+
277+
expect(entry.published_without_password_protection?).to eq(true)
278+
end
279+
280+
it 'returns false for entry published with password protection' do
281+
entry = create(:entry, :published_with_password)
282+
283+
expect(entry.published_without_password_protection?).to eq(false)
284+
end
285+
286+
it 'returns false for non-published entry' do
287+
entry = create(:entry)
288+
289+
expect(entry.published_without_password_protection?).to eq(false)
290+
end
291+
end
252292
end
253293
end

spec/models/pageflow/site_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,31 @@ module Pageflow
161161
expect(result).to eq([matching_site])
162162
end
163163
end
164+
165+
describe 'custom_404_entry validation' do
166+
it 'is valid when custom_404_entry belongs to same site' do
167+
site = create(:site)
168+
entry = create(:entry, site: site)
169+
site.custom_404_entry = entry
170+
171+
expect(site).to be_valid
172+
end
173+
174+
it 'is invalid when custom_404_entry belongs to different site' do
175+
site1 = create(:site)
176+
site2 = create(:site)
177+
entry = create(:entry, site: site2)
178+
site1.custom_404_entry = entry
179+
180+
site1.valid?
181+
expect(site1.errors).to include(:custom_404_entry)
182+
end
183+
184+
it 'is valid when custom_404_entry is nil' do
185+
site = build(:site)
186+
187+
expect(site).to be_valid
188+
end
189+
end
164190
end
165191
end

spec/requests/pageflow/entries_show_request_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,79 @@ module Pageflow
216216
expect(response.body).to include('Not Found')
217217
end
218218

219+
it 'renders custom 404 entry when site has custom_404_entry configured' do
220+
site = create(:site, cname: 'my.example.com')
221+
custom_404_entry = create(:entry, :published,
222+
type_name: 'test',
223+
title: 'Custom 404',
224+
site:)
225+
site.update!(custom_404_entry:)
226+
227+
get('http://my.example.com/non-existent-entry')
228+
229+
expect(response.status).to eq(404)
230+
expect(response.body).to include('Custom 404 published rendered by entry type frontend app')
231+
end
232+
233+
it 'falls back to default 404 when site has no custom_404_entry' do
234+
create(:site, cname: 'my.example.com')
235+
236+
get('http://my.example.com/non-existent-entry')
237+
238+
expect(response.status).to eq(404)
239+
expect(response.body).to include("The page you've requested cannot be found.")
240+
end
241+
242+
it 'renders site-specific custom 404 entry' do
243+
site1 = create(:site, cname: 'site1.example.com')
244+
site2 = create(:site, cname: 'site2.example.com')
245+
246+
custom_404_entry1 = create(:entry, :published,
247+
type_name: 'test',
248+
title: 'Site 1 Not Found',
249+
site: site1)
250+
custom_404_entry2 = create(:entry, :published,
251+
type_name: 'test',
252+
title: 'Site 2 Not Found',
253+
site: site2)
254+
255+
site1.update!(custom_404_entry: custom_404_entry1)
256+
site2.update!(custom_404_entry: custom_404_entry2)
257+
258+
get('http://site1.example.com/non-existent')
259+
expect(response.status).to eq(404)
260+
expect(response.body).to include('Site 1 Not Found')
261+
262+
get('http://site2.example.com/non-existent')
263+
expect(response.status).to eq(404)
264+
expect(response.body).to include('Site 2 Not Found')
265+
end
266+
267+
it 'falls back to default 404 when custom_404_entry is not published' do
268+
site = create(:site, cname: 'my.example.com')
269+
unpublished_404_entry = create(:entry, type_name: 'test', site:)
270+
site.update!(custom_404_entry: unpublished_404_entry)
271+
272+
get('http://my.example.com/non-existent-entry')
273+
274+
expect(response.status).to eq(404)
275+
expect(response.body).to include("The page you've requested cannot be found.")
276+
end
277+
278+
it 'falls back to default 404 when custom_404_entry is password protected' do
279+
site = create(:site, cname: 'my.example.com')
280+
password_protected_404_entry = create(:entry, :published_with_password,
281+
type_name: 'test',
282+
password: 'secret123',
283+
site:)
284+
site.update!(custom_404_entry: password_protected_404_entry)
285+
286+
get('http://my.example.com/non-existent-entry')
287+
288+
expect(response.status).to eq(404)
289+
expect(response.body).to include("The page you've requested cannot be found.")
290+
end
291+
219292
it 'responds with forbidden for entry published with password' do
220293
entry = create(:entry, :published_with_password,
221294
password: 'abc123abc',

0 commit comments

Comments
 (0)