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

18
require 'uri'
19
require 'cgi'
20

21 22
class Unauthorized < Exception; end

23
class ApplicationController < ActionController::Base
24
  include Redmine::I18n
25
  include Redmine::Pagination
26 27
  include RoutesHelper
  helper :routes
28

jplang's avatar
jplang committed
29 30 31
  class_attribute :accept_api_auth_actions
  class_attribute :accept_rss_auth_actions
  class_attribute :model_object
edavis10's avatar
edavis10 committed
32

33
  layout 'base'
34

jplang's avatar
jplang committed
35
  protect_from_forgery
36 37 38 39 40 41 42

  def verify_authenticity_token
    unless api_request?
      super
    end
  end

43
  def handle_unverified_request
44 45 46
    unless api_request?
      super
      cookies.delete(autologin_cookie_name)
47
      self.logged_user = nil
48
      set_localization
49
      render_error :status => 422, :message => "Invalid form authenticity token."
50
    end
51
  end
52

53
  before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54

55
  rescue_from ::Unauthorized, :with => :deny_access
56
  rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57

jplang's avatar
jplang committed
58
  include Redmine::Search::Controller
59 60
  include Redmine::MenuManager::MenuController
  helper Redmine::MenuManager::MenuHelper
61

62 63 64
  def session_expiration
    if session[:user_id]
      if session_expired? && !try_to_autologin
65
        set_localization(User.active.find_by_id(session[:user_id]))
66
        self.logged_user = nil
67
        flash[:error] = l(:error_session_expired)
68
        require_login
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
      else
        session[:atime] = Time.now.utc.to_i
      end
    end
  end

  def session_expired?
    if Setting.session_lifetime?
      unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
        return true
      end
    end
    if Setting.session_timeout?
      unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
        return true
      end
    end
    false
  end

  def start_user_session(user)
    session[:user_id] = user.id
    session[:ctime] = Time.now.utc.to_i
    session[:atime] = Time.now.utc.to_i
93 94 95
    if user.must_change_password?
      session[:pwd] = '1'
    end
96 97
  end

jplang's avatar
jplang committed
98
  def user_setup
99
    # Check the settings cache for each request
100
    Setting.check_cache
101
    # Find the current user
102
    User.current = find_current_user
