Feature #44646 » 0001-Allow-administrators-to-upload-a-custom-favicon.patch
| 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 '/' |