To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / controllers / application_controller.rb @ 1345:1f9cdee56991

History | View | Annotate | Download (18.5 KB)

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

    
18
require 'uri'
19
require 'cgi'
20

    
21
class Unauthorized < Exception; end
22

    
23
class ApplicationController < ActionController::Base
24
  include Redmine::I18n
25

    
26
  class_attribute :accept_api_auth_actions
27
  class_attribute :accept_rss_auth_actions
28
  class_attribute :model_object
29

    
30
  layout 'base'
31

    
32
  protect_from_forgery
33
  def handle_unverified_request
34
    super
35
    cookies.delete(:autologin)
36
  end
37

    
38
  before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
39

    
40
  rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
41
  rescue_from ::Unauthorized, :with => :deny_access
42
  rescue_from ::ActionView::MissingTemplate, :with => :missing_template
43

    
44
  include Redmine::Search::Controller
45
  include Redmine::MenuManager::MenuController
46
  helper Redmine::MenuManager::MenuHelper
47

    
48
  def session_expiration
49
    if session[:user_id]
50
      if session_expired? && !try_to_autologin
51
        reset_session
52
        flash[:error] = l(:error_session_expired)
53
        redirect_to signin_url
54
      else
55
        session[:atime] = Time.now.utc.to_i
56
      end
57
    end
58
  end
59

    
60
  def session_expired?
61
    if Setting.session_lifetime?
62
      unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
63
        return true
64
      end
65
    end
66
    if Setting.session_timeout?
67
      unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
68
        return true
69
      end
70
    end
71
    false
72
  end
73

    
74
  def start_user_session(user)
75
    session[:user_id] = user.id
76
    session[:ctime] = Time.now.utc.to_i
77
    session[:atime] = Time.now.utc.to_i
78
  end
79

    
80
  def user_setup
81
    # Check the settings cache for each request
82
    Setting.check_cache
83
    # Find the current user
84
    User.current = find_current_user
