##// END OF EJS Templates
Respond with 404 on ActionView::MissingTemplate (#11503)....
Jean-Philippe Lang -
r10021:327660eb7f7e
parent child
Show More
@@ -1,549 +1,554
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25
26 26 class_attribute :accept_api_auth_actions
27 27 class_attribute :accept_rss_auth_actions
28 28 class_attribute :model_object
29 29
30 30 layout 'base'
31 31
32 32 protect_from_forgery
33 33 def handle_unverified_request
34 34 super
35 35 cookies.delete(:autologin)
36 36 end
37 37
38 38 before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
39 39
40 40 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
41 41 rescue_from ::Unauthorized, :with => :deny_access
42 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
42 43
43 44 include Redmine::Search::Controller
44 45 include Redmine::MenuManager::MenuController
45 46 helper Redmine::MenuManager::MenuHelper
46 47
47 48 def session_expiration
48 49 if session[:user_id]
49 50 if session_expired? && !try_to_autologin
50 51 reset_session
51 52 flash[:error] = l(:error_session_expired)
52 53 redirect_to signin_url
53 54 else
54 55 session[:atime] = Time.now.utc.to_i
55 56 end
56 57 end
57 58 end
58 59
59 60 def session_expired?
60 61 if Setting.session_lifetime?
61 62 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
62 63 return true
63 64 end
64 65 end
65 66 if Setting.session_timeout?
66 67 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
67 68 return true
68 69 end
69 70 end
70 71 false
71 72 end
72 73
73 74 def start_user_session(user)
74 75 session[:user_id] = user.id
75 76 session[:ctime] = Time.now.utc.to_i
76 77 session[:atime] = Time.now.utc.to_i
77 78 end
78 79
79 80 def user_setup
80 81 # Check the settings cache for each request
81 82 Setting.check_cache
82 83 # Find the current user
83 84 User.current = find_current_user
84 85 end
85 86
86 87 # Returns the current user or nil if no user is logged in
87 88 # and starts a session if needed
88 89 def find_current_user
89 90 user = nil
90 91 unless api_request?
91 92 if session[:user_id]
92 93 # existing session
93 94 user = (User.active.find(session[:user_id]) rescue nil)
94 95 elsif autologin_user = try_to_autologin
95 96 user = autologin_user
96 97 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
97 98 # RSS key authentication does not start a session
98 99 user = User.find_by_rss_key(params[:key])
99 100 end
100 101 end
101 102 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
102 103 if (key = api_key_from_request)
103 104 # Use API key
104 105 user = User.find_by_api_key(key)
105 106 else
106 107 # HTTP Basic, either username/password or API key/random
107 108 authenticate_with_http_basic do |username, password|
108 109 user = User.try_to_login(username, password) || User.find_by_api_key(username)
109 110 end
110 111 end
111 112 end
112 113 user
113 114 end
114 115
115 116 def try_to_autologin
116 117 if cookies[:autologin] && Setting.autologin?
117 118 # auto-login feature starts a new session
118 119 user = User.try_to_autologin(cookies[:autologin])
119 120 if user
120 121 reset_session
121 122 start_user_session(user)
122 123 end
123 124 user
124 125 end
125 126 end
126 127
127 128 # Sets the logged in user
128 129 def logged_user=(user)
129 130 reset_session
130 131 if user && user.is_a?(User)
131 132 User.current = user
132 133 start_user_session(user)
133 134 else
134 135 User.current = User.anonymous
135 136 end
136 137 end
137 138
138 139 # Logs out current user
139 140 def logout_user
140 141 if User.current.logged?
141 142 cookies.delete :autologin
142 143 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
143 144 self.logged_user = nil
144 145 end
145 146 end
146 147
147 148 # check if login is globally required to access the application
148 149 def check_if_login_required
149 150 # no check needed if user is already logged in
150 151 return true if User.current.logged?
151 152 require_login if Setting.login_required?
152 153 end
153 154
154 155 def set_localization
155 156 lang = nil
156 157 if User.current.logged?
157 158 lang = find_language(User.current.language)
158 159 end
159 160 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
160 161 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
161 162 if !accept_lang.blank?
162 163 accept_lang = accept_lang.downcase
163 164 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
164 165 end
165 166 end
166 167 lang ||= Setting.default_language
167 168 set_language_if_valid(lang)
168 169 end
169 170
170 171 def require_login
171 172 if !User.current.logged?
172 173 # Extract only the basic url parameters on non-GET requests
173 174 if request.get?
174 175 url = url_for(params)
175 176 else
176 177 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
177 178 end
178 179 respond_to do |format|
179 180 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
180 181 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
181 182 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
182 183 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
183 184 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
184 185 end
185 186 return false
186 187 end
187 188 true
188 189 end
189 190
190 191 def require_admin
191 192 return unless require_login
192 193 if !User.current.admin?
193 194 render_403
194 195 return false
195 196 end
196 197 true
197 198 end
198 199
199 200 def deny_access
200 201 User.current.logged? ? render_403 : require_login
201 202 end
202 203
203 204 # Authorize the user for the requested action
204 205 def authorize(ctrl = params[:controller], action = params[:action], global = false)
205 206 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
206 207 if allowed
207 208 true
208 209 else
209 210 if @project && @project.archived?
210 211 render_403 :message => :notice_not_authorized_archived_project
211 212 else
212 213 deny_access
213 214 end
214 215 end
215 216 end
216 217
217 218 # Authorize the user for the requested action outside a project
218 219 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
219 220 authorize(ctrl, action, global)
220 221 end
221 222
222 223 # Find project of id params[:id]
223 224 def find_project
224 225 @project = Project.find(params[:id])
225 226 rescue ActiveRecord::RecordNotFound
226 227 render_404
227 228 end
228 229
229 230 # Find project of id params[:project_id]
230 231 def find_project_by_project_id
231 232 @project = Project.find(params[:project_id])
232 233 rescue ActiveRecord::RecordNotFound
233 234 render_404
234 235 end
235 236
236 237 # Find a project based on params[:project_id]
237 238 # TODO: some subclasses override this, see about merging their logic
238 239 def find_optional_project
239 240 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
240 241 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
241 242 allowed ? true : deny_access
242 243 rescue ActiveRecord::RecordNotFound
243 244 render_404
244 245 end
245 246
246 247 # Finds and sets @project based on @object.project
247 248 def find_project_from_association
248 249 render_404 unless @object.present?
249 250
250 251 @project = @object.project
251 252 end
252 253
253 254 def find_model_object
254 255 model = self.class.model_object
255 256 if model
256 257 @object = model.find(params[:id])
257 258 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
258 259 end
259 260 rescue ActiveRecord::RecordNotFound
260 261 render_404
261 262 end
262 263
263 264 def self.model_object(model)
264 265 self.model_object = model
265 266 end
266 267
267 268 # Filter for bulk issue operations
268 269 def find_issues
269 270 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
270 271 raise ActiveRecord::RecordNotFound if @issues.empty?
271 272 if @issues.detect {|issue| !issue.visible?}
272 273 deny_access
273 274 return
274 275 end
275 276 @projects = @issues.collect(&:project).compact.uniq
276 277 @project = @projects.first if @projects.size == 1
277 278 rescue ActiveRecord::RecordNotFound
278 279 render_404
279 280 end
280 281
281 282 # make sure that the user is a member of the project (or admin) if project is private
282 283 # used as a before_filter for actions that do not require any particular permission on the project
283 284 def check_project_privacy
284 285 if @project && !@project.archived?
285 286 if @project.visible?
286 287 true
287 288 else
288 289 deny_access
289 290 end
290 291 else
291 292 @project = nil
292 293 render_404
293 294 false
294 295 end
295 296 end
296 297
297 298 def back_url
298 299 params[:back_url] || request.env['HTTP_REFERER']
299 300 end
300 301
301 302 def redirect_back_or_default(default)
302 303 back_url = CGI.unescape(params[:back_url].to_s)
303 304 if !back_url.blank?
304 305 begin
305 306 uri = URI.parse(back_url)
306 307 # do not redirect user to another host or to the login or register page
307 308 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
308 309 redirect_to(back_url)
309 310 return
310 311 end
311 312 rescue URI::InvalidURIError
312 313 # redirect to default
313 314 end
314 315 end
315 316 redirect_to default
316 317 false
317 318 end
318 319
319 320 # Redirects to the request referer if present, redirects to args or call block otherwise.
320 321 def redirect_to_referer_or(*args, &block)
321 322 redirect_to :back
322 323 rescue ::ActionController::RedirectBackError
323 324 if args.any?
324 325 redirect_to *args
325 326 elsif block_given?
326 327 block.call
327 328 else
328 329 raise "#redirect_to_referer_or takes arguments or a block"
329 330 end
330 331 end
331 332
332 333 def render_403(options={})
333 334 @project = nil
334 335 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
335 336 return false
336 337 end
337 338
338 339 def render_404(options={})
339 340 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
340 341 return false
341 342 end
342 343
343 344 # Renders an error response
344 345 def render_error(arg)
345 346 arg = {:message => arg} unless arg.is_a?(Hash)
346 347
347 348 @message = arg[:message]
348 349 @message = l(@message) if @message.is_a?(Symbol)
349 350 @status = arg[:status] || 500
350 351
351 352 respond_to do |format|
352 353 format.html {
353 354 render :template => 'common/error', :layout => use_layout, :status => @status
354 355 }
355 format.atom { head @status }
356 format.xml { head @status }
357 format.js { head @status }
358 format.json { head @status }
356 format.any { head @status }
359 357 end
360 358 end
361
359
360 # Handler for ActionView::MissingTemplate exception
361 def missing_template
362 logger.warn "Missing template, responding with 404"
363 @project = nil
364 render_404
365 end
366
362 367 # Filter for actions that provide an API response
363 368 # but have no HTML representation for non admin users
364 369 def require_admin_or_api_request
365 370 return true if api_request?
366 371 if User.current.admin?
367 372 true
368 373 elsif User.current.logged?
369 374 render_error(:status => 406)
370 375 else
371 376 deny_access
372 377 end
373 378 end
374 379
375 380 # Picks which layout to use based on the request
376 381 #
377 382 # @return [boolean, string] name of the layout to use or false for no layout
378 383 def use_layout
379 384 request.xhr? ? false : 'base'
380 385 end
381 386
382 387 def invalid_authenticity_token
383 388 if api_request?
384 389 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
385 390 end
386 391 render_error "Invalid form authenticity token."
387 392 end
388 393
389 394 def render_feed(items, options={})
390 395 @items = items || []
391 396 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
392 397 @items = @items.slice(0, Setting.feeds_limit.to_i)
393 398 @title = options[:title] || Setting.app_title
394 399 render :template => "common/feed.atom", :layout => false,
395 400 :content_type => 'application/atom+xml'
396 401 end
397 402
398 403 def self.accept_rss_auth(*actions)
399 404 if actions.any?
400 405 self.accept_rss_auth_actions = actions
401 406 else
402 407 self.accept_rss_auth_actions || []
403 408 end
404 409 end
405 410
406 411 def accept_rss_auth?(action=action_name)
407 412 self.class.accept_rss_auth.include?(action.to_sym)
408 413 end
409 414
410 415 def self.accept_api_auth(*actions)
411 416 if actions.any?
412 417 self.accept_api_auth_actions = actions
413 418 else
414 419 self.accept_api_auth_actions || []
415 420 end
416 421 end
417 422
418 423 def accept_api_auth?(action=action_name)
419 424 self.class.accept_api_auth.include?(action.to_sym)
420 425 end
421 426
422 427 # Returns the number of objects that should be displayed
423 428 # on the paginated list
424 429 def per_page_option
425 430 per_page = nil
426 431 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
427 432 per_page = params[:per_page].to_s.to_i
428 433 session[:per_page] = per_page
429 434 elsif session[:per_page]
430 435 per_page = session[:per_page]
431 436 else
432 437 per_page = Setting.per_page_options_array.first || 25
433 438 end
434 439 per_page
435 440 end
436 441
437 442 # Returns offset and limit used to retrieve objects
438 443 # for an API response based on offset, limit and page parameters
439 444 def api_offset_and_limit(options=params)
440 445 if options[:offset].present?
441 446 offset = options[:offset].to_i
442 447 if offset < 0
443 448 offset = 0
444 449 end
445 450 end
446 451 limit = options[:limit].to_i
447 452 if limit < 1
448 453 limit = 25
449 454 elsif limit > 100
450 455 limit = 100
451 456 end
452 457 if offset.nil? && options[:page].present?
453 458 offset = (options[:page].to_i - 1) * limit
454 459 offset = 0 if offset < 0
455 460 end
456 461 offset ||= 0
457 462
458 463 [offset, limit]
459 464 end
460 465
461 466 # qvalues http header parser
462 467 # code taken from webrick
463 468 def parse_qvalues(value)
464 469 tmp = []
465 470 if value
466 471 parts = value.split(/,\s*/)
467 472 parts.each {|part|
468 473 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
469 474 val = m[1]
470 475 q = (m[2] or 1).to_f
471 476 tmp.push([val, q])
472 477 end
473 478 }
474 479 tmp = tmp.sort_by{|val, q| -q}
475 480 tmp.collect!{|val, q| val}
476 481 end
477 482 return tmp
478 483 rescue
479 484 nil
480 485 end
481 486
482 487 # Returns a string that can be used as filename value in Content-Disposition header
483 488 def filename_for_content_disposition(name)
484 489 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
485 490 end
486 491
487 492 def api_request?
488 493 %w(xml json).include? params[:format]
489 494 end
490 495
491 496 # Returns the API key present in the request
492 497 def api_key_from_request
493 498 if params[:key].present?
494 499 params[:key].to_s
495 500 elsif request.headers["X-Redmine-API-Key"].present?
496 501 request.headers["X-Redmine-API-Key"].to_s
497 502 end
498 503 end
499 504
500 505 # Renders a warning flash if obj has unsaved attachments
501 506 def render_attachment_warning_if_needed(obj)
502 507 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
503 508 end
504 509
505 510 # Sets the `flash` notice or error based the number of issues that did not save
506 511 #
507 512 # @param [Array, Issue] issues all of the saved and unsaved Issues
508 513 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
509 514 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
510 515 if unsaved_issue_ids.empty?
511 516 flash[:notice] = l(:notice_successful_update) unless issues.empty?
512 517 else
513 518 flash[:error] = l(:notice_failed_to_save_issues,
514 519 :count => unsaved_issue_ids.size,
515 520 :total => issues.size,
516 521 :ids => '#' + unsaved_issue_ids.join(', #'))
517 522 end
518 523 end
519 524
520 525 # Rescues an invalid query statement. Just in case...
521 526 def query_statement_invalid(exception)
522 527 logger.error "Query::StatementInvalid: #{exception.message}" if logger
523 528 session.delete(:query)
524 529 sort_clear if respond_to?(:sort_clear)
525 530 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
526 531 end
527 532
528 533 # Renders a 200 response for successfull updates or deletions via the API
529 534 def render_api_ok
530 535 # head :ok would return a response body with one space
531 536 render :text => '', :status => :ok, :layout => nil
532 537 end
533 538
534 539 # Renders API response on validation failure
535 540 def render_validation_errors(objects)
536 541 if objects.is_a?(Array)
537 542 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
538 543 else
539 544 @error_messages = objects.errors.full_messages
540 545 end
541 546 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
542 547 end
543 548
544 549 # Overrides #_include_layout? so that #render with no arguments
545 550 # doesn't use the layout for api requests
546 551 def _include_layout?(*args)
547 552 api_request? ? false : super
548 553 end
549 554 end
@@ -1,63 +1,68
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 ApplicationTest < ActionController::IntegrationTest
21 21 include Redmine::I18n
22 22
23 23 fixtures :projects, :trackers, :issue_statuses, :issues,
24 24 :enumerations, :users, :issue_categories,
25 25 :projects_trackers,
26 26 :roles,
27 27 :member_roles,
28 28 :members,
29 29 :enabled_modules,
30 30 :workflows
31 31
32 32 def test_set_localization
33 33 Setting.default_language = 'en'
34 34
35 35 # a french user
36 36 get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
37 37 assert_response :success
38 38 assert_tag :tag => 'h2', :content => 'Projets'
39 39 assert_equal :fr, current_language
40 40
41 41 # then an italien user
42 42 get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'it;q=0.8,en-us;q=0.5,en;q=0.3'
43 43 assert_response :success
44 44 assert_tag :tag => 'h2', :content => 'Progetti'
45 45 assert_equal :it, current_language
46 46
47 47 # not a supported language: default language should be used
48 48 get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz'
49 49 assert_response :success
50 50 assert_tag :tag => 'h2', :content => 'Projects'
51 51 end
52 52
53 53 def test_token_based_access_should_not_start_session
54 54 # issue of a private project
55 55 get 'issues/4.atom'
56 56 assert_response 302
57 57
58 58 rss_key = User.find(2).rss_key
59 59 get "issues/4.atom?key=#{rss_key}"
60 60 assert_response 200
61 61 assert_nil session[:user_id]
62 62 end
63
64 def test_missing_template_should_respond_with_404
65 get '/login.png'
66 assert_response 404
67 end
63 68 end
General Comments 0
You need to be logged in to leave comments. Login now