File: app/controllers/application_controller.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: Unauthorized#21
inherits from
  Exception ( Builtin-Module )
  class: ApplicationController#23
includes
  I18n ( Redmine )
  MenuController ( Redmine::MenuManager )
  Controller ( Redmine::Search )
inherits from
  Base ( ActionController )
has properties
method: handle_unverified_request #30
method: delete_broken_cookies #38
method: params_filter #50
method: utf8nize! / 1 #56
method: user_setup #84
method: find_current_user #93
method: logged_user= / 1 #119
method: check_if_login_required #130
method: set_localization #136
method: require_login #152
method: require_admin #172
method: deny_access #181
method: authorize / 3 #186
method: authorize_global / 3 #200
method: find_project #205
method: find_project_by_project_id #212
method: find_optional_project #220
method: find_project_from_association #229
method: find_model_object #235
class method: model_object / 1 #245
method: find_issues #250
method: check_project_privacy #265
method: back_url #279
method: redirect_back_or_default / 1 #283
method: render_403 / 1 #301
method: render_404 / 1 #307
method: render_error / 1 #313
method: require_admin_or_api_request #333
method: use_layout #347
method: invalid_authenticity_token #351
method: render_feed / 2 #358
class method: accept_rss_auth / 1 #367
method: accept_rss_auth? / 1 #375
class method: accept_api_auth / 1 #379
method: accept_api_auth? / 1 #387
method: per_page_option #393
method: api_offset_and_limit / 1 #408
method: parse_qvalues / 1 #432
method: filename_for_content_disposition / 1 #452
method: api_request? #456
method: api_key_from_request #461
method: render_attachment_warning_if_needed / 1 #470
method: set_flash_from_bulk_issue_save / 2 #478
method: query_statement_invalid / 1 #490
method: render_validation_errors / 1 #498
method: default_template / 1 #509
method: pick_layout / 1 #523

Class Hierarchy

Code

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