allow anonymous viewing of features
[opensuse:openfate.git] / app / controllers / feature_controller.rb
1 class FeatureController < ApplicationController
2   
3   before_filter :require_auth, :except => [:index]
4   before_filter :validate_params, :except => [:auto_complete_for_feature_tag]
5   before_filter :add_boxes, :get_feature, :except => [:new, :create, :revert, :get_unsavedbox, :new_comment, :auto_complete_for_feature_tag]
6   before_filter :setup_new_feature_env, :check_member, :only => [:new, :create]
7
8   layout "application", :except => [:add_comment]
9   auto_complete_for :feature, :tag
10
11   @@cached_tags = nil
12   @@cached_tags_at = nil
13
14   # Render a single feature
15   def index
16     if @feature.nil?
17       flash[:notice] = "Feature ##{params[:id]} does either not exist or you are not authorized to access it."
18       redirect_to :controller => "main", :action => "index"
19       return
20     end
21     if ( (isOpenFate || isPartnerFate) && @feature.type == 'idea') then
22       flash[:error] = "Document ##{params[:id]} is a idea, not a feature. Please try to access it with <a href='http://ideas.opensuse.org/#{params[:id]}'>ideas.opensuse.org</a>."
23       redirect_to :controller => "main", :action => "index"
24       return
25     elsif ( (isIdeas) && @feature.type != 'idea')
26       flash[:error] = "Document ##{params[:id]} is a feature, not a idea. Please try to access it with <a href='http://features.opensuse.org/#{params[:id]}'>features.opensuse.org</a>."
27       redirect_to :controller => "main", :action => "index"
28       return
29     end
30     
31     if (params[:contenttype] == 'text/xml')
32       #render (:text) seems to not be able to change the content-type
33       render(:inline => "<%= @feature.xml %>", :content_type => 'text/xml' )
34     elsif (params[:contenttype] == 'text/plain')
35       render(:text => @feature.render('text', session[:user]), :content_type => 'text/plain' )
36     elsif (params[:contenttype] == 'text/print')
37       @css = 'feature-print.css'
38       render :template => 'feature/index', :layout => false
39     end
40   end
41   
42   def votes
43     res = Feature.votes(params[:id], session[:user].uid)
44     if res.nil? then
45       render :text => "No voting data avaible."
46     else
47       @voting_sum = res["sum"]
48       @voting_up = res["up"]
49       @voting_down = res["down"]
50       @has_voted = res["has_voted"]
51       render :partial => "voting"
52     end
53   end
54
55   def vote_up
56     logger.info( "#{session[:user].uid} is voting up  #{params[:id]} " )
57     Feature.vote_up(params[:id], session[:user].uid)
58     redirect_to :action => :votes, :id => params[:id]
59   end
60   
61   def vote_down
62     logger.info( "#{session[:user].uid} is voting down  #{params[:id]} " )
63     Feature.vote_down(params[:id], session[:user].uid)
64     redirect_to :action => :votes, :id => params[:id]
65   end
66   
67   
68   def tags
69     @tags = Feature.tags(params[:id])
70     if @tags.nil? then
71       render :text => "No tagging data avaible."
72     else
73       render :partial => "tagging"
74     end
75   end
76   
77   
78   def tag
79     logger.info( "#{session[:user].uid} is tagging #{params[:id]} with #{params[:tag]}" )
80     Feature.tag(params[:id], params[:tag])
81     render :partial => "tagging"
82   end
83   
84
85   def auto_complete_for_feature_tag
86     if @@cached_tags.nil? || (!@@cached_tags_at.nil? && (Time.now - @@cached_tags_at) > 5.minutes) then
87       logger.info( "Tagging auto-complete cache outdated, refreshing." )
88       re = Regexp.new("^#{params[:tag]}", "i")
89       @@cached_tags = Feature.all_tags.sort.collect(&:first).select { |t| t.match re }
90       @thetags = Feature.all_tags.sort.collect(&:first).select { |t| t.match re }
91       @@cached_tags_at = Time.now
92     end
93     @thetags = @@cached_tags
94     render :inline => "<%= content_tag(:ul, @thetags.map { |t| content_tag(:li, h(t)) }) %>"
95   end
96   
97
98   def attachment
99   
100   
101   end
102   
103   
104   def new
105   end
106   
107   
108   def create
109     errors = Array.new
110     if !params[:title] || params[:title].empty? then
111       errors << "Please enter a title"
112     end
113     if !params[:description] || params[:description].empty? then
114       errors << "Please enter a description"
115     else
116       params[:description] = richtextify(params[:description])
117     end
118     if !params[:products] || params[:products].length == 0 then
119       errors << "Please select at least one product"
120     end
121     if !params[:priority] || params[:priority].empty? then
122       errors << "Please select at a priority"
123     end
124     
125     if( !validate_richtext( params[:description] ) ) then
126       errors << "The description is not valid richtext: " + @validation_error
127     end
128     
129     if params[:usecase] && !params[:usecase].empty? then
130       params[:usecase] = richtextify(params[:usecase])
131       if( !validate_richtext( params[:usecase] ) ) then
132         errors << "The usecase is not valid richtext: " + @validation_error
133       end
134     end
135     
136     if params[:testcase] && !params[:testcase].empty? then
137       params[:testcase] = richtextify(params[:testcase])
138       if( !validate_richtext( params[:testcase] ) ) then
139         errors << "The testcase is not valid richtext: " + @validation_error
140       end
141     end
142     
143     if !errors.empty? then 
144       flash.now[:error] = "There were errors:<br><ul>"
145       errors.each do |e|
146         flash.now[:error] << "<li>#{e}</li>"
147       end
148       flash.now[:error] << "</ul>"
149       render :action => :new
150       return
151     end
152     
153     begin
154       xml = %Q[
155         <feature xmlns:k="http://inttools.suse.de/sxkeeper/schema/keeper" k:schemarevision="12" >
156         <title>#{params[:title]}</title>
157         <description><richtext>#{params[:description]}</richtext></description>
158         <partnercontext><organization>#{session[:user].organization}</organization></partnercontext>]
159       params[:products].each do |p|
160         xml += %Q[
161           <productcontext>
162             <product>
163               <productid>#{p}</productid>
164             </product>
165             <status><unconfirmed/></status>
166             <priority>
167               <#{params[:priority]}/>
168               <owner><role>requester</role></owner>
169             </priority>
170           </productcontext>]
171       end
172       xml += %Q[
173         <actor>
174           <role>requester</role>
175           <person>
176             <userid>#{session[:user].uid}</userid>
177           </person>
178         </actor>]
179       
180       if params[:testcase] && !params[:testcase].empty? then
181         xml += %Q[
182           <testcase><richtext>#{params[:testcase]}</richtext></testcase>]
183       end
184       if params[:usecase] && !params[:usecase].empty? then
185         xml += %Q[
186           <usecase><richtext>#{params[:usecase]}</richtext></usecase>]
187       end
188       
189       xml += "</feature>"
190       logger.debug xml
191       
192       f = Feature.new
193       f.xml = XML::Smart.string(xml)
194       response = f.save session[:user].uid
195       if response.code == "201" then
196         xml = XML::Smart.string(response.body)
197         id = xml.find("k:docchange/k:id", {"k" => "http://inttools.suse.de/sxkeeper/schema/keeper"}).first.text
198         flash[:notice] = "Your feature has been created with the id #{id}"
199         redirect_to :action => :index, :id => id
200       else
201         render :template => 'feature/new'
202       end
203     rescue Exception=>e
204       logger.error "Saving Feature ##{params[:id]} failed: \n#{extract_errormessage(e.to_s)}"
205       flash.now[:error] = "Saving Feature ##{params[:id]} failed: \n#{extract_errormessage(e.to_s)}"
206       render :template => 'feature/new'
207     end
208   end
209   
210   
211   def add_comment
212     comment = richtextify(params[:comment])
213     if( validate_richtext( comment ) )
214       if (@feature.add_comment( comment, session[:user], params[:replyto] )) 
215         session[:editedFeatures][params[:id]] = @feature.xml.to_s
216       end
217     else
218       @replyto = params[:replyto]
219       @oldcomment = comment
220       @comment_error = @validation_error
221     end
222     render :partial => "featurehtml"
223   end
224   
225   
226   def edit_title
227     logger.debug "Setting title: " + CGI.escapeHTML(params[:newtitle])
228     if (@feature.set_title( CGI.escapeHTML(params[:newtitle]) )) 
229       session[:editedFeatures][params[:id]] = @feature.xml.to_s
230     end
231     render :partial => "featurehtml"
232   end
233   
234   
235   def edit_description
236     desc = richtextify(params[:newdescription])
237     if( validate_richtext( desc ) )
238       logger.debug "Setting description: " + params[:newdescription]
239       if (@feature.set_description( desc ))
240         session[:editedFeatures][params[:id]] = @feature.xml.to_s
241       end
242     else
243       logger.debug "Validation failed: " + params[:newdescription] + desc
244       @newdescription_value = desc
245       @newdescription_error = @validation_error
246       logger.debug @newdescription_error
247     end
248     render :partial => "featurehtml"
249   end
250   
251   
252   def edit_usecase
253     uc = richtextify(params[:newusecase])
254     if( validate_richtext( uc ) )
255       logger.debug "Setting usecase: " + params[:newusecase]
256       if (@feature.set_usecase( uc )) 
257         session[:editedFeatures][params[:id]] = @feature.xml.to_s
258       end
259     else
260       logger.debug "Validation failed: " + params[:newusecase] + uc
261       @newusecase_value = uc
262       @newusecase_error = @validation_error
263       logger.debug @newusecase_error
264     end
265     render :partial => "featurehtml"
266   end
267   
268   
269   def edit_testcase
270     tc = richtextify(params[:newtestcase])
271     if( validate_richtext( tc ) )
272       logger.debug "Setting testcase: " + params[:newtestcase]
273       if (@feature.set_testcase( tc ))
274         session[:editedFeatures][params[:id]] = @feature.xml.to_s
275       end
276     else
277       logger.debug "Validation failed: " + params[:newtestcase] + tc
278       @newtestcase_value = tc
279       @newtestcase_error = @validation_error
280       logger.debug @newtestcase_error
281     end
282     render :partial => "featurehtml"
283   end
284   
285   
286   def remove_actor
287     @feature.remove_actor(params[:uid], params[:role])
288     session[:editedFeatures][params[:id]] = @feature.xml.to_s
289     render :partial => "featurehtml"
290   end
291   
292   
293   def add_myself_as
294     if params[:role] != "developer" && params[:role] != "interested" then
295       flash.now[:error] = "Invalid role"
296     elsif (@feature.actors(session[:user].uid, params[:role]).size > 0)
297       logger.error("Tried to add user #{session[:user].uid} twice to #{params[:role]}")
298     else
299       @feature.add_actor(CGI::escapeHTML(session[:user].uid), params[:role])
300        session[:editedFeatures][params[:id]] = @feature.xml.to_s
301     end
302     render :partial => "featurehtml"
303   end
304   
305   
306   def save
307     begin
308       @feature.save( session[:user].uid )
309       session[:editedFeatures].delete(params[:id])
310       flash.now[:success] = "Feature ##{params[:id]} saved."
311       get_feature
312       @force_box_refresh = true
313     rescue Exception=>e
314       logger.error "Saving Feature ##{params[:id]} failed: \n#{extract_errormessage(e.to_s)}"
315       allowedCodes = [ '452', '409' ]
316       if (allowedCodes.include?(extract_errorcode(e.to_s)))
317         flash.now[:error] = "Saving Feature ##{params[:id]} failed: " + extract_errormessage(e.to_s)
318       else 
319         ExceptionNotifier.deliver_exception_notification(e, self, request, {})
320         flash.now[:error] = "Saving Feature ##{params[:id]} failed due to an internal error. The development team has been notified."
321       end
322     end
323     render :partial => "featurehtml"
324   end
325   
326   
327   def revert
328     if session[:editedFeatures].has_key?(params[:id]) then 
329       session[:editedFeatures].delete(params[:id])
330       flash.now[:notice] = "Your changes to feature ##{params[:id]} have been reverted."
331       @force_box_refresh = true
332     end
333     get_feature
334     render :partial => "featurehtml"
335   end
336   
337   
338   def get_unsavedbox
339     render :partial => "layouts/unsavedbox"
340   end  
341   
342   
343   def get_editbox
344     render :partial => "editbox"
345   end
346   
347
348   private
349   
350
351   def add_boxes
352     if (session[:user].isAuthenticated)
353       @partials = [ ['feature/editbox', {}], 
354                  ['feature/exportbox', {}], 
355                  ['layouts/unsavedbox', {}] ]
356     else 
357       @partials = [ ['feature/exportbox', {}] ]
358     end
359   end
360   
361   
362   def validate_params
363     if params.has_key?(:id) and /\A\d*\z/.match(params[:id]).nil? then
364       flash[:error] = "'#{params[:id]}' is not a valid id."
365       redirect_to :controller => "main", :action => :index
366       return
367     end
368     if params.has_key?(:replyto) and /\A\d*\z/.match(params[:replyto]).nil? then
369       flash[:error] = "'#{params[:replyto]}' is not a valid comment number."
370       redirect_to :controller => "main", :action => :index
371       return
372     end
373   end
374   
375   
376   def setup_new_feature_env
377     if isOpenFate then
378       @products = $productlist.products( "openSUSE", true )
379       @productsize = [@products.length, 10].min
380     else
381       @products = $productlist.products( "", true )
382       @productsize = [@products.length, 10].min
383     end
384     @productsmultiple = true
385     if @products.length == 1 then 
386       params[:products] = @products.first.first
387       @productsmultiple = false
388     elsif @products.length == 0 then
389       flash[:error] = "There are currently no products open for new features"
390       redirect_to :controller => :main, :action => "index"
391       return
392     end
393   end
394
395   
396   # is called on incoming field values to create a valid richtext from user input
397   def richtextify(content)
398     logger.debug content
399     content.gsub!( /&/, "&amp;" );
400     # Escape all < and > that are not part of an element declaration
401     content.gsub!( />/, "&gt;" );
402     content.gsub!( /</, "&lt;" );
403     content.gsub!( /&lt;(\/?\w+)(\s\w+=['"][^'"]*['"])*[\/]?&gt;/, "<\\1\\2>" );
404     
405     # if the text starts and ends with an element or starts/ends with <p>, don't add elements
406    if( content.index(/\A<.*>\Z/m).nil? && 
407         content.index(/\A[^<]*<p>/m).nil? && content.index(/<\/p>[^>]*\Z/m).nil? ) then
408       content = "<p>" + content + "</p>"
409    end
410     # make a new paragraph on double newlines
411     content.gsub!( /(<p>[^<]*)\n\n/, "\\1</p>\n<p>" );
412     logger.debug "Richtextified content: " + content
413     return content
414   end
415
416   
417   def validate_richtext(content)
418     dtd = LibXML::XML::Dtd.new(<<EOF)
419 <!ELEMENT richtext ( p | a | b | ul | ol | em | pre | h3 | tt )*>
420
421 <!ELEMENT p (#PCDATA | b | ul | ol | a | tt | em | pre)*>
422 <!ELEMENT a (#PCDATA)>
423 <!ATTLIST a
424           href CDATA #REQUIRED>
425 <!ELEMENT b (#PCDATA | b | ul | ol | a | tt | em | p | pre)*>
426 <!ATTLIST b
427           class CDATA #IMPLIED>
428 <!ELEMENT ul (li+)>
429 <!ELEMENT ol (li)+>
430 <!ELEMENT li (#PCDATA | b | tt | em | a | p | ul)*>
431 <!ELEMENT em (#PCDATA)>
432 <!ELEMENT pre (#PCDATA | tt | b | em | a )*>
433 <!ATTLIST pre
434           class CDATA #IMPLIED>
435 <!ELEMENT h3 (#PCDATA)>
436 <!ELEMENT tt (#PCDATA | b | ul | ol | a | tt | em | p | pre)*>
437 <!ATTLIST tt
438           class CDATA #IMPLIED>
439 EOF
440
441     begin
442       doc = LibXML::XML::Document.string( "<richtext>" + content + "</richtext>" )
443       doc.validate(dtd)
444     rescue => e
445       @validation_error = e.to_s
446       return false
447     else
448       return true
449     end
450   end
451    
452
453   def check_member
454     if (!session[:user].isMember && session[:user].organization == 'openSUSE.org')
455       flash[:notice] = "Sorry, only approved openSUSE members can create new feature requests at the moment. \n" + 
456                        "Please check <a href='http://en.opensuse.org/Members'>http://en.opensuse.org/Members</a> for information about openSUSE membership."
457       redirect_to :controller => "main", :action => "index"
458     end
459   end
460   
461   
462   def self.logger 
463     RAILS_DEFAULT_LOGGER
464   end
465
466 end