[webui] escape the request comments the same as log files
[opensuse:build-service.git] / src / webui / app / helpers / application_helper.rb
1 require 'md5'
2
3 require 'action_view/helpers/asset_tag_helper.rb'
4 module ActionView
5   module Helpers
6
7     @@rails_root = nil
8     def real_public
9       return @@rails_root if @@rails_root
10       @@rails_root = Pathname.new("#{RAILS_ROOT}/public").realpath
11     end
12
13     @@icon_cache = Hash.new
14     
15     def rewrite_asset_path(_source)
16       if @@icon_cache[_source]
17         return @@icon_cache[_source]
18       end
19       new_path = "/vendor/#{CONFIG['theme']}#{_source}"
20       if File.exists?("#{RAILS_ROOT}/public#{new_path}")
21         source = new_path
22       elsif File.exists?("#{RAILS_ROOT}/public#{_source}")
23         source = _source
24       else
25         return super(_source)
26       end
27       source=Pathname.new("#{RAILS_ROOT}/public#{source}").realpath
28       source="/" + Pathname.new(source).relative_path_from(real_public)
29       Rails.logger.debug "using themed file: #{_source} -> #{source}"
30       source = super(source)
31       @@icon_cache[_source] = source
32     end
33
34     def compute_asset_host(source)
35       if CONFIG['use_static'] 
36         if ActionController::Base.relative_url_root
37           source = source.slice(ActionController::Base.relative_url_root.length..-1)
38         end
39         if source =~ %r{^/themes}
40           return "https://static.opensuse.org"
41         elsif source =~ %r{^/images} or source =~ %r{^/javascripts} or source =~ %r{^/stylesheets}
42           return "https://static.opensuse.org/hosts/#{CONFIG['use_static']}"
43         end
44       end
45       super(source)
46     end
47
48   end
49 end
50
51 # Methods added to this helper will be available to all templates in the application.
52 module ApplicationHelper
53   
54   def logged_in?
55     !session[:login].nil?
56   end
57   
58   def user
59     if logged_in?
60       begin
61         @user ||= find_cached(Person, session[:login] )
62       rescue Object => e
63         logger.error "Cannot load person data for #{session[:login]} in application_helper"
64       end
65     end
66     return @user
67   end
68
69   def link_to_project project
70     link_to project, :controller => "project", :action => :show,
71       :project => project
72   end
73
74   def link_to_package project, package
75     link_to package, :controller => "package", :action => :show,
76       :project => project, :package => package
77   end
78
79   def repo_url(project, repo='' )
80     if defined? DOWNLOAD_URL
81       "#{DOWNLOAD_URL}/" + project.to_s.gsub(/:/,':/') + "/#{repo}"
82     else
83       nil
84     end
85   end
86
87   def focus_id( id )
88     javascript_tag(
89       "document.getElementById('#{id}').focus();"
90     )
91   end
92
93
94   def focus_and_select_id( id )
95     javascript_tag(
96       "document.getElementById('#{id}').focus();" +
97         "document.getElementById('#{id}').select();"
98     )
99   end
100
101
102   def get_frontend_url_for( opt={} )
103     opt[:host] ||= Object.const_defined?(:EXTERNAL_FRONTEND_HOST) ? EXTERNAL_FRONTEND_HOST : FRONTEND_HOST
104     opt[:port] ||= Object.const_defined?(:EXTERNAL_FRONTEND_PORT) ? EXTERNAL_FRONTEND_PORT : FRONTEND_PORT
105     opt[:protocol] ||= FRONTEND_PROTOCOL
106
107     if not opt[:controller]
108       logger.error "No controller given for get_frontend_url_for()."
109       return
110     end
111
112     return "#{opt[:protocol]}://#{opt[:host]}:#{opt[:port]}/#{opt[:controller]}"
113   end
114
115   def bugzilla_url(email_list="", desc="")
116     return '' if BUGZILLA_HOST.nil?
117     assignee = email_list.first if email_list
118     if email_list.length > 1
119       cc = ("&cc=" + email_list[1..-1].join("&cc=")) if email_list
120     end
121     URI.escape("#{BUGZILLA_HOST}/enter_bug.cgi?classification=7340&product=openSUSE.org&component=3rd party software&assigned_to=#{assignee}#{cc}&short_desc=#{desc}")
122   end
123
124   def hinted_text_field_tag(name, value = nil, hint = "Click and enter text", options={})
125     value = value.nil? ? hint : value
126     text_field_tag name, value, {:onfocus => "if($(this).value == '#{hint}'){$(this).value = ''}",
127       :onblur => "if($(this).value == ''){$(this).value = '#{hint}'}",
128     }.update(options.stringify_keys)
129   end
130
131
132   def get_random_sponsor_image
133     sponsors = ["/themes/bento/images/sponsors/sponsor_amd.png",
134       "/themes/bento/images/sponsors/sponsor_b1-systems.png",
135       "/themes/bento/images/sponsors/sponsor_ip-exchange2.png"]
136     return sponsors[rand(sponsors.size)]
137   end
138
139
140   def link_to_remote_if(condition, name, options = {}, html_options = nil, &block)
141     if condition
142       link_to_remote(name, options, html_options)
143     else
144       if block_given?
145         block.arity <= 1 ? yield(name) : yield(name, options, html_options)
146       else
147         name
148       end
149     end
150   end
151
152   def image_url(source)
153     abs_path = image_path(source)
154     unless abs_path =~ /^http/
155       abs_path = "#{request.protocol}#{request.host_with_port}#{abs_path}"
156     end
157     abs_path
158   end
159
160   def gravatar_image(email, size=20)
161     hash = MD5::md5(email.downcase)
162     return image_tag "https://secure.gravatar.com/avatar/#{hash}?s=#{size}&d=" + image_url('local/default_face.png'), 
163       :alt => "Gravatar for #{email}", :width => size, :height => size
164   end
165
166   def fuzzy_time_string(time)
167     diff = Time.now - Time.parse(time)
168     return "now" if diff < 60
169     return (diff/60).to_i.to_s + " min ago" if diff < 3600
170     diff = Integer(diff/3600) # now hours
171     return diff.to_s + (diff == 1 ? " hour ago" : " hours ago") if diff < 24
172     diff = Integer(diff/24) # now days
173     return diff.to_s + (diff == 1 ? " day ago" : " days ago") if diff < 14
174     diff_w = Integer(diff/7) # now weeks
175     return diff_w.to_s + (diff_w == 1 ? " week ago" : " weeks ago") if diff < 63
176     diff_m = Integer(diff/30.5) # roughly months
177     return diff_m.to_s + " months ago"
178   end
179
180   def package_exists?(project, package)
181     if Package.find_cached(package, :project => project )
182       return true
183     else
184       return false
185     end
186   end
187
188   def status_for( repo, arch, package )
189     @statushash[repo][arch][package] || ActiveXML::XMLNode.new("<status package='#{package}'/>")
190   end
191
192   def status_id_for( repo, arch, package )
193     valid_xml_id("id-#{package}_#{repo}_#{arch}")
194   end
195
196   def arch_repo_table_cell(repo, arch, packname)
197     status = status_for(repo, arch, packname)
198     status_id = status_id_for( repo, arch, packname)
199     link_title = status.has_element?(:details) ? status.details.to_s : nil
200     if status.has_attribute? 'code'
201       code = status.code.to_s
202       theclass="status_" + code.gsub(/[- ]/,'_')
203     else
204       code = ''
205       theclass=''
206     end
207     
208     out = "<td class='#{theclass} buildstatus'>"
209     if ["unresolvable", "blocked"].include? code 
210       out += link_to code, "#", :title => link_title, :id => status_id
211       content_for :ready_function do
212         "$('a##{status_id}').click(function() { alert('#{link_title.gsub(/\'/, '\'')}'); return false; });\n"
213       end
214     elsif ["-","excluded"].include? code
215       out += code
216     else
217       out += link_to code.gsub(/\s/, "&nbsp;"), {:action => :live_build_log,
218         :package => packname, :project => @project.to_s, :arch => arch,
219         :controller => "package", :repository => repo}, {:title => link_title, :rel => 'nofollow'}
220     end 
221     out += "</td>"
222     return out.html_safe
223   end
224
225   
226   def repo_status_icon( status )
227     icon = case status
228     when "published" then "icons/lorry.png"
229     when "publishing" then "icons/cog_go.png"
230     when "outdated_published" then "icons/lorry_error.png"
231     when "outdated_publishing" then "icons/cog_error.png"
232     when "unpublished" then "icons/lorry_flatbed.png"
233     when "outdated_unpublished" then "icons/lorry_error.png"
234     when "building" then "icons/cog.png"
235     when "outdated_building" then "icons/cog_error.png"
236     when "finished" then "icons/time.png"
237     when "outdated_finished" then "icons/time_error.png"
238     when "blocked" then "icons/time.png"
239     when "outdated_blocked" then "icons/time_error.png"
240     when "broken" then "icons/exclamation.png"
241     when "outdated_broken" then "icons/exclamation.png"
242     when "scheduling" then "icons/cog.png"
243     when "outdated_scheduling" then "icons/cog_error.png"
244     else "icons/eye.png"
245     end
246
247     outdated = nil
248     if status =~ /^outdated_/
249       status.gsub!( %r{^outdated_}, '' )
250       outdated = true
251     end
252     description = case status
253     when "published" then "Repository has been published"
254     when "publishing" then "Repository is created right now"
255     when "unpublished" then "Build finished, but repository publishing is disabled"
256     when "building" then "Build jobs exists"
257     when "finished" then "Build jobs have been processed, new repository is not yet created"
258     when "blocked" then "No build possible atm, waiting for jobs in other repositories"
259     when "broken" then "The setup of repository is broken, build not possible"
260     when "scheduling" then "The repository state is calculated right now"
261     else "Unknown state of repository"
262     end
263
264     description = "State needs recalculations, former state was: " + description if outdated
265
266     image_tag icon, :size => "16x16", :title => description, :alt => description
267   end
268
269
270   def flag_status(flags, repository, arch)
271     image = nil
272     flag = nil
273
274     flags.each do |f|
275
276       if f.has_attribute? :repository
277         next if f.repository.to_s != repository
278       else
279         next if repository
280       end
281       if f.has_attribute? :arch
282         next  if f.arch.to_s != arch
283       else
284         next if arch 
285       end
286
287       flag = f
288       break
289     end
290
291     if flag
292       if flag.has_attribute? :explicit
293         if flag.element_name == 'disable'
294           image = "#{flags.element_name}_disabled_blue.png"
295         else
296           image = "#{flags.element_name}_enabled_blue.png"
297         end
298       else
299         if flag.element_name == 'disable'
300           image = "#{flags.element_name}_disabled_grey.png"
301         else
302           image = "#{flags.element_name}_enabled_grey.png"
303         end
304       end
305
306       if @user and @user.is_maintainer?(@project, @package)
307         opts = { :project => @project, :repository => repository, :arch => arch, :package => @package, :flag => flags.element_name, :action => :change_flag }
308         out = "<div class='flagimage'>" + image_tag(image) + "<div class='hidden flagtoggle'>"
309         unless flag.has_attribute? :explicit and flag.element_name == 'disable'
310           out += 
311             "<div class='nowrap'>" +
312             image_tag("#{flags.element_name}_disabled_blue.png", :alt => '0', :size => "24x24") +
313             link_to("Explicitly disable", opts.merge({ :cmd => :set_flag, :status => :disable }), {:class => :flag_trigger}) +
314             "</div>"
315         end
316         if flag.element_name == 'disable'
317           out += 
318             "<div class='nowrap'>" +
319             image_tag("#{flags.element_name}_enabled_grey.png", :alt => '1', :size => "24x24") +
320             link_to("Take default", opts.merge({ :cmd => :remove_flag }),:class => :flag_trigger) +
321             "</div>"
322         else
323           out += 
324             "<div class='nowrap'>" +
325             image_tag("#{flags.element_name}_disabled_grey.png", :alt => '0', :size => "24x24") +
326             link_to("Take default", opts.merge({ :cmd => :remove_flag }), :class => :flag_trigger)+
327             "</div>"
328         end if flag.has_attribute? :explicit
329         unless flag.has_attribute? :explicit and flag.element_name != 'disable'
330           out += 
331             "<div class='nowrap'>" +
332             image_tag("#{flags.element_name}_enabled_blue.png", :alt => '1', :size => "24x24") +
333             link_to("Explicitly enable", opts.merge({ :cmd => :set_flag, :status => :enable }), :class => :flag_trigger) +
334             "</div>"
335         end
336         out += "</div></div>"
337         out.html_safe
338       else
339         image_tag(image)
340       end
341     else
342       ""
343     end
344   end
345
346   def plural( count, singular, plural)
347     count > 1 ? plural : singular
348   end
349
350   def valid_xml_id(rawid)
351     rawid = '_' + rawid if rawid !~ /^[A-Za-z_]/ # xs:ID elements have to start with character or '_'
352     ERB::Util::h(rawid.gsub(/[+&: .]/, '_'))
353   end
354
355   def format_comment(comment)
356     comment ||= '-'
357     comment = ERB::Util::h(comment).gsub(%r{[\n\r]}, '<br/>')
358     # Proper-width tab expansion - a gem from perlfaq4:
359     while comment.sub!(/\t+/) {' ' * ($&.length * 8 - $`.length % 8)}
360     end
361     # Newlines...`
362     comment = '<br/>' + comment
363     comment.gsub!(/[\n\r]/, "<br />")
364     # Initial space must be protected, or it may/will be eaten.
365     comment.gsub!(%{<br/> }, "<br/>&nbsp;")
366     # Keep lines breakable by retaining U+20. Keep the width by
367     # transforming every other space into U+A0. The browser will
368     # display U+A0 as U+20, which means it is safe for copy and paste
369     # to a terminal. Avoid any other characters (U+2002/&ensp;) because
370     # they will not be transformed to U+20 during C&P.
371     comment.gsub!(/  /, " &nbsp;")
372
373     # always prepend a newline so the following code can eat up leading spaces over all lines
374     comment.gsub!('(<br/> *) ', '\1&nbsp;')
375     comment.gsub!(%r{^<br/>}, '')
376     comment = "<code>" + comment + "</code>"
377     return comment.html_safe
378   end
379
380   def tab(text, opts)
381     opts[:package] = @package.to_s if @package
382     opts[:project] = @project.to_s
383     if @current_action.to_s == opts[:action].to_s
384       link = "<li class='selected'>"
385     else
386       link = "<li>"
387     end
388     link += link_to(h(text), opts)
389     link += "</li>"
390     return link.html_safe
391   end
392
393   # Shortens a text if it longer than 'length'. 
394   def elide(text, length = 20, mode = :middle)
395     shortened_text = text.to_s      # make sure it's a String
396
397     return "..." if length <= 3     # corner case
398
399     if text.length > length
400       case mode
401       when :left                    # shorten at the beginning
402         shortened_text = "..." + text[text.length - length + 3 .. text.length]
403       when :middle                  # shorten in the middle
404         pre = text[0 .. length / 2 - 2]
405         offset = 2                  # depends if (shortened) length is even or odd
406         offset = 1 if length.odd?
407         post = text[text.length - length / 2 + offset .. text.length]
408         shortened_text = pre + "..." + post
409       when :right                   # shorten at the end
410         shortened_text = text[0 .. length - 4 ] + "..."
411       end
412     end
413     return shortened_text
414   end
415
416   def elide_two(text1, text2, overall_length = 40, mode = :middle)
417     half_length = overall_length / 2
418     text1_free = half_length - text1.length
419     text1_free = 0 if text1_free < 0
420     text2_free = half_length - text2.length
421     text2_free = 0 if text2_free < 0
422     return [elide(text1, half_length + text2_free, mode), elide(text2, half_length + text1_free, mode)]
423   end
424
425   def escape_and_transform_nonprintables(text)
426     text = CGI.escapeHTML(text)
427     # Proper-width tab expansion - a gem from perlfaq4:
428     while text.sub!(/\t+/) {' ' * ($&.length * 8 - $`.length % 8)}
429     end
430     # Newlines...
431     text.gsub!(/[\n\r]/, "<br />\n")
432     # Initial space must be protected, or it may/will be eaten.
433     text.gsub!(/^ /, "&nbsp;")
434     # Keep lines breakable by retaining U+20. Keep the width by
435     # transforming every other space into U+A0. The browser will
436     # display U+A0 as U+20, which means it is safe for copy and paste
437     # to a terminal. Avoid any other characters (U+2002/&ensp;) because
438     # they will not be transformed to U+20 during C&P.
439     text.gsub!(/  /, " &nbsp;")
440     return text
441   end
442
443   def reload_to_remote(opts)
444     {:title => "Reload", :url => nil, :update => nil}.merge(opts)
445
446     id = valid_xml_id(opts[:title])
447     return link_to_remote(image_tag("arrow_refresh.png", :title => opts[:title], :title => opts[:title], :id => id + "_reload") +
448                           image_tag("ajax-loader.gif", :id => id + "_spinner", :class => "hidden"),
449                           :url => opts[:url], :update => opts[:update],
450                           :loading => "$('##{id}_spinner').show(); $('##{id}_reload').hide()",
451                           :complete => "$('##{id}_spinner').hide(); $('##{id}_reload').show()")
452   end
453
454   # Same as redirect_to(:back) if there is a valid HTTP referer, otherwise redirect_to()
455   def redirect_back_or_to(options = {}, response_status = {})
456     if request.env["HTTP_REFERER"]
457       redirect_to(:back)
458     else
459       redirect_to(options, response_status)
460     end
461   end
462 end
463