application_controller.rb
522 lines
| 16.2 KiB
| text/x-ruby
|
RubyLexer
|
r5296 | # Redmine - project management software | ||
# Copyright (C) 2006-2011 Jean-Philippe Lang | ||||
|
r330 | # | ||
# 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. | ||||
|
r5629 | # | ||
|
r330 | # 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. | ||||
|
r5629 | # | ||
|
r330 | # 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. | ||||
|
r1686 | require 'uri' | ||
|
r1891 | require 'cgi' | ||
|
r1686 | |||
|
r6043 | class Unauthorized < Exception; end | ||
|
r330 | class ApplicationController < ActionController::Base | ||
|
r2430 | include Redmine::I18n | ||
|
r2773 | |||
|
r1726 | layout 'base' | ||
|
r4352 | exempt_from_layout 'builder', 'rsb' | ||
|
r6405 | |||
|
r6195 | protect_from_forgery | ||
|
r6196 | def handle_unverified_request | ||
super | ||||
cookies.delete(:autologin) | ||||
end | ||||
|
r2979 | # Remove broken cookie after upgrade from 0.8.x (#4292) | ||
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360 | ||||
# TODO: remove it when Rails is fixed | ||||
before_filter :delete_broken_cookies | ||||
def delete_broken_cookies | ||||
if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/ | ||||
|
r5629 | cookies.delete '_redmine_session' | ||
|
r3071 | redirect_to home_path | ||
return false | ||||
|
r2979 | end | ||
end | ||||
|
r5629 | |||
|
r663 | before_filter :user_setup, :check_if_login_required, :set_localization | ||
|
r330 | filter_parameter_logging :password | ||
|
r5629 | |||
|
r2980 | rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token | ||
|
r6043 | rescue_from ::Unauthorized, :with => :deny_access | ||
|
r5629 | |||
|
r2829 | include Redmine::Search::Controller | ||
|
r1062 | include Redmine::MenuManager::MenuController | ||
helper Redmine::MenuManager::MenuHelper | ||||
|
r5629 | |||
|
r3326 | Redmine::Scm::Base.all.each do |scm| | ||
|
r558 | require_dependency "repository/#{scm.underscore}" | ||
end | ||||
|
r2979 | |||
|
r663 | def user_setup | ||
|
r1016 | # Check the settings cache for each request | ||
|
r674 | Setting.check_cache | ||
|
r1016 | # Find the current user | ||
|
r2679 | User.current = find_current_user | ||
|
r1016 | end | ||
|
r5629 | |||
|
r1016 | # Returns the current user or nil if no user is logged in | ||
|
r2679 | # and starts a session if needed | ||
|
r1016 | def find_current_user | ||
|
r330 | if session[:user_id] | ||
|
r663 | # existing session | ||
|
r2077 | (User.active.find(session[:user_id]) rescue nil) | ||
|
r663 | elsif cookies[:autologin] && Setting.autologin? | ||
|
r2679 | # auto-login feature starts a new session | ||
user = User.try_to_autologin(cookies[:autologin]) | ||||
session[:user_id] = user.id if user | ||||
user | ||||
|
r6077 | elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? | ||
|
r2679 | # RSS key authentication does not start a session | ||
|
r1016 | User.find_by_rss_key(params[:key]) | ||
|
r6077 | elsif Setting.rest_api_enabled? && accept_api_auth? | ||
if (key = api_key_from_request) | ||||
|
r3105 | # Use API key | ||
|
r4459 | User.find_by_api_key(key) | ||
|
r3105 | else | ||
# HTTP Basic, either username/password or API key/random | ||||
authenticate_with_http_basic do |username, password| | ||||
User.try_to_login(username, password) || User.find_by_api_key(username) | ||||
end | ||||
end | ||||
|
r330 | end | ||
end | ||||
|
r3105 | |||
|
r2460 | # Sets the logged in user | ||
def logged_user=(user) | ||||
|
r2966 | reset_session | ||
|
r2460 | if user && user.is_a?(User) | ||
User.current = user | ||||
session[:user_id] = user.id | ||||
else | ||||
User.current = User.anonymous | ||||
end | ||||
end | ||||
|
r5629 | |||
|
r330 | # check if login is globally required to access the application | ||
def check_if_login_required | ||||
|
r511 | # no check needed if user is already logged in | ||
|
r663 | return true if User.current.logged? | ||
|
r330 | require_login if Setting.login_required? | ||
|
r5629 | end | ||
|
r330 | def set_localization | ||
|
r2430 | lang = nil | ||
if User.current.logged? | ||||
lang = find_language(User.current.language) | ||||
end | ||||
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE'] | ||||
|
r3588 | accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first | ||
|
r2430 | if !accept_lang.blank? | ||
|
r3588 | accept_lang = accept_lang.downcase | ||
|
r2430 | lang = find_language(accept_lang) || find_language(accept_lang.split('-').first) | ||
|
r330 | end | ||
|
r2430 | end | ||
lang ||= Setting.default_language | ||||
set_language_if_valid(lang) | ||||
|
r330 | end | ||
|
r5629 | |||
|
r330 | def require_login | ||
|
r663 | if !User.current.logged? | ||
|
r2936 | # 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 | ||||
|
r3104 | respond_to do |format| | ||
format.html { redirect_to :controller => "account", :action => "login", :back_url => url } | ||||
|
r3105 | format.atom { redirect_to :controller => "account", :action => "login", :back_url => url } | ||
|
r3565 | format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } | ||
|
r3713 | format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } | ||
|
r3565 | format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } | ||
|
r3104 | end | ||
|
r330 | return false | ||
end | ||||
true | ||||
end | ||||
def require_admin | ||||
return unless require_login | ||||
|
r663 | if !User.current.admin? | ||
|
r492 | render_403 | ||
|
r330 | return false | ||
end | ||||
true | ||||
end | ||||
|
r5629 | |||
|
r1777 | def deny_access | ||
User.current.logged? ? render_403 : require_login | ||||
end | ||||
|
r330 | |||
|
r663 | # Authorize the user for the requested action | ||
|
r2651 | def authorize(ctrl = params[:controller], action = params[:action], global = false) | ||
|
r4122 | allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global) | ||
|
r4171 | if allowed | ||
true | ||||
else | ||||
if @project && @project.archived? | ||||
render_403 :message => :notice_not_authorized_archived_project | ||||
else | ||||
deny_access | ||||
end | ||||
end | ||||
|
r330 | end | ||
|
r2651 | |||
# 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 | ||||
|
r3256 | |||
# Find project of id params[:id] | ||||
def find_project | ||||
@project = Project.find(params[:id]) | ||||
rescue ActiveRecord::RecordNotFound | ||||
render_404 | ||||
end | ||||
|
r3477 | |||
|
r3961 | # 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 | ||||
|
r3602 | # 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 | ||||
|
r3477 | # Finds and sets @project based on @object.project | ||
def find_project_from_association | ||||
render_404 unless @object.present? | ||||
|
r5629 | |||
|
r3477 | @project = @object.project | ||
rescue ActiveRecord::RecordNotFound | ||||
render_404 | ||||
end | ||||
|
r3483 | def find_model_object | ||
model = self.class.read_inheritable_attribute('model_object') | ||||
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) | ||||
write_inheritable_attribute('model_object', model) | ||||
end | ||||
|
r3824 | |||
# Filter for bulk issue operations | ||||
def find_issues | ||||
@issues = Issue.find_all_by_id(params[:id] || params[:ids]) | ||||
raise ActiveRecord::RecordNotFound if @issues.empty? | ||||
|
r5296 | if @issues.detect {|issue| !issue.visible?} | ||
deny_access | ||||
return | ||||
end | ||||
|
r4114 | @projects = @issues.collect(&:project).compact.uniq | ||
@project = @projects.first if @projects.size == 1 | ||||
rescue ActiveRecord::RecordNotFound | ||||
render_404 | ||||
end | ||||
|
r5629 | |||
|
r4114 | # Check if project is unique before bulk operations | ||
def check_project_uniqueness | ||||
unless @project | ||||
|
r3824 | # TODO: let users bulk edit/move/destroy issues from different projects | ||
render_error 'Can not bulk edit/move/destroy issues from different projects' | ||||
return false | ||||
end | ||||
end | ||||
|
r5629 | |||
|
r330 | # 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 | ||||
|
r1223 | if @project && @project.active? | ||
if @project.is_public? || User.current.member_of?(@project) || User.current.admin? | ||||
true | ||||
else | ||||
User.current.logged? ? render_403 : require_login | ||||
end | ||||
else | ||||
|
r546 | @project = nil | ||
render_404 | ||||
|
r1223 | false | ||
|
r546 | end | ||
|
r330 | end | ||
|
r3798 | def back_url | ||
params[:back_url] || request.env['HTTP_REFERER'] | ||||
end | ||||
|
r5 | def redirect_back_or_default(default) | ||
|
r1891 | back_url = CGI.unescape(params[:back_url].to_s) | ||
|
r1686 | if !back_url.blank? | ||
|
r2124 | begin | ||
uri = URI.parse(back_url) | ||||
# do not redirect user to another host or to the login or register page | ||||
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) | ||||
|
r3071 | redirect_to(back_url) | ||
return | ||||
|
r2124 | end | ||
rescue URI::InvalidURIError | ||||
# redirect to default | ||||
|
r1686 | end | ||
|
r5 | end | ||
|
r1686 | redirect_to default | ||
|
r5491 | false | ||
|
r5 | end | ||
|
r5629 | |||
|
r4171 | def render_403(options={}) | ||
|
r492 | @project = nil | ||
|
r4172 | render_error({:message => :notice_not_authorized, :status => 403}.merge(options)) | ||
|
r492 | return false | ||
end | ||||
|
r5629 | |||
|
r4172 | def render_404(options={}) | ||
render_error({:message => :notice_file_not_found, :status => 404}.merge(options)) | ||||
|
r330 | return false | ||
end | ||||
|
r5629 | |||
|
r4172 | # Renders an error response | ||
def render_error(arg) | ||||
arg = {:message => arg} unless arg.is_a?(Hash) | ||||
|
r5629 | |||
|
r4172 | @message = arg[:message] | ||
@message = l(@message) if @message.is_a?(Symbol) | ||||
@status = arg[:status] || 500 | ||||
|
r5629 | |||
|
r3196 | respond_to do |format| | ||
|
r4172 | format.html { | ||
render :template => 'common/error', :layout => use_layout, :status => @status | ||||
|
r3196 | } | ||
|
r4172 | format.atom { head @status } | ||
format.xml { head @status } | ||||
format.js { head @status } | ||||
format.json { head @status } | ||||
|
r3196 | end | ||
|
r1080 | end | ||
|
r3835 | |||
# 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 | ||||
|
r5629 | |||
|
r2980 | def invalid_authenticity_token | ||
|
r3218 | if api_request? | ||
logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)." | ||||
end | ||||
|
r2980 | render_error "Invalid form authenticity token." | ||
end | ||||
|
r5629 | |||
def render_feed(items, options={}) | ||||
|
r675 | @items = items || [] | ||
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime } | ||||
|
r1295 | @items = @items.slice(0, Setting.feeds_limit.to_i) | ||
|
r663 | @title = options[:title] || Setting.app_title | ||
|
r7453 | render :template => "common/feed.atom", :layout => false, | ||
:content_type => 'application/atom+xml' | ||||
|
r663 | end | ||
|
r6405 | |||
|
r6077 | # TODO: remove in Redmine 1.4 | ||
|
r663 | def self.accept_key_auth(*actions) | ||
|
r6088 | ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." | ||
|
r6077 | accept_rss_auth(*actions) | ||
|
r663 | end | ||
|
r5629 | |||
|
r6077 | # TODO: remove in Redmine 1.4 | ||
|
r663 | def accept_key_auth_actions | ||
|
r6088 | ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." | ||
|
r6077 | self.class.accept_rss_auth | ||
end | ||||
|
r6405 | |||
|
r6077 | def self.accept_rss_auth(*actions) | ||
if actions.any? | ||||
write_inheritable_attribute('accept_rss_auth_actions', actions) | ||||
else | ||||
read_inheritable_attribute('accept_rss_auth_actions') || [] | ||||
end | ||||
end | ||||
|
r6405 | |||
|
r6077 | def accept_rss_auth?(action=action_name) | ||
self.class.accept_rss_auth.include?(action.to_sym) | ||||
end | ||||
|
r6405 | |||
|
r6077 | def self.accept_api_auth(*actions) | ||
if actions.any? | ||||
write_inheritable_attribute('accept_api_auth_actions', actions) | ||||
else | ||||
read_inheritable_attribute('accept_api_auth_actions') || [] | ||||
end | ||||
end | ||||
|
r6405 | |||
|
r6077 | def accept_api_auth?(action=action_name) | ||
self.class.accept_api_auth.include?(action.to_sym) | ||||
|
r663 | end | ||
|
r5629 | |||
|
r1013 | # 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 | ||||
|
r4457 | # 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 | ||||
|
r4375 | if offset < 0 | ||
offset = 0 | ||||
end | ||||
end | ||||
|
r4457 | limit = options[:limit].to_i | ||
|
r4375 | if limit < 1 | ||
limit = 25 | ||||
elsif limit > 100 | ||||
limit = 100 | ||||
end | ||||
|
r4457 | if offset.nil? && options[:page].present? | ||
offset = (options[:page].to_i - 1) * limit | ||||
offset = 0 if offset < 0 | ||||
end | ||||
offset ||= 0 | ||||
|
r5629 | |||
|
r4375 | [offset, limit] | ||
end | ||||
|
r5629 | |||
|
r330 | # 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 | ||||
|
r2430 | rescue | ||
nil | ||||
|
r330 | end | ||
|
r5629 | |||
|
r1039 | # Returns a string that can be used as filename value in Content-Disposition header | ||
def filename_for_content_disposition(name) | ||||
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name | ||||
end | ||||
|
r5629 | |||
|
r3218 | def api_request? | ||
%w(xml json).include? params[:format] | ||||
end | ||||
|
r5629 | |||
|
r4459 | # Returns the API key present in the request | ||
def api_key_from_request | ||||
if params[:key].present? | ||||
params[:key] | ||||
elsif request.headers["X-Redmine-API-Key"].present? | ||||
request.headers["X-Redmine-API-Key"] | ||||
end | ||||
end | ||||
|
r3414 | |||
# 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 | ||||
|
r3582 | |||
|
r3826 | # Sets the `flash` notice or error based the number of issues that did not save | ||
# | ||||
# @param [Array, Issue] issues all of the saved and unsaved Issues | ||||
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved | ||||
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids) | ||||
if unsaved_issue_ids.empty? | ||||
flash[:notice] = l(:notice_successful_update) unless issues.empty? | ||||
else | ||||
flash[:error] = l(:notice_failed_to_save_issues, | ||||
:count => unsaved_issue_ids.size, | ||||
:total => issues.size, | ||||
:ids => '#' + unsaved_issue_ids.join(', #')) | ||||
end | ||||
end | ||||
|
r3582 | # 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 | ||||
|
r3652 | # Converts the errors on an ActiveRecord object into a common JSON format | ||
def object_errors_to_json(object) | ||||
object.errors.collect do |attribute, error| | ||||
{ attribute => error } | ||||
end.to_json | ||||
end | ||||
|
r4341 | |||
# Renders API response on validation failure | ||||
def render_validation_errors(object) | ||||
options = { :status => :unprocessable_entity, :layout => false } | ||||
options.merge!(case params[:format] | ||||
when 'xml'; { :xml => object.errors } | ||||
when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance | ||||
else | ||||
raise "Unknown format #{params[:format]} in #render_validation_errors" | ||||
end | ||||
) | ||||
render options | ||||
end | ||||
|
r5629 | |||
|
r4352 | # Overrides #default_template so that the api template | ||
# is used automatically if it exists | ||||
def default_template(action_name = self.action_name) | ||||
if api_request? | ||||
begin | ||||
return self.view_paths.find_template(default_template_name(action_name), 'api') | ||||
rescue ::ActionView::MissingTemplate | ||||
# the api template was not found | ||||
# fallback to the default behaviour | ||||
end | ||||
end | ||||
super | ||||
end | ||||
|
r5629 | |||
|
r4352 | # Overrides #pick_layout so that #render with no arguments | ||
# doesn't use the layout for api requests | ||||
def pick_layout(*args) | ||||
api_request? ? nil : super | ||||
end | ||||
|
r663 | end | ||