jplang's avatar
jplang committed
103
    logger.info("  Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
104
  end
105

106
  # Returns the current user or nil if no user is logged in
107
  # and starts a session if needed
108
  def find_current_user
109 110
    user = nil
    unless api_request?
111
      if session[:user_id]
112 113 114 115 116 117 118 119 120 121
        # existing session
        user = (User.active.find(session[:user_id]) rescue nil)
      elsif autologin_user = try_to_autologin
        user = autologin_user
      elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
        # RSS key authentication does not start a session
        user = User.find_by_rss_key(params[:key])
      end
    end
    if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
jplang's avatar
jplang committed
122
      if (key = api_key_from_request)
123
        # Use API key
124
        user = User.find_by_api_key(key)
125
      elsif request.authorization.to_s =~ /\ABasic /i
126 127
        # HTTP Basic, either username/password or API key/random
        authenticate_with_http_basic do |username, password|
128
          user = User.try_to_login(username, password) || User.find_by_api_key(username)
129
        end
130 131 132 133
        if user && user.must_change_password?
          render_error :message => 'You must change your password', :status => 403
          return
        end
134
      end
135 136 137 138 139 140 141 142 143 144
      # Switch user if requested by an admin user
      if user && user.admin? && (username = api_switch_user_from_request)
        su = User.find_by_login(username)
        if su && su.active?
          logger.info("  User switched by: #{user.login} (id=#{user.id})") if logger
          user = su
        else
          render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
        end
      end
145
    end
146
    user
147
  end
148

149 150 151 152 153 154 155 156 157 158 159 160
  def force_logout_if_password_changed
    passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
    # Make sure we force logout only for web browser sessions, not API calls
    # if the password was changed after the session creation.
    if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
      reset_session
      set_localization
      flash[:error] = l(:error_session_expired)
      redirect_to signin_url
    end
  end

161 162 163 164
  def autologin_cookie_name
    Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
  end

165
  def try_to_autologin
166
    if cookies[autologin_cookie_name] && Setting.autologin?
167
      # auto-login feature starts a new session
168
      user = User.try_to_autologin(cookies[autologin_cookie_name])
169 170 171 172 173 174 175 176
      if user
        reset_session
        start_user_session(user)
      end
      user
    end
  end

177 178
  # Sets the logged in user
  def logged_user=(user)
179
    reset_session
180 181
    if user && user.is_a?(User)
      User.current = user
182
      start_user_session(user)
183 184 185 186
    else
      User.current = User.anonymous
    end
  end
187

188 189 190
  # Logs out current user
  def logout_user
    if User.current.logged?
191
      cookies.delete(autologin_cookie_name)
192 193 194 195 196
      Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
      self.logged_user = nil
    end
  end

197 198
  # check if login is globally required to access the application
  def check_if_login_required
199
    # no check needed if user is already logged in
jplang's avatar
jplang committed
200
    return true if User.current.logged?
201
    require_login if Setting.login_required?
202 203
  end

204 205 206
  def check_password_change
    if session[:pwd]
      if User.current.must_change_password?
207
        flash[:error] = l(:error_password_expired)
208 209 210 211 212 213 214
        redirect_to my_password_path
      else
        session.delete(:pwd)
      end
    end
  end

215
  def set_localization(user=User.current)
216
    lang = nil
217 218
    if user && user.logged?
      lang = find_language(user.language)
219
    end
220
    if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
221
      accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
222
      if !accept_lang.blank?
223
        accept_lang = accept_lang.downcase
224
        lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
225
      end
226 227 228
    end
    lang ||= Setting.default_language
    set_language_if_valid(lang)
229
  end
230

231
  def require_login
jplang's avatar
jplang committed
232
    if !User.current.logged?
233 234 235 236 237 238
      # Extract only the basic url parameters on non-GET requests
      if request.get?
        url = url_for(params)
      else
        url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
      end
239
      respond_to do |format|
240 241 242 243
        format.html {
          if request.xhr?
            head :unauthorized
          else
jplang's avatar
jplang committed
244
            redirect_to signin_path(:back_url => url)
245 246
          end
        }
247
        format.any(:atom, :pdf, :csv) {
jplang's avatar
jplang committed
248
          redirect_to signin_path(:back_url => url)
249
        }
250
        format.xml  { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
edavis10's avatar
edavis10 committed
251
        format.js   { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
252
        format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
253
        format.any  { head :unauthorized }
254
      end
255 256 257 258 259 260 261
      return false
    end
    true
  end

  def require_admin
    return unless require_login
jplang's avatar
jplang committed
262
    if !User.current.admin?
263
      render_403
264 265 266 267
      return false
    end
    true
  end
268

269 270 271
  def deny_access
    User.current.logged? ? render_403 : require_login
  end
272

jplang's avatar
jplang committed
273
  # Authorize the user for the requested action
274
  def authorize(ctrl = params[:controller], action = params[:action], global = false)
275
    allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
276 277 278 279 280 281 282 283 284
    if allowed
      true
    else
      if @project && @project.archived?
        render_403 :message => :notice_not_authorized_archived_project
      else
        deny_access
      end
    end
285
  end
286 287 288 289 290

  # Authorize the user for the requested action outside a project
  def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
    authorize(ctrl, action, global)
  end
291 292 293 294 295 296 297

  # Find project of id params[:id]
  def find_project
    @project = Project.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end
298

299 300 301 302 303 304 305
  # Find project of id params[:project_id]
  def find_project_by_project_id
    @project = Project.find(params[:project_id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

306 307 308 309 310 311 312 313 314 315
  # Find a project based on params[:project_id]
  # TODO: some subclasses override this, see about merging their logic
  def find_optional_project
    @project = Project.find(params[:project_id]) unless params[:project_id].blank?
    allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
    allowed ? true : deny_access
  rescue ActiveRecord::RecordNotFound
    render_404
  end

316 317 318
  # Finds and sets @project based on @object.project
  def find_project_from_association
    render_404 unless @object.present?
319

320 321 322
    @project = @object.project
  end

323
  def find_model_object
jplang's avatar
jplang committed
324
    model = self.class.model_object
325 326 327 328 329 330 331 332 333
    if model
      @object = model.find(params[:id])
      self.instance_variable_set('@' + controller_name.singularize, @object) if @object
    end
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def self.model_object(model)
jplang's avatar
jplang committed
334
    self.model_object = model
335
  end
336

jplang's avatar
jplang committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350
  # Find the issue whose id is the :id parameter
  # Raises a Unauthorized exception if the issue is not visible
  def find_issue
    # Issue.visible.find(...) can not be used to redirect user to the login form
    # if the issue actually exists but requires authentication
    @issue = Issue.find(params[:id])
    raise Unauthorized unless @issue.visible?
    @project = @issue.project
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  # Find issues with a single :id param or :ids array param
  # Raises a Unauthorized exception if one of the issues is not visible
351
  def find_issues
352
    @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
353
    raise ActiveRecord::RecordNotFound if @issues.empty?
jplang's avatar
jplang committed
354
    raise Unauthorized unless @issues.all?(&:visible?)
355 356 357 358 359
    @projects = @issues.collect(&:project).compact.uniq
    @project = @projects.first if @projects.size == 1
  rescue ActiveRecord::RecordNotFound
    render_404
  end
360

jplang's avatar
jplang committed
361 362 363 364 365 366 367 368 369 370
  def find_attachments
    if (attachments = params[:attachments]).present?
      att = attachments.values.collect do |attachment|
        Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
      end
      att.compact!
    end
    @attachments = att || []
  end

371 372 373
  # make sure that the user is a member of the project (or admin) if project is private
  # used as a before_filter for actions that do not require any particular permission on the project
  def check_project_privacy
374
    if @project && !@project.archived?
jplang's avatar
jplang committed
375
      if @project.visible?
376 377
        true
      else
jplang's avatar
jplang committed
378
        deny_access
379 380
      end
    else
381 382
      @project = nil
      render_404
383
      false
384
    end
385 386
  end

387
  def back_url
388 389 390 391 392
    url = params[:back_url]
    if url.nil? && referer = request.env['HTTP_REFERER']
      url = CGI.unescape(referer.to_s)
    end
    url
393 394
  end

395
  def redirect_back_or_default(default, options={})
396
    back_url = params[:back_url].to_s
397 398 399
    if back_url.present? && valid_back_url?(back_url)
      redirect_to(back_url)
      return
400 401 402
    elsif options[:referer]
      redirect_to_referer_or default
      return
jplang's avatar
v0.2.0  
jplang committed
403
    end
404
    redirect_to default
405
    false
jplang's avatar
v0.2.0  
jplang committed
406
  end
407

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
  # Returns true if back_url is a valid url for redirection, otherwise false
  def valid_back_url?(back_url)
    if CGI.unescape(back_url).include?('..')
      return false
    end

    begin
      uri = URI.parse(back_url)
    rescue URI::InvalidURIError
      return false
    end

    if uri.host.present? && uri.host != request.host
      return false
    end

    if uri.path.match(%r{/(login|account/register)})
      return false
    end

    if relative_url_root.present? && !uri.path.starts_with?(relative_url_root)
      return false
    end

    return true
  end
  private :valid_back_url?

jplang's avatar
jplang committed
436 437 438 439 440 441 442 443 444 445 446 447 448
  # Redirects to the request referer if present, redirects to args or call block otherwise.
  def redirect_to_referer_or(*args, &block)
    redirect_to :back
  rescue ::ActionController::RedirectBackError
    if args.any?
      redirect_to *args
    elsif block_given?
      block.call
    else
      raise "#redirect_to_referer_or takes arguments or a block"
    end
  end

449
  def render_403(options={})
450
    @project = nil
451
    render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
452 453
    return false
  end
454

455 456
  def render_404(options={})
    render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
457 458
    return false
  end
459

460 461 462
  # Renders an error response
  def render_error(arg)
    arg = {:message => arg} unless arg.is_a?(Hash)
463

464 465 466
    @message = arg[:message]
    @message = l(@message) if @message.is_a?(Symbol)
    @status = arg[:status] || 500
467

468
    respond_to do |format|
469 470
      format.html {
        render :template => 'common/error', :layout => use_layout, :status => @status
471
      }
472
      format.any { head @status }
473
    end
474
  end
475 476 477 478 479 480 481 482

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

483 484 485 486 487 488 489 490 491 492 493 494
  # Filter for actions that provide an API response
  # but have no HTML representation for non admin users
  def require_admin_or_api_request
    return true if api_request?
    if User.current.admin?
      true
    elsif User.current.logged?
      render_error(:status => 406)
    else
      deny_access
    end
  end
495 496 497 498 499 500 501

  # Picks which layout to use based on the request
  #
  # @return [boolean, string] name of the layout to use or false for no layout
  def use_layout
    request.xhr? ? false : 'base'
  end
502 503

  def render_feed(items, options={})
jplang's avatar
jplang committed
504
    @items = (items || []).to_a
505
    @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
506
    @items = @items.slice(0, Setting.feeds_limit.to_i)
jplang's avatar
jplang committed
507
    @title = options[:title] || Setting.app_title
508
    render :template => "common/feed", :formats => [:atom], :layout => false,
509
           :content_type => 'application/atom+xml'
jplang's avatar
jplang committed
510
  end
511

jplang's avatar
jplang committed
512 513
  def self.accept_rss_auth(*actions)
    if actions.any?
jplang's avatar
jplang committed
514
      self.accept_rss_auth_actions = actions
jplang's avatar
jplang committed
515
    else
jplang's avatar
jplang committed
516
      self.accept_rss_auth_actions || []
jplang's avatar
jplang committed
517 518
    end
  end
519

jplang's avatar
jplang committed
520 521 522
  def accept_rss_auth?(action=action_name)
    self.class.accept_rss_auth.include?(action.to_sym)
  end
523

jplang's avatar
jplang committed
524 525
  def self.accept_api_auth(*actions)
    if actions.any?
jplang's avatar
jplang committed
526
      self.accept_api_auth_actions = actions
jplang's avatar
jplang committed
527
    else
jplang's avatar
jplang committed
528
      self.accept_api_auth_actions || []
jplang's avatar
jplang committed
529 530
    end
  end
531

jplang's avatar
jplang committed
532 533
  def accept_api_auth?(action=action_name)
    self.class.accept_api_auth.include?(action.to_sym)
jplang's avatar
jplang committed
534
  end
535

536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
  # Returns the number of objects that should be displayed
  # on the paginated list
  def per_page_option
    per_page = nil
    if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
      per_page = params[:per_page].to_s.to_i
      session[:per_page] = per_page
    elsif session[:per_page]
      per_page = session[:per_page]
    else
      per_page = Setting.per_page_options_array.first || 25
    end
    per_page
  end

551 552 553 554 555
  # Returns offset and limit used to retrieve objects
  # for an API response based on offset, limit and page parameters
  def api_offset_and_limit(options=params)
    if options[:offset].present?
      offset = options[:offset].to_i
556 557 558 559
      if offset < 0
        offset = 0
      end
    end
560
    limit = options[:limit].to_i
561 562 563 564 565
    if limit < 1
      limit = 25
    elsif limit > 100
      limit = 100
    end
566 567 568 569 570
    if offset.nil? && options[:page].present?
      offset = (options[:page].to_i - 1) * limit
      offset = 0 if offset < 0
    end
    offset ||= 0
571

572 573
    [offset, limit]
  end
574

575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
  # qvalues http header parser
  # code taken from webrick
  def parse_qvalues(value)
    tmp = []
    if value
      parts = value.split(/,\s*/)
      parts.each {|part|
        if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
          val = m[1]
          q = (m[2] or 1).to_f
          tmp.push([val, q])
        end
      }
      tmp = tmp.sort_by{|val, q| -q}
      tmp.collect!{|val, q| val}
    end
    return tmp
592 593
  rescue
    nil
594
  end
595

596 597
  # Returns a string that can be used as filename value in Content-Disposition header
  def filename_for_content_disposition(name)
598
    request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
599
  end
600

601 602 603
  def api_request?
    %w(xml json).include? params[:format]
  end
604

605 606 607
  # Returns the API key present in the request
  def api_key_from_request
    if params[:key].present?
608
      params[:key].to_s
609
    elsif request.headers["X-Redmine-API-Key"].present?
610
      request.headers["X-Redmine-API-Key"].to_s
611 612
    end
  end
613

614 615 616 617 618
  # Returns the API 'switch user' value if present
  def api_switch_user_from_request
    request.headers["X-Redmine-Switch-User"].to_s.presence
  end

619 620 621 622
  # Renders a warning flash if obj has unsaved attachments
  def render_attachment_warning_if_needed(obj)
    flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
  end
623 624 625 626 627 628 629 630 631

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

632 633
  # Renders a 200 response for successfull updates or deletions via the API
  def render_api_ok
634 635 636 637 638 639 640
    render_api_head :ok
  end

  # Renders a head API response
  def render_api_head(status)
    # #head would return a response body with one space
    render :text => '', :status => status, :layout => nil
641 642
  end

643
  # Renders API response on validation failure
644
  # for an object or an array of objects
jplang's avatar
jplang committed
645
  def render_validation_errors(objects)
646 647 648 649 650 651
    messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
    render_api_errors(messages)
  end

  def render_api_errors(*messages)
    @error_messages = messages.flatten
jplang's avatar
jplang committed
652
    render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
653
  end
654

jplang's avatar
jplang committed
655
  # Overrides #_include_layout? so that #render with no arguments
656
  # doesn't use the layout for api requests
jplang's avatar
jplang committed
657 658
  def _include_layout?(*args)
    api_request? ? false : super
659
  end
jplang's avatar
jplang committed
660
end