##// END OF EJS Templates
Don't error if an invalid setting is given....
Jean-Philippe Lang -
r15348:596a196f2e7e
parent child
Show More
@@ -1,293 +1,295
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Setting < ActiveRecord::Base
19 19
20 20 DATE_FORMATS = [
21 21 '%Y-%m-%d',
22 22 '%d/%m/%Y',
23 23 '%d.%m.%Y',
24 24 '%d-%m-%Y',
25 25 '%m/%d/%Y',
26 26 '%d %b %Y',
27 27 '%d %B %Y',
28 28 '%b %d, %Y',
29 29 '%B %d, %Y'
30 30 ]
31 31
32 32 TIME_FORMATS = [
33 33 '%H:%M',
34 34 '%I:%M %p'
35 35 ]
36 36
37 37 ENCODINGS = %w(US-ASCII
38 38 windows-1250
39 39 windows-1251
40 40 windows-1252
41 41 windows-1253
42 42 windows-1254
43 43 windows-1255
44 44 windows-1256
45 45 windows-1257
46 46 windows-1258
47 47 windows-31j
48 48 ISO-2022-JP
49 49 ISO-2022-KR
50 50 ISO-8859-1
51 51 ISO-8859-2
52 52 ISO-8859-3
53 53 ISO-8859-4
54 54 ISO-8859-5
55 55 ISO-8859-6
56 56 ISO-8859-7
57 57 ISO-8859-8
58 58 ISO-8859-9
59 59 ISO-8859-13
60 60 ISO-8859-15
61 61 KOI8-R
62 62 UTF-8
63 63 UTF-16
64 64 UTF-16BE
65 65 UTF-16LE
66 66 EUC-JP
67 67 Shift_JIS
68 68 CP932
69 69 GB18030
70 70 GBK
71 71 ISCII91
72 72 EUC-KR
73 73 Big5
74 74 Big5-HKSCS
75 75 TIS-620)
76 76
77 77 cattr_accessor :available_settings
78 78 self.available_settings ||= {}
79 79
80 80 validates_uniqueness_of :name, :if => Proc.new {|setting| setting.new_record? || setting.name_changed?}
81 81 validates_inclusion_of :name, :in => Proc.new {available_settings.keys}
82 82 validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting|
83 83 (s = available_settings[setting.name]) && s['format'] == 'int'
84 84 }
85 85 attr_protected :id
86 86
87 87 # Hash used to cache setting values
88 88 @cached_settings = {}
89 89 @cached_cleared_on = Time.now
90 90
91 91 def value
92 92 v = read_attribute(:value)
93 93 # Unserialize serialized settings
94 94 if available_settings[name]['serialized'] && v.is_a?(String)
95 95 v = YAML::load(v)
96 96 v = force_utf8_strings(v)
97 97 end
98 98 v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank?
99 99 v
100 100 end
101 101
102 102 def value=(v)
103 103 v = v.to_yaml if v && available_settings[name] && available_settings[name]['serialized']
104 104 write_attribute(:value, v.to_s)
105 105 end
106 106
107 107 # Returns the value of the setting named name
108 108 def self.[](name)
109 109 v = @cached_settings[name]
110 110 v ? v : (@cached_settings[name] = find_or_default(name).value)
111 111 end
112 112
113 113 def self.[]=(name, v)
114 114 setting = find_or_default(name)
115 115 setting.value = (v ? v : "")
116 116 @cached_settings[name] = nil
117 117 setting.save
118 118 setting.value
119 119 end
120 120
121 121 # Updates multiple settings from params and sends a security notification if needed
122 122 def self.set_all_from_params(settings)
123 settings = (settings || {}).dup.symbolize_keys
123 return false unless settings.is_a?(Hash)
124 settings = settings.dup.symbolize_keys
124 125 changes = []
125 126 settings.each do |name, value|
127 next unless available_settings[name.to_s]
126 128 previous_value = Setting[name]
127 129 set_from_params name, value
128 130 if available_settings[name.to_s]['security_notifications'] && Setting[name] != previous_value
129 131 changes << name
130 132 end
131 133 end
132 134 if changes.any?
133 135 Mailer.security_settings_updated(changes)
134 136 end
135 137 true
136 138 end
137 139
138 140 # Sets a setting value from params
139 141 def self.set_from_params(name, params)
140 142 params = params.dup
141 143 params.delete_if {|v| v.blank? } if params.is_a?(Array)
142 144 params.symbolize_keys! if params.is_a?(Hash)
143 145
144 146 m = "#{name}_from_params"
145 147 if respond_to? m
146 148 self[name.to_sym] = send m, params
147 149 else
148 150 self[name.to_sym] = params
149 151 end
150 152 end
151 153
152 154 # Returns a hash suitable for commit_update_keywords setting
153 155 #
154 156 # Example:
155 157 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
156 158 # Setting.commit_update_keywords_from_params(params)
157 159 # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
158 160 def self.commit_update_keywords_from_params(params)
159 161 s = []
160 162 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
161 163 attributes = params.except(:keywords).keys
162 164 params[:keywords].each_with_index do |keywords, i|
163 165 next if keywords.blank?
164 166 s << attributes.inject({}) {|h, a|
165 167 value = params[a][i].to_s
166 168 h[a.to_s] = value if value.present?
167 169 h
168 170 }.merge('keywords' => keywords)
169 171 end
170 172 end
171 173 s
172 174 end
173 175
174 176 # Helper that returns an array based on per_page_options setting
175 177 def self.per_page_options_array
176 178 per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
177 179 end
178 180
179 181 # Helper that returns a Hash with single update keywords as keys
180 182 def self.commit_update_keywords_array
181 183 a = []
182 184 if commit_update_keywords.is_a?(Array)
183 185 commit_update_keywords.each do |rule|
184 186 next unless rule.is_a?(Hash)
185 187 rule = rule.dup
186 188 rule.delete_if {|k, v| v.blank?}
187 189 keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
188 190 next if keywords.empty?
189 191 a << rule.merge('keywords' => keywords)
190 192 end
191 193 end
192 194 a
193 195 end
194 196
195 197 def self.openid?
196 198 Object.const_defined?(:OpenID) && self[:openid].to_i > 0
197 199 end
198 200
199 201 # Checks if settings have changed since the values were read
200 202 # and clears the cache hash if it's the case
201 203 # Called once per request
202 204 def self.check_cache
203 205 settings_updated_on = Setting.maximum(:updated_on)
204 206 if settings_updated_on && @cached_cleared_on <= settings_updated_on
205 207 clear_cache
206 208 end
207 209 end
208 210
209 211 # Clears the settings cache
210 212 def self.clear_cache
211 213 @cached_settings.clear
212 214 @cached_cleared_on = Time.now
213 215 logger.info "Settings cache cleared." if logger
214 216 end
215 217
216 218 def self.define_plugin_setting(plugin)
217 219 if plugin.settings
218 220 name = "plugin_#{plugin.id}"
219 221 define_setting name, {'default' => plugin.settings[:default], 'serialized' => true}
220 222 end
221 223 end
222 224
223 225 # Defines getter and setter for each setting
224 226 # Then setting values can be read using: Setting.some_setting_name
225 227 # or set using Setting.some_setting_name = "some value"
226 228 def self.define_setting(name, options={})
227 229 available_settings[name.to_s] = options
228 230
229 231 src = <<-END_SRC
230 232 def self.#{name}
231 233 self[:#{name}]
232 234 end
233 235
234 236 def self.#{name}?
235 237 self[:#{name}].to_i > 0
236 238 end
237 239
238 240 def self.#{name}=(value)
239 241 self[:#{name}] = value
240 242 end
241 243 END_SRC
242 244 class_eval src, __FILE__, __LINE__
243 245 end
244 246
245 247 def self.load_available_settings
246 248 YAML::load(File.open("#{Rails.root}/config/settings.yml")).each do |name, options|
247 249 define_setting name, options
248 250 end
249 251 end
250 252
251 253 def self.load_plugin_settings
252 254 Redmine::Plugin.all.each do |plugin|
253 255 define_plugin_setting(plugin)
254 256 end
255 257 end
256 258
257 259 load_available_settings
258 260 load_plugin_settings
259 261
260 262 private
261 263
262 264 def force_utf8_strings(arg)
263 265 if arg.is_a?(String)
264 266 arg.dup.force_encoding('UTF-8')
265 267 elsif arg.is_a?(Array)
266 268 arg.map do |a|
267 269 force_utf8_strings(a)
268 270 end
269 271 elsif arg.is_a?(Hash)
270 272 arg = arg.dup
271 273 arg.each do |k,v|
272 274 arg[k] = force_utf8_strings(v)
273 275 end
274 276 arg
275 277 else
276 278 arg
277 279 end
278 280 end
279 281
280 282 # Returns the Setting instance for the setting named name
281 283 # (record found in database or new record with default value)
282 284 def self.find_or_default(name)
283 285 name = name.to_s
284 286 raise "There's no setting named #{name}" unless available_settings.has_key?(name)
285 287 setting = where(:name => name).order(:id => :desc).first
286 288 unless setting
287 289 setting = new
288 290 setting.name = name
289 291 setting.value = available_settings[name]['default']
290 292 end
291 293 setting
292 294 end
293 295 end
@@ -1,248 +1,257
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class SettingsControllerTest < Redmine::ControllerTest
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :users
23 23
24 24 def setup
25 25 User.current = nil
26 26 @request.session[:user_id] = 1 # admin
27 27 end
28 28
29 29 def teardown
30 30 Setting.delete_all
31 31 Setting.clear_cache
32 32 end
33 33
34 34 def test_index
35 35 get :index
36 36 assert_response :success
37 37
38 38 assert_select 'input[name=?][value=?]', 'settings[app_title]', Setting.app_title
39 39 end
40 40
41 41 def test_get_edit
42 42 get :edit
43 43 assert_response :success
44 44
45 45 assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]'
46 46 end
47 47
48 48 def test_get_edit_should_preselect_default_issue_list_columns
49 49 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
50 50 get :edit
51 51 assert_response :success
52 52 end
53 53
54 54 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
55 55 assert_select 'option', 4
56 56 assert_select 'option[value=tracker]', :text => 'Tracker'
57 57 assert_select 'option[value=subject]', :text => 'Subject'
58 58 assert_select 'option[value=status]', :text => 'Status'
59 59 assert_select 'option[value=updated_on]', :text => 'Updated'
60 60 end
61 61
62 62 assert_select 'select[id=available_columns]' do
63 63 assert_select 'option[value=tracker]', 0
64 64 assert_select 'option[value=priority]', :text => 'Priority'
65 65 end
66 66 end
67 67
68 68 def test_get_edit_without_trackers_should_succeed
69 69 Tracker.delete_all
70 70
71 71 get :edit
72 72 assert_response :success
73 73 end
74 74
75 75 def test_post_edit_notifications
76 76 post :edit, :params => {
77 77 :settings => {
78 78 :mail_from => 'functional@test.foo',
79 79 :bcc_recipients => '0',
80 80 :notified_events => %w(issue_added issue_updated news_added),
81 81 :emails_footer => 'Test footer'
82 82 }
83 83 }
84 84 assert_redirected_to '/settings'
85 85 assert_equal 'functional@test.foo', Setting.mail_from
86 86 assert !Setting.bcc_recipients?
87 87 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
88 88 assert_equal 'Test footer', Setting.emails_footer
89 89 end
90 90
91 91 def test_edit_commit_update_keywords
92 92 with_settings :commit_update_keywords => [
93 93 {"keywords" => "fixes, resolves", "status_id" => "3"},
94 94 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
95 95 ] do
96 96 get :edit
97 97 end
98 98 assert_response :success
99 99 assert_select 'tr.commit-keywords', 2
100 100 assert_select 'tr.commit-keywords:nth-child(1)' do
101 101 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
102 102 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
103 103 assert_select 'option[value="3"][selected=selected]'
104 104 end
105 105 end
106 106 assert_select 'tr.commit-keywords:nth-child(2)' do
107 107 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
108 108 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
109 109 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
110 110 end
111 111 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
112 112 assert_select 'option[value="100"][selected=selected]', :text => '100 %'
113 113 end
114 114 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
115 115 assert_select 'option[value="2"][selected=selected]', :text => 'Feature request'
116 116 end
117 117 end
118 118 end
119 119
120 120 def test_edit_without_commit_update_keywords_should_show_blank_line
121 121 with_settings :commit_update_keywords => [] do
122 122 get :edit
123 123 end
124 124 assert_response :success
125 125 assert_select 'tr.commit-keywords', 1 do
126 126 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
127 127 end
128 128 end
129 129
130 130 def test_post_edit_commit_update_keywords
131 131 post :edit, :params => {
132 132 :settings => {
133 133 :commit_update_keywords => {
134 134 :keywords => ["resolves", "closes"],
135 135 :status_id => ["3", "5"],
136 136 :done_ratio => ["", "100"],
137 137 :if_tracker_id => ["", "2"]
138 138 }
139 139 }
140 140 }
141 141 assert_redirected_to '/settings'
142 142 assert_equal([
143 143 {"keywords" => "resolves", "status_id" => "3"},
144 144 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
145 145 ], Setting.commit_update_keywords)
146 146 end
147 147
148 def test_post_edit_with_invalid_setting_should_not_error
149 post :edit, :params => {
150 :settings => {
151 :invalid_setting => '1'
152 }
153 }
154 assert_redirected_to '/settings'
155 end
156
148 157 def test_post_edit_should_send_security_notification_for_notified_settings
149 158 ActionMailer::Base.deliveries.clear
150 159 post :edit, :params => {
151 160 :settings => {
152 161 :login_required => 1
153 162 }
154 163 }
155 164 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
156 165 assert_mail_body_match '0.0.0.0', mail
157 166 assert_mail_body_match I18n.t(:setting_login_required), mail
158 167 assert_select_email do
159 168 assert_select 'a[href^=?]', 'http://localhost:3000/settings'
160 169 end
161 170 # All admins should receive this
162 171 recipients = [mail.bcc, mail.cc].flatten
163 172 User.active.where(admin: true).each do |admin|
164 173 assert_include admin.mail, recipients
165 174 end
166 175 end
167 176
168 177 def test_post_edit_should_not_send_security_notification_for_non_notified_settings
169 178 ActionMailer::Base.deliveries.clear
170 179 post :edit, :params => {
171 180 :settings => {
172 181 :app_title => 'MineRed'
173 182 }
174 183 }
175 184 assert_nil (mail = ActionMailer::Base.deliveries.last)
176 185 end
177 186
178 187 def test_post_edit_should_not_send_security_notification_for_unchanged_settings
179 188 ActionMailer::Base.deliveries.clear
180 189 post :edit, :params => {
181 190 :settings => {
182 191 :login_required => 0
183 192 }
184 193 }
185 194 assert_nil (mail = ActionMailer::Base.deliveries.last)
186 195 end
187 196
188 197
189 198 def test_get_plugin_settings
190 199 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
191 200 Redmine::Plugin.register :foo do
192 201 settings :partial => "foo_plugin/foo_plugin_settings"
193 202 end
194 203 Setting.plugin_foo = {'sample_setting' => 'Plugin setting value'}
195 204
196 205 get :plugin, :params => {:id => 'foo'}
197 206 assert_response :success
198 207
199 208 assert_select 'form[action="/settings/plugin/foo"]' do
200 209 assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value'
201 210 end
202 211 ensure
203 212 Redmine::Plugin.unregister(:foo)
204 213 end
205 214
206 215 def test_get_invalid_plugin_settings
207 216 get :plugin, :params => {:id => 'none'}
208 217 assert_response 404
209 218 end
210 219
211 220 def test_get_non_configurable_plugin_settings
212 221 Redmine::Plugin.register(:foo) {}
213 222
214 223 get :plugin, :params => {:id => 'foo'}
215 224 assert_response 404
216 225
217 226 ensure
218 227 Redmine::Plugin.unregister(:foo)
219 228 end
220 229
221 230 def test_post_plugin_settings
222 231 Redmine::Plugin.register(:foo) do
223 232 settings :partial => 'not blank', # so that configurable? is true
224 233 :default => {'sample_setting' => 'Plugin setting value'}
225 234 end
226 235
227 236 post :plugin, :params => {
228 237 :id => 'foo',
229 238 :settings => {'sample_setting' => 'Value'}
230 239 }
231 240 assert_redirected_to '/settings/plugin/foo'
232 241
233 242 assert_equal({'sample_setting' => 'Value'}, Setting.plugin_foo)
234 243 end
235 244
236 245 def test_post_non_configurable_plugin_settings
237 246 Redmine::Plugin.register(:foo) {}
238 247
239 248 post :plugin, :params => {
240 249 :id => 'foo',
241 250 :settings => {'sample_setting' => 'Value'}
242 251 }
243 252 assert_response 404
244 253
245 254 ensure
246 255 Redmine::Plugin.unregister(:foo)
247 256 end
248 257 end
General Comments 0
You need to be logged in to leave comments. Login now