Project

General

Profile

Feature #44646 » 0001-Allow-administrators-to-upload-a-custom-favicon.patch

Go MAEDA, 2026-10-20 10:57

View differences:

app/controllers/favicons_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
class FaviconsController < ApplicationController
21
  # The favicon is also shown on the login page
22
  skip_before_action :check_if_login_required, :check_password_change, :check_twofa_activation
23

  
24
  def show
25
    favicon = Setting.favicon
26
    if favicon['digest'] != params[:digest]
27
      render_404
28
      return
29
    end
30

  
31
    # The URL changes whenever the favicon is replaced
32
    expires_in 1.year, :public => true, :immutable => true
33
    send_data Base64.decode64(favicon['data']), :type => favicon['content_type'], :disposition => 'inline'
34
  end
35
end
app/controllers/settings_controller.rb
26 26
  before_action :require_admin
27
  require_sudo_mode :index, :edit, :plugin
27
  require_sudo_mode :index, :edit, :delete_favicon, :plugin
28 28
  def index
29 29
    edit
......
65 65
    Redmine::Themes.rescan
66 66
  end
67
  def delete_favicon
68
    Setting.favicon = {}
69
    flash[:notice] = l(:notice_successful_delete)
70
    redirect_to settings_path(:tab => 'display')
71
  end
72

  
67 73
  def plugin
68 74
    @plugin = Redmine::Plugin.find(params[:id])
69 75
    unless @plugin.configurable?
app/helpers/application_helper.rb
1856 1856
  end
1857 1857
  def favicon
1858
    favicon_link_tag(favicon_path, rel: "shortcut icon")
1858
    options = {rel: "shortcut icon"}
1859
    options[:type] = Setting.favicon['content_type'] if Setting.favicon.present?
1860
    favicon_link_tag(favicon_path, options)
1859 1861
  end
1860 1862
  # Returns the path to the favicon
1861 1863
  def favicon_path
1864
    return custom_favicon_path(Setting.favicon['digest']) if Setting.favicon.present?
1865

  
1862 1866
    icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : 'favicon.ico'
1863 1867
    image_path(icon)
1864 1868
  end
app/models/setting.rb
82 82
                  Big5-HKSCS
83 83
                  TIS-620)
84
  FAVICON_MAX_SIZE = 32.kilobytes
85
  FAVICON_CONTENT_TYPES = %w(image/png image/vnd.microsoft.icon)
86

  
84 87
  cattr_accessor :available_settings
85 88
  self.available_settings ||= {}
......
208 211
        end
209 212
      end
210 213
    end
214
    if settings.key?(:favicon)
215
      file = settings[:favicon]
216
      unless file.respond_to?(:original_filename) &&
217
             file.size <= FAVICON_MAX_SIZE &&
218
             FAVICON_CONTENT_TYPES.include?(Marcel::MimeType.for(file))
219
        messages << [:favicon, l('activerecord.errors.messages.invalid')]
220
      end
221
    end
211 222
    messages
212 223
  end
......
254 265
    params
255 266
  end
267
  def self.favicon_from_params(file)
268
    data = file.read
269
    {
270
      'content_type' => Marcel::MimeType.for(StringIO.new(data)),
271
      'data' => Base64.strict_encode64(data),
272
      'digest' => Digest::SHA256.hexdigest(data)[0, 8]
273
    }
274
  end
275

  
256 276
  def self.default_issue_due_date_offset_from_params(params)
257 277
    params.to_s.strip
258 278
  end
app/views/settings/_display.html.erb
1
<%= form_tag({:action => 'edit', :tab => 'display'}) do %>
1
<%= form_tag({:action => 'edit', :tab => 'display'}, :multipart => true) do %>
2 2
<div class="box tabular settings">
3 3
<p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
4
<p>
5
  <%= setting_label :favicon %>
6
  <% if Setting.favicon.present? %>
7
    <%= image_tag custom_favicon_path(Setting.favicon['digest']), :size => 32, :alt => '' %>
8
    <%= delete_link settings_favicon_path %>
9
    <br>
10
  <% end %>
11
  <%= file_field_tag 'settings[favicon]', :accept => '.ico,.png' %>
12
  <em class="info"><%= l(:text_setting_favicon_format, :max_size => number_to_human_size(Setting::FAVICON_MAX_SIZE)) %></em>
13
</p>
14

  
4 15
<p><%= setting_select :default_language, lang_options_for_select(false) %></p>
5 16
<p><%= setting_check_box :force_default_language_for_anonymous %></p>
config/locales/en.yml
440 440
  setting_app_title: Application title
441 441
  setting_welcome_text: Welcome text
442 442
  setting_default_language: Default language
443
  setting_favicon: Custom favicon
443 444
  setting_login_required: Authentication required
444 445
  setting_self_registration: Self-registration
445 446
  setting_show_custom_fields_on_registration: Show custom fields on registration
......
1407 1408
  text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it.
