From c06723ee715512c2033c7786c48f15674585b56b Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 26 Jun 2026 01:59:57 +0200 Subject: Stage 4: Rails 5.2 -> 6.1 on Ruby 2.7.2 - routing-filter 0.6.3 -> 0.7.0 (Rails 6.1 compatibility) - RSS named routes rss_xml/rss_rdf added - RouteWithParams workarounds: will_paginate_patch, content_path shim, safe_path helper - Paperclip removed, replaced with FileAttachment concern (preserves URL scheme) - Assets resource moved to /admin/assets (Sprockets middleware conflict) - ApplicationRecord base class added, all models migrated - Strong parameters added to Assets, Occurrences, Events, MenuItems controllers - update_attributes -> update throughout - render :nothing -> head :ok/:not_found throughout - language_selector rewritten (removes :overwrite_params) - Environment files updated for Rails 6.1 (eager_load, public_file_server, ActionMailer) - Arel::Visitors::DepthFirst and Integer/Float duration patches removed from test_helper - AssetsController tests added (10 tests covering upload, variants, destroy, auth) - ImageMagick geometry: 460x250! for headline crop (not # which is invalid in IM6) 129 runs, 311 assertions, 5 failures (all pre-existing), 0 errors --- app/assets/config/manifest.js | 0 app/controllers/assets_controller.rb | 17 ++-- app/controllers/content_controller.rb | 9 ++- app/controllers/events_controller.rb | 10 ++- app/controllers/menu_items_controller.rb | 15 ++-- app/controllers/nodes_controller.rb | 6 +- app/controllers/occurrences_controller.rb | 11 ++- app/controllers/pages_controller.rb | 2 +- app/controllers/users_controller.rb | 2 +- app/helpers/admin_helper.rb | 6 +- app/helpers/link_helper.rb | 25 +++++- app/models/application_record.rb | 3 + app/models/asset.rb | 17 +--- app/models/concerns/file_attachment.rb | 124 +++++++++++++++++++++++++++++ app/models/event.rb | 2 +- app/models/menu_item.rb | 2 +- app/models/node.rb | 2 +- app/models/occurrence.rb | 2 +- app/models/page.rb | 2 +- app/models/permission.rb | 2 +- app/models/related_asset.rb | 2 +- app/models/user.rb | 2 +- app/views/content/_search.html.erb | 2 +- app/views/content/_tags.html.erb | 2 +- app/views/layouts/application.html.erb | 9 +-- app/views/layouts/application.html.erb.bak | 54 ------------- 26 files changed, 218 insertions(+), 112 deletions(-) create mode 100644 app/assets/config/manifest.js create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/file_attachment.rb delete mode 100644 app/views/layouts/application.html.erb.bak (limited to 'app') diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index a11bbdd..d150e06 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -7,10 +7,9 @@ class AssetsController < ApplicationController layout 'admin' def index - @assets = Asset.all.paginate( - :page => params[:page], - :per_page => 20, - :order => 'id DESC' + @assets = Asset.order('id DESC').paginate( + :page => params[:page], + :per_page => 20 ) end @@ -44,7 +43,7 @@ class AssetsController < ApplicationController # POST /assets # POST /assets.xml def create - @asset = Asset.new(params[:asset]) + @asset = Asset.new(asset_params) respond_to do |format| if @asset.save @@ -64,7 +63,7 @@ class AssetsController < ApplicationController @asset = Asset.find(params[:id]) respond_to do |format| - if @asset.update_attributes(params[:asset]) + if @asset.update(asset_params) flash[:notice] = 'Asset was successfully updated.' format.html { redirect_to(@asset) } format.xml { head :ok } @@ -86,4 +85,10 @@ class AssetsController < ApplicationController format.xml { head :ok } end end + + private + + def asset_params + params.require(:asset).permit(:name, :upload) + end end diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 876bccf..8d33105 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -15,13 +15,14 @@ class ContentController < ApplicationController if @page and @page.public? render( - :file => @page.valid_template, + :template => @page.valid_template, :layout => true ) else render( - :file => Rails.root.join('public', '404.html'), - :status => 404 + :file => Rails.root.join('public', '404.html').to_s, + :status => 404, + :layout => false ) end @@ -32,7 +33,7 @@ class ContentController < ApplicationController @images = @page.assets.images render :file => "content/gallery" else - render :nothing => true, :status => 404 + head :not_found end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 6eba476..7695e9b 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -47,7 +47,7 @@ class EventsController < ApplicationController # POST /events # POST /events.xml def create - @event = Event.new(params[:event]) + @event = Event.new(event_params) respond_to do |format| if @event.save @@ -67,7 +67,7 @@ class EventsController < ApplicationController @event = Event.find(params[:id]) respond_to do |format| - if @event.update_attributes(params[:event]) + if @event.update(event_params) flash[:notice] = 'Event was successfully updated.' format.html { redirect_to(edit_node_path(@event.node)) } format.xml { head :ok } @@ -89,4 +89,10 @@ class EventsController < ApplicationController format.xml { head :ok } end end + + private + + def event_params + params.require(:event).permit(:start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location) + end end diff --git a/app/controllers/menu_items_controller.rb b/app/controllers/menu_items_controller.rb index 4018693..1b1eb59 100644 --- a/app/controllers/menu_items_controller.rb +++ b/app/controllers/menu_items_controller.rb @@ -14,11 +14,11 @@ class MenuItemsController < ApplicationController end def new - @menu_item = MenuItem.new params[:menu_item] + @menu_item = MenuItem.new menu_item_params end def create - if MenuItem.create( params[:menu_item] ) + if MenuItem.create( menu_item_params ) redirect_to menu_items_path else render :new @@ -32,7 +32,7 @@ class MenuItemsController < ApplicationController def update @menu_item = MenuItem.find( params[:id] ) - if @menu_item.update_attributes( params[:menu_item] ) + if @menu_item.update( menu_item_params ) redirect_to menu_items_path else render :edit @@ -48,10 +48,15 @@ class MenuItemsController < ApplicationController def sort params[:menu_items].each_with_index do |item_id, index| menu_item = MenuItem.find(item_id) - menu_item.update_attributes(:position => index + 1) + menu_item.update(:position => index + 1) end - render :nothing => true + head :ok end + private + + def menu_item_params + params.require(:menu_item).permit(:node_id, :path, :position, :type, :type_id) + end end diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 482d0ac..bd60b27 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -36,7 +36,7 @@ class NodesController < ApplicationController @node.slug = params[:title].parameterize.to_s if @node.save - @node.draft.update_attributes(:title => params[:title]) + @node.draft.update(:title => params[:title]) case params[:kind] when "update" @node.draft.tag_list.add("update") @@ -70,10 +70,10 @@ class NodesController < ApplicationController end def update - @node.update_attributes(node_params) + @node.update(node_params) @draft = @node.find_or_create_draft current_user @draft.tag_list = params[:tag_list] - if @draft.update_attributes( page_params ) + if @draft.update( page_params ) flash[:notice] = "Draft has been saved: #{Time.now}" respond_to do |format| format.html { redirect_to edit_node_path(@node) } diff --git a/app/controllers/occurrences_controller.rb b/app/controllers/occurrences_controller.rb index 61b42ff..0f30ce3 100644 --- a/app/controllers/occurrences_controller.rb +++ b/app/controllers/occurrences_controller.rb @@ -45,7 +45,7 @@ class OccurrencesController < ApplicationController # POST /occurrences # POST /occurrences.xml def create - @occurrence = Occurrence.new(params[:occurrence]) + @occurrence = Occurrence.new(occurrence_params) respond_to do |format| if @occurrence.save @@ -65,7 +65,7 @@ class OccurrencesController < ApplicationController @occurrence = Occurrence.find(params[:id]) respond_to do |format| - if @occurrence.update_attributes(params[:occurrence]) + if @occurrence.update(occurrence_params) flash[:notice] = 'Occurrence was successfully updated.' format.html { redirect_to(@occurrence) } format.xml { head :ok } @@ -87,4 +87,11 @@ class OccurrencesController < ApplicationController format.xml { head :ok } end end + + private + + def occurrence_params + params.require(:occurrence).permit(:start_time, :end_time, :node_id, :event_id) + end + end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index f5609eb..a40bf10 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -22,6 +22,6 @@ class PagesController < ApplicationController page = Page.find(params[:id]) page.update_assets(params[:images]) - render :nothing => true, :status => 200 + head :ok end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 72e6058..98fd534 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -36,7 +36,7 @@ class UsersController < ApplicationController permitted = user_params permitted.delete(:admin) unless current_user.is_admin? - if @user.update_attributes(permitted) + if @user.update(permitted) flash[:notice] = "Updated user #{@user.login}" redirect_to user_path(@user) else diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 389f6dc..e5c3d5c 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -1,11 +1,11 @@ module AdminHelper - + def language_selector case I18n.locale when :de - link_to raw('English'), url_for(:overwrite_params => {:locale => :en}) + link_to raw('English'), url_for(params.permit!.to_h.merge('locale' => 'en')) when :en - link_to raw('Deutsch'), url_for(:overwrite_params => {:locale => :de}) + link_to raw('Deutsch'), url_for(params.permit!.to_h.merge('locale' => 'de')) end end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 85d8fbe..39ec495 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -45,9 +45,28 @@ module LinkHelper "Locked by #{@node.lock_owner.login}\n" + "Last modified #{@page.updated_at.to_s(:db)}" - link_to( - 'Unlock', unlock_node_path(@node), :method => :put, :data => { :confirm => message } + link_to 'Unlock', safe_path(:unlock_node_path, @node), :method => :put, :data => { :confirm => message } + end + + # Rails 6.1 workaround: content_path named helper returns RouteWithParams + # when called from within a catch-all glob route request context. + # Rails 6.1 workaround: named route helpers return RouteWithParams when called + # from within a catch-all glob route request context. + # Remove this method when upgrading to Rails 7.0+, where this is fixed. + def safe_path(name, *args) + Rails.application.routes.url_helpers.send(name, *args) + end + + def content_path(page_path = nil, options = {}) + if page_path.is_a?(Hash) + options = page_path + page_path = options.delete(:page_path) + end + options[:locale] ||= params[:locale] || I18n.locale + Rails.application.routes.url_helpers.content_path( + Array(page_path).join("/").sub(/^\//, ""), + options ) end - + end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/asset.rb b/app/models/asset.rb index f6526f2..aca0ee8 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -1,20 +1,11 @@ -class Asset < ActiveRecord::Base +class Asset < ApplicationRecord + + include FileAttachment has_many :related_assets, :dependent => :destroy has_many :pages, :through => :related_assets - has_attached_file( - :upload, - :path => ":rails_root/public/system/:attachment/:id/:style/:filename", - :url => "/system/:attachment/:id/:style/:filename", - :styles => { - :medium => "300x300", - :thumb => "100x100", - :headline => "460x250#" - } - ) - - scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png"]) } + scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png", "image/webp"]) } scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb new file mode 100644 index 0000000..b3ff0f1 --- /dev/null +++ b/app/models/concerns/file_attachment.rb @@ -0,0 +1,124 @@ +# FileAttachment — minimal drop-in replacement for Paperclip's has_attached_file. +# +# Provides the same interface used throughout this codebase: +# asset.upload.url -> "/system/uploads/:id/original/:filename" +# asset.upload.url(:thumb) -> "/system/uploads/:id/thumb/:filename" +# asset.upload.content_type -> string +# asset.upload.size -> integer (bytes) +# +# Files are stored at: +# Rails.root/public/system/uploads/:id/:style/:filename +# +# Image variants are generated via ImageMagick (convert) on upload. +# Non-image files get only an original, no variants. +# +# To replace an asset: assign a new file to asset.upload= and save. +# The filename is fixed on first upload and preserved on replacement, +# keeping all public URLs stable. +# +# Future: if more sophisticated asset management is needed (versioning, +# S3, on-demand resizing), replace this module and keep the interface. + +module FileAttachment + extend ActiveSupport::Concern + + STYLES = { + medium: { geometry: "300x300>", format: nil }, + thumb: { geometry: "100x100>", format: nil }, + headline: { geometry: "460x250!", format: nil } + }.freeze + + IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze + + included do + attr_reader :upload + + after_initialize :build_upload_proxy + after_save :process_upload + before_destroy :delete_upload_files + end + + def upload=(uploaded_file) + return if uploaded_file.blank? + @pending_upload = uploaded_file + # Populate the database columns immediately so validations can use them + self.upload_file_name = sanitize_filename(uploaded_file.original_filename) + self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip + self.upload_file_size = uploaded_file.size + self.upload_updated_at = Time.current + build_upload_proxy + end + + private + + def build_upload_proxy + @upload = UploadProxy.new(self) + end + + def process_upload + return unless @pending_upload + uploaded_file = @pending_upload + @pending_upload = nil + + original_path = file_path(:original) + FileUtils.mkdir_p(File.dirname(original_path)) + FileUtils.cp(uploaded_file.tempfile.path, original_path) + + if IMAGE_CONTENT_TYPES.include?(upload_content_type) + generate_variants(original_path) + end + end + + def generate_variants(original_path) + STYLES.each do |style, options| + dest_path = file_path(style) + FileUtils.mkdir_p(File.dirname(dest_path)) + system("convert", original_path, "-resize", options[:geometry], dest_path) + end + end + + def delete_upload_files + dir = Rails.root.join("public", "system", "uploads", id.to_s) + FileUtils.rm_rf(dir) if Dir.exist?(dir) + end + + def file_path(style) + Rails.root.join( + "public", "system", "uploads", + id.to_s, style.to_s, upload_file_name + ).to_s + end + + def sanitize_filename(filename) + File.basename(filename).gsub(/[^\w\.\-]/, '_') + end + + # Proxy object returned by asset.upload, providing the Paperclip-compatible + # interface used in views: .url, .url(:style), .content_type, .size + class UploadProxy + def initialize(record) + @record = record + end + + def url(style = :original) + return "" if @record.upload_file_name.blank? + "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}" + end + + def content_type + @record.upload_content_type.to_s + end + + def size + @record.upload_file_size.to_i + end + + def present? + @record.upload_file_name.present? + end + + def blank? + !present? + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 23deed6..94a22e3 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,4 +1,4 @@ -class Event < ActiveRecord::Base +class Event < ApplicationRecord # Associations diff --git a/app/models/menu_item.rb b/app/models/menu_item.rb index eb82347..7769b7f 100644 --- a/app/models/menu_item.rb +++ b/app/models/menu_item.rb @@ -1,4 +1,4 @@ -class MenuItem < ActiveRecord::Base +class MenuItem < ApplicationRecord default_scope -> { where(:type => "MenuItem") } diff --git a/app/models/node.rb b/app/models/node.rb index d760f0a..f7a70d0 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -1,4 +1,4 @@ -class Node < ActiveRecord::Base +class Node < ApplicationRecord # Mixins and Plugins acts_as_nested_set diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 8457ffd..3baf447 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -1,7 +1,7 @@ # TODO Make a gem out of the c wrapper require 'chaos_calendar' -class Occurrence < ActiveRecord::Base +class Occurrence < ApplicationRecord # Associations diff --git a/app/models/page.rb b/app/models/page.rb index 93debf8..d1e7439 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -1,6 +1,6 @@ require 'xml' -class Page < ActiveRecord::Base +class Page < ApplicationRecord PUBLIC_TEMPLATE_PATH = File.join(%w(custom page_templates public)) FULL_PUBLIC_TEMPLATE_PATH = Rails.root.join('app', 'views', PUBLIC_TEMPLATE_PATH) diff --git a/app/models/permission.rb b/app/models/permission.rb index f304538..1383a4b 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -1,4 +1,4 @@ -class Permission < ActiveRecord::Base +class Permission < ApplicationRecord # Validations validates_presence_of :user_id, :node_id, :granted validates_inclusion_of :granted, :in => [true, false] diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 2b61c51..8f16460 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb @@ -1,4 +1,4 @@ -class RelatedAsset < ActiveRecord::Base +class RelatedAsset < ApplicationRecord belongs_to :page belongs_to :asset diff --git a/app/models/user.rb b/app/models/user.rb index a2540b5..92ac33a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ require 'digest/sha1' -class User < ActiveRecord::Base +class User < ApplicationRecord # Mixins and Plugins include Authentication include Authentication::ByPassword diff --git a/app/views/content/_search.html.erb b/app/views/content/_search.html.erb index aa91424..f732fca 100644 --- a/app/views/content/_search.html.erb +++ b/app/views/content/_search.html.erb @@ -1,3 +1,3 @@ -<%= form_tag search_path, :method => 'get' do %> +<%= form_tag safe_path(:search_path), :method => 'get' do %>
<%= text_field_tag :search_term, params[:search_term], :placeholder => 'suchen', :type => 'search' %>
<% end %> diff --git a/app/views/content/_tags.html.erb b/app/views/content/_tags.html.erb index 169ae84..387f51c 100644 --- a/app/views/content/_tags.html.erb +++ b/app/views/content/_tags.html.erb @@ -3,7 +3,7 @@

Tags

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2a46f09..84dcdc6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -16,8 +16,8 @@ <%= stylesheet_link_tag "shadowbox" %> <%= javascript_include_tag 'public' %> - <%= auto_discovery_link_tag(:atom, {:locale => :de, :controller => "rss", :action => "updates", :format => :xml}) %> - <%= auto_discovery_link_tag(:rss, {:locale => :de, :controller => "rss", :action => "updates", :format => :rdf}) %> + <%= auto_discovery_link_tag(:atom, '/rss/updates.xml', title: "ATOM") %> + <%= auto_discovery_link_tag(:rss, '/rss/updates.rdf', title: "RSS") %>