Make branch_and_path() support tags as well
[gitorious:taladars-gitorious-saltation.git] / app / controllers / application_controller.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
4 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
5 #
6 #   This program is free software: you can redistribute it and/or modify
7 #   it under the terms of the GNU Affero General Public License as published by
8 #   the Free Software Foundation, either version 3 of the License, or
9 #   (at your option) any later version.
10 #
11 #   This program is distributed in the hope that it will be useful,
12 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #   GNU Affero General Public License for more details.
15 #
16 #   You should have received a copy of the GNU Affero General Public License
17 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #++
19
20 # Filters added to this controller apply to all controllers in the application.
21 # Likewise, all the methods added will be available for all controllers.
22
23 class ApplicationController < ActionController::Base
24   include AuthenticatedSystem
25   include ExceptionNotifiable
26   protect_from_forgery
27
28   before_filter :public_and_logged_in
29   before_filter :require_current_eula
30
31   include SslRequirement # Need to be included after the above
32
33   after_filter :mark_flash_status
34
35   filter_parameter_logging :password, :password_confirmation
36
37   layout :pick_layout_based_on_site
38
39   rescue_from ActiveRecord::RecordNotFound, :with => :render_not_found
40   rescue_from ActionController::UnknownController, :with => :render_not_found
41   rescue_from ActionController::UnknownAction, :with => :render_not_found
42   rescue_from Grit::GitRuby::Repository::NoSuchPath, :with => :render_not_found
43   rescue_from Grit::Git::GitTimeout, :with => :render_git_timeout
44   rescue_from RecordThrottling::LimitReachedError, :with => :render_throttled_record
45
46   def rescue_action(exception)
47     return super if RAILS_ENV != "production"
48
49     case exception
50       # Can't catch RoutingError with rescue_from it seems,
51       # so do it the old-fashioned way
52     when ActionController::RoutingError
53       render_not_found
54     else
55       super
56     end
57   end
58
59   def current_site
60     @current_site || Site.default
61   end
62
63   protected
64     # Sets the before_filters needed to be able to render in a Site specific
65     # context. +options+ is the options for the before_filters
66     def self.renders_in_site_specific_context(options = {})
67       before_filter :find_current_site, options
68       before_filter :redirect_to_current_site_subdomain, options
69     end
70
71     # Sets the before_filters needed to make sure the requests are rendered
72     # in the "global" (eg without any Site specific layouts + subdomains).
73     # +options+ is the options for the before_filter
74     def self.renders_in_global_context(options = {})
75       before_filter :require_global_site_context, options
76     end
77
78     # return the url with the +repo+.owner prefixed if it's a mainline repo,
79     # otherwise return the +path_spec+
80     # if +path_spec+ is an array (and no +args+ given) it'll use that as the
81     # polymorphic-url-style (eg [@project, @repo, @foo])
82     def repo_owner_path(repo, path_spec, *args)
83       if repo.team_repo?
84         if path_spec.is_a?(Symbol)
85           return send("group_#{path_spec}", *args.unshift(repo.owner))
86         else
87           return *unshifted_polymorphic_path(repo, path_spec)
88         end
89       elsif repo.user_repo?
90         if path_spec.is_a?(Symbol)
91           return send("user_#{path_spec}", *args.unshift(repo.owner))
92         else
93           return *unshifted_polymorphic_path(repo, path_spec)
94         end
95       else
96         if path_spec.is_a?(Symbol)
97           return send(path_spec, *args)
98         else
99           return *path_spec
100         end
101       end
102     end
103     helper_method :repo_owner_path
104
105     def require_user_has_ssh_keys
106       unless current_user.ssh_keys.count > 0
107         flash[:error] = I18n.t "application.require_ssh_keys_error"
108         redirect_to new_user_key_path(current_user)
109         return
110       end
111     end
112
113     def require_current_user
114       unless @user == current_user
115         flash[:error] = I18n.t "application.require_current_user", :title => current_user.title
116         redirect_to user_path(current_user)
117         return
118       end
119     end
120
121     def require_not_logged_in
122       redirect_to root_path if logged_in?
123     end
124
125     def require_current_eula
126       if logged_in?
127         unless current_user.terms_accepted?
128           store_location
129           flash[:error] = I18n.t "views.license.terms_not_accepted"
130           redirect_to user_license_path(current_user)
131           return
132         end
133       end
134       return true
135     end
136
137     def find_repository_owner
138       if params[:user_id]
139         @owner = User.find_by_login!(params[:user_id])
140         @containing_project = Project.find_by_slug!(params[:project_id]) if params[:project_id]
141       elsif params[:group_id]
142         @owner = Group.find_by_name!(params[:group_id])
143         @containing_project = Project.find_by_slug!(params[:project_id]) if params[:project_id]
144       elsif params[:project_id]
145         @owner = Project.find_by_slug!(params[:project_id])
146         @project = @owner
147       else
148         raise ActiveRecord::RecordNotFound
149       end
150     end
151
152     def find_repository_owner_and_repository
153       find_repository_owner
154       @owner.repositories.find_by_name!(params[:id])
155     end
156
157     def find_project
158       @project = Project.find_by_slug!(params[:project_id])
159     end
160
161     def find_project_and_repository
162       @project = Project.find_by_slug!(params[:project_id])
163       # We want to look in all repositories that's somehow within this project
164       # realm, not just @project.repositories
165       @repository = Repository.find_by_name_and_project_id!(params[:repository_id], @project.id)
166     end
167
168     def check_repository_for_commits
169       unless @repository.has_commits?
170         flash[:notice] = I18n.t "application.no_commits_notice"
171         redirect_to project_repository_path(@project, @repository) and return
172       end
173     end
174
175     def render_not_found
176       render :template => "#{RAILS_ROOT}/public/404.html", :status => 404, :layout => "application"
177     end
178
179     def render_git_timeout
180       render :partial => "/shared/git_timeout", :layout => "application" and return
181     end
182
183     def render_throttled_record
184       render :partial => "/shared/throttled_record",
185         :layout => "application", :status => 412 # precondition failed
186       return false
187     end
188
189     def public_and_logged_in
190       login_required unless GitoriousConfig['public_mode']
191     end
192
193     def mark_flash_status
194       unless flash.empty?
195         headers['X-Has-Flash'] = "true"
196       end
197     end
198
199     # turns ["foo", "bar"] route globbing parameters into "foo/bar"
200     # Note that while the path components will be uri unescaped, any
201     # '+' will be preserved
202     def desplat_path(*paths)
203       # we temporarily swap the + out with a magic byte, so
204       # filenames/branches with +'s won't get unescaped to a space
205       paths.flatten.compact.map do |p|
206         CGI.unescape(p.gsub("+", "\001")).gsub("\001", '+')
207       end.join("/")
208     end
209     helper_method :desplat_path
210
211     # turns "foo/bar" into ["foo", "bar"] for route globbing
212     def ensplat_path(path)
213       path.split("/").select{|p| !p.blank? }
214     end
215     helper_method :ensplat_path
216
217     # Returns an array like [branch_ref, *tree_path]
218     def branch_with_tree(branch_ref, tree_path)
219       tree_path = tree_path.is_a?(Array) ? tree_path : ensplat_path(tree_path)
220       ensplat_path(branch_ref) + tree_path
221     end
222     helper_method :branch_with_tree
223
224     def branch_and_path(branch_and_path, git)
225       branch_and_path = desplat_path(branch_and_path)
226       branch_ref = path = nil
227       heads = Array(git.heads).map{|h| h.name }.sort{|a,b| b.length <=> a.length }
228       heads.each do |head|
229         if "#{branch_and_path}/".starts_with?("#{head}/")
230           branch_ref = head
231           path = ensplat_path(branch_and_path.sub(head, "")) || []
232           break
233         end
234       end
235       unless path
236         tags = Array(git.tags).map{|t| t.name }.sort{|a,b| b.length <=> a.length }
237         tags.each do |tag|
238           if "#{branch_and_path}/".starts_with?("#{tag}/")
239             branch_ref = tag
240             path = ensplat_path(branch_and_path.sub(tag, "")) || []
241             break
242           end
243         end
244       end
245       unless path # fallback
246         path = ensplat_path(branch_and_path)[1..-1]
247         branch_ref = ensplat_path(branch_and_path)[0]
248       end
249       [branch_ref, path]
250     end
251
252     def find_current_site
253       @current_site ||= begin
254         if @project
255           @project.site
256         else
257           if !subdomain_without_common.blank?
258             Site.find_by_subdomain(subdomain_without_common)
259           end
260         end
261       end
262     end
263
264     def pick_layout_based_on_site
265       if current_site && current_site.subdomain
266         current_site.subdomain
267       else
268         "application"
269       end
270     end
271
272     def subdomain_without_common
273       tld_length = GitoriousConfig["gitorious_host"].split(".").length - 1
274       request.subdomains(tld_length).select{|s| s !~ /^(ww.|secure)$/}.first
275     end
276
277     def redirect_to_current_site_subdomain
278       return unless request.get?
279       if !current_site.subdomain.blank?
280         if subdomain_without_common != current_site.subdomain
281           url_parameters = {:only_path => false, :host => "#{current_site.subdomain}.#{GitoriousConfig["gitorious_host"]}#{request.port_string}"}.merge(params)
282           redirect_to url_parameters
283         end
284       elsif !subdomain_without_common.blank?
285         redirect_to_top_domain
286       end
287     end
288
289     def require_global_site_context
290       unless subdomain_without_common.blank?
291         redirect_to_top_domain
292       end
293     end
294
295     def redirect_to_top_domain
296       host_without_subdomain = {
297         :only_path => false,
298         :host => GitoriousConfig["gitorious_host"]
299       }
300       if ![80, 443].include?(request.port)
301         host_without_subdomain[:host] << ":#{request.port}"
302       end
303       redirect_to host_without_subdomain
304     end
305
306     def self.skip_session(options = {})
307       skip_before_filter :public_and_logged_in, options
308       skip_before_filter :require_current_eula, options
309       skip_after_filter :mark_flash_status, options
310       before_filter :skip_session_expiry, options
311     end
312
313     def skip_session_expiry
314       request.session_options[:expire_after] = nil
315     end
316     
317     def cache_forever
318       headers["Cache-Control"] = "public, max-age=315360000"
319     end
320
321     # A wrapper around ActionPack's #stale?, that always returns true
322     # if there's data in the flash hash
323     def stale_conditional?(etag, last_modified)
324       return true unless flash.empty?
325       stale?(:etag => [etag, current_user], :last_modified => last_modified)
326     end
327
328   private
329     def unshifted_polymorphic_path(repo, path_spec)
330       if path_spec[0].is_a?(Symbol)
331         path_spec.insert(1, repo.owner)
332       else
333         path_spec.unshift(repo.owner)
334       end
335     end
336 end