1408 1409
  text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="%{url}">%{url}</a> to generate their avatars.
1409 1410
  text_setting_avatar_enabled_imagemagick_unavailable: ImageMagick is not available, so users cannot set their avatar.
1411
  text_setting_favicon_format: "PNG or ICO, up to %{max_size}"
1410 1412
  text_setting_gravatar_enabled_fallback: If enabled, Gravatar is used for users who have not set their avatar in Redmine.
1411 1413
  text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, this application will be able to read and write all data and impersonate other users.
1412 1414
  text_oauth_admin_permission_info: This application requests full administrative access. If you are an Administrator (or become one in the future), it will be able to read and write all data and impersonate other users on your behalf. If you want to avoid this, authorize it as a user without Administrator privileges instead.
config/routes.rb
424 424
    end
425 425
  end
426
  get 'favicons/:digest', :to => 'favicons#show', :digest => /\h{8}/, :as => 'custom_favicon', :format => false
427

  
426 428
  match 'settings', :controller => 'settings', :action => 'index', :via => :get
427 429
  match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
430
  delete 'settings/favicon', :controller => 'settings', :action => 'delete_favicon', :as => 'settings_favicon'
428 431
  match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
429 432
  match 'sys/projects', :to => 'sys#projects', :via => :get
config/settings.yml
324 324
  default: 100
325 325
ui_theme:
326 326
  default: ''
327
favicon:
328
  serialized: true
329
  default: {}
327 330
emails_footer:
328 331
  default: |-
329 332
    You have received this notification because you have either subscribed to it, or are involved in it.
test/functional/favicons_controller_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class FaviconsControllerTest < Redmine::ControllerTest
23
  def test_show_should_be_cached_and_available_without_login
24
    data = File.binread(file_fixture('2010/11/101123161450_testfile_1.png'))
25
    digest = Digest::SHA256.hexdigest(data)[0, 8]
26
    favicon = {'content_type' => 'image/png', 'data' => Base64.strict_encode64(data), 'digest' => digest}
27
    with_settings :favicon => favicon, :login_required => '1' do
28
      get :show, :params => {:digest => digest}
29
      assert_response :success
30

  
31
      assert_equal 'image/png', response.media_type
32
      assert_equal data, response.body
33
      assert_equal "max-age=#{1.year.to_i}, public, immutable", response.headers['Cache-Control']
34
    end
35
  end
36
end
test/functional/settings_controller_test.rb
299 299
    Redmine::Plugin.unregister(:foo)
300 300
  end
301
  def test_post_edit_should_store_favicon
302
    with_settings :favicon => {} do
303
      post :edit, :params => {
304
        :settings => {:favicon => fixture_file_upload('2010/11/101123161450_testfile_1.png', 'image/png')}
305
      }
306
      assert_redirected_to '/settings'
307

  
308
      data = File.binread(file_fixture('2010/11/101123161450_testfile_1.png'))
309
      assert_equal 'image/png', Setting.favicon['content_type']
310
      assert_equal data, Base64.decode64(Setting.favicon['data'])
311
      assert_equal Digest::SHA256.hexdigest(data)[0, 8], Setting.favicon['digest']
312
    end
313
  end
314

  
315
  def test_post_edit_should_not_store_favicon_that_is_not_an_icon_image
316
    post :edit, :params => {:settings => {:favicon => fixture_file_upload('testfile.svg', 'image/svg+xml')}}
317
    assert_response :success
318

  
319
    assert_select_error /Custom favicon is invalid/
320
    assert_equal({}, Setting.favicon)
321
  end
322

  
323
  def test_delete_favicon
324
    with_settings :favicon => {'content_type' => 'image/png', 'data' => 'AA==', 'digest' => '0' * 8} do
325
      delete :delete_favicon
326
      assert_redirected_to '/settings?tab=display'
327

  
328
      assert_equal({}, Setting.favicon)
329
    end
330
  end
331

  
301 332
  def test_post_mail_handler_delimiters_should_not_save_invalid_regex_delimiters
302 333
    post :edit, :params => {
303 334
      :settings => {
test/integration/lib/redmine/themes_test.rb
76 76
    @theme.favicons.delete 'a.ico'
77 77
  end
78
  def test_use_custom_favicon_over_theme_favicon
79
    @theme.favicons.unshift('a.ico')
80
    favicon = {'content_type' => 'image/png', 'data' => 'AA==', 'digest' => '0' * 8}
81
    with_settings :favicon => favicon do
82
      get '/'
83

  
84
      assert_response :success
85
      assert_select 'link[rel="shortcut icon"][type="image/png"][href=?]', "/favicons/#{'0' * 8}"
86
    end
87
  ensure
88
    @theme.favicons.delete 'a.ico'
89
  end
90

  
78 91
  def test_use_only_one_theme_favicon_if_theme_provides_many
79 92
    @theme.favicons.unshift('b.ico', 'a.png')
80 93
    get '/'
(1-1/2)