85
    logger.info("  Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
86
  end
87

    
88
  # Returns the current user or nil if no user is logged in
89
  # and starts a session if needed
90
  def find_current_user
91
    user = nil
92
    unless api_request?
93
      if session[:user_id]
94
        # existing session
95
        user = (User.active.find(session[:user_id]) rescue nil)
96
      elsif autologin_user = try_to_autologin
97
        user = autologin_user
98
      elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
99
        # RSS key authentication does not start a session
100
        user = User.find_by_rss_key(params[:key])
101
      end
102
    end
103
    if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
104
      if (key = api_key_from_request)
105
        # Use API key
106
        user = User.find_by_api_key(key)
107
      else
108
        # HTTP Basic, either username/password or API key/random
109
        authenticate_with_http_basic do |username, password|
110
          user = User.try_to_login(username, password) || User.find_by_api_key(username)
111
        end
112
      end
113
      # Switch user if requested by an admin user
114
      if user && user.admin? && (username = api_switch_user_from_request)
115
        su = User.find_by_login(username)
116
        if su && su.active?
117
          logger.info("  User switched by: #{user.login} (id=#{user.id})") if logger
118
          user = su
119
        else
120
          render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
121
        end
122
      end
123
    end
124
    user
125
  end
126

    
127
  def try_to_autologin
128
    if cookies[:autologin] && Setting.autologin?
129
      # auto-login feature starts a new session
130
      user = User.try_to_autologin(cookies[:autologin])
131
      if user
132
        reset_session
133
        start_user_session(user)
134
      end
135
      user
136
    end
137
  end
138

    
139
  # Sets the logged in user
140
  def logged_user=(user)
141
    reset_session
142
    if user && user.is_a?(User)
143
      User.current = user
144
      start_user_session(user)
145
    else
146
      User.current = User.anonymous
147
    end
148
  end
149

    
150
  # Logs out current user
151
  def logout_user
152
    if User.current.logged?
153
      cookies.delete :autologin
154
      Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
155
      self.logged_user = nil
156
    end
157
  end
158

    
159
  # check if login is globally required to access the application
160
  def check_if_login_required
161
    # no check needed if user is already logged in
162
    return true if User.current.logged?
163
    require_login if Setting.login_required?
164
  end
165

    
166
  def set_localization
167
    lang = nil
168
    if User.current.logged?
169
      lang = find_language(User.current.language)
170
    end
171
    if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
172
      accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
173
      if !accept_lang.blank?
174
        accept_lang = accept_lang.downcase
175
        lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
176
      end
177
    end
178
    lang ||= Setting.default_language
179
    set_language_if_valid(lang)
180
  end
181

    
182
  def require_login
183
    if !User.current.logged?
184
      # Extract only the basic url parameters on non-GET requests
185
      if request.get?
186
        url = url_for(params)
187
      else
188
        url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
189
      end
190
      respond_to do |format|
191
        format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
192
        format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
193
        format.xml  { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
194
        format.js   { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
195
        format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
196
      end
197
      return false
198
    end
199
    true
200
  end
201

    
202
  def require_admin
203
    return unless require_login
204
    if !User.current.admin?
205
      render_403
206
      return false
207
    end
208
    true
209
  end
210

    
211
  def deny_access
212
    User.current.logged? ? render_403 : require_login
213
  end
214

    
215
  # Authorize the user for the requested action
216
  def authorize(ctrl = params[:controller], action = params[:action], global = false)
217
    allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
218
    if allowed
219
      true
220
    else
221
      if @project && @project.archived?
222
        render_403 :message => :notice_not_authorized_archived_project
223
      else
224
        deny_access
225
      end
226
    end
227
  end
228

    
229
  # Authorize the user for the requested action outside a project
230
  def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
231
    authorize(ctrl, action, global)
232
  end
233

    
234
  # Find project of id params[:id]
235
  def find_project
236
    @project = Project.find(params[:id])
237
  rescue ActiveRecord::RecordNotFound
238
    User.current.logged? ? render_404 : require_login
239
  end
240

    
241
  # Find project of id params[:project_id]
242
  def find_project_by_project_id
243
    @project = Project.find(params[:project_id])
244
  rescue ActiveRecord::RecordNotFound
245
    User.current.logged? ? render_404 : require_login
246
  end
247

    
248
  # Find a project based on params[:project_id]
249
  # TODO: some subclasses override this, see about merging their logic
250
  def find_optional_project
251
    @project = Project.find(params[:project_id]) unless params[:project_id].blank?
252
    allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
253
    allowed ? true : deny_access
254
  rescue ActiveRecord::RecordNotFound
255
    render_404
256
  end
257

    
258
  # Finds and sets @project based on @object.project
259
  def find_project_from_association
260
    render_404 unless @object.present?
261

    
262
    @project = @object.project
263
  end
264

    
265
  def find_model_object
266
    model = self.class.model_object
267
    if model
268
      @object = model.find(params[:id])
269
      self.instance_variable_set('@' + controller_name.singularize, @object) if @object
270
    end
271
  rescue ActiveRecord::RecordNotFound
272
    render_404
273
  end
274

    
275
  def self.model_object(model)
276
    self.model_object = model
277
  end
278

    
279
  # Find the issue whose id is the :id parameter
280
  # Raises a Unauthorized exception if the issue is not visible
281
  def find_issue
282
    # Issue.visible.find(...) can not be used to redirect user to the login form
283
    # if the issue actually exists but requires authentication
284
    @issue = Issue.find(params[:id])
285
    raise Unauthorized unless @issue.visible?
286
    @project = @issue.project
287
  rescue ActiveRecord::RecordNotFound
288
    render_404
289
  end
290

    
291
  # Find issues with a single :id param or :ids array param
292
  # Raises a Unauthorized exception if one of the issues is not visible
293
  def find_issues
294
    @issues = Issue.find_all_by_id(params[:id] || params[:ids])
295
    raise ActiveRecord::RecordNotFound if @issues.empty?
296
    raise Unauthorized unless @issues.all?(&:visible?)
297
    @projects = @issues.collect(&:project).compact.uniq
298
    @project = @projects.first if @projects.size == 1
299
  rescue ActiveRecord::RecordNotFound
300
    render_404
301
  end
302

    
303
  # make sure that the user is a member of the project (or admin) if project is private
304
  # used as a before_filter for actions that do not require any particular permission on the project
305
  def check_project_privacy
306
    if @project && !@project.archived?
307
      if @project.visible?
308
        true
309
      else
310
        deny_access
311
      end
312
    else
313
      @project = nil
314
      render_404
315
      false
316
    end
317
  end
318

    
319
  def back_url
320
    url = params[:back_url]
321
    if url.nil? && referer = request.env['HTTP_REFERER']
322
      url = CGI.unescape(referer.to_s)
323
    end
324
    url
325
  end
326

    
327
  def redirect_back_or_default(default)
328
    back_url = params[:back_url].to_s
329
    if back_url.present?
330
      begin
331
        uri = URI.parse(back_url)
332
        # do not redirect user to another host or to the login or register page
333
        if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
334
          # soundsoftware: if back_url is the home page,
335
          # change it to My Page (#125)
336
          if (uri.path == home_path)
337
            if (uri.path =~ /\/$/)
338
              uri.path = uri.path + "my"
339
            else
340
              uri.path = uri.path + "/my"
341
            end
342
          end
343
          # soundsoftware: if login page is https but back_url http,
344
          # switch back_url to https to ensure cookie validity (#83)
345
          if (uri.scheme == "http") && (URI.parse(request.url).scheme == "https")
346
            uri.scheme = "https"
347
          end
348
          back_url = uri.to_s
349
          redirect_to(back_url)
350
          return
351
        end
352
      rescue URI::InvalidURIError
353
        logger.warn("Could not redirect to invalid URL #{back_url}")
354
        # redirect to default
355
      end
356
    end
357
    redirect_to default
358
    false
359
  end
360

    
361
  # Redirects to the request referer if present, redirects to args or call block otherwise.
362
  def redirect_to_referer_or(*args, &block)
363
    redirect_to :back
364
  rescue ::ActionController::RedirectBackError
365
    if args.any?
366
      redirect_to *args
367
    elsif block_given?
368
      block.call
369
    else
370
      raise "#redirect_to_referer_or takes arguments or a block"
371
    end
372
  end
373

    
374
  def render_403(options={})
375
    @project = nil
376
    render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
377
    return false
378
  end
379

    
380
  def render_404(options={})
381
    render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
382
    return false
383
  end
384

    
385
  # Renders an error response
386
  def render_error(arg)
387
    arg = {:message => arg} unless arg.is_a?(Hash)
388

    
389
    @message = arg[:message]
390
    @message = l(@message) if @message.is_a?(Symbol)
391
    @status = arg[:status] || 500
392

    
393
    respond_to do |format|
394
      format.html {
395
        render :template => 'common/error', :layout => use_layout, :status => @status
396
      }
397
      format.any { head @status }
398
    end
399
  end
400

    
401
  # Handler for ActionView::MissingTemplate exception
402
  def missing_template
403
    logger.warn "Missing template, responding with 404"
404
    @project = nil
405
    render_404
406
  end
407

    
408
  # Filter for actions that provide an API response
409
  # but have no HTML representation for non admin users
410
  def require_admin_or_api_request
411
    return true if api_request?
412
    if User.current.admin?
413
      true
414
    elsif User.current.logged?
415
      render_error(:status => 406)
416
    else
417
      deny_access
418
    end
419
  end
420

    
421
  # Picks which layout to use based on the request
422
  #
423
  # @return [boolean, string] name of the layout to use or false for no layout
424
  def use_layout
425
    request.xhr? ? false : 'base'
426
  end
427

    
428
  def invalid_authenticity_token
429
    if api_request?
430
      logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
431
    end
432
    render_error "Invalid form authenticity token.  Perhaps your session has timed out; try reloading the form and entering your details again."
433
  end
434

    
435
  def render_feed(items, options={})
436
    @items = items || []
437
    @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
438
    @items = @items.slice(0, Setting.feeds_limit.to_i)
439
    @title = options[:title] || Setting.app_title
440
    render :template => "common/feed", :formats => [:atom], :layout => false,
441
           :content_type => 'application/atom+xml'
442
  end
443

    
444
  def self.accept_rss_auth(*actions)
445
    if actions.any?
446
      self.accept_rss_auth_actions = actions
447
    else
448
      self.accept_rss_auth_actions || []
449
    end
450
  end
451

    
452
  def accept_rss_auth?(action=action_name)
453
    self.class.accept_rss_auth.include?(action.to_sym)
454
  end
455

    
456
  def self.accept_api_auth(*actions)
457
    if actions.any?
458
      self.accept_api_auth_actions = actions
459
    else
460
      self.accept_api_auth_actions || []
461
    end
462
  end
463

    
464
  def accept_api_auth?(action=action_name)
465
    self.class.accept_api_auth.include?(action.to_sym)
466
  end
467

    
468
  # Returns the number of objects that should be displayed
469
  # on the paginated list
470
  def per_page_option
471
    per_page = nil
472
    if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
473
      per_page = params[:per_page].to_s.to_i
474
      session[:per_page] = per_page
475
    elsif session[:per_page]
476
      per_page = session[:per_page]
477
    else
478
      per_page = Setting.per_page_options_array.first || 25
479
    end
480
    per_page
481
  end
482

    
483
  # Returns offset and limit used to retrieve objects
484
  # for an API response based on offset, limit and page parameters
485
  def api_offset_and_limit(options=params)
486
    if options[:offset].present?
487
      offset = options[:offset].to_i
488
      if offset < 0
489
        offset = 0
490
      end
491
    end
492
    limit = options[:limit].to_i
493
    if limit < 1
494
      limit = 25
495
    elsif limit > 100
496
      limit = 100
497
    end
498
    if offset.nil? && options[:page].present?
499
      offset = (options[:page].to_i - 1) * limit
500
      offset = 0 if offset < 0
501
    end
502
    offset ||= 0
503

    
504
    [offset, limit]
505
  end
506

    
507
  # qvalues http header parser
508
  # code taken from webrick
509
  def parse_qvalues(value)
510
    tmp = []
511
    if value
512
      parts = value.split(/,\s*/)
513
      parts.each {|part|
514
        if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
515
          val = m[1]
516
          q = (m[2] or 1).to_f
517
          tmp.push([val, q])
518
        end
519
      }
520
      tmp = tmp.sort_by{|val, q| -q}
521
      tmp.collect!{|val, q| val}
522
    end
523
    return tmp
524
  rescue
525
    nil
526
  end
527

    
528
  # Returns a string that can be used as filename value in Content-Disposition header
529
  def filename_for_content_disposition(name)
530
    request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
531
  end
532

    
533
  def api_request?
534
    %w(xml json).include? params[:format]
535
  end
536

    
537
  # Returns the API key present in the request
538
  def api_key_from_request
539
    if params[:key].present?
540
      params[:key].to_s
541
    elsif request.headers["X-Redmine-API-Key"].present?
542
      request.headers["X-Redmine-API-Key"].to_s
543
    end
544
  end
545

    
546
  # Returns the API 'switch user' value if present
547
  def api_switch_user_from_request
548
    request.headers["X-Redmine-Switch-User"].to_s.presence
549
  end
550

    
551
  # Renders a warning flash if obj has unsaved attachments
552
  def render_attachment_warning_if_needed(obj)
553
    flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
554
  end
555

    
556
  # Sets the `flash` notice or error based the number of issues that did not save
557
  #
558
  # @param [Array, Issue] issues all of the saved and unsaved Issues
559
  # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
560
  def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
561
    if unsaved_issue_ids.empty?
562
      flash[:notice] = l(:notice_successful_update) unless issues.empty?
563
    else
564
      flash[:error] = l(:notice_failed_to_save_issues,
565
                        :count => unsaved_issue_ids.size,
566
                        :total => issues.size,
567
                        :ids => '#' + unsaved_issue_ids.join(', #'))
568
    end
569
  end
570

    
571
  # Rescues an invalid query statement. Just in case...
572
  def query_statement_invalid(exception)
573
    logger.error "Query::StatementInvalid: #{exception.message}" if logger
574
    session.delete(:query)
575
    sort_clear if respond_to?(:sort_clear)
576
    render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
577
  end
578

    
579
  # Renders a 200 response for successfull updates or deletions via the API
580
  def render_api_ok
581
    render_api_head :ok
582
  end
583

    
584
  # Renders a head API response
585
  def render_api_head(status)
586
    # #head would return a response body with one space
587
    render :text => '', :status => status, :layout => nil
588
  end
589

    
590
  # Renders API response on validation failure
591
  def render_validation_errors(objects)
592
    if objects.is_a?(Array)
593
      @error_messages = objects.map {|object| object.errors.full_messages}.flatten
594
    else
595
      @error_messages = objects.errors.full_messages
596
    end
597
    render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
598
  end
599

    
600
  # Overrides #_include_layout? so that #render with no arguments
601
  # doesn't use the layout for api requests
602
  def _include_layout?(*args)
603
    api_request? ? false : super
604
  end
605
end