##// END OF EJS Templates
Auto-populate fields while creating a new user with LDAP (#10286)....
Jean-Philippe Lang -
r10850:7b8ebb7e3ffc
parent child
Show More
@@ -1,82 +1,96
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 class AuthSourcesController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :ldap_authentication
21 21
22 22 before_filter :require_admin
23 23 before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
24 24
25 25 def index
26 26 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
27 27 end
28 28
29 29 def new
30 30 klass_name = params[:type] || 'AuthSourceLdap'
31 31 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
32 32 render_404 unless @auth_source
33 33 end
34 34
35 35 def create
36 36 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
37 37 if @auth_source.save
38 38 flash[:notice] = l(:notice_successful_create)
39 39 redirect_to auth_sources_path
40 40 else
41 41 render :action => 'new'
42 42 end
43 43 end
44 44
45 45 def edit
46 46 end
47 47
48 48 def update
49 49 if @auth_source.update_attributes(params[:auth_source])
50 50 flash[:notice] = l(:notice_successful_update)
51 51 redirect_to auth_sources_path
52 52 else
53 53 render :action => 'edit'
54 54 end
55 55 end
56 56
57 57 def test_connection
58 58 begin
59 59 @auth_source.test_connection
60 60 flash[:notice] = l(:notice_successful_connection)
61 61 rescue Exception => e
62 62 flash[:error] = l(:error_unable_to_connect, e.message)
63 63 end
64 64 redirect_to auth_sources_path
65 65 end
66 66
67 67 def destroy
68 68 unless @auth_source.users.exists?
69 69 @auth_source.destroy
70 70 flash[:notice] = l(:notice_successful_delete)
71 71 end
72 72 redirect_to auth_sources_path
73 73 end
74 74
75 def autocomplete_for_new_user
76 results = AuthSource.search(params[:term])
77
78 render :json => results.map {|result| {
79 'value' => result[:login],
80 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})",
81 'login' => result[:login].to_s,
82 'firstname' => result[:firstname].to_s,
83 'lastname' => result[:lastname].to_s,
84 'mail' => result[:mail].to_s,
85 'auth_source_id' => result[:auth_source_id].to_s
86 }}
87 end
88
75 89 private
76 90
77 91 def find_auth_source
78 92 @auth_source = AuthSource.find(params[:id])
79 93 rescue ActiveRecord::RecordNotFound
80 94 render_404
81 95 end
82 96 end
@@ -1,74 +1,92
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 # Generic exception for when the AuthSource can not be reached
19 19 # (eg. can not connect to the LDAP)
20 20 class AuthSourceException < Exception; end
21 21 class AuthSourceTimeoutException < AuthSourceException; end
22 22
23 23 class AuthSource < ActiveRecord::Base
24 24 include Redmine::SubclassFactory
25 25 include Redmine::Ciphering
26 26
27 27 has_many :users
28 28
29 29 validates_presence_of :name
30 30 validates_uniqueness_of :name
31 31 validates_length_of :name, :maximum => 60
32 32
33 33 def authenticate(login, password)
34 34 end
35 35
36 36 def test_connection
37 37 end
38 38
39 39 def auth_method_name
40 40 "Abstract"
41 41 end
42 42
43 43 def account_password
44 44 read_ciphered_attribute(:account_password)
45 45 end
46 46
47 47 def account_password=(arg)
48 48 write_ciphered_attribute(:account_password, arg)
49 49 end
50 50
51 def searchable?
52 false
53 end
54
55 def self.search(q)
56 results = []
57 AuthSource.all.each do |source|
58 begin
59 if source.searchable?
60 results += source.search(q)
61 end
62 rescue AuthSourceException => e
63 logger.error "Error while searching users in #{source.name}: #{e.message}"
64 end
65 end
66 results
67 end
68
51 69 def allow_password_changes?
52 70 self.class.allow_password_changes?
53 71 end
54 72
55 73 # Does this auth source backend allow password changes?
56 74 def self.allow_password_changes?
57 75 false
58 76 end
59 77
60 78 # Try to authenticate a user not yet registered against available sources
61 79 def self.authenticate(login, password)
62 80 AuthSource.where(:onthefly_register => true).all.each do |source|
63 81 begin
64 82 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
65 83 attrs = source.authenticate(login, password)
66 84 rescue => e
67 85 logger.error "Error during authentication: #{e.message}"
68 86 attrs = nil
69 87 end
70 88 return attrs if attrs
71 89 end
72 90 return nil
73 91 end
74 92 end
@@ -1,173 +1,201
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 'iconv'
19 19 require 'net/ldap'
20 20 require 'net/ldap/dn'
21 21 require 'timeout'
22 22
23 23 class AuthSourceLdap < AuthSource
24 24 validates_presence_of :host, :port, :attr_login
25 25 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
26 26 validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
27 27 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
28 28 validates_numericality_of :port, :only_integer => true
29 29 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
30 30 validate :validate_filter
31 31
32 32 before_validation :strip_ldap_attributes
33 33
34 34 def initialize(attributes=nil, *args)
35 35 super
36 36 self.port = 389 if self.port == 0
37 37 end
38 38
39 39 def authenticate(login, password)
40 40 return nil if login.blank? || password.blank?
41 41
42 42 with_timeout do
43 43 attrs = get_user_dn(login, password)
44 44 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
45 45 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
46 46 return attrs.except(:dn)
47 47 end
48 48 end
49 49 rescue Net::LDAP::LdapError => e
50 50 raise AuthSourceException.new(e.message)
51 51 end
52 52
53 53 # test the connection to the LDAP
54 54 def test_connection
55 55 with_timeout do
56 56 ldap_con = initialize_ldap_con(self.account, self.account_password)
57 57 ldap_con.open { }
58 58 end
59 59 rescue Net::LDAP::LdapError => e
60 60 raise AuthSourceException.new(e.message)
61 61 end
62 62
63 63 def auth_method_name
64 64 "LDAP"
65 65 end
66 66
67 # Returns true if this source can be searched for users
68 def searchable?
69 !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
70 end
71
72 # Searches the source for users and returns an array of results
73 def search(q)
74 q = q.to_s.strip
75 return [] unless searchable? && q.present?
76
77 results = []
78 search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
79 ldap_con = initialize_ldap_con(self.account, self.account_password)
80 ldap_con.search(:base => self.base_dn,
81 :filter => search_filter,
82 :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
83 :size => 10) do |entry|
84 attrs = get_user_attributes_from_ldap_entry(entry)
85 attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
86 results << attrs
87 end
88 results
89 rescue Net::LDAP::LdapError => e
90 raise AuthSourceException.new(e.message)
91 end
92
67 93 private
68 94
69 95 def with_timeout(&block)
70 96 timeout = self.timeout
71 97 timeout = 20 unless timeout && timeout > 0
72 98 Timeout.timeout(timeout) do
73 99 return yield
74 100 end
75 101 rescue Timeout::Error => e
76 102 raise AuthSourceTimeoutException.new(e.message)
77 103 end
78 104
79 105 def ldap_filter
80 106 if filter.present?
81 107 Net::LDAP::Filter.construct(filter)
82 108 end
83 109 rescue Net::LDAP::LdapError
84 110 nil
85 111 end
86 112
113 def base_filter
114 filter = Net::LDAP::Filter.eq("objectClass", "*")
115 if f = ldap_filter
116 filter = filter & f
117 end
118 filter
119 end
120
87 121 def validate_filter
88 122 if filter.present? && ldap_filter.nil?
89 123 errors.add(:filter, :invalid)
90 124 end
91 125 end
92 126
93 127 def strip_ldap_attributes
94 128 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
95 129 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
96 130 end
97 131 end
98 132
99 133 def initialize_ldap_con(ldap_user, ldap_password)
100 134 options = { :host => self.host,
101 135 :port => self.port,
102 136 :encryption => (self.tls ? :simple_tls : nil)
103 137 }
104 138 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
105 139 Net::LDAP.new options
106 140 end
107 141
108 142 def get_user_attributes_from_ldap_entry(entry)
109 143 {
110 144 :dn => entry.dn,
111 145 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
112 146 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
113 147 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
114 148 :auth_source_id => self.id
115 149 }
116 150 end
117 151
118 152 # Return the attributes needed for the LDAP search. It will only
119 153 # include the user attributes if on-the-fly registration is enabled
120 154 def search_attributes
121 155 if onthefly_register?
122 156 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
123 157 else
124 158 ['dn']
125 159 end
126 160 end
127 161
128 162 # Check if a DN (user record) authenticates with the password
129 163 def authenticate_dn(dn, password)
130 164 if dn.present? && password.present?
131 165 initialize_ldap_con(dn, password).bind
132 166 end
133 167 end
134 168
135 169 # Get the user's dn and any attributes for them, given their login
136 170 def get_user_dn(login, password)
137 171 ldap_con = nil
138 172 if self.account && self.account.include?("$login")
139 173 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
140 174 else
141 175 ldap_con = initialize_ldap_con(self.account, self.account_password)
142 176 end
143 login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
144 object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
145 177 attrs = {}
146
147 search_filter = object_filter & login_filter
148 if f = ldap_filter
149 search_filter = search_filter & f
150 end
178 search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
151 179
152 180 ldap_con.search( :base => self.base_dn,
153 181 :filter => search_filter,
154 182 :attributes=> search_attributes) do |entry|
155 183
156 184 if onthefly_register?
157 185 attrs = get_user_attributes_from_ldap_entry(entry)
158 186 else
159 187 attrs = {:dn => entry.dn}
160 188 end
161 189
162 190 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
163 191 end
164 192
165 193 attrs
166 194 end
167 195
168 196 def self.get_attr(entry, attr_name)
169 197 if !attr_name.blank?
170 198 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
171 199 end
172 200 end
173 201 end
@@ -1,12 +1,30
1 1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
2 2
3 3 <%= labelled_form_for @user do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <% if email_delivery_enabled? %>
6 6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
7 7 <% end %>
8 8 <p>
9 9 <%= submit_tag l(:button_create) %>
10 10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
11 11 </p>
12 12 <% end %>
13
14 <% if @auth_sources.present? && @auth_sources.any?(&:searchable?) %>
15 <%= javascript_tag do %>
16 observeAutocompleteField('user_login', '<%= escape_javascript autocomplete_for_new_user_auth_sources_path %>', {
17 select: function(event, ui) {
18 $('input#user_firstname').val(ui.item.firstname);
19 $('input#user_lastname').val(ui.item.lastname);
20 $('input#user_mail').val(ui.item.mail);
21 $('select#user_auth_source_id option').each(function(){
22 if ($(this).attr('value') == ui.item.auth_source_id) {
23 $(this).attr('selected', true);
24 $('select#user_auth_source_id').trigger('change');
25 }
26 });
27 }
28 });
29 <% end %>
30 <% end %>
@@ -1,341 +1,344
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 RedmineApp::Application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26
27 27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post]
28 28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post]
29 29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post]
30 30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post]
31 31
32 32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 34
35 35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
36 36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 39
40 40 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
41 41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 44
45 45 # Misc issue routes. TODO: move into resources
46 46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
48 48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
49 49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 50
51 51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53 53
54 54 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
55 55 get '/issues/gantt', :to => 'gantts#show'
56 56
57 57 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
58 58 get '/issues/calendar', :to => 'calendars#show'
59 59
60 60 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
61 61 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
62 62
63 63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74 74
75 75 resources :users
76 76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79 79
80 80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87 87
88 88 resources :projects do
89 89 member do
90 90 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
91 91 post 'modules'
92 92 post 'archive'
93 93 post 'unarchive'
94 94 post 'close'
95 95 post 'reopen'
96 96 match 'copy', :via => [:get, :post]
97 97 end
98 98
99 99 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
100 100 collection do
101 101 get 'autocomplete'
102 102 end
103 103 end
104 104
105 105 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
106 106
107 107 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
108 108 resources :issues, :only => [:index, :new, :create] do
109 109 resources :time_entries, :controller => 'timelog' do
110 110 collection do
111 111 get 'report'
112 112 end
113 113 end
114 114 end
115 115 # issue form update
116 116 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
117 117
118 118 resources :files, :only => [:index, :new, :create]
119 119
120 120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
121 121 collection do
122 122 put 'close_completed'
123 123 end
124 124 end
125 125 get 'versions.:format', :to => 'versions#index'
126 126 get 'roadmap', :to => 'versions#index', :format => false
127 127 get 'versions', :to => 'versions#index'
128 128
129 129 resources :news, :except => [:show, :edit, :update, :destroy]
130 130 resources :time_entries, :controller => 'timelog' do
131 131 get 'report', :on => :collection
132 132 end
133 133 resources :queries, :only => [:new, :create]
134 134 resources :issue_categories, :shallow => true
135 135 resources :documents, :except => [:show, :edit, :update, :destroy]
136 136 resources :boards
137 137 resources :repositories, :shallow => true, :except => [:index, :show] do
138 138 member do
139 139 match 'committers', :via => [:get, :post]
140 140 end
141 141 end
142 142
143 143 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
144 144 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
145 145 member do
146 146 get 'rename'
147 147 post 'rename'
148 148 get 'history'
149 149 get 'diff'
150 150 match 'preview', :via => [:post, :put]
151 151 post 'protect'
152 152 post 'add_attachment'
153 153 end
154 154 collection do
155 155 get 'export'
156 156 get 'date_index'
157 157 end
158 158 end
159 159 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
160 160 get 'wiki/:id/:version', :to => 'wiki#show'
161 161 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
162 162 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
163 163 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
164 164 end
165 165
166 166 resources :issues do
167 167 collection do
168 168 match 'bulk_edit', :via => [:get, :post]
169 169 post 'bulk_update'
170 170 end
171 171 resources :time_entries, :controller => 'timelog' do
172 172 collection do
173 173 get 'report'
174 174 end
175 175 end
176 176 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
177 177 end
178 178 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
179 179
180 180 resources :queries, :except => [:show]
181 181
182 182 resources :news, :only => [:index, :show, :edit, :update, :destroy]
183 183 match '/news/:id/comments', :to => 'comments#create', :via => :post
184 184 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
185 185
186 186 resources :versions, :only => [:show, :edit, :update, :destroy] do
187 187 post 'status_by', :on => :member
188 188 end
189 189
190 190 resources :documents, :only => [:show, :edit, :update, :destroy] do
191 191 post 'add_attachment', :on => :member
192 192 end
193 193
194 194 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
195 195
196 196 resources :time_entries, :controller => 'timelog', :except => :destroy do
197 197 collection do
198 198 get 'report'
199 199 get 'bulk_edit'
200 200 post 'bulk_update'
201 201 end
202 202 end
203 203 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
204 204 # TODO: delete /time_entries for bulk deletion
205 205 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
206 206
207 207 get 'projects/:id/activity', :to => 'activities#index'
208 208 get 'projects/:id/activity.:format', :to => 'activities#index'
209 209 get 'activity', :to => 'activities#index'
210 210
211 211 # repositories routes
212 212 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
213 213 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
214 214
215 215 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
216 216 :to => 'repositories#changes'
217 217
218 218 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
219 219 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
220 220 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
221 221 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
222 222 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
223 223 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
224 224 :controller => 'repositories',
225 225 :format => false,
226 226 :constraints => {
227 227 :action => /(browse|show|entry|raw|annotate|diff)/,
228 228 :rev => /[a-z0-9\.\-_]+/
229 229 }
230 230
231 231 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
232 232 get 'projects/:id/repository/graph', :to => 'repositories#graph'
233 233
234 234 get 'projects/:id/repository/changes(/*path(.:ext))',
235 235 :to => 'repositories#changes'
236 236
237 237 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
238 238 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
239 239 get 'projects/:id/repository/revision', :to => 'repositories#revision'
240 240 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
241 241 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
242 242 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
243 243 :controller => 'repositories',
244 244 :format => false,
245 245 :constraints => {
246 246 :action => /(browse|show|entry|raw|annotate|diff)/,
247 247 :rev => /[a-z0-9\.\-_]+/
248 248 }
249 249 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
250 250 :controller => 'repositories',
251 251 :action => /(browse|show|entry|raw|changes|annotate|diff)/
252 252 get 'projects/:id/repository/:action(/*path(.:ext))',
253 253 :controller => 'repositories',
254 254 :action => /(browse|show|entry|raw|changes|annotate|diff)/
255 255
256 256 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
257 257 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
258 258
259 259 # additional routes for having the file name at the end of url
260 260 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
261 261 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
262 262 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
263 263 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
264 264 resources :attachments, :only => [:show, :destroy]
265 265
266 266 resources :groups do
267 267 member do
268 268 get 'autocomplete_for_user'
269 269 end
270 270 end
271 271
272 272 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
273 273 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
274 274 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
275 275 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
276 276
277 277 resources :trackers, :except => :show do
278 278 collection do
279 279 match 'fields', :via => [:get, :post]
280 280 end
281 281 end
282 282 resources :issue_statuses, :except => :show do
283 283 collection do
284 284 post 'update_issue_done_ratio'
285 285 end
286 286 end
287 287 resources :custom_fields, :except => :show
288 288 resources :roles do
289 289 collection do
290 290 match 'permissions', :via => [:get, :post]
291 291 end
292 292 end
293 293 resources :enumerations, :except => :show
294 294 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
295 295
296 296 get 'projects/:id/search', :controller => 'search', :action => 'index'
297 297 get 'search', :controller => 'search', :action => 'index'
298 298
299 299 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
300 300
301 301 match 'admin', :controller => 'admin', :action => 'index', :via => :get
302 302 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
303 303 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
304 304 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
305 305 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
306 306 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
307 307
308 308 resources :auth_sources do
309 309 member do
310 310 get 'test_connection', :as => 'try_connection'
311 311 end
312 collection do
313 get 'autocomplete_for_new_user'
314 end
312 315 end
313 316
314 317 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
315 318 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
316 319 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
317 320 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
318 321 match 'settings', :controller => 'settings', :action => 'index', :via => :get
319 322 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
320 323 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
321 324
322 325 match 'sys/projects', :to => 'sys#projects', :via => :get
323 326 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
324 327 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
325 328
326 329 match 'uploads', :to => 'attachments#upload', :via => :post
327 330
328 331 get 'robots.txt', :to => 'welcome#robots'
329 332
330 333 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
331 334 file = File.join(plugin_dir, "config/routes.rb")
332 335 if File.exists?(file)
333 336 begin
334 337 instance_eval File.read(file)
335 338 rescue Exception => e
336 339 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
337 340 exit 1
338 341 end
339 342 end
340 343 end
341 344 end
@@ -1,586 +1,586
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 if (checked) {
6 6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
7 7 } else {
8 8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
9 9 }
10 10 }
11 11
12 12 function toggleCheckboxesBySelector(selector) {
13 13 var all_checked = true;
14 14 $(selector).each(function(index) {
15 15 if (!$(this).is(':checked')) { all_checked = false; }
16 16 });
17 17 $(selector).attr('checked', !all_checked)
18 18 }
19 19
20 20 function showAndScrollTo(id, focus) {
21 21 $('#'+id).show();
22 22 if (focus!=null) {
23 23 $('#'+focus).focus();
24 24 }
25 25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
26 26 }
27 27
28 28 function toggleRowGroup(el) {
29 29 var tr = $(el).parents('tr').first();
30 30 var n = tr.next();
31 31 tr.toggleClass('open');
32 32 while (n.length && !n.hasClass('group')) {
33 33 n.toggle();
34 34 n = n.next('tr');
35 35 }
36 36 }
37 37
38 38 function collapseAllRowGroups(el) {
39 39 var tbody = $(el).parents('tbody').first();
40 40 tbody.children('tr').each(function(index) {
41 41 if ($(this).hasClass('group')) {
42 42 $(this).removeClass('open');
43 43 } else {
44 44 $(this).hide();
45 45 }
46 46 });
47 47 }
48 48
49 49 function expandAllRowGroups(el) {
50 50 var tbody = $(el).parents('tbody').first();
51 51 tbody.children('tr').each(function(index) {
52 52 if ($(this).hasClass('group')) {
53 53 $(this).addClass('open');
54 54 } else {
55 55 $(this).show();
56 56 }
57 57 });
58 58 }
59 59
60 60 function toggleAllRowGroups(el) {
61 61 var tr = $(el).parents('tr').first();
62 62 if (tr.hasClass('open')) {
63 63 collapseAllRowGroups(el);
64 64 } else {
65 65 expandAllRowGroups(el);
66 66 }
67 67 }
68 68
69 69 function toggleFieldset(el) {
70 70 var fieldset = $(el).parents('fieldset').first();
71 71 fieldset.toggleClass('collapsed');
72 72 fieldset.children('div').toggle();
73 73 }
74 74
75 75 function hideFieldset(el) {
76 76 var fieldset = $(el).parents('fieldset').first();
77 77 fieldset.toggleClass('collapsed');
78 78 fieldset.children('div').hide();
79 79 }
80 80
81 81 function initFilters(){
82 82 $('#add_filter_select').change(function(){
83 83 addFilter($(this).val(), '', []);
84 84 });
85 85 $('#filters-table td.field input[type=checkbox]').each(function(){
86 86 toggleFilter($(this).val());
87 87 });
88 88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
89 89 toggleFilter($(this).val());
90 90 });
91 91 $('#filters-table .toggle-multiselect').live('click',function(){
92 92 toggleMultiSelect($(this).siblings('select'));
93 93 });
94 94 $('#filters-table input[type=text]').live('keypress', function(e){
95 95 if (e.keyCode == 13) submit_query_form("query_form");
96 96 });
97 97 }
98 98
99 99 function addFilter(field, operator, values) {
100 100 var fieldId = field.replace('.', '_');
101 101 var tr = $('#tr_'+fieldId);
102 102 if (tr.length > 0) {
103 103 tr.show();
104 104 } else {
105 105 buildFilterRow(field, operator, values);
106 106 }
107 107 $('#cb_'+fieldId).attr('checked', true);
108 108 toggleFilter(field);
109 109 $('#add_filter_select').val('').children('option').each(function(){
110 110 if ($(this).attr('value') == field) {
111 111 $(this).attr('disabled', true);
112 112 }
113 113 });
114 114 }
115 115
116 116 function buildFilterRow(field, operator, values) {
117 117 var fieldId = field.replace('.', '_');
118 118 var filterTable = $("#filters-table");
119 119 var filterOptions = availableFilters[field];
120 120 var operators = operatorByType[filterOptions['type']];
121 121 var filterValues = filterOptions['values'];
122 122 var i, select;
123 123
124 124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
125 125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
126 126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
127 127 '<td class="values"></td>'
128 128 );
129 129 filterTable.append(tr);
130 130
131 131 select = tr.find('td.operator select');
132 132 for (i=0;i<operators.length;i++){
133 133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
134 134 if (operators[i] == operator) {option.attr('selected', true)};
135 135 select.append(option);
136 136 }
137 137 select.change(function(){toggleOperator(field)});
138 138
139 139 switch (filterOptions['type']){
140 140 case "list":
141 141 case "list_optional":
142 142 case "list_status":
143 143 case "list_subprojects":
144 144 tr.find('td.values').append(
145 145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
146 146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
147 147 );
148 148 select = tr.find('td.values select');
149 149 if (values.length > 1) {select.attr('multiple', true)};
150 150 for (i=0;i<filterValues.length;i++){
151 151 var filterValue = filterValues[i];
152 152 var option = $('<option>');
153 153 if ($.isArray(filterValue)) {
154 154 option.val(filterValue[1]).text(filterValue[0]);
155 155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
156 156 } else {
157 157 option.val(filterValue).text(filterValue);
158 158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
159 159 }
160 160 select.append(option);
161 161 }
162 162 break;
163 163 case "date":
164 164 case "date_past":
165 165 tr.find('td.values').append(
166 166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
167 167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
168 168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
169 169 );
170 170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
171 171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
172 172 $('#values_'+fieldId).val(values[0]);
173 173 break;
174 174 case "string":
175 175 case "text":
176 176 tr.find('td.values').append(
177 177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
178 178 );
179 179 $('#values_'+fieldId).val(values[0]);
180 180 break;
181 181 case "relation":
182 182 tr.find('td.values').append(
183 183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
184 184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
185 185 );
186 186 $('#values_'+fieldId).val(values[0]);
187 187 select = tr.find('td.values select');
188 188 for (i=0;i<allProjects.length;i++){
189 189 var filterValue = allProjects[i];
190 190 var option = $('<option>');
191 191 option.val(filterValue[1]).text(filterValue[0]);
192 192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
193 193 select.append(option);
194 194 }
195 195 case "integer":
196 196 case "float":
197 197 tr.find('td.values').append(
198 198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
199 199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
200 200 );
201 201 $('#values_'+fieldId+'_1').val(values[0]);
202 202 $('#values_'+fieldId+'_2').val(values[1]);
203 203 break;
204 204 }
205 205 }
206 206
207 207 function toggleFilter(field) {
208 208 var fieldId = field.replace('.', '_');
209 209 if ($('#cb_' + fieldId).is(':checked')) {
210 210 $("#operators_" + fieldId).show().removeAttr('disabled');
211 211 toggleOperator(field);
212 212 } else {
213 213 $("#operators_" + fieldId).hide().attr('disabled', true);
214 214 enableValues(field, []);
215 215 }
216 216 }
217 217
218 218 function enableValues(field, indexes) {
219 219 var fieldId = field.replace('.', '_');
220 220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
221 221 if ($.inArray(index, indexes) >= 0) {
222 222 $(this).removeAttr('disabled');
223 223 $(this).parents('span').first().show();
224 224 } else {
225 225 $(this).val('');
226 226 $(this).attr('disabled', true);
227 227 $(this).parents('span').first().hide();
228 228 }
229 229
230 230 if ($(this).hasClass('group')) {
231 231 $(this).addClass('open');
232 232 } else {
233 233 $(this).show();
234 234 }
235 235 });
236 236 }
237 237
238 238 function toggleOperator(field) {
239 239 var fieldId = field.replace('.', '_');
240 240 var operator = $("#operators_" + fieldId);
241 241 switch (operator.val()) {
242 242 case "!*":
243 243 case "*":
244 244 case "t":
245 245 case "ld":
246 246 case "w":
247 247 case "lw":
248 248 case "l2w":
249 249 case "m":
250 250 case "lm":
251 251 case "y":
252 252 case "o":
253 253 case "c":
254 254 enableValues(field, []);
255 255 break;
256 256 case "><":
257 257 enableValues(field, [0,1]);
258 258 break;
259 259 case "<t+":
260 260 case ">t+":
261 261 case "><t+":
262 262 case "t+":
263 263 case ">t-":
264 264 case "<t-":
265 265 case "><t-":
266 266 case "t-":
267 267 enableValues(field, [2]);
268 268 break;
269 269 case "=p":
270 270 case "=!p":
271 271 case "!p":
272 272 enableValues(field, [1]);
273 273 break;
274 274 default:
275 275 enableValues(field, [0]);
276 276 break;
277 277 }
278 278 }
279 279
280 280 function toggleMultiSelect(el) {
281 281 if (el.attr('multiple')) {
282 282 el.removeAttr('multiple');
283 283 } else {
284 284 el.attr('multiple', true);
285 285 }
286 286 }
287 287
288 288 function submit_query_form(id) {
289 289 selectAllOptions("selected_columns");
290 290 $('#'+id).submit();
291 291 }
292 292
293 293 function showTab(name) {
294 294 $('div#content .tab-content').hide();
295 295 $('div.tabs a').removeClass('selected');
296 296 $('#tab-content-' + name).show();
297 297 $('#tab-' + name).addClass('selected');
298 298 return false;
299 299 }
300 300
301 301 function moveTabRight(el) {
302 302 var lis = $(el).parents('div.tabs').first().find('ul').children();
303 303 var tabsWidth = 0;
304 304 var i = 0;
305 305 lis.each(function(){
306 306 if ($(this).is(':visible')) {
307 307 tabsWidth += $(this).width() + 6;
308 308 }
309 309 });
310 310 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
311 311 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
312 312 lis.eq(i).hide();
313 313 }
314 314
315 315 function moveTabLeft(el) {
316 316 var lis = $(el).parents('div.tabs').first().find('ul').children();
317 317 var i = 0;
318 318 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
319 319 if (i>0) {
320 320 lis.eq(i-1).show();
321 321 }
322 322 }
323 323
324 324 function displayTabsButtons() {
325 325 var lis;
326 326 var tabsWidth = 0;
327 327 var el;
328 328 $('div.tabs').each(function() {
329 329 el = $(this);
330 330 lis = el.find('ul').children();
331 331 lis.each(function(){
332 332 if ($(this).is(':visible')) {
333 333 tabsWidth += $(this).width() + 6;
334 334 }
335 335 });
336 336 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
337 337 el.find('div.tabs-buttons').hide();
338 338 } else {
339 339 el.find('div.tabs-buttons').show();
340 340 }
341 341 });
342 342 }
343 343
344 344 function setPredecessorFieldsVisibility() {
345 345 var relationType = $('#relation_relation_type');
346 346 if (relationType.val() == "precedes" || relationType.val() == "follows") {
347 347 $('#predecessor_fields').show();
348 348 } else {
349 349 $('#predecessor_fields').hide();
350 350 }
351 351 }
352 352
353 353 function showModal(id, width) {
354 354 var el = $('#'+id).first();
355 355 if (el.length == 0 || el.is(':visible')) {return;}
356 356 var title = el.find('h3.title').text();
357 357 el.dialog({
358 358 width: width,
359 359 modal: true,
360 360 resizable: false,
361 361 dialogClass: 'modal',
362 362 title: title
363 363 });
364 364 el.find("input[type=text], input[type=submit]").first().focus();
365 365 }
366 366
367 367 function hideModal(el) {
368 368 var modal;
369 369 if (el) {
370 370 modal = $(el).parents('.ui-dialog-content');
371 371 } else {
372 372 modal = $('#ajax-modal');
373 373 }
374 374 modal.dialog("close");
375 375 }
376 376
377 377 function submitPreview(url, form, target) {
378 378 $.ajax({
379 379 url: url,
380 380 type: 'post',
381 381 data: $('#'+form).serialize(),
382 382 success: function(data){
383 383 $('#'+target).html(data);
384 384 }
385 385 });
386 386 }
387 387
388 388 function collapseScmEntry(id) {
389 389 $('.'+id).each(function() {
390 390 if ($(this).hasClass('open')) {
391 391 collapseScmEntry($(this).attr('id'));
392 392 }
393 393 $(this).hide();
394 394 });
395 395 $('#'+id).removeClass('open');
396 396 }
397 397
398 398 function expandScmEntry(id) {
399 399 $('.'+id).each(function() {
400 400 $(this).show();
401 401 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
402 402 expandScmEntry($(this).attr('id'));
403 403 }
404 404 });
405 405 $('#'+id).addClass('open');
406 406 }
407 407
408 408 function scmEntryClick(id, url) {
409 409 el = $('#'+id);
410 410 if (el.hasClass('open')) {
411 411 collapseScmEntry(id);
412 412 el.addClass('collapsed');
413 413 return false;
414 414 } else if (el.hasClass('loaded')) {
415 415 expandScmEntry(id);
416 416 el.removeClass('collapsed');
417 417 return false;
418 418 }
419 419 if (el.hasClass('loading')) {
420 420 return false;
421 421 }
422 422 el.addClass('loading');
423 423 $.ajax({
424 424 url: url,
425 425 success: function(data){
426 426 el.after(data);
427 427 el.addClass('open').addClass('loaded').removeClass('loading');
428 428 }
429 429 });
430 430 return true;
431 431 }
432 432
433 433 function randomKey(size) {
434 434 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
435 435 var key = '';
436 436 for (i = 0; i < size; i++) {
437 437 key += chars[Math.floor(Math.random() * chars.length)];
438 438 }
439 439 return key;
440 440 }
441 441
442 442 // Can't use Rails' remote select because we need the form data
443 443 function updateIssueFrom(url) {
444 444 $.ajax({
445 445 url: url,
446 446 type: 'post',
447 447 data: $('#issue-form').serialize()
448 448 });
449 449 }
450 450
451 451 function updateBulkEditFrom(url) {
452 452 $.ajax({
453 453 url: url,
454 454 type: 'post',
455 455 data: $('#bulk_edit_form').serialize()
456 456 });
457 457 }
458 458
459 function observeAutocompleteField(fieldId, url) {
459 function observeAutocompleteField(fieldId, url, options) {
460 460 $(document).ready(function() {
461 $('#'+fieldId).autocomplete({
461 $('#'+fieldId).autocomplete($.extend({
462 462 source: url,
463 463 minLength: 2
464 });
464 }, options));
465 465 });
466 466 }
467 467
468 468 function observeSearchfield(fieldId, targetId, url) {
469 469 $('#'+fieldId).each(function() {
470 470 var $this = $(this);
471 471 $this.attr('data-value-was', $this.val());
472 472 var check = function() {
473 473 var val = $this.val();
474 474 if ($this.attr('data-value-was') != val){
475 475 $this.attr('data-value-was', val);
476 476 $.ajax({
477 477 url: url,
478 478 type: 'get',
479 479 data: {q: $this.val()},
480 480 success: function(data){ $('#'+targetId).html(data); },
481 481 beforeSend: function(){ $this.addClass('ajax-loading'); },
482 482 complete: function(){ $this.removeClass('ajax-loading'); }
483 483 });
484 484 }
485 485 };
486 486 var reset = function() {
487 487 if (timer) {
488 488 clearInterval(timer);
489 489 timer = setInterval(check, 300);
490 490 }
491 491 };
492 492 var timer = setInterval(check, 300);
493 493 $this.bind('keyup click mousemove', reset);
494 494 });
495 495 }
496 496
497 497 function observeProjectModules() {
498 498 var f = function() {
499 499 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
500 500 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
501 501 $('#project_trackers').show();
502 502 }else{
503 503 $('#project_trackers').hide();
504 504 }
505 505 };
506 506
507 507 $(window).load(f);
508 508 $('#project_enabled_module_names_issue_tracking').change(f);
509 509 }
510 510
511 511 function initMyPageSortable(list, url) {
512 512 $('#list-'+list).sortable({
513 513 connectWith: '.block-receiver',
514 514 tolerance: 'pointer',
515 515 update: function(){
516 516 $.ajax({
517 517 url: url,
518 518 type: 'post',
519 519 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
520 520 });
521 521 }
522 522 });
523 523 $("#list-top, #list-left, #list-right").disableSelection();
524 524 }
525 525
526 526 var warnLeavingUnsavedMessage;
527 527 function warnLeavingUnsaved(message) {
528 528 warnLeavingUnsavedMessage = message;
529 529
530 530 $('form').submit(function(){
531 531 $('textarea').removeData('changed');
532 532 });
533 533 $('textarea').change(function(){
534 534 $(this).data('changed', 'changed');
535 535 });
536 536 window.onbeforeunload = function(){
537 537 var warn = false;
538 538 $('textarea').blur().each(function(){
539 539 if ($(this).data('changed')) {
540 540 warn = true;
541 541 }
542 542 });
543 543 if (warn) {return warnLeavingUnsavedMessage;}
544 544 };
545 545 };
546 546
547 547 function setupAjaxIndicator() {
548 548
549 549 $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings) {
550 550
551 551 if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') {
552 552 $('#ajax-indicator').show();
553 553 }
554 554 });
555 555
556 556 $('#ajax-indicator').bind('ajaxStop', function() {
557 557 $('#ajax-indicator').hide();
558 558 });
559 559 }
560 560
561 561 function hideOnLoad() {
562 562 $('.hol').hide();
563 563 }
564 564
565 565 function addFormObserversForDoubleSubmit() {
566 566 $('form[method=post]').each(function() {
567 567 if (!$(this).hasClass('multiple-submit')) {
568 568 $(this).submit(function(form_submission) {
569 569 if ($(form_submission.target).attr('data-submitted')) {
570 570 form_submission.preventDefault();
571 571 } else {
572 572 $(form_submission.target).attr('data-submitted', true);
573 573 }
574 574 });
575 575 }
576 576 });
577 577 }
578 578
579 579 function blockEventPropagation(event) {
580 580 event.stopPropagation();
581 581 event.preventDefault();
582 582 }
583 583
584 584 $(document).ready(setupAjaxIndicator);
585 585 $(document).ready(hideOnLoad);
586 586 $(document).ready(addFormObserversForDoubleSubmit);
@@ -1,152 +1,168
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 AuthSourcesControllerTest < ActionController::TestCase
21 21 fixtures :users, :auth_sources
22 22
23 23 def setup
24 24 @request.session[:user_id] = 1
25 25 end
26 26
27 27 def test_index
28 28 get :index
29 29
30 30 assert_response :success
31 31 assert_template 'index'
32 32 assert_not_nil assigns(:auth_sources)
33 33 end
34 34
35 35 def test_new
36 36 get :new
37 37
38 38 assert_response :success
39 39 assert_template 'new'
40 40
41 41 source = assigns(:auth_source)
42 42 assert_equal AuthSourceLdap, source.class
43 43 assert source.new_record?
44 44
45 45 assert_select 'form#auth_source_form' do
46 46 assert_select 'input[name=type][value=AuthSourceLdap]'
47 47 assert_select 'input[name=?]', 'auth_source[host]'
48 48 end
49 49 end
50 50
51 51 def test_new_with_invalid_type_should_respond_with_404
52 52 get :new, :type => 'foo'
53 53 assert_response 404
54 54 end
55 55
56 56 def test_create
57 57 assert_difference 'AuthSourceLdap.count' do
58 58 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
59 59 assert_redirected_to '/auth_sources'
60 60 end
61 61
62 62 source = AuthSourceLdap.order('id DESC').first
63 63 assert_equal 'Test', source.name
64 64 assert_equal '127.0.0.1', source.host
65 65 assert_equal 389, source.port
66 66 assert_equal 'cn', source.attr_login
67 67 end
68 68
69 69 def test_create_with_failure
70 70 assert_no_difference 'AuthSourceLdap.count' do
71 71 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '', :port => '389', :attr_login => 'cn'}
72 72 assert_response :success
73 73 assert_template 'new'
74 74 end
75 75 assert_error_tag :content => /host can&#x27;t be blank/i
76 76 end
77 77
78 78 def test_edit
79 79 get :edit, :id => 1
80 80
81 81 assert_response :success
82 82 assert_template 'edit'
83 83
84 84 assert_select 'form#auth_source_form' do
85 85 assert_select 'input[name=?]', 'auth_source[host]'
86 86 end
87 87 end
88 88
89 89 def test_edit_should_not_contain_password
90 90 AuthSource.find(1).update_column :account_password, 'secret'
91 91
92 92 get :edit, :id => 1
93 93 assert_response :success
94 94 assert_select 'input[value=secret]', 0
95 95 assert_select 'input[name=dummy_password][value=?]', /x+/
96 96 end
97 97
98 98 def test_edit_invalid_should_respond_with_404
99 99 get :edit, :id => 99
100 100 assert_response 404
101 101 end
102 102
103 103 def test_update
104 104 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '192.168.0.10', :port => '389', :attr_login => 'uid'}
105 105 assert_redirected_to '/auth_sources'
106 106
107 107 source = AuthSourceLdap.find(1)
108 108 assert_equal 'Renamed', source.name
109 109 assert_equal '192.168.0.10', source.host
110 110 end
111 111
112 112 def test_update_with_failure
113 113 put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '', :port => '389', :attr_login => 'uid'}
114 114 assert_response :success
115 115 assert_template 'edit'
116 116 assert_error_tag :content => /host can&#x27;t be blank/i
117 117 end
118 118
119 119 def test_destroy
120 120 assert_difference 'AuthSourceLdap.count', -1 do
121 121 delete :destroy, :id => 1
122 122 assert_redirected_to '/auth_sources'
123 123 end
124 124 end
125 125
126 126 def test_destroy_auth_source_in_use
127 127 User.find(2).update_attribute :auth_source_id, 1
128 128
129 129 assert_no_difference 'AuthSourceLdap.count' do
130 130 delete :destroy, :id => 1
131 131 assert_redirected_to '/auth_sources'
132 132 end
133 133 end
134 134
135 135 def test_test_connection
136 136 AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
137 137
138 138 get :test_connection, :id => 1
139 139 assert_redirected_to '/auth_sources'
140 140 assert_not_nil flash[:notice]
141 141 assert_match /successful/i, flash[:notice]
142 142 end
143 143
144 144 def test_test_connection_with_failure
145 145 AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
146 146
147 147 get :test_connection, :id => 1
148 148 assert_redirected_to '/auth_sources'
149 149 assert_not_nil flash[:error]
150 150 assert_include 'Something went wrong', flash[:error]
151 151 end
152
153 def test_autocomplete_for_new_user
154 AuthSource.expects(:search).with('foo').returns([
155 {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1},
156 {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1}
157 ])
158
159 get :autocomplete_for_new_user, :term => 'foo'
160 assert_response :success
161 assert_equal 'application/json', response.content_type
162 json = ActiveSupport::JSON.decode(response.body)
163 assert_kind_of Array, json
164 assert_equal 2, json.size
165 assert_equal 'foo1', json.first['value']
166 assert_equal 'foo1 (John Smith)', json.first['label']
167 end
152 168 end
@@ -1,55 +1,59
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 RoutingAuthSourcesTest < ActionController::IntegrationTest
21 21 def test_auth_sources
22 22 assert_routing(
23 23 { :method => 'get', :path => "/auth_sources" },
24 24 { :controller => 'auth_sources', :action => 'index' }
25 25 )
26 26 assert_routing(
27 27 { :method => 'get', :path => "/auth_sources/new" },
28 28 { :controller => 'auth_sources', :action => 'new' }
29 29 )
30 30 assert_routing(
31 31 { :method => 'post', :path => "/auth_sources" },
32 32 { :controller => 'auth_sources', :action => 'create' }
33 33 )
34 34 assert_routing(
35 35 { :method => 'get', :path => "/auth_sources/1234/edit" },
36 36 { :controller => 'auth_sources', :action => 'edit',
37 37 :id => '1234' }
38 38 )
39 39 assert_routing(
40 40 { :method => 'put', :path => "/auth_sources/1234" },
41 41 { :controller => 'auth_sources', :action => 'update',
42 42 :id => '1234' }
43 43 )
44 44 assert_routing(
45 45 { :method => 'delete', :path => "/auth_sources/1234" },
46 46 { :controller => 'auth_sources', :action => 'destroy',
47 47 :id => '1234' }
48 48 )
49 49 assert_routing(
50 50 { :method => 'get', :path => "/auth_sources/1234/test_connection" },
51 51 { :controller => 'auth_sources', :action => 'test_connection',
52 52 :id => '1234' }
53 53 )
54 assert_routing(
55 { :method => 'get', :path => "/auth_sources/autocomplete_for_new_user" },
56 { :controller => 'auth_sources', :action => 'autocomplete_for_new_user' }
57 )
54 58 end
55 59 end
@@ -1,130 +1,154
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 AuthSourceLdapTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22 fixtures :auth_sources
23 23
24 24 def setup
25 25 end
26 26
27 27 def test_create
28 28 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName')
29 29 assert a.save
30 30 end
31 31
32 32 def test_should_strip_ldap_attributes
33 33 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
34 34 :attr_firstname => 'givenName ')
35 35 assert a.save
36 36 assert_equal 'givenName', a.reload.attr_firstname
37 37 end
38 38
39 39 def test_replace_port_zero_to_389
40 40 a = AuthSourceLdap.new(
41 41 :name => 'My LDAP', :host => 'ldap.example.net', :port => 0,
42 42 :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
43 43 :attr_firstname => 'givenName ')
44 44 assert a.save
45 45 assert_equal 389, a.port
46 46 end
47 47
48 48 def test_filter_should_be_validated
49 49 set_language_if_valid 'en'
50 50
51 51 a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn')
52 52 a.filter = "(mail=*@redmine.org"
53 53 assert !a.valid?
54 54 assert_include "LDAP filter is invalid", a.errors.full_messages
55 55
56 56 a.filter = "(mail=*@redmine.org)"
57 57 assert a.valid?
58 58 end
59 59
60 60 if ldap_configured?
61 61 context '#authenticate' do
62 62 setup do
63 63 @auth = AuthSourceLdap.find(1)
64 64 @auth.update_attribute :onthefly_register, true
65 65 end
66 66
67 67 context 'with a valid LDAP user' do
68 68 should 'return the user attributes' do
69 69 attributes = @auth.authenticate('example1','123456')
70 70 assert attributes.is_a?(Hash), "An hash was not returned"
71 71 assert_equal 'Example', attributes[:firstname]
72 72 assert_equal 'One', attributes[:lastname]
73 73 assert_equal 'example1@redmine.org', attributes[:mail]
74 74 assert_equal @auth.id, attributes[:auth_source_id]
75 75 attributes.keys.each do |attribute|
76 76 assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned"
77 77 end
78 78 end
79 79 end
80 80
81 81 context 'with an invalid LDAP user' do
82 82 should 'return nil' do
83 83 assert_equal nil, @auth.authenticate('nouser','123456')
84 84 end
85 85 end
86 86
87 87 context 'without a login' do
88 88 should 'return nil' do
89 89 assert_equal nil, @auth.authenticate('','123456')
90 90 end
91 91 end
92 92
93 93 context 'without a password' do
94 94 should 'return nil' do
95 95 assert_equal nil, @auth.authenticate('edavis','')
96 96 end
97 97 end
98 98
99 99 context 'without filter' do
100 100 should 'return any user' do
101 101 assert @auth.authenticate('example1','123456')
102 102 assert @auth.authenticate('edavis', '123456')
103 103 end
104 104 end
105 105
106 106 context 'with filter' do
107 107 setup do
108 108 @auth.filter = "(mail=*@redmine.org)"
109 109 end
110 110
111 111 should 'return user who matches the filter only' do
112 112 assert @auth.authenticate('example1','123456')
113 113 assert_nil @auth.authenticate('edavis', '123456')
114 114 end
115 115 end
116 116 end
117 117
118 118 def test_authenticate_should_timeout
119 119 auth_source = AuthSourceLdap.find(1)
120 120 auth_source.timeout = 1
121 121 def auth_source.initialize_ldap_con(*args); sleep(5); end
122 122
123 123 assert_raise AuthSourceTimeoutException do
124 124 auth_source.authenticate 'example1', '123456'
125 125 end
126 126 end
127
128 def test_search_should_return_matching_entries
129 results = AuthSource.search("exa")
130 assert_equal 1, results.size
131 result = results.first
132 assert_kind_of Hash, result
133 assert_equal "example1", result[:login]
134 assert_equal "Example", result[:firstname]
135 assert_equal "One", result[:lastname]
136 assert_equal "example1@redmine.org", result[:mail]
137 assert_equal 1, result[:auth_source_id]
138 end
139
140 def test_search_with_no_match_should_return_an_empty_array
141 results = AuthSource.search("wro")
142 assert_equal [], results
143 end
144
145 def test_search_with_exception_should_return_an_empty_array
146 Net::LDAP.stubs(:new).raises(Net::LDAP::LdapError, 'Cannot connect')
147
148 results = AuthSource.search("exa")
149 assert_equal [], results
150 end
127 151 else
128 152 puts '(Test LDAP server not configured)'
129 153 end
130 154 end
General Comments 0
You need to be logged in to leave comments. Login now