summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/nodes_controller.rb2
-rw-r--r--app/models/node.rb27
-rw-r--r--app/models/page.rb36
-rw-r--r--config/locales/de.yml4
-rw-r--r--config/locales/en.yml4
-rw-r--r--db/schema.rb4
6 files changed, 71 insertions, 6 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index ef47f258..47b8573a 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -282,7 +282,7 @@ class NodesController < ApplicationController
282 def page_params 282 def page_params
283 params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, 283 params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name,
284 :published_at, :user_id, :slug, :parent_node_id, 284 :published_at, :user_id, :slug, :parent_node_id,
285 :external_url) 285 :external_url, :redirect, :redirect_node_id)
286 end 286 end
287 287
288 def find_node 288 def find_node
diff --git a/app/models/node.rb b/app/models/node.rb
index 6fc6c3dc..52e06d2d 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -236,6 +236,32 @@ class Node < ApplicationRecord
236 raise ActiveRecord::RecordInvalid.new(self) 236 raise ActiveRecord::RecordInvalid.new(self)
237 end 237 end
238 238
239 if self.draft.redirect.present?
240 if self.draft.redirect_node_id == self.id
241 errors.add(:base, :redirect_to_self)
242 raise ActiveRecord::RecordInvalid.new(self)
243 end
244
245 if self.draft.redirect_node_id.present?
246 target = Node.find_by(:id => self.draft.redirect_node_id)
247
248 unless target
249 errors.add(:base, :redirect_target_missing)
250 raise ActiveRecord::RecordInvalid.new(self)
251 end
252
253 if target.head&.redirect.present?
254 errors.add(:base, :redirect_to_redirect)
255 raise ActiveRecord::RecordInvalid.new(self)
256 end
257 end
258
259 if Page.redirecting_to(self.id).exists?
260 errors.add(:base, :redirect_would_chain)
261 raise ActiveRecord::RecordInvalid.new(self)
262 end
263 end
264
239 path_before = self.unique_name 265 path_before = self.unique_name
240 266
241 ActiveRecord::Base.transaction do 267 ActiveRecord::Base.transaction do
@@ -602,6 +628,7 @@ class Node < ApplicationRecord
602 def self.search(term, _ = {}) 628 def self.search(term, _ = {})
603 joins(head: :translations) 629 joins(head: :translations)
604 .where("page_translations.search_vector @@ plainto_tsquery('simple', ?)", term) 630 .where("page_translations.search_vector @@ plainto_tsquery('simple', ?)", term)
631 .where(:pages => { :redirect => nil })
605 .distinct 632 .distinct
606 end 633 end
607 634
diff --git a/app/models/page.rb b/app/models/page.rb
index c2dc227b..9115ccb0 100644
--- a/app/models/page.rb
+++ b/app/models/page.rb
@@ -4,6 +4,7 @@ class Page < ApplicationRecord
4 4
5 PUBLIC_TEMPLATE_PATH = File.join(%w(custom page_templates public)) 5 PUBLIC_TEMPLATE_PATH = File.join(%w(custom page_templates public))
6 FULL_PUBLIC_TEMPLATE_PATH = Rails.root.join('app', 'views', PUBLIC_TEMPLATE_PATH) 6 FULL_PUBLIC_TEMPLATE_PATH = Rails.root.join('app', 'views', PUBLIC_TEMPLATE_PATH)
7 REDIRECT_MODES = %w[temporary permanent].freeze
7 8
8 # Mixins and Plugins 9 # Mixins and Plugins
9 acts_as_taggable 10 acts_as_taggable
@@ -23,6 +24,7 @@ class Page < ApplicationRecord
23 validates :external_url, :format => { :with => %r{\Ahttps?://}i, 24 validates :external_url, :format => { :with => %r{\Ahttps?://}i,
24 :allow_blank => true, 25 :allow_blank => true,
25 :message => :must_be_http } 26 :message => :must_be_http }
27 validates :redirect, :inclusion => { :in => REDIRECT_MODES }, :allow_nil => true
26 validates_format_of :slug, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/, 28 validates_format_of :slug, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/,
27 :unless => -> { slug.blank? } 29 :unless => -> { slug.blank? }
28 validate :page_slug_not_reserved 30 validate :page_slug_not_reserved
@@ -181,7 +183,6 @@ class Page < ApplicationRecord
181 end 183 end
182 184
183 def valid_template 185 def valid_template
184
185 if template_name && template_exists? 186 if template_name && template_exists?
186 public_template_path 187 public_template_path
187 else 188 else
@@ -212,15 +213,16 @@ class Page < ApplicationRecord
212 self.slug = page.slug 213 self.slug = page.slug
213 self.parent_node_id = page.parent_node_id 214 self.parent_node_id = page.parent_node_id
214 self.external_url = page.external_url 215 self.external_url = page.external_url
216 self.redirect = page.redirect
217 self.redirect_node_id = page.redirect_node_id
215 self.tag_list = page.tag_list 218 self.tag_list = page.tag_list
216 self.template_name ||= page.template_name 219 self.template_name ||= page.template_name
217 self.published_at = page.published_at 220 self.published_at = page.published_at
218 221
219 # Clone translated attributes -- update each locale in place rather 222 # Clone translated attributes, update each locale in place rather
220 # than delete-and-recreate, so a locale whose content is genuinely 223 # than delete-and-recreate, so a locale whose content is genuinely
221 # unchanged keeps its real created_at/updated_at instead of looking 224 # unchanged keeps its real created_at/updated_at instead of looking
222 # freshly touched on every single save (which was silently defeating 225 # freshly touched on every single save.
223 # Page.find_with_outdated_translations' whole staleness comparison).
224 # search_vector is excluded deliberately: it's DB-trigger-maintained 226 # search_vector is excluded deliberately: it's DB-trigger-maintained
225 # from title/abstract, not real content, and comparing a precomputed 227 # from title/abstract, not real content, and comparing a precomputed
226 # tsvector risked a false "changed" from representation noise alone. 228 # tsvector risked a false "changed" from representation noise alone.
@@ -309,6 +311,32 @@ class Page < ApplicationRecord
309 published_at.nil? ? true : published_at < Time.now 311 published_at.nil? ? true : published_at < Time.now
310 end 312 end
311 313
314 # The destination this page sends visitors to, or nil. An internal target
315 # wins over an external one. A target that is restricted or has no head is
316 # no destination at all, so the page renders itself rather than linking to
317 # nothing. The banner partial calls this too, so the precedence cannot
318 # drift between the redirect and the link.
319 def redirect_target
320 return nil if redirect.blank?
321
322 if redirect_node_id.present?
323 node = Node.find_by(:id => redirect_node_id)
324 return nil unless node&.head && !node.restricted?
325 return node.unique_name
326 end
327
328 external_url.presence
329 end
330
331 def redirect_status
332 redirect == "permanent" ? :moved_permanently : :found
333 end
334
335 # Nodes whose published page redirects here
336 def self.redirecting_to(node_id)
337 Node.where(:head_id => where(:redirect_node_id => node_id).select(:id))
338 end
339
312 # The address this page will have once published. 340 # The address this page will have once published.
313 def prospective_unique_name 341 def prospective_unique_name
314 return nil if parent_node_id.nil? 342 return nil if parent_node_id.nil?
diff --git a/config/locales/de.yml b/config/locales/de.yml
index bb99ac30..e94dfb7e 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -140,6 +140,10 @@ de:
140 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" 140 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden"
141 not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern" 141 not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern"
142 attach_with_autosave: "An einen Node mit ungespeicherten Änderungen im Editor können keine Assets angehängt werden" 142 attach_with_autosave: "An einen Node mit ungespeicherten Änderungen im Editor können keine Assets angehängt werden"
143 redirect_to_self: "Eine Seite kann nicht auf sich selbst weiterleiten"
144 redirect_target_missing: "Die Seite, auf die weitergeleitet werden sollte, gibt es nicht mehr."
145 redirect_to_redirect: "Auf eine Seite, die selbst weiterleitet, kann nicht weitergeleitet werden"
146 redirect_would_chain: "Eine andere Seite leitet bereits hierher weiter, daher kann diese Seite nicht weiterleiten"
143 page: 147 page:
144 attributes: 148 attributes:
145 slug: 149 slug:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cce5f12a..54d973e2 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -91,6 +91,10 @@ en:
91 attach_in_trash: "Cannot attach assets to a node in the Trash" 91 attach_in_trash: "Cannot attach assets to a node in the Trash"
92 not_permitted: "Only Redaktion members may change published content in this section" 92 not_permitted: "Only Redaktion members may change published content in this section"
93 attach_with_autosave: "Cannot attach assets to a node with unsaved changes in the editor" 93 attach_with_autosave: "Cannot attach assets to a node with unsaved changes in the editor"
94 redirect_to_self: "A page cannot redirect to itself"
95 redirect_target_missing: "The page this was to redirect to no longer exists."
96 redirect_to_redirect: "Cannot redirect to a page that itself redirects"
97 redirect_would_chain: "Another page already redirects here, so this page cannot redirect onward"
94 page: 98 page:
95 attributes: 99 attributes:
96 slug: 100 slug:
diff --git a/db/schema.rb b/db/schema.rb
index 05dc1ef1..888e96f0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
10# 10#
11# It's strongly recommended that you check this file into your version control system. 11# It's strongly recommended that you check this file into your version control system.
12 12
13ActiveRecord::Schema[8.1].define(version: 2026_08_09_135746) do 13ActiveRecord::Schema[8.1].define(version: 2026_08_19_124104) do
14 # These are extensions that must be enabled in order to support this database 14 # These are extensions that must be enabled in order to support this database
15 enable_extension "pg_catalog.plpgsql" 15 enable_extension "pg_catalog.plpgsql"
16 16
@@ -150,6 +150,8 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_09_135746) do
150 t.integer "parent_node_id" 150 t.integer "parent_node_id"
151 t.string "preview_token" 151 t.string "preview_token"
152 t.datetime "published_at", precision: nil 152 t.datetime "published_at", precision: nil
153 t.string "redirect"
154 t.integer "redirect_node_id"
153 t.integer "revision" 155 t.integer "revision"
154 t.string "slug" 156 t.string "slug"
155 t.string "template_name", limit: 255 157 t.string "template_name", limit: 255